Mercurial > hg > python
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() |