comparison 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
comparison
equal deleted inserted replaced
1:0a3abe59e364 2:e07789816ca5
1 #!/usr/bin/python
2 '''Attempt at flexible mailout functionality
3 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
4
5 Sends the body as a message from me with subject to destinations per
6 lines in the addr-file selected by COLSPECs (to:) or -c/-b COLSPECs (Cc:/Bcc:)
7
8 -n for dry run, prints to stdout
9 -c for Cc column(s)
10 -C for static Cc
11 -b for Bcc columns(s)
12 -B for static Bcc
13 -a for attachment file column(s)
14 -A for attachment file pattern column(s)
15 -SA for static attachment files
16 -u Use unicode for attachments
17 -s for substitute into body
18 -S for columns to substitute as such
19 -p for augmentation pattern for a column
20
21 COLSPEC is of the form a[:n[:f[:g]]] selects from addr-file, which must be tsv
22 a gives the column for an email address
23 n (optional) gives column for a name
24 f gives format for the name: FS, SF or S.F for
25 forenames surname (fornames space separated)
26 surname forenames (space separated)
27 surname, forenames (space separated)
28 default is FS
29 _ will be replaced by space in surnames
30 g gives column for gender (for pronouns), m or f
31 COLPAT takes the form i:template, where i selects an address column
32 and template is a string containing exactly 1 "%s", which is replaced with
33 the column value to give the string which will be used for COLSPEC
34 references to that column, e.g. 1:S%s@sms.ed.ac.uk
35 if column 1 contains bare student numbers
36
37 -s enables body substitution. body may contain
38 %(fi)s first forename of column i
39 %(si)s surname
40 %(fsi)s all forenames
41 %(i)s the undivided original and/or -S col value
42 if there is a supplied gender
43 %(pni)s 'he'/'she'
44 %(pai)s 'him'/'her'
45 %(pgi)s 'his/her'
46
47 All column indices are 1-origin, as for cut'''
48
49 import smtplib, sys, re, os.path, codecs
50 from email.mime.text import MIMEText
51
52 addrPat=re.compile("<([^>]*)>")
53
54 def usage(hint=None):
55 if hint is None:
56 print __doc__
57 exit()
58 else:
59 print >>sys.stderr,"Trouble with your commandline at %s\n %s"%(hint,
60 __doc__)
61 exit(1)
62
63 def parseCols(specs,where):
64 return [Column(s,where) for s in specs.split(',')]
65
66 def parsePat(spec):
67 (c,t)=spec.split(':')
68 c=int(c)
69 found=False
70 for colTab in (ccCols,bccCols,toCols,attCols):
71 if c in colTab:
72 colTab[c].addTemplate(t)
73 found=True
74 if not found:
75 print >>sys.stderr, "Warning, template supplied for column %s, but no use of the column found!"%c
76
77 def addrList(addrFields,cols,att=False):
78 global someExpand
79 if att and someExpand:
80 # There were some file patterns
81 return itertools.chain(*(c.fullAddr(addrFields,True) for c in cols.values()))
82 else:
83 return [c.fullAddr(addrFields) for c in cols.values()]
84
85 def addrLine(hdr,addrFields,cols):
86 return "%s: %s"%(hdr,", ".join(addrList(addrFields,cols)))
87
88 def subDict(addrFields):
89 res={}
90 for c in names.values():
91 c.subDo(addrFields,res)
92 for c in subs.values():
93 if c not in names:
94 c.subDo(addrFields,res)
95 return res
96
97 bccCols={}
98 ccCols={}
99 attCols={}
100 toCols={}
101 names={}
102 subs={}
103 CC=[]
104 BCC=[]
105 rawCols={}
106
107 class Column:
108 _expand=False
109 def __init__(self,spec,where):
110 global names, subs
111 parts=spec.split(':')
112 if (len(parts)<1 or len(parts)>4):
113 print >>sys.stderr, "col spec. must have 1--4 :-separated parts: %s"%parts
114 usage('colspec')
115 self.a=int(parts[0])
116 if len(parts)>1:
117 self.n=int(parts[1])
118 if len(parts)>2:
119 self.f=parts[2]
120 else:
121 self.f='FS'
122 if len(parts)>3:
123 self.g=int(parts[3])
124 else:
125 self.g=None
126 else:
127 self.n=None
128 if self.a<=0:
129 print >>sys.stderr, "addr column index %s not allowed -- 1-origin indexing"%self.a
130 exit(2)
131 if self.a in where:
132 print >>sys.stderr, "duplicate column %s"%self.a
133 exit(2)
134 if self.n is not None:
135 if self.n<=0:
136 print >>sys.stderr, "name column index %s not allowed -- 1-origin indexing"%self.n
137 exit(3)
138 if self.n in where:
139 print >>sys.stderr, "can't use column %s as both name and address"%self.n
140 exit(3)
141 if self.n in names:
142 print >>sys.stderr, "attempt to redefine %s from \"%s\" to \"%s\""%(self.n,names[self.n],self)
143 exit(3)
144 if self.f not in ('FS','SF','S.F'):
145 print >>sys.stderr, "name format %s not recognised"%self.f
146 exit(4)
147 where[self.a]=self
148 if self.n is not None:
149 if isinstance(self,RawColumn):
150 subs[self.n]=self
151 else:
152 names[self.n]=self
153
154 def __str__(self):
155 if self.n is None:
156 return str(self.a)
157 else:
158 return "%s:%s"%(self.a,self.n)
159
160 def __repr__(self):
161 return str(self)
162
163 def addTemplate(self,template):
164 try:
165 print >>sys.stderr,"Attempt to overwrite existing template \"%s\" for %s with \"%s\""%(self.template,
166 self.a,
167 template)
168 except AttributeError:
169 self.template=template
170
171 def name(self):
172 return self.n
173
174 def expAddr(self,fields):
175 addr=fields[self.a-1]
176 try:
177 return self.template%addr
178 except AttributeError:
179 return addr
180
181 def fullAddr(self,fields,att=False):
182 global someExpand
183 if self.n is None:
184 res=self.expAddr(fields)
185 if att and someExpand:
186 if self._expand:
187 return glob.iglob(res)
188 else:
189 return [res]
190 else:
191 return res
192 else:
193 return '"%s" <%s>'%(fields[self.n-1].replace('_',' '),self.expAddr(fields))
194
195 def subDo(self,addrFields,dict):
196 f=addrFields[self.n-1]
197 dict[str(self.n)]=f
198 nparts=f.split(' ')
199 if self.f=='FS':
200 sur=nparts.pop()
201 elif self.f=='SF':
202 sur=nparts.pop(0)
203 elif self.f=='S.F':
204 sur=nparts.pop(0)[:-1]
205 fores=nparts
206 dict['fs%s'%self.n]=' '.join(fores)
207 dict['f%s'%self.n]=fores[0]
208 dict['s%s'%self.n]=sur.replace('_',' ')
209 if self.g is not None:
210 gg=addrFields[self.g-1]
211 if gg=='m':
212 dict['pn%s'%self.n]='he'
213 dict['pa%s'%self.n]='him'
214 dict['pg%s'%self.n]='his'
215 elif gg=='f':
216 dict['pn%s'%self.n]='she'
217 dict['pa%s'%self.n]='her'
218 dict['pg%s'%self.n]='her'
219 else:
220 print >>sys.stderr,"Warning, unrecognised gender in column %s: %s"%(self.n,gg)
221
222 def setExpand(self):
223 self._expand=True
224
225 class RawColumn(Column):
226 '''Not for person names, just raw text'''
227
228 def subDo(self,addrFields,dict):
229 f=addrFields[self.n-1]
230 dict[str(self.n)]=f
231
232 def doAtt(msg,att,codec):
233 (mt,enc)=mimetypes.guess_type(att)
234 (tp,subtp)=mt.split('/',2)
235 if tp=='text':
236 attf=codecs.open(att,'r',codec)
237 atm=MIMEText(attf.read(),subtp,codec)
238 elif tp=='application':
239 from email.mime.application import MIMEApplication
240 attf=open(att,'r')
241 atm=MIMEApplication(attf.read(),subtp)
242 else:
243 print >>sys.stderr, "Help: Media type %s (for attachment %s) not supported"%(mt,att)
244 exit(5)
245 atm.add_header('Content-Disposition','attachment',
246 filename=os.path.basename(att))
247 msg.attach(atm)
248
249 dryrun=False
250 sys.argv.pop(0)
251 doSub=False
252 pats=[]
253 someExpand=False
254 codec='iso-8859-1'
255 staticAtts=[]
256 while sys.argv:
257 if sys.argv[0]=='-n':
258 dryrun=True
259 sys.argv.pop(0)
260 elif sys.argv[0]=='-c' and ccCols=={}:
261 sys.argv.pop(0)
262 if sys.argv:
263 parseCols(sys.argv.pop(0),ccCols)
264 else:
265 usage('cc')
266 elif sys.argv[0]=='-C' and CC==[]:
267 sys.argv.pop(0)
268 if sys.argv:
269 CC=sys.argv.pop(0).split(',')
270 else:
271 usage('CC')
272 elif sys.argv[0]=='-b' and bccCols=={}:
273 sys.argv.pop(0)
274 if sys.argv:
275 parseCols(sys.argv.pop(0),bccCols)
276 else:
277 usage('bcc')
278 elif sys.argv[0]=='-B' and BCC==[]:
279 sys.argv.pop(0)
280 if sys.argv:
281 BCC=sys.argv.pop(0).split(',')
282 else:
283 usage('BCC')
284 elif sys.argv[0] in ('-a','-A','-SA'): # and attCols=={}
285 expand=sys.argv[0]=='-A'
286 static=sys.argv[0]=='-SA'
287 sys.argv.pop(0)
288 if sys.argv:
289 if static:
290 staticAtts=sys.argv.pop(0).split(',')
291 else:
292 pc=parseCols(sys.argv.pop(0),attCols)
293 if expand:
294 import itertools, glob
295 someExpand=True
296 for c in pc:
297 c.setExpand()
298 from email.mime.multipart import MIMEMultipart
299 import mimetypes
300 else:
301 usage('attachment')
302 elif sys.argv[0]=='-u':
303 sys.argv.pop(0)
304 codec='utf-8'
305 elif sys.argv[0]=='-s':
306 sys.argv.pop(0)
307 doSub=True
308 elif sys.argv[0]=='-S' and rawCols=={}:
309 sys.argv.pop(0)
310 if sys.argv:
311 for c in sys.argv.pop(0).split(','):
312 RawColumn("%s:%s"%(c,c),rawCols)
313 else:
314 usage('raw subs')
315 elif sys.argv[0]=='-p':
316 sys.argv.pop(0)
317 if sys.argv:
318 pats.append(sys.argv.pop(0))
319 else:
320 usage('pat')
321 elif sys.argv[0][0]=='-':
322 print sys.argv
323 usage()
324 else:
325 break
326
327 if sys.argv:
328 parseCols(sys.argv.pop(0),toCols)
329 else:
330 usage('to')
331
332 pats=[parsePat(p) for p in pats]
333
334 if sys.argv:
335 subj=sys.argv.pop(0)
336 else:
337 usage('subj')
338
339 if sys.argv:
340 af=sys.argv.pop(0)
341 if af=='-':
342 addrFile=sys.stdin
343 else:
344 try:
345 addrFile=open(af,'r')
346 except:
347 usage('addr: %s'%sys.exc_value)
348 else:
349 usage('addr')
350
351 if sys.argv:
352 bf=sys.argv.pop(0)
353 try:
354 bodyFile=open(bf,'r')
355 except:
356 usage('body: %s'%sys.exc_value)
357 else:
358 usage('body')
359
360 try:
361 sig=open("/home/ht/.signature","r")
362 signature=sig.read().rstrip()
363 except:
364 signature=None
365
366 CS=', '
367 body=bodyFile.read().rstrip()
368 if not dryrun:
369 mailer=smtplib.SMTP()
370 mailer.connect()
371 for l in addrFile:
372 addrFields=l.rstrip().split('\t')
373 if doSub:
374 bodyPlus=body%subDict(addrFields)
375 else:
376 bodyPlus=body
377 if signature is not None:
378 bodyPlus+="\n--\n"
379 bodyPlus+=signature
380 if attCols or staticAtts:
381 msg=MIMEMultipart()
382 msg.attach(MIMEText(bodyPlus))
383 else:
384 msg=MIMEText(bodyPlus)
385 #to=addrLine("To",addrFields,toCols)
386 to=addrList(addrFields,toCols)
387 #msg=to
388 #recips=addrPat.findall(to)
389 msg['To']=CS.join(to)
390 recips=[]+list(to)
391 cc=CC
392 if ccCols:
393 cc+=addrList(addrFields,ccCols)
394 if cc!=[]:
395 msg["Cc"]=CS.join(cc)
396 recips+=list(cc)
397 bcc=BCC
398 if bccCols:
399 bcc+=addrList(addrFields,bccCols)
400 if bcc!=[]:
401 msg["Bcc"]=CS.join(bcc)
402 recips+=list(bcc)
403 msg["Subject"]=subj
404 for att in staticAtts:
405 doAtt(msg,att,codec)
406 if attCols:
407 for att in addrList(addrFields,attCols,True):
408 doAtt(msg,att,codec)
409 if dryrun:
410 print recips
411 print msg.as_string()
412 exit()
413 print "mailing to %s"%recips
414 mailer.sendmail("ht@inf.ed.ac.uk",recips,msg.as_string())
415 mailer.quit()