Permalink
Cannot retrieve contributors at this time
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?
ctools/cmail.py
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
executable file
294 lines (242 sloc)
11.6 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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() |