1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
|
"""Miscellaneous essential routines.
This inncludes actual message transmission routines, address checking and
message and address munging, a handy-dandy routine to map a function on all
the maillists, the Logging routines, and whatever else doesn't belong
elsewhere."""
__version__ = "$Revision: 508 $"
import sys, string, fcntl, os, random, regsub, re
import mm_cfg
# Valid toplevel domains for when we check the validity of an email address.
valid_toplevels = ["com", "edu", "gov", "int", "mil", "net", "org",
"inc", "af", "al", "dz", "as", "ad", "ao", "ai", "aq", "ag", "ar",
"am", "aw", "au", "at", "az", "bs", "bh", "bd", "bb", "by", "be",
"bz", "bj", "bm", "bt", "bo", "ba", "bw", "bv", "br", "io", "bn",
"bg", "bf", "bi", "kh", "cm", "ca", "cv", "ky", "cf", "td", "cl",
"cn", "cx", "cc", "co", "km", "cg", "ck", "cr", "ci", "hr", "cu",
"cy", "cz", "dk", "dj", "dm", "do", "tp", "ec", "eg", "sv", "gq",
"ee", "et", "fk", "fo", "fj", "fi", "fr", "gf", "pf", "tf", "ga",
"gm", "ge", "de", "gh", "gi", "gb", "uk", "gr", "gl", "gd", "gp",
"gu", "gt", "gn", "gw", "gy", "ht", "hm", "hn", "hk", "hu", "is",
"in", "id", "ir", "iq", "ie", "il", "it", "jm", "jp", "jo", "kz",
"ke", "ki", "kp", "kr", "kw", "kg", "la", "lv", "lb", "ls", "lr",
"ly", "li", "lt", "lu", "mo", "mg", "mw", "my", "mv", "ml", "mt",
"mh", "mq", "mr", "mu", "mx", "fm", "md", "mc", "mn", "ms", "ma",
"mz", "mm", "na", "nr", "np", "an", "nl", "nt", "nc", "nz", "ni",
"ne", "ng", "nu", "nf", "mp", "no", "om", "pk", "pw", "pa", "pg",
"py", "pe", "ph", "pn", "pl", "pt", "pr", "qa", "re", "ro", "ru",
"rw", "kn", "lc", "vc", "sm", "st", "sa", "sn", "sc", "sl", "sg",
"sk", "si", "sb", "so", "za", "es", "lk", "sh", "pm", "sd", "sr",
"sj", "sz", "se", "ch", "sy", "tw", "tj", "tz", "th", "tg", "tk",
"to", "tt", "tn", "tr", "tm", "tc", "tv", "ug", "ua", "um", "us",
"uy", "uz", "vu", "va", "ve", "vn", "vg", "vi", "wf", "eh", "ws",
"ye", "yu", "zr", "zm", "zw", "su"]
def list_names():
"""Return the names of all lists in default list directory."""
got = []
for fn in os.listdir(mm_cfg.LIST_DATA_DIR):
if not (
os.path.exists(
os.path.join(os.path.join(mm_cfg.LIST_DATA_DIR, fn),
'config.db'))):
continue
got.append(fn)
return got
def SendTextToUser(subject, text, recipient, sender, add_headers=[]):
import mm_message
msg = mm_message.OutgoingMessage()
msg.SetSender(sender)
msg.SetHeader('Subject', subject, 1)
msg.SetBody(QuotePeriods(text))
DeliverToUser(msg, recipient, add_headers=add_headers)
def DeliverToUser(msg, recipient, add_headers=[]):
"""Use sendmail to deliver message.
Optional argument add_headers should be a list of headers to be added
to the message, e.g. for Errors-To and X-No-Archive."""
# We fork to ensure no deadlock. Otherwise, even if sendmail is
# invoked in forking mode, if it eg detects a bad address before
# forking, then it will try deliver to the errorsto addr *in the
# foreground*. If the errorsto happens to be the list owner for a list
# that is doing the send - and holding a lock - then the delivery will
# hang pending release of the lock - deadlock.
if os.fork():
return
try:
file = os.popen(mm_cfg.SENDMAIL_CMD % (msg.GetSender(),
repr(recipient)),
'w')
try:
msg.headers.remove('\n')
except ValueError:
pass
if not msg.getheader('to'):
msg.headers.append('To: %s\n' % recipient)
for i in add_headers:
if i and i[-1] != '\n':
i = i + '\n'
msg.headers.append(i)
file.write(string.join(msg.headers, '')+ '\n')
file.write(QuotePeriods(msg.body))
file.close()
finally:
os._exit(0)
def QuotePeriods(text):
return string.join(string.split(text, '\n.\n'), '\n .\n')
def ValidEmail(str):
"""Verify that the an email address isn't grossly invalid."""
# Pretty minimal, cheesy check. We could do better...
if ((string.find(str, '|') <> -1) or (string.find(str, ';') <> -1)
or str[0] == '-'):
raise mm_err.MMHostileAddress
if string.find(str, '/') <> -1:
if os.path.isdir(os.path.split(str)[0]):
raise mm_err.MMHostileAddress
user, domain_parts = ParseEmail(str)
if not domain_parts:
if string.find(str, '@') < 1:
return 0
else:
return 1
if len(domain_parts) < 2:
return 0
## if domain_parts[-1] not in valid_toplevels:
## if len(domain_parts) <> 4:
## return 0
## try:
## domain_parts = map(eval, domain_parts)
## except:
## return 0
## for i in domain_parts:
## if i < 0 or i > 255:
## return 0
return 1
#
def GetPathPieces(path):
l = string.split(path, '/')
try:
while 1:
l.remove('')
except ValueError:
pass
return l
def MakeDirTree(path, perms=0775, verbose=0):
made_part = '/'
path_parts = GetPathPieces(path)
for item in path_parts:
made_part = os.path.join(made_part, item)
if os.path.exists(made_part):
if not os.path.isdir(made_part):
raise "RuntimeError", ("Couldn't make dir tree for %s. (%s"
" already exists)" % (path, made_part))
else:
ou = os.umask(0)
try:
os.mkdir(made_part, perms)
finally:
os.umask(ou)
if verbose:
print 'made directory: ', madepart
# This takes an email address, and returns a tuple containing (user,host)
def ParseEmail(email):
user = None
domain = None
email = string.lower(email)
at_sign = string.find(email, '@')
if at_sign < 1:
return (email, None)
user = email[:at_sign]
rest = email[at_sign+1:]
domain = string.split(rest, '.')
return (user, domain)
# Return 1 if the 2 addresses match. 0 otherwise.
# Might also want to match if there's any common domain name...
# There's password protection anyway.
def AddressesMatch(addr1, addr2):
"True when username matches and host addr of one addr contains other's."
user1, domain1 = ParseEmail(addr1)
user2, domain2 = ParseEmail(addr2)
if user1 != user2:
return 0
if domain1 == domain2:
return 1
elif not domain1 or not domain2:
return 0
for i in range(-1 * min(len(domain1), len(domain2)), 0):
# By going from most specific component of host part we're likely
# to hit a difference sooner.
if domain1[i] != domain2[i]:
return 0
return 1
def FindMatchingAddresses(name, array):
"""Given an email address, and a list of email addresses, returns the
subset of the list that matches the given address. Should sort based
on exactness of match, just in case."""
def CallAddressesMatch (x, y=name):
return AddressesMatch(x,y)
matches = filter(CallAddressesMatch, array)
return matches
def GetRandomSeed():
chr1 = int(random.random() * 57) + 65
chr2 = int(random.random() * 57) + 65
return "%c%c" % (chr1, chr2)
def SnarfMessage(msg):
if msg.unixfrom:
text = msg.unixfrom + string.join(msg.headers, '') + '\n' + msg.body
else:
text = string.join(msg.headers, '') + '\r\n' + msg.body
return (msg.GetSender(), text)
def QuoteHyperChars(str):
arr = regsub.splitx(str, '[<>"&]')
i = 1
while i < len(arr):
if arr[i] == '<':
arr[i] = '<'
elif arr[i] == '>':
arr[i] = '>'
elif arr[i] == '"':
arr[i] = '"'
else: #if arr[i] == '&':
arr[i] = '&'
i = i + 2
return string.join(arr, '')
# Just changing these two functions should be enough to control the way
# that email address obscuring is handled.
def ObscureEmail(addr, for_text=0):
"""Make email address unrecognizable to web spiders, but invertable.
When for_text option is set (not default), make a sentence fragment
instead of a token."""
if for_text:
return re.sub("@", " at ", addr)
else:
return re.sub("@", "__at__", addr)
def UnobscureEmail(addr):
"""Invert ObscureEmail() conversion."""
# Contrived to act as an identity operation on already-unobscured
# emails, so routines expecting obscured ones will accept both.
return re.sub("__at__", "@", addr)
def map_maillists(func, names=None, unlock=None, verbose=0):
"""Apply function (of one argument) to all list objs in turn.
Returns a list of the results.
Optional arg 'names' specifies which lists, default all.
Optional arg unlock says to unlock immediately after instantiation.
Optional arg verbose says to print list name as it's about to be
instantiated, CR when instantiation is complete, and result of
application as it shows."""
from maillist import MailList
if names == None: names = list_names()
got = []
for i in names:
if verbose: print i,
l = MailList(i)
if verbose: print
if unlock and l.Locked():
l.Unlock()
got.append(apply(func, (l,)))
if verbose: print got[-1]
if not unlock:
l.Unlock()
del l
return got
class Logger:
"""File-based logger, writes to named category files in mm_cfg.LOG_DIR."""
def __init__(self, category, nofail=1):
"""Nofail (by default) says to fallback to sys.stderr if write
fails to category file. A message is emitted, but the IOError is
caught. Set nofail=0 if you want to handle the error in your code,
instead."""
self.__category=category
self.__f = None
self.__nofail = nofail
def __get_f(self):
if self.__f:
return self.__f
else:
fname = os.path.join(mm_cfg.LOG_DIR, self.__category)
try:
ou = os.umask(002)
try:
f = self.__f = open(fname, 'a+')
finally:
os.umask(ou)
except IOError, msg:
if not self.__nofail:
raise IOError, msg, sys.exc_info()[2]
else:
f = self.__f = sys.stderr
f.write("logger open %s failed %s, using stderr\n"
% (fname, msg))
return f
def flush(self):
f = self.__get_f()
if hasattr(f, 'flush'):
f.flush()
def write(self, msg):
f = self.__get_f()
try:
f.write(msg)
except IOError, msg:
f = self.__f = sys.stderr
f.write("logger write %s failed %s, using stderr\n"
% (fname, msg))
def writelines(self, lines):
for l in lines:
self.write(l)
def close(self):
if not self.__f:
return
self.__get_f().close()
def __del__(self):
try:
if self.__f and self.__f != sys.stderr:
self.close()
except:
pass
class StampedLogger(Logger):
"""Record messages in log files, including date stamp and optional label.
If manual_reprime is on (off by default), then timestamp prefix will
included only on first .write() and on any write immediately following
a call to the .reprime() method. This is useful for when StampedLogger
is substituting for sys.stderr, where you'd like to see the grouping of
multiple writes under a single timestamp (and there is often is one
group, for uncaught exceptions where a script is bombing).
In any case, the identifying prefix will only follow writes that start
on a new line.
Nofail (by default) says to fallback to sys.stderr if write fails to
category file. A message is emitted, but the IOError is caught.
Initialize with nofail=0 if you want to handle the error in your code,
instead."""
def __init__(self, category, label=None, manual_reprime=0, nofail=1):
"If specified, optional label is included after timestamp."
self.label = label
self.manual_reprime = manual_reprime
self.primed = 1
self.bol = 1
Logger.__init__(self, category, nofail=nofail)
def reprime(self):
"""Reset so timestamp will be included with next write."""
self.primed = 1
def write(self, msg):
import time
if not self.bol:
prefix = ""
else:
if not self.manual_reprime or self.primed:
stamp = time.strftime("%b %d %H:%M:%S %Y ",
time.localtime(time.time()))
self.primed = 0
else:
stamp = ""
if self.label == None:
label = ""
else:
label = "%s:" % self.label
prefix = stamp + label
Logger.write(self, "%s %s" % (prefix, msg))
if msg and msg[-1] == '\n':
self.bol = 1
else:
self.bol = 0
def writelines(self, lines):
first = 1
for l in lines:
if first:
self.write(l)
first = 0
else:
if l and l[0] not in [' ', '\t', '\n']:
Logger.write(self, ' ' + l)
else:
Logger.write(self, l)
|