PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/tags/mailman-1_0b2/mailman/Mailman/MailList.py

#
Python | 815 lines | 666 code | 96 blank | 53 comment | 80 complexity | e9fb03713bbe76588bee3656d64140d0 MD5 | raw file
Possible License(s): GPL-2.0
  1. "The class representing a mailman maillist. Mixes in many feature classes."
  2. __version__ = "$Revision: 440 $"
  3. try:
  4. import mm_cfg
  5. except ImportError:
  6. raise RuntimeError, ('missing mm_cfg - has mm_cfg_dist been configured '
  7. 'for the site?')
  8. import sys, os, marshal, string, posixfile, time
  9. import re
  10. import mm_utils, mm_err
  11. from mm_admin import ListAdmin
  12. from mm_deliver import Deliverer
  13. from mm_mailcmd import MailCommandHandler
  14. from mm_html import HTMLFormatter
  15. from mm_archive import Archiver
  16. from mm_digest import Digester
  17. from mm_security import SecurityManager
  18. from mm_bouncer import Bouncer
  19. # Note:
  20. # an _ in front of a member variable for the MailList class indicates
  21. # a variable that does not save when we marshal our state.
  22. # Expression for generally matching the "Re: " prefix in message subject lines:
  23. SUBJ_REGARDS_PREFIX = "[rR][eE][: ]*[ ]*"
  24. # Use mixins here just to avoid having any one chunk be too large.
  25. class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin,
  26. Archiver, Digester, SecurityManager, Bouncer):
  27. def __init__(self, name=None):
  28. MailCommandHandler.__init__(self)
  29. self._internal_name = name
  30. self._ready = 0
  31. self._log_files = {} # 'class': log_file_obj
  32. if name:
  33. if name not in mm_utils.list_names():
  34. raise mm_err.MMUnknownListError, 'list not found'
  35. self._full_path = os.path.join(mm_cfg.LIST_DATA_DIR, name)
  36. # Load in the default values so that old data files aren't
  37. # hosed by new versions of the program.
  38. self.InitVars(name)
  39. self.Load()
  40. def __del__(self):
  41. for f in self._log_files.values():
  42. f.close()
  43. def GetAdminEmail(self):
  44. return '%s-admin@%s' % (self._internal_name, self.host_name)
  45. def GetRequestEmail(self):
  46. return '%s-request@%s' % (self._internal_name, self.host_name)
  47. def GetListEmail(self):
  48. return '%s@%s' % (self._internal_name, self.host_name)
  49. def GetScriptURL(self, script_name):
  50. return os.path.join(self.web_page_url, '%s/%s' %
  51. (script_name, self._internal_name))
  52. def GetOptionsURL(self, addr):
  53. options = self.GetScriptURL('options')
  54. if self.obscure_addresses:
  55. treated = mm_utils.ObscureEmail(addr, for_text=0)
  56. else:
  57. treated = addr
  58. return os.path.join(options, treated)
  59. def GetUserOption(self, user, option):
  60. if option == mm_cfg.Digests:
  61. return user in self.digest_members
  62. if not self.user_options.has_key(user):
  63. return 0
  64. return not not self.user_options[user] & option
  65. def SetUserOption(self, user, option, value):
  66. if not self.user_options.has_key(user):
  67. self.user_options[user] = 0
  68. if value:
  69. self.user_options[user] = self.user_options[user] | option
  70. else:
  71. self.user_options[user] = self.user_options[user] & ~(option)
  72. if not self.user_options[user]:
  73. del self.user_options[user]
  74. self.Save()
  75. def FindUser(self, email):
  76. matches = mm_utils.FindMatchingAddresses(email,
  77. (self.members
  78. + self.digest_members))
  79. if not matches or not len(matches):
  80. return None
  81. return matches[0]
  82. def InitVars(self, name='', admin='', crypted_password=''):
  83. """Assign default values - some will be overriden by stored state."""
  84. # Non-configurable list info
  85. self._internal_name = name
  86. self._lock_file = None
  87. self._mime_separator = '__--__--'
  88. # Must save this state, even though it isn't configurable
  89. self.volume = 1
  90. self.members = [] # self.digest_members is initted in mm_digest
  91. self.data_version = mm_cfg.VERSION
  92. self.last_post_time = 0
  93. self.post_id = 1. # A float so it never has a chance to overflow.
  94. self.user_options = {}
  95. # This stuff is configurable
  96. self.filter_prog = mm_cfg.DEFAULT_FILTER_PROG
  97. self.dont_respond_to_post_requests = 0
  98. self.num_spawns = mm_cfg.DEFAULT_NUM_SPAWNS
  99. self.advertised = mm_cfg.DEFAULT_LIST_ADVERTISED
  100. self.max_num_recipients = mm_cfg.DEFAULT_MAX_NUM_RECIPIENTS
  101. self.max_message_size = mm_cfg.DEFAULT_MAX_MESSAGE_SIZE
  102. self.web_page_url = mm_cfg.DEFAULT_URL
  103. self.owner = [admin]
  104. self.reply_goes_to_list = mm_cfg.DEFAULT_REPLY_GOES_TO_LIST
  105. self.posters = []
  106. self.forbidden_posters = []
  107. self.admin_immed_notify = mm_cfg.DEFAULT_ADMIN_IMMED_NOTIFY
  108. self.moderated = mm_cfg.DEFAULT_MODERATED
  109. self.require_explicit_destination = \
  110. mm_cfg.DEFAULT_REQUIRE_EXPLICIT_DESTINATION
  111. self.acceptable_aliases = mm_cfg.DEFAULT_ACCEPTABLE_ALIASES
  112. self.bounce_matching_headers = \
  113. mm_cfg.DEFAULT_BOUNCE_MATCHING_HEADERS
  114. self.real_name = '%s%s' % (string.upper(self._internal_name[0]),
  115. self._internal_name[1:])
  116. self.description = ''
  117. self.info = ''
  118. self.welcome_msg = ''
  119. self.goodbye_msg = ''
  120. self.open_subscribe = mm_cfg.DEFAULT_OPEN_SUBSCRIBE
  121. self.private_roster = mm_cfg.DEFAULT_PRIVATE_ROSTER
  122. self.obscure_addresses = mm_cfg.DEFAULT_OBSCURE_ADDRESSES
  123. self.member_posting_only = mm_cfg.DEFAULT_MEMBER_POSTING_ONLY
  124. self.web_subscribe_requires_confirmation = \
  125. mm_cfg.DEFAULT_WEB_SUBSCRIBE_REQUIRES_CONFIRMATION
  126. self.host_name = mm_cfg.DEFAULT_HOST_NAME
  127. # Analogs to these are initted in Digester.InitVars
  128. self.nondigestable = mm_cfg.DEFAULT_NONDIGESTABLE
  129. Digester.InitVars(self) # has configurable stuff
  130. SecurityManager.InitVars(self, crypted_password)
  131. HTMLFormatter.InitVars(self)
  132. Archiver.InitVars(self) # has configurable stuff
  133. ListAdmin.InitVars(self)
  134. Bouncer.InitVars(self)
  135. # These need to come near the bottom because they're dependent on
  136. # other settings.
  137. self.subject_prefix = mm_cfg.DEFAULT_SUBJECT_PREFIX % self.__dict__
  138. self.msg_header = mm_cfg.DEFAULT_MSG_HEADER
  139. self.msg_footer = mm_cfg.DEFAULT_MSG_FOOTER
  140. def GetConfigInfo(self):
  141. config_info = {}
  142. config_info['digest'] = Digester.GetConfigInfo(self)
  143. config_info['archive'] = Archiver.GetConfigInfo(self)
  144. config_info['general'] = [
  145. "Fundamental list characteristics, including descriptive"
  146. " info and basic behaviors.",
  147. ('real_name', mm_cfg.String, 50, 0,
  148. 'The public name of this list'),
  149. ('owner', mm_cfg.EmailList, (3,30), 0,
  150. "The list admin's email address - having multiple"
  151. " admins/addresses is ok."),
  152. ('description', mm_cfg.String, 50, 0,
  153. 'A one sentence description of this list.'),
  154. ('info', mm_cfg.Text, (7, 50), 0,
  155. 'A descriptive paragraph about the list.',
  156. "The text will be treated as html <em>except</em> that newlines"
  157. " newlines will be translated to &lt;br&gt; - so you can use"
  158. " links, preformatted text, etc, but don't put in carriage"
  159. " returns except where you mean to separate paragraphs. And"
  160. " review your changes - bad html (like some unterminated HTML"
  161. " constructs) can prevent display of the entire listinfo page."),
  162. ('subject_prefix', mm_cfg.String, 10, 0,
  163. 'Prefix for subject line of list postings.',
  164. "Text prefixed to posting subject lines to distinguish"
  165. " maillist messages in mailbox summaries."),
  166. ('welcome_msg', mm_cfg.Text, (4, 50), 0,
  167. 'List-specific text prepended to new-subscriber welcome message'),
  168. ('goodbye_msg', mm_cfg.Text, (4, 50), 0,
  169. 'Text sent to people leaving the list. If empty, no special'
  170. ' text will be added to the unsubscribe message.'),
  171. ('reply_goes_to_list', mm_cfg.Radio, ('Poster', 'List'), 0,
  172. 'Are replies to a the original post directed to poster'
  173. ' or to the list?',
  174. "List postings include headers which designate where replies"
  175. " to the posts are directed. This option picks whether the"
  176. " headers should be contrived to direct the replies to the"
  177. " original poster or to the list as a whole."),
  178. ('admin_immed_notify', mm_cfg.Radio, ('No', 'Yes'), 0,
  179. 'Should administrator get immediate notifice of new requests, '
  180. 'as well as daily notices about collected ones?',
  181. "List admins are sent daily reminders of pending admin approval"
  182. " requests, like subscriptions to a moderated list or postings"
  183. " that are being held for one reason or another. Setting this"
  184. " option causes notices to be sent immediately on the arrival"
  185. " of new requests, as well."),
  186. ('dont_respond_to_post_requests', mm_cfg.Radio, ('Yes', 'No'), 0,
  187. 'Send mail to poster when their posting is held for approval?',
  188. "Approval notices are sent when mail triggers certain of the"
  189. " limits <em>except</em> routine list moderation and spam"
  190. " filters, for which notices are <em>not</em> sent. This"
  191. " option overrides ever sending the notice."),
  192. # XXX UNSAFE! Perhaps more selective capability could be
  193. # offered, with some kind of super-admin option, but for now
  194. # let's not even expose this. (Apparently was never
  195. # implemented, anyway.)
  196. ## ('filter_prog', mm_cfg.String, 40, 0,
  197. ## 'Program for pre-processing text, if any? '
  198. ## '(Useful, eg, for signature auto-stripping, etc...)'),
  199. ('max_message_size', mm_cfg.Number, 3, 0,
  200. 'Maximum length in Kb of a message body. Use 0 for no limit.'),
  201. ('num_spawns', mm_cfg.Number, 3, 0,
  202. 'Number of outgoing connections to open at once '
  203. '(expert users only).',
  204. "This determines the maximum number of batches into which"
  205. " a mass posting will be divided."),
  206. ('host_name', mm_cfg.Host, 50, 0, 'Host name this list prefers',
  207. "The host_name is the preferred name for email to mailman-related"
  208. " addresses on this host, and generally should be the mail"
  209. " host's exchanger address, if any."),
  210. # I suspect this should not be changeable by arbitrary list admins.
  211. ## ('web_page_url', mm_cfg.String, 50, 0,
  212. ## 'Base URL for Mailman web interface',
  213. ## "This is the common root of all mailman URLs concerning this"
  214. ## " list."),
  215. ]
  216. config_info['privacy'] = [
  217. "List access policies, including anti-spam measures,"
  218. " covering members and outsiders."
  219. ' (See also the <a href="%s">Archival Options section</a> for'
  220. ' separate archive-privacy settings.)'
  221. % os.path.join(self.GetScriptURL('admin'), 'archive'),
  222. "Subscribing",
  223. ('advertised', mm_cfg.Radio, ('No', 'Yes'), 0,
  224. 'Advertise this list when people ask what lists are on '
  225. 'this machine?'),
  226. ('open_subscribe', mm_cfg.Radio, ('No', 'Yes'), 0,
  227. 'Are subscribes done without admins approval (ie, is this'
  228. ' an <em>open</em> list)?',
  229. "Disabling this option makes the list <em>closed</em>, where"
  230. " members are admitted only at the discretion of the list"
  231. " administrator."),
  232. ('web_subscribe_requires_confirmation', mm_cfg.Radio,
  233. ('None', 'Requestor confirms via email', 'Admin approves'), 0,
  234. 'What confirmation is required for on-the-web subscribes?',
  235. "This option determines whether web-initiated subscribes"
  236. " require further confirmation, either from the subscribed"
  237. " address or from the list administrator. Absence of"
  238. " <em>any</em> confirmation makes web-based subscription a"
  239. " tempting opportunity for mischievous subscriptions by third"
  240. " parties."),
  241. "Membership exposure",
  242. ('private_roster', mm_cfg.Radio,
  243. ('Anyone', 'List members', 'List admin only'), 0,
  244. 'Who can view subscription list?',
  245. "When set, the list of subscribers is protected by"
  246. " member or admin password authentication."),
  247. ('obscure_addresses', mm_cfg.Radio, ('No', 'Yes'), 0,
  248. "Show member addrs so they're not directly recognizable"
  249. ' as email addrs?',
  250. "Setting this option causes member email addresses to be"
  251. " transformed when they are presented on list web pages (both"
  252. " in text and as links), so they're not trivially"
  253. " recognizable as email addresses. The intention is to"
  254. " to prevent the addresses from being snarfed up by"
  255. " automated web scanners for use by spammers."),
  256. "General posting filters",
  257. ('moderated', mm_cfg.Radio, ('No', 'Yes'), 0,
  258. 'Must posts be approved by a moderator?',
  259. "If the 'posters' option has any entries then it supercedes"
  260. " this setting."),
  261. ('member_posting_only', mm_cfg.Radio, ('No', 'Yes'), 0,
  262. 'Restrict posting privilege to only list members?'),
  263. ('posters', mm_cfg.EmailList, (5, 30), 1,
  264. 'Addresses blessed for posting to this list. (Adding'
  265. ' anyone to this list implies moderation of everyone else.)',
  266. "Adding any entries to this list supercedes the setting of"
  267. " the list-moderation option."),
  268. "Spam-specific posting filters",
  269. ('require_explicit_destination', mm_cfg.Radio, ('No', 'Yes'), 0,
  270. 'Must posts have list named in destination (to, cc) field'
  271. ' (or be among the acceptable alias names, specified below)?',
  272. "Many (in fact, most) spams do not explicitly name their myriad"
  273. " destinations in the explicit destination addresses - in fact,"
  274. " often the to field has a totally bogus address for"
  275. " obfuscation. The constraint applies only to the stuff in"
  276. " the address before the '@' sign, but still catches all such"
  277. " spams."
  278. "<p>The cost is that the list will not accept unhindered any"
  279. " postings relayed from other addresses, unless <ol>"
  280. " <li>The relaying address has the same name, or"
  281. " <li>The relaying address name is included on the options that"
  282. " specifies acceptable aliases for the list. </ol>"),
  283. ('acceptable_aliases', mm_cfg.Text, ('4', '30'), 0,
  284. 'Alias names (regexps) which qualify as explicit to or cc'
  285. ' destination names for this list.',
  286. "Alternate list names (the stuff before the '@') that are to be"
  287. " accepted when the explicit-destination constraint (a prior"
  288. " option) is active. This enables things like cascading"
  289. " maillists and relays while the constraint is still"
  290. " preventing random spams."),
  291. ('max_num_recipients', mm_cfg.Number, 3, 0,
  292. 'Ceiling on acceptable number of recipients for a posting.',
  293. "If a posting has this number, or more, of recipients, it is"
  294. " held for admin approval. Use 0 for no ceiling."),
  295. ('forbidden_posters', mm_cfg.EmailList, (5, 30), 1,
  296. 'Addresses whose postings are always held for approval.',
  297. "Email addresses whose posts should always be held for"
  298. " approval, no matter what other options you have set."
  299. " See also the subsequent option which applies to arbitrary"
  300. " content of arbitrary headers."),
  301. ('bounce_matching_headers', mm_cfg.Text, ('6', '50'), 0,
  302. 'Hold posts with header value matching a specified regexp.',
  303. "Use this option to prohibit posts according to specific header"
  304. " values. The target value is a regular-expression for"
  305. " matching against the specified header. The match is done"
  306. " disregarding letter case. Lines beginning with '#' are"
  307. " ignored as comments."
  308. "<p>For example:<pre>to: .*@public.com </pre> says"
  309. " to hold all postings with a <em>to</em> mail header"
  310. " containing '@public.com' anywhere among the addresses."
  311. "<p>Note that leading whitespace is trimmed from the"
  312. " regexp. This can be circumvented in a number of ways, eg"
  313. " by escaping or bracketing it."
  314. "<p> See also the <em>forbidden_posters</em> option for"
  315. " a related mechanism."),
  316. ]
  317. config_info['nondigest'] = [
  318. "Policies concerning immediately delivered list traffic.",
  319. ('nondigestable', mm_cfg.Toggle, ('No', 'Yes'), 1,
  320. 'Can subscribers choose to receive mail immediately,'
  321. ' rather than in batched digests?'),
  322. ('msg_header', mm_cfg.Text, (4, 55), 0,
  323. 'Header added to mail sent to regular list members',
  324. "Text prepended to the top of every immediately-delivery"
  325. " message. <p>" + mm_err.MESSAGE_DECORATION_NOTE),
  326. ('msg_footer', mm_cfg.Text, (4, 55), 0,
  327. 'Footer added to mail sent to regular list members',
  328. "Text appended to the bottom of every immediately-delivery"
  329. " message. <p>" + mm_err.MESSAGE_DECORATION_NOTE),
  330. ]
  331. config_info['bounce'] = Bouncer.GetConfigInfo(self)
  332. return config_info
  333. def Create(self, name, admin, crypted_password):
  334. if name in mm_utils.list_names():
  335. raise ValueError, 'List %s already exists.' % name
  336. else:
  337. mm_utils.MakeDirTree(os.path.join(mm_cfg.LIST_DATA_DIR, name))
  338. self._full_path = os.path.join(mm_cfg.LIST_DATA_DIR, name)
  339. self._internal_name = name
  340. self.Lock()
  341. self.InitVars(name, admin, crypted_password)
  342. self._ready = 1
  343. self.InitTemplates()
  344. self.Save()
  345. self.CreateFiles()
  346. def CreateFiles(self):
  347. # Touch these files so they have the right dir perms no matter what.
  348. # A "just-in-case" thing. This shouldn't have to be here.
  349. ou = os.umask(002)
  350. try:
  351. import mm_archive
  352. ## open(os.path.join(self._full_path,
  353. ## mm_archive.ARCHIVE_PENDING), "a+").close()
  354. ## open(os.path.join(self._full_path,
  355. ## mm_archive.ARCHIVE_RETAIN), "a+").close()
  356. open(os.path.join(mm_cfg.LOCK_DIR, '%s.lock' %
  357. self._internal_name), 'a+').close()
  358. open(os.path.join(self._full_path, "next-digest"), "a+").close()
  359. open(os.path.join(self._full_path, "next-digest-topics"),
  360. "a+").close()
  361. finally:
  362. os.umask(ou)
  363. def Save(self):
  364. # If more than one client is manipulating the database at once, we're
  365. # pretty hosed. That's a good reason to make this a daemon not a
  366. # program.
  367. self.IsListInitialized()
  368. ou = os.umask(002)
  369. try:
  370. file = open(os.path.join(self._full_path, 'config.db'), 'w')
  371. finally:
  372. os.umask(ou)
  373. dict = {}
  374. for (key, value) in self.__dict__.items():
  375. if key[0] <> '_':
  376. dict[key] = value
  377. marshal.dump(dict, file)
  378. file.close()
  379. def Load(self):
  380. self.Lock()
  381. try:
  382. file = open(os.path.join(self._full_path, 'config.db'), 'r')
  383. except IOError:
  384. raise mm_cfg.MMBadListError, 'Failed to access config info'
  385. try:
  386. dict = marshal.load(file)
  387. except (EOFError, ValueError, TypeError):
  388. raise mm_cfg.MMBadListError, 'Failed to unmarshal config info'
  389. for (key, value) in dict.items():
  390. setattr(self, key, value)
  391. file.close()
  392. self._ready = 1
  393. self.CheckValues()
  394. self.CheckVersion(dict)
  395. def LogMsg(self, kind, msg, *args):
  396. """Append a message to the log file for messages of specified kind."""
  397. # For want of a better fallback, we use sys.stderr if we can't get
  398. # a log file. We need a better way to warn of failed log access...
  399. if self._log_files.has_key(kind):
  400. logf = self._log_files[kind]
  401. else:
  402. logf = self._log_files[kind] = mm_utils.StampedLogger(kind)
  403. logf.write("%s\n" % (msg % args))
  404. logf.flush()
  405. def CheckVersion(self, stored_state):
  406. """Migrate prior version's state to new structure, if changed."""
  407. if self.data_version == mm_cfg.VERSION:
  408. return
  409. else:
  410. from versions import Update
  411. Update(self, stored_state)
  412. self.data_version = mm_cfg.VERSION
  413. self.Save()
  414. def CheckValues(self):
  415. """Normalize selected values to known formats."""
  416. if self.web_page_url and self.web_page_url[-1] != '/':
  417. self.web_page_url = self.web_page_url + '/'
  418. def IsListInitialized(self):
  419. if not self._ready:
  420. raise mm_err.MMListNotReady
  421. def AddMember(self, name, password, digest=0, web_subscribe=0):
  422. self.IsListInitialized()
  423. # Remove spaces... it's a common thing for people to add...
  424. name = string.join(string.split(string.lower(name)), '')
  425. # Validate the e-mail address to some degree.
  426. if not mm_utils.ValidEmail(name):
  427. raise mm_err.MMBadEmailError
  428. if self.IsMember(name):
  429. raise mm_err.MMAlreadyAMember
  430. if digest and not self.digestable:
  431. raise mm_err.MMCantDigestError
  432. elif not digest and not self.nondigestable:
  433. raise mm_err.MMMustDigestError
  434. if self.open_subscribe:
  435. if (web_subscribe and self.web_subscribe_requires_confirmation):
  436. if self.web_subscribe_requires_confirmation == 1:
  437. # Requester confirmation required.
  438. raise mm_err.MMWebSubscribeRequiresConfirmation
  439. else:
  440. # Admin approval required.
  441. self.AddRequest('add_member', digest, name, password)
  442. else:
  443. # No approval required.
  444. self.ApprovedAddMember(name, password, digest)
  445. else:
  446. # Blanket admin approval requred...
  447. self.AddRequest('add_member', digest, name, password)
  448. def ApprovedAddMember(self, name, password, digest, noack=0):
  449. # XXX klm: It *might* be nice to leave the case of the name alone,
  450. # but provide a common interface that always returns the
  451. # lower case version for computations.
  452. name = string.lower(name)
  453. if self.IsMember(name):
  454. raise mm_err.MMAlreadyAMember
  455. if digest:
  456. self.digest_members.append(name)
  457. kind = " (D)"
  458. else:
  459. self.members.append(name)
  460. kind = ""
  461. self.LogMsg("subscribe", "%s: new%s %s",
  462. self._internal_name, kind, name)
  463. self.passwords[name] = password
  464. self.Save()
  465. if not noack:
  466. self.SendSubscribeAck(name, password, digest)
  467. def DeleteMember(self, name, whence=None):
  468. self.IsListInitialized()
  469. # FindMatchingAddresses *should* never return more than 1 address.
  470. # However, should log this, just to make sure.
  471. aliases = mm_utils.FindMatchingAddresses(name, self.members +
  472. self.digest_members)
  473. if not len(aliases):
  474. raise mm_err.MMNoSuchUserError
  475. def DoActualRemoval(alias, me=self):
  476. kind = "(unfound)"
  477. try:
  478. del me.passwords[alias]
  479. except KeyError:
  480. pass
  481. try:
  482. me.members.remove(alias)
  483. kind = "regular"
  484. except ValueError:
  485. pass
  486. try:
  487. me.digest_members.remove(alias)
  488. kind = "digest"
  489. except ValueError:
  490. pass
  491. map(DoActualRemoval, aliases)
  492. if self.goodbye_msg and len(self.goodbye_msg):
  493. self.SendUnsubscribeAck(name)
  494. self.ClearBounceInfo(name)
  495. self.Save()
  496. if whence: whence = "; %s" % whence
  497. else: whence = ""
  498. self.LogMsg("subscribe", "%s: deleted %s%s",
  499. self._internal_name, name, whence)
  500. def IsMember(self, address):
  501. return len(mm_utils.FindMatchingAddresses(address, self.members +
  502. self.digest_members))
  503. def HasExplicitDest(self, msg):
  504. """True if list name or any acceptable_alias is included among the
  505. to or cc addrs."""
  506. # Note that qualified host can be different! This allows, eg, for
  507. # relaying from remote lists that have the same name. Still
  508. # stringent, but offers a way to provide for remote exploders.
  509. lowname = string.lower(self.real_name)
  510. recips = []
  511. # First check all dests against simple name:
  512. for recip in msg.getaddrlist('to') + msg.getaddrlist('cc'):
  513. curr = string.lower(string.split(recip[1], '@')[0])
  514. if lowname == curr:
  515. return 1
  516. recips.append(curr)
  517. # ... and only then try the regexp acceptable aliases.
  518. for recip in recips:
  519. for alias in string.split(self.acceptable_aliases, '\n'):
  520. stripped = string.strip(alias)
  521. if stripped and re.match(stripped, recip):
  522. return 1
  523. return 0
  524. def parse_matching_header_opt(self):
  525. """Return a list of triples [(field name, regex, line), ...]."""
  526. # - Blank lines and lines with '#' as first char are skipped.
  527. # - Leading whitespace in the matchexp is trimmed - you can defeat
  528. # that by, eg, containing it in gratuitous square brackets.
  529. all = []
  530. for line in string.split(self.bounce_matching_headers, '\n'):
  531. stripped = string.strip(line)
  532. if not stripped or (stripped[0] == "#"):
  533. # Skip blank lines and lines *starting* with a '#'.
  534. continue
  535. else:
  536. try:
  537. h, e = re.split(":[ ]*", line)
  538. all.append((h, e, line))
  539. except ValueError:
  540. # Whoops - some bad data got by:
  541. self.LogMsg("config", "%s - "
  542. "bad bounce_matching_header line %s"
  543. % (self.real_name, `line`))
  544. return all
  545. def HasMatchingHeader(self, msg):
  546. """True if named header field (case-insensitive matches regexp.
  547. Case insensitive.
  548. Returns constraint line which matches or empty string for no
  549. matches."""
  550. pairs = self.parse_matching_header_opt()
  551. for field, matchexp, line in pairs:
  552. fragments = msg.getallmatchingheaders(field)
  553. subjs = []
  554. l = len(field)
  555. for f in fragments:
  556. # Consolidate header lines, stripping header name & whitespace.
  557. if (len(f) > l
  558. and f[l] == ":"
  559. and string.lower(field) == string.lower(f[0:l])):
  560. # Non-continuation line - trim header name:
  561. subjs.append(f[l+1:])
  562. elif not subjs:
  563. # Whoops - non-continuation that matches?
  564. subjs.append(f)
  565. else:
  566. # Continuation line.
  567. subjs[-1] = subjs[-1] + f
  568. for s in subjs:
  569. if re.search(matchexp, s, re.I):
  570. return line
  571. return 0
  572. #msg should be an IncomingMessage object.
  573. def Post(self, msg, approved=0):
  574. self.IsListInitialized()
  575. msgapproved = self.ExtractApproval(msg)
  576. if not approved:
  577. approved = msgapproved
  578. sender = msg.GetSender()
  579. # If it's the admin, which we know by the approved variable,
  580. # we can skip a large number of checks.
  581. if not approved:
  582. if len(self.forbidden_posters):
  583. addrs = mm_utils.FindMatchingAddresses(sender,
  584. self.forbidden_posters)
  585. if len(addrs):
  586. self.AddRequest('post', mm_utils.SnarfMessage(msg),
  587. mm_err.FORBIDDEN_SENDER_MSG,
  588. msg.getheader('subject'))
  589. if len(self.posters):
  590. addrs = mm_utils.FindMatchingAddresses(sender, self.posters)
  591. if not len(addrs):
  592. self.AddRequest('post', mm_utils.SnarfMessage(msg),
  593. 'Only approved posters may post without '
  594. 'moderator approval.',
  595. msg.getheader('subject'))
  596. elif self.moderated:
  597. self.AddRequest('post', mm_utils.SnarfMessage(msg),
  598. mm_err.MODERATED_LIST_MSG,
  599. msg.getheader('subject'))
  600. if self.member_posting_only and not self.IsMember(sender):
  601. self.AddRequest('post', mm_utils.SnarfMessage(msg),
  602. 'Postings from member addresses only.',
  603. msg.getheader('subject'))
  604. if self.max_num_recipients > 0:
  605. recips = []
  606. toheader = msg.getheader('to')
  607. if toheader:
  608. recips = recips + string.split(toheader, ',')
  609. ccheader = msg.getheader('cc')
  610. if ccheader:
  611. recips = recips + string.split(ccheader, ',')
  612. if len(recips) > self.max_num_recipients:
  613. self.AddRequest('post', mm_utils.SnarfMessage(msg),
  614. 'Too many recipients.',
  615. msg.getheader('subject'))
  616. if (self.require_explicit_destination and
  617. not self.HasExplicitDest(msg)):
  618. self.AddRequest('post', mm_utils.SnarfMessage(msg),
  619. mm_err.IMPLICIT_DEST_MSG,
  620. msg.getheader('subject'))
  621. if self.bounce_matching_headers:
  622. triggered = self.HasMatchingHeader(msg)
  623. if triggered:
  624. # Darn - can't include the matching line for the admin
  625. # message because the info would also go to the sender.
  626. self.AddRequest('post', mm_utils.SnarfMessage(msg),
  627. mm_err.SUSPICIOUS_HEADER_MSG,
  628. msg.getheader('subject'))
  629. if self.max_message_size > 0:
  630. if len(msg.body)/1024. > self.max_message_size:
  631. self.AddRequest('post', mm_utils.SnarfMessage(msg),
  632. 'Message body too long (>%dk)' %
  633. self.max_message_size,
  634. msg.getheader('subject'))
  635. # Prepend the subject_prefix to the subject line.
  636. subj = msg.getheader('subject')
  637. prefix = self.subject_prefix
  638. if not subj:
  639. msg.SetHeader('Subject', '%s(no subject)' % prefix)
  640. elif not re.match("(re:? *)?" + re.escape(self.subject_prefix),
  641. subj, re.I):
  642. msg.SetHeader('Subject', '%s%s' % (prefix, subj))
  643. if self.digestable:
  644. self.SaveForDigest(msg)
  645. if self.archive:
  646. self.ArchiveMail(msg)
  647. dont_send_to_sender = 0
  648. ack_post = 0
  649. # Try to get the address the list thinks this sender is
  650. sender = self.FindUser(msg.GetSender())
  651. if sender:
  652. if self.GetUserOption(sender, mm_cfg.DontReceiveOwnPosts):
  653. dont_send_to_sender = 1
  654. if self.GetUserOption(sender, mm_cfg.AcknowlegePosts):
  655. ack_post = 1
  656. # Deliver the mail.
  657. recipients = self.members[:]
  658. if dont_send_to_sender:
  659. recipients.remove(sender)
  660. def DeliveryEnabled(x, s=self, v=mm_cfg.DisableDelivery):
  661. return not s.GetUserOption(x, v)
  662. recipients = filter(DeliveryEnabled, recipients)
  663. self.DeliverToList(msg, recipients,
  664. self.msg_header % self.__dict__,
  665. self.msg_footer % self.__dict__)
  666. if ack_post:
  667. self.SendPostAck(msg, sender)
  668. self.last_post_time = time.time()
  669. self.post_id = self.post_id + 1
  670. self.Save()
  671. def Locked(self):
  672. try:
  673. return self._lock_file and 1
  674. except AttributeError:
  675. return 0
  676. def Lock(self):
  677. try:
  678. if self._lock_file:
  679. return
  680. except AttributeError:
  681. return
  682. ou = os.umask(0)
  683. try:
  684. self._lock_file = posixfile.open(
  685. os.path.join(mm_cfg.LOCK_DIR, '%s.lock' % self._internal_name),
  686. 'a+')
  687. finally:
  688. os.umask(ou)
  689. self._lock_file.lock('w|', 1)
  690. def Unlock(self):
  691. self._lock_file.lock('u')
  692. self._lock_file.close()
  693. self._lock_file = None
  694. def __repr__(self):
  695. if self.Locked(): status = " (locked)"
  696. else: status = ""
  697. return ("<%s.%s %s%s at %s>"
  698. % (self.__module__, self.__class__.__name__,
  699. `self._internal_name`, status, hex(id(self))[2:]))