Skip to content
Permalink
main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
executable file 294 lines (242 sloc) 11.6 KB
#!/usr/bin/python3
# vi: sw=8 ts=8 expandtab
from __future__ import print_function
#-----------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------
import os, re, sys, time, getopt, getpass, smtplib
#-----------------------------------------------------------------------
# Constants
#-----------------------------------------------------------------------
PORT_STARTTLS = 587
#-----------------------------------------------------------------------
# Functions
#-----------------------------------------------------------------------
def Usage(msg=None):
print ("""
cmail [-h HELO_HOST ] [-f MAIL_FROM] -s <server> EKEY TFILE [DATAFILE [DATAFILE .. ]]
Fills mail template file TFILE with records read from clause file.
Each record is emailed to the value in the with the key of EKEY.
If DATAFILE is not given, then data is read from standard input.
If EKEY is a full email address (i.e. contianing @), then all mails
are sent to the address. Useful for testing.
Options:
-h HELO Name of host passed in SMTP HELO command.
Default is "%s".
-f FROM Return addressed passwd in SMTP MAIL FROM command.
Default is "%s".
-s SERVER Send email via mail server SERVER. If none given,
email is formatted and sent to standard out.
This is useful for testing.
-t Use StartTLS on port %s. Will prompt user for account
and password.
-T FILE Use StartTLS on port %s. Will read server host name,
email account name, and password, from first, second and
third line.
-L LOG Log file to record each mail sent
-c N1,N2 Only send emails for records N1 through N2.
-v Display progress
""" % (DEF_HELO, DEF_FROM, PORT_STARTTLS, PORT_STARTTLS))
if msg:
print()
print(" ",msg)
print()
raise SystemExit
def print_error(msg):
print()
print(" ERROR: ", msg)
print()
sys.exit()
def process_opts(opts):
return dict(opts)
# Read email user name and password from console
def get_user_pass():
user = input("Enter email account name: ")
passwd = getpass.getpass("Enter email account password: ")
return user, passwd
# Read email user name and password from plain text file
def read_server_user_pass_file(fname):
server_name, user, passwd = '', '', ''
with open(fname) as fin:
server_name = fin.readline().strip()
user = fin.readline().strip()
passwd = fin.readline().strip()
return server_name, user, passwd
def write_verbose(msg):
if VERBOSE:
print("VERBOSE: ", time.strftime("%Y-%m-%d %H:%M:%D"), msg)
# Read clauses, returns list containting lineno and corresponding line.
def read_clauses(fnames,lower_case=False):
lineno = 0
cur_lineno, cur_fname, clause = None, None, []
for fname in fnames:
clause = []
# Open fname
if fname=="-":
fin = sys.stdin
else:
fin = open(fname,"r")
for lineno, line in enumerate(fin.readlines()):
line = line.strip()
# End of clause
if not line:
if clause:
yield cur_fname, cur_lineno+1, clause
cur_lineno, cur_fname, clause = None, None, []
# Add line to clause
else:
if cur_fname ==None: cur_fname = fname
if cur_lineno==None: cur_lineno = lineno
if lower_case:
clause.append(line.lower())
else:
clause.append(line)
# Close file
if not fname=="-":
fin.close()
# return last clause in file
if clause:
yield cur_fname, cur_lineno+1, clause
cur_lineno, cur_fname, clause = None, None, []
# Clauses are defined to have
# A leading string, with no blanks, that is terminated by a space or colon
# The leading strings is the key, the remainder the value
# The colon and space are not part of the key or value
def clause2dict(clause):
# Get list of keys and values
fields = []
for line in clause:
for pos,c in enumerate(line):
if c in (' ',':'):
k = line[:pos].strip()
v = line[pos+1:].strip()
fields.append((k,v))
break
# For keys with multiple values, concatenate values
cdict = {}
for k,v in fields:
cdict[k] = cdict[k]+"/"+ v if k in cdict else v
return cdict
def fill_template(template,clausedict):
# Collect placeholders (these are strings, with no spaced, surrounded by {}
placeholders = [ p.strip("{}") for p in re.findall("{\S+}",template) ]
# Ensure that all placeholders exist in clause
keys_missing = set(placeholders).difference(clausedict.keys())
# Error, missing required keys. Return None, and let calling code handle the error
if keys_missing: return keys_missing, None
msg = template
for k in placeholders:
msg = msg.replace('{%s}' % k, clausedict[k])
return None, msg
def writelog(logfile, *vals):
with open(logfile,"a") as flog:
timestr = time.strftime("%Y-%m-%d %H:%M:%S")
print(timestr, *vals, file=flog)
#-----------------------------------------------------------------------
# Initialize Global Variables
#-----------------------------------------------------------------------
# Default envelope from address: MAIL FROM:
user = os.environ.get("USER","localuser")
host = os.environ.get("HOSTNAME","localhost")
DEF_FROM = user + "@" + host
DEF_HELO = host
DEF_LOG = "mail.log"
VERBOSE = False
DEBUG = False
#-----------------------------------------------------------------------
# Main
#-----------------------------------------------------------------------
def main():
global VERBOSE
# Get options
opts, args = getopt.getopt(sys.argv[1:], "df:h:s:vtL:T:c:");
opts = process_opts(opts)
logfile = opts.get('-L', DEF_LOG)
helo = opts.get('-h', DEF_HELO)
from_address = opts.get('-f', DEF_FROM)
server_name = opts.get('-s', None)
VERBOSE = '-v' in opts
DEBUG = '-d' in opts
# Set min and max record number to mail
if '-c' in opts:
parts = opts['-c'].split(',')
if len(parts)!=2:
Usage("ERROR: Invalid argument to -c (%s)" % opts['-c'])
cmin, cmax = int(parts[0])-1, int(parts[1])-1
# Send all items in clause file
else:
cmin = cmax = None
# Get servername and msg-file from command-line
if len(args)<2: Usage()
# Make sure that user entered server_name OR '-t' option
if server_name==None and not '-T' in opts:
Usage(" ERROR: You must use either the -s option or the -T option to supply a mail server name")
# Get file name for email address (ekey) and template file (tfile)
ekey, tfile = args[0:2]
# Get list of clause files (or use standard input)
fnames = args[2:] if len(args)>2 else ['-']
# Read mail template
fin = open(tfile,"r")
template = fin.read()
fin.close()
# Get authentication info
if '-T' in opts or '-t' in opts:
# Get username and password
if '-t' in opts:
username, password = get_user_pass()
else:
config_server_name, username, password = read_server_user_pass_file(opts['-T'])
# Let user override configured server name with name from the command line
if server_name==None: server_name = config_server_name
else:
username = None
# Read records and send emails
for cnt, (fname, lineno, clause) in enumerate(read_clauses(fnames,lower_case=False)):
clausedict = clause2dict(clause)
if '@' in ekey:
to_addresses = [ekey]
else:
if not ekey in clausedict:
print_error("Clause in file %s at line %s is missing following email address field key '%s': " % (fname,lineno,ekey))
else:
to_addresses = [clausedict[ekey]]
keys_missing, msg = fill_template(template, clausedict)
if keys_missing:
print_error("Clause in file %s at line %s is missing following fields required for template (%s): " % (fname,lineno,",".join(keys_missing)))
if cmin==None or cnt>=cmin and cnt<=cmax:
# Print debug info instead of emailing
if DEBUG:
print()
print("DEBUG: cnt = %d" % cnt)
print("DEBUG: server_name = %s" % server_name)
if username: print("DEBUG: username = %s" % username)
print("DEBUG: from_address = %s" % from_address)
print("DEBUG: to_addresses = %s" % " / ".join(to_addresses))
print("DEBUG: Text of email message follows:")
print(msg)
# Use SSL
elif '-t' in opts or '-T' in opts:
write_verbose("STARTTLS: open server connection")
sm = smtplib.SMTP(server_name, PORT_STARTTLS)
write_verbose("STARTTLS: starttls")
sm.starttls()
write_verbose("STARTTLS: login")
sm.login(username, password)
write_verbose("STARTTLS: ehlo")
sm.ehlo(helo)
write_verbose("STARTTLS: sendmail")
sm.sendmail(from_address, to_addresses, msg)
write_verbose("STARTTLS: finished")
# Use standard SMTP (port 25)
else:
write_verbose("SMTP: open server connection")
sm = smtplib.SMTP(server_name)
write_verbose("SMTP: helo")
sm.helo(helo)
write_verbose("SMTP: sendmail")
sm.sendmail(from_address, to_addresses, msg)
write_verbose("SMTP: finished")
# Log results
if not DEBUG:
writelog(logfile, server_name, helo, from_address, " ".join(to_addresses))
main()