PageRenderTime 50ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/tags/pre-autoconf/mailman/Mailman/Utils.py

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