#! /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()
Diff to Previous Revision
--- revision 4 2010-07-19 18:38:14
+++ revision 5 2012-08-26 11:23:54
@@ -1,46 +1,57 @@
-#! /usr/bin/env python
+#! /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.
+ 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:
- Coding Standards:
+ <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
- Profile code:
- python -m cProfile -o <SCRIPT_NAME>.prof <SCRIPT_NAME>
- Check Code:
- pychecker.bat <SCRIPT_NAME>
-TODO:
- CheckPythonVersion(minumun. maximum, equals.)
-'''
-
-
-# PRAGMAS.
-# Must be first code statements.
-from __future__ import print_function # Enable Python 3 print() functionality.
+ 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__ = '2010.07.19.01 GA'
+__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.
-from datetime import datetime
-from email.generator import Generator
-from email.iterators import *
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-from email.utils import COMMASPACE, CRLF, formatdate
-from optparse import OptionParser
+import argparse
+import datetime
+import email.mime.multipart
+import email.mime.text
+import email.utils
import os
import smtplib
import sys
@@ -48,151 +59,169 @@
# CONSTANTS.
PROGRAM_NAME = sys.argv[0]
-ERROR_MESSAGE_PREFIX = '*ERROR*: '
+
+# 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 a string detailing how this program is used.'''
-
- msg = __doc__.replace('<SCRIPT_NAME>', PROGRAM_NAME)
- 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}
-SYNOPSIS:
- {0} [OPTIONS]{1}'''.format(PROGRAM_NAME, msg)
-
- return usage
-
-def getProgramArguments():
- '''Get program arguments.
-Returns a tuple of arguement details.
-The 1st value is a dict of options.
-The 2nd value is a list of positional arguments.'''
-
- getOptParser = OptionParser(usage())
- getOptParser.add_option('-b', '--body', action='store', type='string', dest='body',
- help='Body of email. All lines from file used if file path provided.')
- getOptParser.add_option('-c', '--cc', action='store', type='string', dest='cc',
- help='Who the email is also to be sent to (CC). One email address per line if file path provided.')
- getOptParser.add_option('-d', '--debug', action='store_true', dest='debug',
- help='Increase verbosity to help debugging.')
- getOptParser.add_option('-f', '--from', action='store', type='string', dest='frm',
- help='Who the email is from. Only first line of file used if file path provided.')
- getOptParser.add_option('-m', '--machine', action='store', type='string', dest='smtphost',
- help='The name of SMTP host used to send the email. Only first line of file used if file path provided.')
- getOptParser.add_option('-s', '--subject', action='store', type='string', dest='subject',
- help='The subject of the email. Only first line of file used if file path provided.')
- getOptParser.add_option('-t', '--to', action='store', type='string', dest='to',
- help='Who the email is to be sent to. One email address per line if file path provided.')
- getOptParser.add_option('-V', '--version', action='store_true', dest='version',
- help='Program version details.')
-
- (dOpts, lPosArgs) = getOptParser.parse_args()
-
- if dOpts.version:
- # Display version details and exit.
- msg = __doc__
- msg += 'RELEASE:\n {0}'.format(__version__)
- print(msg)
- sys.exit()
-
- if not (dOpts.smtphost and dOpts.to and dOpts.frm and dOpts.subject and dOpts.body and len(lPosArgs) == 0):
- getOptParser.print_help()
- # Linux/Unix command line programs (CLI) return a program status of 2 for command line syntax errors.
- sys.exit(2)
-
- return dOpts, lPosArgs
-
-def checkPythonVersion(tMinVersion):
- '''Throw an exception if python interperter is less than the supplied version tuple.
-e.g. (2, 6, 0, 'final', 0)'''
-
- iMinVerLen = len(tMinVersion)
- tPyVersion = sys.version_info # e.g. (2, 6, 0, 'final', 0)
-
- # Python interperter is less than supplied version error message.
- sErrMsg = '%sPython Version %s ' % (ERROR_MESSAGE_PREFIX, '.'.join(map(str, tMinVersion)))
- sErrMsg += "Or Higher Is Required To Run '%s'!" % (sys.argv[0])
-
- if iMinVerLen > 0 and tPyVersion[0] < tMinVersion[0]:
- raise RuntimeError, sErrMsg
- if iMinVerLen > 1 and tPyVersion[1] < tMinVersion[1]:
- raise RuntimeError, sErrMsg
- if iMinVerLen > 2 and tPyVersion[2] < tMinVersion[2]:
- raise RuntimeError, sErrMsg
- if iMinVerLen > 3 and tPyVersion[3] != tMinVersion[3]:
- raise RuntimeError, sErrMsg
- if iMinVerLen > 4 and tPyVersion[3] < tMinVersion[4]:
- raise RuntimeError, sErrMsg
-
-def checkInput(sInput):
- '''Check if input is a path to a file. If it is read the file and return the contents.
-The input is returned if it is not a file path that exists.'''
-
- lData = []
- if os.path.exists(sInput):
- foIn = open(sInput, 'r')
- for sLine in foIn:
- lData.append(sLine.strip())
- foIn.close()
+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:
- lData.append(sInput)
-
- return lData
-
-def printTimeDelta(dtStart, dtEnd, stream=sys.stdout):
- '''Print the duration time to the relevant stream.'''
-
- delta = dtEnd - dtStart
- print('\nDuration: %s' % (delta), file=stream)
+ data.append(filePathOrValue)
+ return data
def main():
- '''Program entry point.'''
+ """Program entry point."""
# Store when program started.
- started = datetime.now()
-
- tArgs = getProgramArguments()
- dOpts = tArgs[0]
-
- msg = MIMEMultipart()
-
- lFrom = checkInput(dOpts.frm)
- msg['From'] = lFrom[0]
-
- lTo = checkInput(dOpts.to)
- msg['To'] = COMMASPACE.join(lTo)
-
- if dOpts.cc:
- # Optional.
- lCc = checkInput(dOpts.cc)
- msg['Cc'] = COMMASPACE.join(lCc)
-
- #msg['Bcc'] = COMMASPACE.join(lBcc)
-
- msg['Date'] = formatdate(localtime=True)
-
- lSubject = checkInput(dOpts.subject)
- msg['Subject'] = lSubject[0]
-
- lBody = checkInput(dOpts.body)
- msg.attach(MIMEText(CRLF.join(lBody)))
-
- lSmtpHost = checkInput(dOpts.smtphost)
- smtpSvr = smtplib.SMTP(lSmtpHost[0])
- if dOpts.debug:
- # Increase display verbosity.
- smtpSvr.set_debuglevel(1)
- smtpSvr.sendmail(lFrom[0], lTo, msg.as_string())
- smtpSvr.quit()
-
- # Store when program finished then display program execution duration.
- stopped = datetime.now()
- printTimeDelta(started, stopped, sys.stderr)
+ 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__':
- checkPythonVersion((2, 6))
main()