Python try / except issue with SMTPLib

Steve Hall picture Steve Hall · Nov 27, 2014 · Viewed 10.2k times · Source

I've written a simple SMTP client using Python's SMTPLib. Just trying to add some error handling - specifically in this instance, when the target server to connect to is unavailable (eg, wrong IP specified!)

Currently, the traceback looks like this:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "smtplib_client.py", line 91, in runClient
    try:
  File "/usr/local/lib/python2.7/smtplib.py", line 251, in __init__
    (code, msg) = self.connect(host, port)
  File "/usr/local/lib/python2.7/smtplib.py", line 311, in connect
    self.sock = self._get_socket(host, port, self.timeout)
  File "/usr/local/lib/python2.7/smtplib.py", line 286, in _get_socket
    return socket.create_connection((host, port), timeout)
  File "/usr/local/lib/python2.7/socket.py", line 571, in create_connection
    raise err
socket.error: [Errno 111] Connection refused

So clearly, it's "create_connection" in socket.py going bang. This has it's own try / except block:

for res in getaddrinfo(host, port, 0, SOCK_STREAM):
    af, socktype, proto, canonname, sa = res
    sock = None
    try:
      sock = socket(af, socktype, proto)
      if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
          sock.settimeout(timeout)
      if source_address:
          sock.bind(source_address)
      sock.connect(sa)
      return sock

    except error as _:
      err = _
      if sock is not None:
          sock.close()

if err is not None:
  raise err
else:
  raise error("getaddrinfo returns an empty list")

My runClient() function looks like:

def runClient(configName = 'default', testFile = './test.xml'):
  cliCfg = getClientConfig(configName)
  print cliCfg.find('logfile').text
  # startLogger(cliCfg.find('logfile').text)
  clientCert = cliCfg.find('cert').text
  clientKey = cliCfg.find('key').text
  serverHost = cliCfg.find('serverhost').text
  serverPort = int(cliCfg.find('serverport').text)

  myMail = MailMessageHandler()
  msgSrc = myMail.readMessageSource(testFile)
  allMsgs = myMail.processMessages(msgSrc)
  inx = 1
  for msg in allMsgs:
    validMsg = True
    requiredKeys = ('ehlo', 'sndr', 'rcpt', 'body')
    for msgItems in requiredKeys:
      if len(msg[msgItems]) == 0:
        validMsg = False
    if validMsg:
      try:
        server = smtplib.SMTP(serverHost, serverPort)
        server.ehlo(msg['ehlo'])
        thisSender = msg['sndr']
        thisRecipient = msg['rcpt']
        thisMessage = MIMEText(msg['body'])
        thisMessage['To'] = email.utils.formataddr(('', thisRecipient))
        thisMessage['From'] = email.utils.formataddr(('', thisSender))
        thisMessage['Subject'] = msg['subject']
        thisMessage['Message-id'] = email.utils.make_msgid()
        now = datetime.now()
        day = now.strftime('%a')
        date = now.strftime('%d %b %Y %X')
        thisMessage['Date'] = day + ', ' + date + ' -0000'
        if msg['tls'].lower() == 'true':
          server.starttls('certs/client/client.key', 'certs/client/client.crt')
          logging.info ("Message: " + thisMessage['Message-id'] + " to be sent over TLS")
        server.sendmail(thisSender, thisRecipient.split(","), thisMessage.as_string())
        logging.info ("Message: " + thisMessage['Message-id'] + " sent successfully to " + serverHost + ":" + cliCfg.find('serverport').text)
        logging.info ("Message: " + thisMessage['Message-id'] + " had sender: " + thisMessage['From'])
        logging.info ("Message: " + thisMessage['Message-id'] + " had recipient(s): " + thisMessage['To'])
      except socket.error as e:
        print "Could not connect to server - is it down? ({0}): {1}".format(e.strerrror)
      except:
        print "Unknown error:", sys.exc_info()[0]
      finally:
          server.quit()
    else:
      print "Improperly formatted source mail - please check"

What I don't get is - the traceback shows the call to raise err. So clearly err is not None, and so it must be set as part of except error as _:.... So the error is initially handled, but as part of the handler, a copy is created (err) - which is subsequently raised outside of the try/except block - so is unhandled. This unhandled error should then get "passed back up" the call stack (get_socket has no try/except block, nor do connect or __init__ - so outside of the try/except block for the original error in create_connection, the copy of the error, err should surely then "cascade" back to the try/except block in my runClient function?

Answer

Steve Hall picture Steve Hall · Nov 27, 2014

Following on from the support from @AndreySobolev I got the following simple code working fine:

import smtplib, socket

try:
   mylib = smtplib.SMTP("127.0.0.1", 25)
except socket.error as e:
   print "could not connect"

So I then returned to my smtplib_client.py, and block commented out most of the "try" section. This worked fine... so bit by bit, I reinstated more and more of the try section.... and each and every time it worked fine. The final version is below. Other than what I do in my except socket.error handler, I can't say that I am aware of anything I have changed - other than I also added a server = None so as to stop the finally section working. Oh, and I also had to add "socket" to my list of imports. Without this I could understand the except not handling correctly - but I don't understand why it wasn't firing at all, or even generating a "not defined" error.... Odd!

Working code:

def runClient(configName = 'default', testFile = './test.xml'):
  cliCfg = getClientConfig(configName)
  print cliCfg.find('logfile').text
  # startLogger(cliCfg.find('logfile').text)
  clientCert = cliCfg.find('cert').text
  clientKey = cliCfg.find('key').text
  serverHost = cliCfg.find('serverhost').text
  serverPort = int(cliCfg.find('serverport').text)

  myMail = MailMessageHandler()
  msgSrc = myMail.readMessageSource(testFile)
  allMsgs = myMail.processMessages(msgSrc)
  inx = 1
  for msg in allMsgs:
    validMsg = True
    requiredKeys = ('ehlo', 'sndr', 'rcpt', 'body')
    for msgItems in requiredKeys:
      if len(msg[msgItems]) == 0:
        validMsg = False
    if validMsg:
      try:
        server = None
        server = smtplib.SMTP(serverHost, serverPort)
        server.ehlo(msg['ehlo'])
        thisSender = msg['sndr']
        thisRecipient = msg['rcpt']
        thisMessage = MIMEText(msg['body'])
        thisMessage['To'] = email.utils.formataddr(('', thisRecipient))
        thisMessage['From'] = email.utils.formataddr(('', thisSender))
        thisMessage['Subject'] = msg['subject']
        thisMessage['Message-id'] = email.utils.make_msgid()
        now = datetime.now()
        day = now.strftime('%a')
        date = now.strftime('%d %b %Y %X')
        thisMessage['Date'] = day + ', ' + date + ' -0000'
        if msg['tls'].lower() == 'true':
          server.starttls('certs/client/client.key', 'certs/client/client.crt')
          logging.info ("Message: " + thisMessage['Message-id'] + " to be sent over TLS")
        server.sendmail(thisSender, thisRecipient.split(","), thisMessage.as_string())
        logging.info ("Message: " + thisMessage['Message-id'] + " sent successfully to " + serverHost + ":" + cliCfg.find('serverport').text)
        logging.info ("Message: " + thisMessage['Message-id'] + " had sender: " + thisMessage['From'])
        logging.info ("Message: " + thisMessage['Message-id'] + " had recipient(s): " + thisMessage['To'])
      except socket.error as e:
        logging.error ("Could not connect to " + serverHost + ":" + cliCfg.find('serverport').text + " - is it listening / up?")
      except:
        print "Unknown error:", sys.exc_info()[0]
      finally:
        if server != None:
          server.quit()
    else:
      print "Improperly formatted source mail - please check"

Baffled, yet relieved! Thanks Andrey!