PageRenderTime 56ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/tags/Snapshot_1_0b4/mailman/Mailman/MailList.py

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