User Tools

Site Tools


programming:python:smtprelaytest
no way to compare when less than two revisions

Differences

This shows you the differences between two versions of the page.


programming:python:smtprelaytest [2008/01/07 17:56] (current) – created crustymonkey
Line 1: Line 1:
 +====== SMTPRelayTest ======
  
 +===== What is it? =====
 +''smtprelaytest.py'' is a simple script to be run from the command-line that will test a mail server to see if it is an open relay.  It will output color coded responses from the mail server as it just simply tests whether or not you can relay mail given the specified HELO name, sender and recipient.  It also supports this test using STARTTLS.  This is basically just a shortcut to using [[wp>netcat]] or [[wp>telnet]].
 +
 +===== The Script =====
 +You can either download it using {{programming:python:smtprelaytest.py.gz|this link}} or copy it from below.
 +<code python>
 +#!/usr/bin/env python
 +
 +__cvsversion__ = '$Id: smtprelaytest.py,v 1.3 2008/01/07 17:41:23 jay Exp $'
 +__author__ = 'Jay Deiman'
 +
 +import smtplib , os , sys , getopt , socket , re
 +
 +class TerminalController:
 +    """
 +    Author of the TerminalController class: Edward Loper
 +    Code copied from: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/475116
 +    
 +    A class that can be used to portably generate formatted output to
 +    a terminal.  
 +    
 +    `TerminalController` defines a set of instance variables whose
 +    values are initialized to the control sequence necessary to
 +    perform a given action.  These can be simply included in normal
 +    output to the terminal:
 +
 +        >>> term = TerminalController()
 +        >>> print 'This is '+term.GREEN+'green'+term.NORMAL
 +
 +    Alternatively, the `render()` method can used, which replaces
 +    '${action}' with the string required to perform 'action':
 +
 +        >>> term = TerminalController()
 +        >>> print term.render('This is ${GREEN}green${NORMAL}')
 +
 +    If the terminal doesn't support a given action, then the value of
 +    the corresponding instance variable will be set to '' As a
 +    result, the above code will still work on terminals that do not
 +    support color, except that their output will not be colored.
 +    Also, this means that you can test whether the terminal supports a
 +    given action by simply testing the truth value of the
 +    corresponding instance variable:
 +
 +        >>> term = TerminalController()
 +        >>> if term.CLEAR_SCREEN:
 +        ...     print 'This terminal supports clearning the screen.'
 +
 +    Finally, if the width and height of the terminal are known, then
 +    they will be stored in the `COLS` and `LINES` attributes.
 +    """
 +    # Cursor movement:
 +    BOL = ''             #: Move the cursor to the beginning of the line
 +    UP = ''              #: Move the cursor up one line
 +    DOWN = ''            #: Move the cursor down one line
 +    LEFT = ''            #: Move the cursor left one char
 +    RIGHT = ''           #: Move the cursor right one char
 +
 +    # Deletion:
 +    CLEAR_SCREEN = ''    #: Clear the screen and move to home position
 +    CLEAR_EOL = ''       #: Clear to the end of the line.
 +    CLEAR_BOL = ''       #: Clear to the beginning of the line.
 +    CLEAR_EOS = ''       #: Clear to the end of the screen
 +
 +    # Output modes:
 +    BOLD = ''            #: Turn on bold mode
 +    BLINK = ''           #: Turn on blink mode
 +    DIM = ''             #: Turn on half-bright mode
 +    REVERSE = ''         #: Turn on reverse-video mode
 +    NORMAL = ''          #: Turn off all modes
 +
 +    # Cursor display:
 +    HIDE_CURSOR = ''     #: Make the cursor invisible
 +    SHOW_CURSOR = ''     #: Make the cursor visible
 +
 +    # Terminal size:
 +    COLS = None          #: Width of the terminal (None for unknown)
 +    LINES = None         #: Height of the terminal (None for unknown)
 +
 +    # Foreground colors:
 +    BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
 +    
 +    # Background colors:
 +    BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
 +    BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
 +    
 +    _STRING_CAPABILITIES = """
 +    BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
 +    CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
 +    BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
 +    HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
 +    _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
 +    _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
 +
 +    def __init__(self, term_stream=sys.stdout):
 +        """
 +        Create a `TerminalController` and initialize its attributes
 +        with appropriate values for the current terminal.
 +        `term_stream` is the stream that will be used for terminal
 +        output; if this stream is not a tty, then the terminal is
 +        assumed to be a dumb terminal (i.e., have no capabilities).
 +        """
 +        # Curses isn't available on all platforms
 +        try: import curses
 +        except: return
 +
 +        # If the stream isn't a tty, then assume it has no capabilities.
 +        if not term_stream.isatty(): return
 +
 +        # Check the terminal type.  If we fail, then assume that the
 +        # terminal has no capabilities.
 +        try: curses.setupterm()
 +        except: return
 +
 +        # Look up numeric capabilities.
 +        self.COLS = curses.tigetnum('cols')
 +        self.LINES = curses.tigetnum('lines')
 +        
 +        # Look up string capabilities.
 +        for capability in self._STRING_CAPABILITIES:
 +            (attrib, cap_name) = capability.split('=')
 +            setattr(self, attrib, self._tigetstr(cap_name) or '')
 +
 +        # Colors
 +        set_fg = self._tigetstr('setf')
 +        if set_fg:
 +            for i,color in zip(range(len(self._COLORS)), self._COLORS):
 +                setattr(self, color, curses.tparm(set_fg, i) or '')
 +        set_fg_ansi = self._tigetstr('setaf')
 +        if set_fg_ansi:
 +            for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
 +                setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
 +        set_bg = self._tigetstr('setb')
 +        if set_bg:
 +            for i,color in zip(range(len(self._COLORS)), self._COLORS):
 +                setattr(self, 'BG_'+color, curses.tparm(set_bg, i) or '')
 +        set_bg_ansi = self._tigetstr('setab')
 +        if set_bg_ansi:
 +            for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
 +                setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
 +
 +    def _tigetstr(self, cap_name):
 +        # String capabilities can include "delays" of the form "$<2>".
 +        # For any modern terminal, we should be able to just ignore
 +        # these, so strip them out.
 +        import curses
 +        cap = curses.tigetstr(cap_name) or ''
 +        return re.sub(r'\$<\d+>[/*]?', '', cap)
 +
 +    def render(self, template):
 +        """
 +        Replace each $-substitutions in the given template string with
 +        the corresponding terminal control string (if it's defined) or
 +        '' (if it's not).
 +        """
 +        return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
 +
 +    def _render_sub(self, match):
 +        s = match.group()
 +        if s == '$$': return s
 +        else: return getattr(self, s[2:-1])
 +
 +# Functions
 +def getHostName():
 +    fqdn = re.compile(r'^(?:[^\s\.]+\.){1,}(?:[^\s\.]+)$')
 +    res = socket.gethostbyaddr('127.0.0.1')
 +    if fqdn.match(res[0]):
 +        return res[0]
 +    for name in res[1]:
 +        if fqdn.match(name):
 +            return name
 +
 +def usage(exitCode=0):
 +    print '%s -r <remote host> [-p <port>] ' % os.path.basename(sys.argv[0]) + \
 +            '-c <recipient> [-f <send from>] [-t <helo name>]'
 +    print """
 +    -h,--help            Help, what you are looking at
 +    -s,--usetls          Use TLS for the connection
 +    -r,--remhost=        Remote hostname or IP
 +    -p,--port=           Remote host port
 +    -t,--helo=           HELO hostname.  An attempt will be made to determine
 +                         the hostname if not supplied
 +    -c,--recip=          The recipient email address to use
 +    -f,--from=           The email address that the request should come
 +                         from.  "test@qwest.net" will be used by default
 +    """
 +    sys.exit(exitCode)
 +    
 +def gAndR(code):
 +    code = int(code)
 +    if code >= 200 and code < 300:
 +        return "${GREEN}"
 +    else:
 +        return "${RED}"
 +    
 +# Config vars
 +remoteHost = ''
 +remotePort = 25
 +heloHost = getHostName()
 +mRecip = ''
 +mFrom = 'test@qwest.net'
 +useTls = False
 +
 +# Get the command line opts
 +shortOpts = 'hsr:p:t:c:f:'
 +longOpts = ['help' , 'usetls' , 'remhost=' , 'port=' , 'helo='
 +            'recip=' , 'from=']
 +try:
 +    optList , junk = getopt.getopt(sys.argv[1:] , shortOpts , longOpts)
 +except getopt.GetoptError , e:
 +    print e
 +    usage(1)
 +for opt , val in optList:
 +    if opt in ('-h' , '--help'):
 +        usage()
 +    elif opt in ('-s' , '--usetls'):
 +        useTls = True
 +    elif opt in ('-r' , '--remhost'):
 +        remoteHost = val
 +    elif opt in ('-p' , '--port'):
 +        remotePort = int(val)
 +    elif opt in ('-t' , '--helo'):
 +        heloHost = val
 +    elif opt in ('-c' , '--recip'):
 +        mRecip = val
 +    elif opt in ('-f' , '--from'):
 +        mFrom = val
 +# If we don't have a remoteHost or recipient, usage and exit
 +if not remoteHost or not mRecip:
 +    usage(1)
 +
 +# Now, establish the connection and try the relay
 +t = TerminalController()
 +s = smtplib.SMTP()
 +print 'Connecting to %s:%d' % (remoteHost , remotePort)
 +resp = s.connect(remoteHost , remotePort)
 +print t.render('%s%d %s${NORMAL}' % (gAndR(resp[0]) , resp[0] , resp[1]))
 +print 'HELOing as %s' % heloHost
 +resp = s.helo(heloHost)
 +print t.render('%s%d %s${NORMAL}' % (gAndR(resp[0]) , resp[0] , resp[1]))
 +if useTls:
 +    print 'Starting a TLS connection'
 +    resp = s.starttls()
 +    print t.render('%s%d %s${NORMAL}' % (gAndR(resp[0]) , resp[0] , resp[1]))
 +mailFrom = 'MAIL FROM: <%s>\r\n' % mFrom
 +print 'Sending: %s' % mailFrom.strip()
 +s.send(mailFrom)
 +resp = s.getreply()
 +print t.render('%s%d %s${NORMAL}' % (gAndR(resp[0]) , resp[0] , resp[1]))
 +mailTo = 'RCPT TO: <%s>\r\n' % mRecip
 +print 'Sending: %s' % mailTo.strip()
 +s.send(mailTo)
 +resp = s.getreply()
 +print t.render('%s%d %s${NORMAL}' % (gAndR(resp[0]) , resp[0] , resp[1]))
 +print 'Sending: QUIT'
 +s.send('QUIT\r\n')
 +resp = s.getreply()
 +print t.render('%s%d %s${NORMAL}' % (gAndR(resp[0]) , resp[0] , resp[1]))
 +s.close()
 +</code>
 +
 +===== Usage =====
 +Using ''smtprelaytest.py -h'' on the command line produces the following:
 +<code>
 +$ ./smtprelaytest.py -h
 +smtprelaytest.py -r <remote host> [-p <port>] -c <recipient> [-f <send from>] [-t <helo name>]
 +
 +    -h,--help            Help, what you are looking at
 +    -s,--usetls          Use TLS for the connection
 +    -r,--remhost=        Remote hostname or IP
 +    -p,--port=           Remote host port
 +    -t,--helo=           HELO hostname.  An attempt will be made to determine
 +                         the hostname if not supplied
 +    -c,--recip=          The recipient email address to use
 +    -f,--from=           The email address that the request should come
 +                         from.  "test@qwest.net" will be used by default
 +</code>
 +
 +A typical lookup, with TLS support would look like this:
 +<code>
 +# ./smtprelaytest.py -s -r 127.0.0.1 -f admin@splitstreams.com -c admin@splitstreams.com
 +Connecting to 127.0.0.1:25
 +220 mail.splitstreams.com ESMTP Postfix
 +HELOing as localhost.splitstreams.com
 +250 mail.splitstreams.com
 +Starting a TLS connection
 +220 2.0.0 Ready to start TLS
 +Sending: MAIL FROM: <admin@splitstreams.com>
 +250 2.1.0 Ok
 +Sending: RCPT TO: <admin@splitstreams.com>
 +250 2.1.5 Ok
 +Sending: QUIT
 +221 2.0.0 Bye
 +</code>
programming/python/smtprelaytest.txt · Last modified: 2008/01/07 17:56 by crustymonkey