Simple Python SMTP mail script – Part 2

Part 1 explained just why I wanted to send a SMTP email from the command line and explained some of the constraints I was under; this is how I got there.

I started out by not really being that familiar with Python, but I will always contend that knowing how to program is independent of the choice of language, so I wasn’t worried.

Fortunately (?), the very old version of Python on my shared hosting meant that I didn’t need to concern myself with the Python 2 vs Python 3 debate1, or which library to choose for some functions (no choice) so I could just get on with it.

Since I wanted to make this a general-purpose tool, I spent some extra time getting SSL on ports 465 and 587 working as well as non-encrypted on port 25. My laptop has the latest version of Python 2.7, so I could test these even if I couldn’t use them on TSOHost.

Here is the script:

#!/usr/bin/env python
#
# Simple smtp mailer program. Designed to work on old versions of Python (2.4).
# Has lots of command line parameters, but intended to run from shell scripts
# so doesn't matter. Tested on ports 25, 465 (encrypted) and 587 (encrypted).
# Only supports plain text auth via username & password.
# The encrypted modes require 2.6 or later as smtplib.SMTP_SSL is not present
# in earlier versions.
#
import smtplib
import datetime, sys, getopt, re
#import email.utils not on tsohost
#from email.mime.text import MIMEText not on tsohost
#SMTP_SSL not in tohost so port 465 and 587 won't work

port = ''
host = ''
mfrom = ''
mto = ''
msubj = ''
username = ''
password = ''
force_encrypt = False
quiet = False

def usage(progName = ''):
    print("Usage: %s -p port -o host -f mailFrom -t mailTo -s subject [opts] 'messageBody'" \
          % progName)
    print("Other options:")
    print("-u username -w password : Where login is required")
    print("-e : Force encryption - prevent plaintext username/password if not encypted")
    print("-q : Quiet")
    print("-h : Help")
    print("messageBody will be taken from stdin if parameter not present")
    
#get command line options    
try:
    myopts, args = getopt.getopt(sys.argv[1:],"p:o:f:t:s:u:w:eqh")
except getopt.error,e:
    print(str(e))
    usage(sys.argv[0])
    sys.exit(2)
 
for o, a in myopts:
    if o == '-p':
        port = a
    elif o == '-o':
        host = a
    elif o == '-f':
        mfrom = a
    elif o == '-t':
        mto = a
    elif o == '-s':
        msubj = a
    elif o == '-u':
        username = a
    elif o == '-w':
        password = a
    elif o == '-e':
        force_encrypt = True
    elif o == '-q':
        quiet = True
    elif o == '-h':
        usage()
        sys.exit()
#check for minimal required args
if(port == '' or host == '' or mfrom == '' or mto == '' or msubj == ''):
    print("Missing args:")
    usage(sys.argv[0])
    sys.exit(2)
        
#read from stdin for message body if no further args
if(len(args) == 0):
    mbody = sys.stdin.read()
else:
    mbody = args[0]

#generate correct mail date format    
date = datetime.datetime.now().strftime("%a, %d %b %Y %H:%M:%S%z")
#generate message header and body
msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\n\r\n%s" \
       % (mfrom, mto, msubj, date, mbody)

#set to 1 for lots of debug messages
debuglevel = 0
#indicate encrypted
encrypted = False

try:
    if(port == '465'):
        smtp = smtplib.SMTP_SSL(host, port)
        encrypted = True
    else:
        smtp = smtplib.SMTP(host, port)
    smtp.set_debuglevel(debuglevel)
    smtp.ehlo()    
    #print(smtp.ehlo_resp)
    if(re.search(r'^STARTTLS\b', smtp.ehlo_resp, re.M | re.I)):
        #print("STARTTLS accepted")
        smtp.starttls()
        smtp.ehlo()
        encrypted = True
        #print(smtp.ehlo_resp)
    #see if login required
    #(order of login & plain not specified and there can be other options)
    if(re.search(r'^AUTH\b.*(?:LOGIN\b.*PLAIN\b|PLAIN\b.*LOGIN\b)', \
                 smtp.ehlo_resp, re.M | re.I)):
        #print("Plain login accepted")
        if(force_encrypt and not encrypted):
            print("Error: Plain text login over non-encrypted connection not allowed.")
            smtp.quit()
            sys.exit(2)
        else:
            smtp.login(username, password)
    #send the mail!
    smtp.sendmail(mfrom, mto, msg)
    smtp.quit()
    if(not quiet):
        print("Mail sent successfully.")
except Exception, e:
    print("Error: unable to send mail.")
    print(str(e))
    sys.exit(2)
#end

That’s all there is to it. The response from smtp.ehlo() is tested to see if STARTTLS is supported; if it is then encryption is kicked in before doing a plain text login provided AUTH PLAIN LOGIN is supported. If the port is 465 then the sequence is started in SSL mode instead by using smtplib.SMTP_SSL.

Usage looks like:

./send-smtp-mail.py -p 25 -o smtp.mail.host \
-f "A User <a.user@me.co.uk>" \
-t "me@me.co.uk" \
-s "Py Test Mail" \
"Test Message Here..."

Pretty simple really and I was pleased with how well the result worked. Python didn’t take much brainpower to get the basics going, at least at the noddy level needed to make something like this work.

It helped to be familiar with the workings of mailservers though. This is the legacy of getting my own home mailserver working using Dovecot & Postfix and having to get relaying through a port 465 tunnel to Virgin Media’s servers working when Postfix doesn’t support port 4652.

Notes:

  1. Ever heard of backwards-compatibilty? Just sayin’…
  2. Mailservers on non-fixed IP addresses are routinely blacklisted, so relaying outgoing mail via an ISP or other mailhost is pretty much a requirement. Running your own mailserver is not very sensible these days, but I’ve been doing it since 2004 so it’s a habit.