PageRenderTime 48ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

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

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