Welcome, guest | Sign In | My Account | Store | Cart

A Command Line Interface (CLI) program to send email.

Python, 227 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
#! /usr/bin/env python2.7
# vim: et sw=4 ts=4:
"""
DESCRIPTION:
    A Command Line Interface (CLI) program to send email.
    If the value to an argument is a file path and the file exists, the file
    will be read line by line and the values in the file will be used.
    When supplying multiple email addresses as an argument, a comma should be
    used to separate them.
    Arguments with spaces should be encolsed in double quotes (").
AUTHOR:
    sfw geek
NOTES:
    <PROG_NAME> = ProgramName
    <FILE_NAME> = <PROG_NAME>.py = ProgramName.py

    Static Analysis:
        pychecker.bat <FILE_NAME>
        pylint <FILE_NAME>
    Profile code:
        python -m cProfile -o <PROG_NAME>.prof <FILE_NAME>
    Vim:
        Remove redundant trailing white space: '\s\+$'.
    Python Style Guide:
        http://google-styleguide.googlecode.com/svn/trunk/pyguide.html
    Docstring Conventions:
        http://www.python.org/dev/peps/pep-0257
"""


# TODO:
#   Implement BCC functionality (FUNC_BCC).


# FUTURE STATEMENTS (compiler directives).
# Enable Python 3 print() functionality.
from __future__ import print_function


# VERSION.
# http://en.wikipedia.org/wiki/Software_release_life_cycle
# Phase Year.Month.Day.Build (YYYY.MM.DD.BB).
__version__ = '2012.08.20.01'
__release_stage__ = 'General Availability (GA)'


# MODULES.
# http://google-styleguide.googlecode.com/svn/trunk/pyguide.html#Imports_formatting
# Standard library imports.
import argparse
import datetime
import email.mime.multipart
import email.mime.text
import email.utils
import os
import smtplib
import sys


# CONSTANTS.
PROGRAM_NAME = sys.argv[0]

# Linux/Unix programs generally use 2 for command line syntax errors and 1 for all other kind of errors.
SYS_EXIT_CODE_SUCCESSFUL = 0
SYS_EXIT_CODE_GENERAL_ERROR = 1
SYS_EXIT_CODE_CMD_LINE_ERROR = 2

COMMA_SPACE = email.utils.COMMASPACE


# DEFINITIONS.
def usage():
    """Return string detailing how this program is used."""

    return '''
    A Command Line Interface (CLI) program to send email.
    If the value to an argument is a file path and the file exists, the file
    will be read line by line and the values in the file will be used.
    When supplying multiple email addresses as an argument, a comma should be
    used to separate them.
    Arguments with spaces should be encolsed in double quotes (").'''

def getProgramArgumentParser():
    """Return argparse object containing program arguments."""

    argParser = argparse.ArgumentParser(description=usage())

    # Mandatory parameters (though not set as required=True or can not use -V on own).
    mandatoryGrp = argParser.add_argument_group('mandatory arguments', 'These arguments must be supplied.')
    mandatoryGrp.add_argument('-b', '--body', action='store', dest='body', type=str,
        help='Body of email.  All lines from file used if file path provided.')
    mandatoryGrp.add_argument('-f', '--from', action='store', dest='frm', type=str,
        help='Who the email is from.  Only first line of file used if file path provided.')
    mandatoryGrp.add_argument('-m', '--machine', action='store', dest='smtphost', type=str,
        help='The name of SMTP host used to send the email.  Only first line of file used if file path provided.')
    mandatoryGrp.add_argument('-s', '--subject', action='store', dest='subject', type=str,
        help='The subject of the email.  Only first line of file used if file path provided.')
    mandatoryGrp.add_argument('-t', '--to', action='store', dest='to', type=str,
        help='Who the email is to be sent to.  One email address per line if file path provided.')

    # Optional parameters.
    optionalGrp = argParser.add_argument_group('extra optional arguments', 'These arguments are not mandatory.')
    optionalGrp.add_argument('-c', '--cc', action='store', dest='cc', type=str,
        help='Who the email is to be Carbon Copied (CC) to.  One email address per line if file path provided.')
    # TODO: FUNC_BCC
    #optionalGrp.add_argument('-B', '--bcc', action='store', dest='bcc', type=str,
    #    help='Who the email is to be Blind Carbon Copied (BCC) to.  One email address per line if file path provided.')
    optionalGrp.add_argument('-d', '--debug', action='store_true', dest='debug',
        help='Increase verbosity to help debugging.')
    optionalGrp.add_argument('-D', '--duration', action='store_true', dest='duration',
        help='Print to standard output the programs execution duration.')
    optionalGrp.add_argument('-V', '--version', action='store_true', dest='version',
        help='Print the version number to the standard output.  This version number should be included in all bug reports.')

    return argParser

def printVersionDetailsAndExit():
    """Print to standard output programs version details and terminate program."""

    msg = '''
NAME:
    {0}
VERSION:
    {1}
    {2}'''.format(PROGRAM_NAME, __version__, __release_stage__)
    print(msg)
    sys.exit(SYS_EXIT_CODE_SUCCESSFUL)

def getDaySuffix(day):
    """Return st, nd, rd, or th for supplied day."""

    if 4 <= day <= 20 or 24 <= day <= 30:
        return 'th'
    return ['st', 'nd', 'rd'][day % 10 - 1]

def printProgramStatus(started, stream=sys.stdout):
    """Print program duration information."""

    NEW_LINE = '\n'
    DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S.%f (%a %d{0} %b %Y)'
    finished = datetime.datetime.now()
    delta = finished - started
    dateTimeStr = started.strftime(DATE_TIME_FORMAT.format(getDaySuffix(started.day)))
    msg = '{1}Started:  {0}{1}'.format(dateTimeStr, NEW_LINE)
    dateTimeStr = finished.strftime(DATE_TIME_FORMAT.format(getDaySuffix(finished.day)))
    msg += 'Finished: {0}{1}'.format(dateTimeStr, NEW_LINE)
    msg += 'Duration: {0} (days hh:mm:ss:ms)'.format(delta)
    print(msg, file=stream)

def getFileContentsOrParameterValue(filePathOrValue):
    """Return list of file contents if parameter is file path, otherwise
    parameter as first entry."""

    data = []
    if os.path.isfile(filePathOrValue):
        with open(filePathOrValue) as foSrc:
            for srcLine in foSrc:
                data.append(srcLine.strip())
    else:
        data.append(filePathOrValue)
    return data

def main():
    """Program entry point."""

    # Store when program started.
    started = datetime.datetime.now()

    # Get parameters supplied to application.
    argParser = getProgramArgumentParser()
    args = argParser.parse_args()

    # Logic for displaying version details or program help.
    if args.version:
        printVersionDetailsAndExit()
    if not (args.smtphost and args.to and args.frm and args.subject and args.body):
        if args.version:
            printVersionDetailsAndExit()
        argParser.print_help()
        sys.exit(SYS_EXIT_CODE_CMD_LINE_ERROR)

    # Process program arguments to get email parts.
    # From can only have one value regardless if stored in file or not (first line in file used).
    fromVal = getFileContentsOrParameterValue(args.frm)[0]
    toData = getFileContentsOrParameterValue(args.to)
    subjectVal = getFileContentsOrParameterValue(args.subject)[0]
    bodyData = getFileContentsOrParameterValue(args.body)
    smtpHostVal = getFileContentsOrParameterValue(args.smtphost)[0]

    # Build multipart MIME message (email).
    multipartMimeMsg = email.mime.multipart.MIMEMultipart()
    multipartMimeMsg['Date'] = email.utils.formatdate(localtime=True)
    multipartMimeMsg['From'] = fromVal
    multipartMimeMsg['To'] = COMMA_SPACE.join(toData)
    multipartMimeMsg['Subject'] = subjectVal
    multipartMimeMsg.attach(email.mime.text.MIMEText(email.utils.CRLF.join(bodyData)))

    # Process optional arguments.
    if args.cc:
        ccData = getFileContentsOrParameterValue(args.cc)
        multipartMimeMsg['Cc'] = COMMA_SPACE.join(ccData)
        toData.extend(ccData) # TODO: check?
    # TODO: FUNC_BCC
    #if args.bcc:
    #    bccData = getFileContentsOrParameterValue(args.bcc)
    #    multipartMimeMsg['Bcc'] = COMMA_SPACE.join(bccData)
    #    toData.extend(bccData) # TODO: similar to CC but is blindness enforced?

    # Python 3.3 supports with statement (context manager) for smtplib.SMTP().
    # http://docs.python.org/dev/library/smtplib.html
    # with smtplib.SMTP(smtpHostVal) as smtpSvr:
    try:
        smtpSvr = smtplib.SMTP(smtpHostVal)
        if args.debug:
            # Increase display verbosity.
            smtpSvr.set_debuglevel(1)
        smtpSvr.sendmail(fromVal, toData, multipartMimeMsg.as_string())
    finally:
        smtpSvr.quit()

    if args.duration:
        printProgramStatus(started)


# Program entry point.
if __name__ == '__main__':
    main()

Provide example of how to send email using argparse and smtplib modules. Handy for sending an email from a batch/script file once a process has completed. The ability to use files as arguments to the program is useful to easily configure email message components (to, cc, from, subject, body).

Addressed bug with CC using fix suggested by Denis Barmenkov (thanks).

1 comment

Denis Barmenkov 10 years, 3 months ago  # | flag

You miss adding lCc to lTo before sendmail() call:

lTo.extend(lCc)
smtpSvr.sendmail(lFrom[0], lTo, msg.as_string())

without this all of lTo members will see lCc members in their mail clients but lCc will not receive copy of the message.

It can be easy verified with your script --debug option.