diff mailer.py @ 2:e07789816ca5

adding more python files from lib/python on origen
author Henry Thompson <ht@markup.co.uk>
date Mon, 09 Mar 2020 16:48:09 +0000
parents
children 2d7c91f89f6b
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mailer.py	Mon Mar 09 16:48:09 2020 +0000
@@ -0,0 +1,415 @@
+#!/usr/bin/python
+'''Attempt at flexible mailout functionality
+Usage: mailer.py [-n] [-s] [-C cc string] [-c COLSPEC[,COLSPEC]*] [-B bcc string] [-b COLSPEC[,COLSPEC]*] [-S col[,col]*] [-a COLSPEC[,COLSPEC]*] [-p COLPAT]* -SA file[,file]* COLSPEC[,COLSPEC]* subject {addr-file|-} body-file
+
+Sends the body as a message from me with subject to destinations per
+lines in the addr-file selected by COLSPECs (to:) or -c/-b COLSPECs (Cc:/Bcc:)
+
+-n for dry run, prints to stdout
+-c for Cc column(s)
+-C for static Cc
+-b for Bcc columns(s)
+-B for static Bcc
+-a for attachment file column(s)
+-A for attachment file pattern column(s)
+-SA for static attachment files
+-u Use unicode for attachments
+-s for substitute into body
+-S for columns to substitute as such
+-p for augmentation pattern for a column
+
+COLSPEC is of the form a[:n[:f[:g]]] selects from addr-file, which must be tsv
+  a gives the column for an email address
+  n (optional) gives column for a name
+  f gives format for the name: FS, SF or S.F for
+      forenames surname (fornames space separated)
+      surname forenames (space separated)
+      surname, forenames (space separated)
+    default is FS
+    _ will be replaced by space in surnames
+  g gives column for gender (for pronouns), m or f
+COLPAT takes the form i:template, where i selects an address column
+and template is a string containing exactly 1 "%s", which is replaced with
+the column value to give the string which will be used for COLSPEC
+references to that column, e.g. 1:S%s@sms.ed.ac.uk
+if column 1 contains bare student numbers
+
+-s enables body substitution.  body may contain
+  %(fi)s  first forename of column i
+  %(si)s  surname
+  %(fsi)s all forenames
+  %(i)s   the undivided original and/or -S col value
+  if there is a supplied gender
+    %(pni)s 'he'/'she'
+    %(pai)s 'him'/'her'
+    %(pgi)s 'his/her'
+
+All column indices are 1-origin, as for cut'''
+
+import smtplib, sys, re, os.path, codecs
+from email.mime.text import MIMEText
+
+addrPat=re.compile("<([^>]*)>")
+
+def usage(hint=None):
+  if hint is None:
+    print __doc__
+    exit()
+  else:
+    print >>sys.stderr,"Trouble with your commandline at %s\n  %s"%(hint,
+                                                                    __doc__)
+    exit(1)
+
+def parseCols(specs,where):
+  return [Column(s,where) for s in specs.split(',')]
+
+def parsePat(spec):
+  (c,t)=spec.split(':')
+  c=int(c)
+  found=False
+  for colTab in (ccCols,bccCols,toCols,attCols):
+    if c in colTab:
+      colTab[c].addTemplate(t)
+      found=True
+  if not found:
+    print >>sys.stderr, "Warning, template supplied for column %s, but no use of the column found!"%c
+
+def addrList(addrFields,cols,att=False):
+  global someExpand
+  if att and someExpand:
+    # There were some file patterns
+    return itertools.chain(*(c.fullAddr(addrFields,True) for c in cols.values()))
+  else:
+    return [c.fullAddr(addrFields) for c in cols.values()]
+
+def addrLine(hdr,addrFields,cols):
+  return "%s: %s"%(hdr,", ".join(addrList(addrFields,cols)))
+
+def subDict(addrFields):
+  res={}
+  for c in names.values():
+    c.subDo(addrFields,res)
+  for c in subs.values():
+    if c not in names:
+      c.subDo(addrFields,res)
+  return res
+
+bccCols={}
+ccCols={}
+attCols={}
+toCols={}
+names={}
+subs={}
+CC=[]
+BCC=[]
+rawCols={}
+
+class Column:
+  _expand=False
+  def __init__(self,spec,where):
+    global names, subs
+    parts=spec.split(':')
+    if (len(parts)<1 or len(parts)>4):
+      print >>sys.stderr, "col spec. must have 1--4 :-separated parts: %s"%parts
+      usage('colspec')
+    self.a=int(parts[0])
+    if len(parts)>1:
+      self.n=int(parts[1])
+      if len(parts)>2:
+        self.f=parts[2]
+      else:
+        self.f='FS'
+      if len(parts)>3:
+        self.g=int(parts[3])
+      else:
+        self.g=None
+    else:
+      self.n=None
+    if self.a<=0:
+      print >>sys.stderr, "addr column index %s not allowed -- 1-origin indexing"%self.a
+      exit(2)
+    if self.a in where:
+      print >>sys.stderr, "duplicate column %s"%self.a
+      exit(2)
+    if self.n is not None:
+      if self.n<=0:
+        print >>sys.stderr, "name column index %s not allowed -- 1-origin indexing"%self.n
+        exit(3)
+      if self.n in where:
+        print >>sys.stderr, "can't use column %s as both name and address"%self.n
+        exit(3)
+      if self.n in names:
+        print >>sys.stderr, "attempt to redefine %s from \"%s\" to \"%s\""%(self.n,names[self.n],self)
+        exit(3)
+      if self.f not in ('FS','SF','S.F'):
+        print >>sys.stderr, "name format %s not recognised"%self.f
+        exit(4)
+    where[self.a]=self
+    if self.n is not None:
+      if isinstance(self,RawColumn):
+        subs[self.n]=self
+      else:
+        names[self.n]=self
+
+  def __str__(self):
+    if self.n is None:
+      return str(self.a)
+    else:
+      return "%s:%s"%(self.a,self.n)
+
+  def __repr__(self):
+    return str(self)
+
+  def addTemplate(self,template):
+    try:
+      print >>sys.stderr,"Attempt to overwrite existing template \"%s\" for %s with \"%s\""%(self.template,
+                                                                                             self.a,
+                                                                                             template)
+    except AttributeError:
+      self.template=template
+    
+  def name(self):
+    return self.n
+
+  def expAddr(self,fields):
+    addr=fields[self.a-1]
+    try:
+      return self.template%addr
+    except AttributeError:
+      return addr
+
+  def fullAddr(self,fields,att=False):
+    global someExpand
+    if self.n is None:
+      res=self.expAddr(fields)
+      if att and someExpand:
+        if self._expand:
+          return glob.iglob(res)
+        else:
+          return [res]
+      else:
+        return res
+    else:
+      return '"%s" <%s>'%(fields[self.n-1].replace('_',' '),self.expAddr(fields))    
+
+  def subDo(self,addrFields,dict):
+    f=addrFields[self.n-1]
+    dict[str(self.n)]=f
+    nparts=f.split(' ')
+    if self.f=='FS':
+      sur=nparts.pop()
+    elif self.f=='SF':
+      sur=nparts.pop(0)
+    elif self.f=='S.F':
+      sur=nparts.pop(0)[:-1]
+    fores=nparts
+    dict['fs%s'%self.n]=' '.join(fores)
+    dict['f%s'%self.n]=fores[0]
+    dict['s%s'%self.n]=sur.replace('_',' ')
+    if self.g is not None:
+      gg=addrFields[self.g-1]
+      if gg=='m':
+        dict['pn%s'%self.n]='he'
+        dict['pa%s'%self.n]='him'
+        dict['pg%s'%self.n]='his'
+      elif gg=='f':
+        dict['pn%s'%self.n]='she'
+        dict['pa%s'%self.n]='her'
+        dict['pg%s'%self.n]='her'
+      else:
+        print >>sys.stderr,"Warning, unrecognised gender in column %s: %s"%(self.n,gg)
+
+  def setExpand(self):
+    self._expand=True
+
+class RawColumn(Column):
+  '''Not for person names, just raw text'''
+  
+  def subDo(self,addrFields,dict):
+    f=addrFields[self.n-1]
+    dict[str(self.n)]=f
+
+def doAtt(msg,att,codec):
+  (mt,enc)=mimetypes.guess_type(att)
+  (tp,subtp)=mt.split('/',2)
+  if tp=='text':
+    attf=codecs.open(att,'r',codec)
+    atm=MIMEText(attf.read(),subtp,codec)
+  elif tp=='application':
+    from email.mime.application import MIMEApplication
+    attf=open(att,'r')
+    atm=MIMEApplication(attf.read(),subtp)
+  else:
+    print >>sys.stderr, "Help: Media type %s (for attachment %s) not supported"%(mt,att)
+    exit(5)
+  atm.add_header('Content-Disposition','attachment',
+                 filename=os.path.basename(att))
+  msg.attach(atm)
+
+dryrun=False
+sys.argv.pop(0)
+doSub=False
+pats=[]
+someExpand=False
+codec='iso-8859-1'
+staticAtts=[]
+while sys.argv:
+  if sys.argv[0]=='-n':
+    dryrun=True
+    sys.argv.pop(0)
+  elif sys.argv[0]=='-c' and ccCols=={}:
+    sys.argv.pop(0)
+    if sys.argv:
+      parseCols(sys.argv.pop(0),ccCols)
+    else:
+      usage('cc')
+  elif sys.argv[0]=='-C' and CC==[]:
+    sys.argv.pop(0)
+    if sys.argv:
+      CC=sys.argv.pop(0).split(',')
+    else:
+      usage('CC')
+  elif sys.argv[0]=='-b' and bccCols=={}:
+    sys.argv.pop(0)
+    if sys.argv:
+      parseCols(sys.argv.pop(0),bccCols)
+    else:
+      usage('bcc')
+  elif sys.argv[0]=='-B' and BCC==[]:
+    sys.argv.pop(0)
+    if sys.argv:
+      BCC=sys.argv.pop(0).split(',')
+    else:
+      usage('BCC')
+  elif sys.argv[0] in ('-a','-A','-SA'): # and attCols=={}
+    expand=sys.argv[0]=='-A'
+    static=sys.argv[0]=='-SA'
+    sys.argv.pop(0)
+    if sys.argv:
+      if static:
+        staticAtts=sys.argv.pop(0).split(',')
+      else:
+        pc=parseCols(sys.argv.pop(0),attCols)
+        if expand:
+          import itertools, glob
+          someExpand=True
+          for c in pc:
+            c.setExpand()
+      from email.mime.multipart import MIMEMultipart
+      import mimetypes
+    else:
+      usage('attachment')
+  elif sys.argv[0]=='-u':
+    sys.argv.pop(0)
+    codec='utf-8'
+  elif sys.argv[0]=='-s':
+    sys.argv.pop(0)
+    doSub=True
+  elif sys.argv[0]=='-S' and rawCols=={}:
+    sys.argv.pop(0)
+    if sys.argv:
+      for c in sys.argv.pop(0).split(','):
+        RawColumn("%s:%s"%(c,c),rawCols)
+    else:
+      usage('raw subs')
+  elif sys.argv[0]=='-p':
+    sys.argv.pop(0)
+    if sys.argv:
+      pats.append(sys.argv.pop(0))
+    else:
+      usage('pat')
+  elif sys.argv[0][0]=='-':
+    print sys.argv
+    usage()
+  else:
+    break
+
+if sys.argv:
+  parseCols(sys.argv.pop(0),toCols)
+else:
+  usage('to')
+
+pats=[parsePat(p) for p in pats]
+
+if sys.argv:
+  subj=sys.argv.pop(0)
+else:
+  usage('subj')
+
+if sys.argv:
+  af=sys.argv.pop(0)
+  if af=='-':
+    addrFile=sys.stdin
+  else:
+    try:
+      addrFile=open(af,'r')
+    except:
+      usage('addr: %s'%sys.exc_value)
+else:
+  usage('addr')
+
+if sys.argv:
+  bf=sys.argv.pop(0)
+  try:
+    bodyFile=open(bf,'r')
+  except:
+    usage('body: %s'%sys.exc_value)
+else:
+  usage('body')
+
+try:
+  sig=open("/home/ht/.signature","r")
+  signature=sig.read().rstrip()
+except:
+  signature=None
+
+CS=', '
+body=bodyFile.read().rstrip()
+if not dryrun:
+  mailer=smtplib.SMTP()
+  mailer.connect()
+for l in addrFile:
+  addrFields=l.rstrip().split('\t')
+  if doSub:
+    bodyPlus=body%subDict(addrFields)
+  else:
+    bodyPlus=body
+  if signature is not None:
+    bodyPlus+="\n--\n"
+    bodyPlus+=signature
+  if attCols or staticAtts:
+    msg=MIMEMultipart()
+    msg.attach(MIMEText(bodyPlus))
+  else:
+    msg=MIMEText(bodyPlus)
+  #to=addrLine("To",addrFields,toCols)
+  to=addrList(addrFields,toCols)
+  #msg=to
+  #recips=addrPat.findall(to)
+  msg['To']=CS.join(to)
+  recips=[]+list(to)
+  cc=CC
+  if ccCols:
+    cc+=addrList(addrFields,ccCols)
+  if cc!=[]:
+    msg["Cc"]=CS.join(cc)
+    recips+=list(cc)
+  bcc=BCC
+  if bccCols:
+    bcc+=addrList(addrFields,bccCols)
+  if bcc!=[]:
+    msg["Bcc"]=CS.join(bcc)
+    recips+=list(bcc)
+  msg["Subject"]=subj
+  for att in staticAtts:
+    doAtt(msg,att,codec)
+  if attCols:
+    for att in addrList(addrFields,attCols,True):
+      doAtt(msg,att,codec)
+  if dryrun:
+    print recips
+    print msg.as_string()
+    exit()
+  print "mailing to %s"%recips
+  mailer.sendmail("ht@inf.ed.ac.uk",recips,msg.as_string())
+mailer.quit()