diff options
| author | bwarsaw | 2001-07-19 06:15:28 +0000 |
|---|---|---|
| committer | bwarsaw | 2001-07-19 06:15:28 +0000 |
| commit | 710cbb1aa4ef5cd47fce015b172cb3f3704a7642 (patch) | |
| tree | c29c61ee1fbb5320fca4916c7ddbe0c50bf76489 /Mailman/MailList.py | |
| parent | dc09ef8a6a00fe392100c4068494002887fb7fa1 (diff) | |
| download | mailman-710cbb1aa4ef5cd47fce015b172cb3f3704a7642.tar.gz mailman-710cbb1aa4ef5cd47fce015b172cb3f3704a7642.tar.zst mailman-710cbb1aa4ef5cd47fce015b172cb3f3704a7642.zip | |
Major overhaul. Summary:
- There's no more GetConfigInfo() in the mixin classes. All admin
ui categories and content comes from components in the new
Mailman.Gui package.
- All MailList attribute access for user-centric information is
moved out of here into the OldStyleMemberships (a subclass, aka
implementation of the MemberAdaptor interface).
- An extension mechanism to MailList objects has been added. Drop
a file called extend.py in the lists/<yourlist> directory; this
gets execfile'd, and the function extend() is called after the
instance is initialized. Use this to override anything
(e.g. the _memberadaptor attribute).
- Lots of code reorganization.
Specific changes:
MailList subclasses: Added TopicMgr
__init__(): Don't call MailCommandHandler.__init__() explicitly.
Instead, loop through base classes calling their __init__()'s if
they have them.
Initialize _memberadaptor to be an OldStyleMemberships instance.
After the instance has been locked and loaded <snort!> execfile
extend.py and run its extend() function, if it has one.
InitTempVars(): Like __init__(), cycle through all base classes,
calling each's InitTempVars() if they have one. Also, instantiate
the gui components by dir'ing the Gui package and instantiating
each callable not prefixed by _ (they will be the individual
classes).
GetMembers(), GetDigestMembers(), GetDeliveryMembers(),
GetDigestDeliveryMembers(), __AddMember(),
GetUserSubscribedAddress(), GetUserCanonicalAddress(),
GetUserOption(), SetUserOption(), FindUser(): Removed.
__getattr__(): Added for auto-delegation first to the _memberadaptor,
and then to the gui components.
__repr__(): Change the printable representation slightly so that
"(unlocked)" is printed if the list is unlocked, and also to not
include the class/module name. Also use %x format instead of
coercing the id to hex().
InitVars(): Add usernames and passwords dicts for traditional
membership management. Like __init__(), cycle through all base
classes, calling each's InitVars() if they have one. Set
self.password here instead of in SecurityManager's InitVar().
GetConfigCategories(): New method which returns a CategoryDict
instance (subclass of UserDict), containing the order
ADMIN_CATEGORIES as extracted from the gui components
GetConfigCategory() methods.
GetConfigSubCategories(): For a particular category, get the list of
subcategories by calling a gui component's
GetConfigSubCategories() method if it has one.
GetConfigInfo(): Delegate to gui components.
Save(): Ignore attributes if their name starts with _ (like before) or
if their value type is MethodType.
AddMember(): Rewrite to use (Python 2.2's) rfc822.parseaddr() which
supplants Utils.ParseAddrs(). We also don't remove spaces from
the name any more, but we do still lowercase the domain. Also,
IsMember() -> isMember(). Call ApprovedAddMember() passing in the
fullname. Same with HoldSubscription().
ApprovedAddMembers(): Use MemberAdaptor API, and
rfc822.parseaddr().
DeleteMember() -> ApprovedDeleteMember(). Also rewrite to use
MemberAdaptor API.
ChangeMemberName(): New method which changes a member's real name,
both for this list, and globally.
ChangeMemberAddress(), ApprovedChangeMemberAddress(): Use
MemberAdaptor API.
ProcessConfirmation(): DeleteMember() -> ApprovedDeleteMember().
GetPreferredLanguage(): Allow name to default to None, meaning return
the list's preferred_language.
Diffstat (limited to 'Mailman/MailList.py')
| -rw-r--r-- | Mailman/MailList.py | 1362 |
1 files changed, 404 insertions, 958 deletions
diff --git a/Mailman/MailList.py b/Mailman/MailList.py index de18d3a4b..965f99b77 100644 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -28,10 +28,15 @@ import re import shutil import socket import urllib +from UserDict import UserDict from urlparse import urlparse from types import * from mimelib.address import getaddresses +# We use this explicitly instead of going through mimelib so that we can pick +# up the RFC 2822-conformant version of rfc822.py that will be included in +# Python 2.2. +from Mailman.pythonlib.rfc822 import parseaddr from Mailman import mm_cfg from Mailman import Utils @@ -39,18 +44,23 @@ from Mailman import Errors from Mailman import LockFile # base classes -from Mailman.ListAdmin import ListAdmin -from Mailman.Deliverer import Deliverer -from Mailman.MailCommandHandler import MailCommandHandler -from Mailman.HTMLFormatter import HTMLFormatter from Mailman.Archiver import Archiver -from Mailman.Digester import Digester -from Mailman.SecurityManager import SecurityManager +from Mailman.Autoresponder import Autoresponder from Mailman.Bouncer import Bouncer +from Mailman.Deliverer import Deliverer +from Mailman.Digester import Digester from Mailman.GatewayManager import GatewayManager -from Mailman.Autoresponder import Autoresponder +from Mailman.HTMLFormatter import HTMLFormatter +from Mailman.ListAdmin import ListAdmin +from Mailman.MailCommandHandler import MailCommandHandler +from Mailman.SecurityManager import SecurityManager +from Mailman.TopicMgr import TopicMgr + +# gui components package +from Mailman import Gui # other useful classes +from Mailman.OldStyleMemberships import OldStyleMemberships from Mailman import Message from Mailman import Pending from Mailman.i18n import _ @@ -61,26 +71,43 @@ EMPTYSTRING = '' -# Note: -# an _ in front of a member variable for the MailList class indicates -# a variable that does not save when we marshal our state. - # Use mixins here just to avoid having any one chunk be too large. - class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, Archiver, Digester, SecurityManager, Bouncer, GatewayManager, - Autoresponder): + Autoresponder, TopicMgr): + + # + # A MailList object's basic Python object model support + # def __init__(self, name=None, lock=1): # No timeout by default. If you want to timeout, open the list # unlocked, then lock explicitly. - MailCommandHandler.__init__(self) + # + # Only one level of mixin inheritance allowed + for baseclass in self.__class__.__bases__: + if hasattr(baseclass, '__init__'): + baseclass.__init__(self) + # Initialize volatile attributes self.InitTempVars(name) + # Default membership adaptor class + self._memberadaptor = OldStyleMemberships(self) if name: if lock: # This will load the database. self.Lock() else: self.Load() + # This extension mechanism allows list-specific overrides of any + # method (well, except __init__(), InitTempVars(), and InitVars() + # I think). + filename = os.path.join(self._full_path, 'extend.py') + dict = {} + try: + execfile(filename, dict) + except IOError, e: + if e.errno <> errno.ENOENT: raise + else: + dict['extend'](self) def __del__(self): try: @@ -89,51 +116,60 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, # List didn't get far enough to have __lock pass - def GetMembers(self): - """returns a list of the members. (all lowercase)""" - return self.members.keys() + def __getattr__(self, name): + # Because we're using delegation, we want to be sure that attribute + # access to a delegated member function gets passed to the + # sub-objects. This of course imposes a specific name resolution + # order. + try: + return getattr(self._memberadaptor, name) + except AttributeError: + for guicomponent in self._gui: + try: + return getattr(guicomponent, name) + except AttributeError: + pass + else: + raise AttributeError, name + + def __repr__(self): + if self.Locked(): + status = '(locked)' + else: + status = '(unlocked)' + return '<mailing list "%s" %s at %x>' % ( + self.internal_name(), status, id(self)) + + + # + # Lock management + # + def Lock(self, timeout=0): + self.__lock.lock(timeout) + # Must reload our database for consistency. Watch out for lists that + # don't exist. + try: + self.Load() + except Errors.MMUnknownListError: + self.Unlock() + raise - def GetDigestMembers(self): - """returns a list of digest members. (all lowercase)""" - return self.digest_members.keys() + def Unlock(self): + self.__lock.unlock(unconditionally=1) - def GetDeliveryMembers(self): - """returns a list of the members with username case preserved.""" - res = [] - for k, v in self.members.items(): - if type(v) is StringType: - res.append(v) - else: - res.append(k) - return res + def Locked(self): + return self.__lock.locked() - def GetDigestDeliveryMembers(self): - """returns a list of the members with username case preserved.""" - res = [] - for k,v in self.digest_members.items(): - if type(v) is StringType: - res.append(v) - else: - res.append(k) - return res - def __AddMember(self, addr, digest): - """adds the appropriate data to the internal members dict. + + # + # Useful accessors + # + def internal_name(self): + return self._internal_name - If the username has upercase letters in it, then the value - in the members dict is the case preserved address, otherwise, - the value is 0. - """ - if Utils.LCDomain(addr) == addr.lower(): - if digest: - self.digest_members[addr] = 0 - else: - self.members[addr] = 0 - else: - if digest: - self.digest_members[addr.lower()] = addr - else: - self.members[addr.lower()] = addr + def fullpath(self): + return self._full_path def GetAdminEmail(self): return '%s-admin@%s' % (self._internal_name, self.host_name) @@ -157,29 +193,6 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, acct, host = tuple(member.split('@')) return "%s%s@%s" % (acct, self.umbrella_member_suffix, host) - def GetUserSubscribedAddress(self, member): - """Return the member's case preserved address. - """ - member = member.lower() - cpuser = self.members.get(member) - if type(cpuser) == IntType: - return member - elif type(cpuser) == StringType: - return cpuser - cpuser = self.digest_members.get(member) - if type(cpuser) == IntType: - return member - elif type(cpuser) == StringType: - return cpuser - return None - - def GetUserCanonicalAddress(self, member): - """Return the member's address lower cased.""" - cpuser = self.GetUserSubscribedAddress(member) - if cpuser is not None: - return cpuser.lower() - return None - def GetRequestEmail(self): return '%s-request@%s' % (self._internal_name, self.host_name) @@ -190,76 +203,16 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, return Utils.ScriptURL(scriptname, self.web_page_url, absolute) + \ '/' + self.internal_name() - def GetOptionsURL(self, addr, obscure=0, absolute=0): - addr = addr.lower() + def GetOptionsURL(self, user, obscure=0, absolute=0): url = self.GetScriptURL('options', absolute) if obscure: - addr = Utils.ObscureEmail(addr) - return '%s/%s' % (url, urllib.quote(addr)) - - def GetUserOption(self, user, option): - """Return user's setting for option, defaulting to 0 if no settings.""" - user = self.GetUserCanonicalAddress(user) - if option == mm_cfg.Digests: - return self.digest_members.has_key(user) - if not self.user_options.has_key(user): - return 0 - return not not self.user_options[user] & option - - def SetUserOption(self, user, option, value, save_list=1): - user = self.GetUserCanonicalAddress(user) - if not self.user_options.has_key(user): - self.user_options[user] = 0 - if value: - self.user_options[user] = self.user_options[user] | option - else: - self.user_options[user] = self.user_options[user] & ~(option) - if not self.user_options[user]: - del self.user_options[user] - if save_list: - self.Save() + user = Utils.ObscureEmail(user) + return '%s/%s' % (url, urllib.quote(user.lower())) - # Here are the rules for the three dictionaries self.members, - # self.digest_members, and self.passwords: - # - # The keys of all these dictionaries are the lowercased version of the - # address. This makes finding a user very quick: just lowercase the name - # you're matching against, and do a has_key() or get() on first - # self.members, then if that returns false, self.digest_members + # - # The value of the key in self.members and self.digest_members is either - # the integer 0, meaning the user was subscribed with an all-lowercase - # address, or a string which would be the address with the username part - # case preserved. Note that for Mailman versions before 1.0b11, the value - # could also have been the integer 1. This is a bug that was caused when - # a user switched from regular to/from digest membership. If this - # happened, you're screwed because there's no way to recover the case - # preserved address. :-( + # Instance and subcomponent initialization # - # The keys for self.passwords is also lowercase, although for versions of - # Mailman before 1.0b11, this was not always true. 1.0b11 has a hack in - # Load() that forces the keys to lowercase. The value for the keys in - # self.passwords is, of course the password in plain text. - - def FindUser(self, email): - """Return the lowercased version of the subscribed email address. - - If email is not subscribed, either as a regular member or digest - member, None is returned. If they are subscribed, the return value is - guaranteed to be lowercased. - """ - # shortcut - lcuser = self.GetUserCanonicalAddress(email) - if lcuser is not None: - return lcuser - matches = Utils.FindMatchingAddresses(email, - self.members, - self.digest_members) - # sadly, matches may or may not be case preserved - if not matches or not len(matches): - return None - return matches[0].lower() - def InitTempVars(self, name): """Set transient variables of this and inherited classes.""" self.__lock = LockFile.LockFile( @@ -273,7 +226,16 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, self._full_path = os.path.join(mm_cfg.LIST_DATA_DIR, name) else: self._full_path = None - ListAdmin.InitTempVars(self) + # Only one level of mixin inheritance allowed + for baseclass in self.__class__.__bases__: + if hasattr(baseclass, 'InitTempVars'): + baseclass.InitTempVars(self) + # Now, initialize our gui components + self._gui = [] + for component in dir(Gui): + if component.startswith('_'): + continue + self._gui.append(getattr(Gui, component)()) def InitVars(self, name=None, admin='', crypted_password=''): """Assign default values - some will be overriden by stored state.""" @@ -290,6 +252,8 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, self.post_id = 1. # A float so it never has a chance to overflow. self.user_options = {} self.language = {} + self.usernames = {} + self.passwords = {} # This stuff is configurable self.dont_respond_to_post_requests = 0 @@ -335,13 +299,13 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, # Analogs to these are initted in Digester.InitVars self.nondigestable = mm_cfg.DEFAULT_NONDIGESTABLE - Digester.InitVars(self) # has configurable stuff - SecurityManager.InitVars(self, crypted_password) - Archiver.InitVars(self) # has configurable stuff - ListAdmin.InitVars(self) - Bouncer.InitVars(self) - GatewayManager.InitVars(self) - Autoresponder.InitVars(self) + # BAW: This should really be set in SecurityManager.InitVars() + self.password = crypted_password + + # Only one level of mixin inheritance allowed + for baseclass in self.__class__.__bases__: + if hasattr(baseclass, 'InitVars'): + baseclass.InitVars(self) # These need to come near the bottom because they're dependent on # other settings. @@ -349,533 +313,60 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, self.msg_header = mm_cfg.DEFAULT_MSG_HEADER self.msg_footer = mm_cfg.DEFAULT_MSG_FOOTER - def GetConfigInfo(self): - config_info = {} - config_info['digest'] = Digester.GetConfigInfo(self) - config_info['archive'] = Archiver.GetConfigInfo(self) - config_info['gateway'] = GatewayManager.GetConfigInfo(self) - config_info['autoreply'] = Autoresponder.GetConfigInfo(self) - - WIDTH = mm_cfg.TEXTFIELDWIDTH - - # Set things up for the language choices - langs = self.GetAvailableLanguages() - langnames = [_(Utils.GetLanguageDescr(L)) for L in langs] - try: - langi = langs.index(self.preferred_language) - except ValueError: - # Someone must have deleted the list's preferred language. Could - # be other trouble lurking! - langi = 0 - - # XXX: Should this text be migrated into the templates dir? - config_info['general'] = [ - _("Fundamental list characteristics, including descriptive" - " info and basic behaviors."), - - ('real_name', mm_cfg.String, WIDTH, 0, - _('The public name of this list (make case-changes only).'), - _('''The capitalization of this name can be changed to make it - presentable in polite company as a proper noun, or to make an - acronym part all upper case, etc. However, the name will be - advertised as the email address (e.g., in subscribe confirmation - notices), so it should <em>not</em> be otherwise altered. (Email - addresses are not case sensitive, but they are sensitive to - almost everything else :-)''')), - - ('owner', mm_cfg.EmailList, (3, WIDTH), 0, - _("""The list administrator email addresses. Multiple - administrator addresses, each on separate line is okay."""), - - _("""There are two ownership roles associated with each mailing - list. The <em>list administrators</em> are the people who have - ultimate control over all parameters of this mailing list. They - are able to change any list configuration variable available - through these administration web pages. - - <p>The <em>list moderators</em> have more limited permissions; - they are not able to change any list configuration variable, but - they are allowed to tend to pending administration requests, - including approving or rejecting held subscription requests, and - disposing of held postings. Of course, the <em>list - administrators</em> can also tend to pending requests. - - <p>In order to split the list ownership duties into - administrators and moderators, you must set a separate moderator - password in the section below, and also provide the email - addresses of the list moderators in this section. Note that the - field you are changing here specifies the list - administators.""")), - - ('moderator', mm_cfg.EmailList, (3, WIDTH), 0, - _("""The list moderator email addresses. Multiple - moderator addresses, each on separate line is okay."""), - - _("""There are two ownership roles associated with each mailing - list. The <em>list administrators</em> are the people who have - ultimate control over all parameters of this mailing list. They - are able to change any list configuration variable available - through these administration web pages. - - <p>The <em>list moderators</em> have more limited permissions; - they are not able to change any list configuration variable, but - they are allowed to tend to pending administration requests, - including approving or rejecting held subscription requests, and - disposing of held postings. Of course, the <em>list - administrators</em> can also tend to pending requests. - - <p>In order to split the list ownership duties into - administrators and moderators, you must set a separate moderator - password in the section below, and also provide the email - addresses of the list moderators in this section. Note that the - field you are changing here specifies the list moderators.""")), - - ('preferred_language', mm_cfg.Select, - (langs, langnames, langi), - 0, - _('Default language for this list.'), - _('''All messages not related to an specific user will be - displayed in this language.''')), - - ('description', mm_cfg.String, WIDTH, 0, - _('A terse phrase identifying this list.'), - - _('''This description is used when the mailing list is listed with - other mailing lists, or in headers, and so forth. It should - be as succinct as you can get it, while still identifying what - the list is.''')), - - ('info', mm_cfg.Text, (7, WIDTH), 0, - _('''An introductory description - a few paragraphs - about the - list. It will be included, as html, at the top of the listinfo - page. Carriage returns will end a paragraph - see the details - for more info.'''), - _("""The text will be treated as html <em>except</em> that - newlines will be translated to <br> - so you can use links, - preformatted text, etc, but don't put in carriage returns except - where you mean to separate paragraphs. And review your changes - - bad html (like some unterminated HTML constructs) can prevent - display of the entire listinfo page.""")), - - ('subject_prefix', mm_cfg.String, WIDTH, 0, - _('Prefix for subject line of list postings.'), - _("""This text will be prepended to subject lines of messages - posted to the list, to distinguish mailing list messages in in - mailbox summaries. Brevity is premium here, it's ok to shorten - long mailing list names to something more concise, as long as it - still identifies the mailing list.""")), - - ('welcome_msg', mm_cfg.Text, (4, WIDTH), 0, - _('''List-specific text prepended to new-subscriber welcome - message'''), - - _("""This value, if any, will be added to the front of the - new-subscriber welcome message. The rest of the welcome message - already describes the important addresses and URLs for the - mailing list, so you don't need to include any of that kind of - stuff here. This should just contain mission-specific kinds of - things, like etiquette policies or team orientation, or that kind - of thing. - - <p>Note that this text will be wrapped, according to the - following rules: - <ul><li>Each paragraph is filled so that no line is longer than - 70 characters. - <li>Any line that begins with whitespace is not filled. - <li>A blank line separates paragraphs. - </ul>""")), - - ('goodbye_msg', mm_cfg.Text, (4, WIDTH), 0, - _('''Text sent to people leaving the list. If empty, no special - text will be added to the unsubscribe message.''')), - - ('reply_goes_to_list', mm_cfg.Radio, - (_('Poster'), _('This list'), _('Explicit address')), 0, - _('''Where are replies to list messages directed? - <tt>Poster</tt> is <em>strongly</em> recommended for most mailing - lists.'''), - - # Details for reply_goes_to_list - _("""This option controls what Mailman does to the - <tt>Reply-To:</tt> header in messages flowing through this - mailing list. When set to <em>Poster</em>, no <tt>Reply-To:</tt> - header is added by Mailman, although if one is present in the - original message, it is not stripped. Setting this value to - either <em>This list</em> or <em>Explicit address</em> causes - Mailman to insert a specific <tt>Reply-To:</tt> header in all - messages, overriding the header in the original message if - necessary (<em>Explicit address</em> inserts the value of <a - href="?VARHELP=general/reply_to_address">reply_to_address</a>). - - <p>There are many reasons not to introduce or override the - <tt>Reply-To:</tt> header. One is that some posters depend on - their own <tt>Reply-To:</tt> settings to convey their valid - return address. Another is that modifying <tt>Reply-To:</tt> - makes it much more difficult to send private replies. See <a - href="http://www.unicom.com/pw/reply-to-harmful.html">`Reply-To' - Munging Considered Harmful</a> for a general discussion of this - issue. See <a - href="http://www.metasystema.org/essays/reply-to-useful.mhtml">Reply-To - Munging Considered Useful</a> for a dissenting opinion. - - <p>Some mailing lists have restricted posting privileges, with a - parallel list devoted to discussions. Examples are `patches' or - `checkin' lists, where software changes are posted by a revision - control system, but discussion about the changes occurs on a - developers mailing list. To support these types of mailing - lists, select <tt>Explicit address</tt> and set the - <tt>Reply-To:</tt> address below to point to the parallel - list.""")), - - ('reply_to_address', mm_cfg.Email, WIDTH, 0, - _('Explicit <tt>Reply-To:</tt> header.'), - # Details for reply_to_address - _("""This is the address set in the <tt>Reply-To:</tt> header - when the <a - href="?VARHELP=general/reply_goes_to_list">reply_goes_to_list</a> - option is set to <em>Explicit address</em>. - - <p>There are many reasons not to introduce or override the - <tt>Reply-To:</tt> header. One is that some posters depend on - their own <tt>Reply-To:</tt> settings to convey their valid - return address. Another is that modifying <tt>Reply-To:</tt> - makes it much more difficult to send private replies. See <a - href="http://www.unicom.com/pw/reply-to-harmful.html">`Reply-To' - Munging Considered Harmful</a> for a general discussion of this - issue. See <a - href="http://www.metasystema.org/essays/reply-to-useful.mhtml">Reply-To - Munging Considered Useful</a> for a dissenting opinion. - - <p>Some mailing lists have restricted posting privileges, with a - parallel list devoted to discussions. Examples are `patches' or - `checkin' lists, where software changes are posted by a revision - control system, but discussion about the changes occurs on a - developers mailing list. To support these types of mailing - lists, specify the explicit <tt>Reply-To:</tt> address here. You - must also specify <tt>Explicit address</tt> in the - <tt>reply_goes_to_list</tt> - variable. - - <p>Note that if the original message contains a - <tt>Reply-To:</tt> header, it will not be changed.""")), - - ('administrivia', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _('''(Administrivia filter) Check postings and intercept ones - that seem to be administrative requests?'''), - - _("""Administrivia tests will check postings to see whether it's - really meant as an administrative request (like subscribe, - unsubscribe, etc), and will add it to the the administrative - requests queue, notifying the administrator of the new request, - in the process.""")), - - ('umbrella_list', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _('''Send password reminders to, eg, "-owner" address instead of - directly to user.'''), - - _("""Set this to yes when this list is intended to cascade only - to other mailing lists. When set, meta notices like - confirmations and password reminders will be directed to an - address derived from the member\'s address - it will have the - value of "umbrella_member_suffix" appended to the member's - account name.""")), - - ('umbrella_member_suffix', mm_cfg.String, WIDTH, 0, - _('''Suffix for use when this list is an umbrella for other - lists, according to setting of previous "umbrella_list" - setting.'''), - - _("""When "umbrella_list" is set to indicate that this list has - other mailing lists as members, then administrative notices like - confirmations and password reminders need to not be sent to the - member list addresses, but rather to the owner of those member - lists. In that case, the value of this setting is appended to - the member's account name for such notices. `-owner' is the - typical choice. This setting has no effect when "umbrella_list" - is "No".""")), - - ('send_reminders', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _('''Send monthly password reminders or no? Overrides the - previous option.''')), - - ('send_welcome_msg', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _('Send welcome message when people subscribe?'), - _("""Turn this on only if you plan on subscribing people manually - and don't want them to know that you did so. This option is most - useful for transparently migrating lists from some other mailing - list manager to Mailman.""")), - - ('admin_immed_notify', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _('''Should administrator get immediate notice of new requests, - as well as daily notices about collected ones?'''), - - _('''List admins are sent daily reminders of pending admin - approval requests, like subscriptions to a moderated list or - postings that are being held for one reason or another. Setting - this option causes notices to be sent immediately on the arrival - of new requests, as well.''')), - - ('admin_notify_mchanges', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _('''Should administrator get notices of subscribes and - unsubscribes?''')), - - ('dont_respond_to_post_requests', mm_cfg.Radio, - (_('Yes'), _('No')), 0, - _('Send mail to poster when their posting is held for approval?'), - - _("""Approval notices are sent when mail triggers certain of the - limits <em>except</em> routine list moderation and spam filters, - for which notices are <em>not</em> sent. This option overrides - ever sending the notice.""")), - - ('max_message_size', mm_cfg.Number, 7, 0, - _('''Maximum length in Kb of a message body. Use 0 for no - limit.''')), - - ('host_name', mm_cfg.Host, WIDTH, 0, - _('Host name this list prefers for email.'), - - _("""The "host_name" is the preferred name for email to - mailman-related addresses on this host, and generally should be - the mail host's exchanger address, if any. This setting can be - useful for selecting among alternative names of a host that has - multiple addresses.""")), - -## ('web_page_url', mm_cfg.String, WIDTH, 0, -## _('''Base URL for Mailman web interface. The URL must end in a -## single "/". See also the details for an important warning when -## changing this value.'''), - -## _("""This is the common root for all Mailman URLs referencing -## this mailing list. It is also used in the listinfo overview of -## mailing lists to identify whether or not this list resides on the -## virtual host identified by the overview URL; i.e. if this value -## is found (anywhere) in the URL, then this list is considered to -## be on that virtual host. If not, then it is excluded from the -## listing. - -## <p><b><font size="+1">Warning:</font></b> setting this value to -## an invalid base URL will render the mailing list unusable. You -## will also not be able to fix this from the web interface! In -## that case, the site administrator will have to fix the mailing -## list from the command line.""")), - ] - if mm_cfg.ALLOW_OPEN_SUBSCRIBE: - sub_cfentry = ('subscribe_policy', mm_cfg.Radio, - # choices - (_('none'), - _('confirm'), - _('require approval'), - _('confirm+approval')), - 0, - _('What steps are required for subscription?<br>'), - _('''None - no verification steps (<em>Not - Recommended </em>)<br> - confirm (*) - email confirmation step required <br> - require approval - require list administrator - approval for subscriptions <br> - confirm+approval - both confirm and approve - - <p>(*) when someone requests a subscription, - Mailman sends them a notice with a unique - subscription request number that they must reply to - in order to subscribe.<br> - - This prevents mischievous (or malicious) people - from creating subscriptions for others without - their consent.''')) - else: - sub_cfentry = ('subscribe_policy', mm_cfg.Radio, - # choices - (_('confirm'), - _('require approval'), - _('confirm+approval')), - 1, - _('What steps are required for subscription?<br>'), - _('''confirm (*) - email confirmation required <br> - require approval - require list administrator - approval for subscriptions <br> - confirm+approval - both confirm and approve - - <p>(*) when someone requests a subscription, - Mailman sends them a notice with a unique - subscription request number that they must reply to - in order to subscribe.<br> This prevents - mischievous (or malicious) people from creating - subscriptions for others without their consent.''')) - - # some helpful values - adminurl = self.GetScriptURL('admin') - - config_info['privacy'] = [ - _("""List access policies, including anti-spam measures, covering - members and outsiders. See also the <a - href="%(admin)s/archive">Archival Options section</a> for separate - archive-privacy settings."""), - - _('Subscribing'), - ('advertised', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _('''Advertise this list when people ask what lists are on this - machine?''')), - - sub_cfentry, - - _("Membership exposure"), - ('private_roster', mm_cfg.Radio, - (_('Anyone'), _('List members'), _('List admin only')), 0, - _('Who can view subscription list?'), - - _('''When set, the list of subscribers is protected by member or - admin password authentication.''')), - - ('obscure_addresses', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _("""Show member addrs so they're not directly recognizable as - email addrs?"""), - _("""Setting this option causes member email addresses to be - transformed when they are presented on list web pages (both in - text and as links), so they're not trivially recognizable as - email addresses. The intention is to to prevent the addresses - from being snarfed up by automated web scanners for use by - spammers.""")), - - _("General posting filters"), - ('moderated', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _('Must posts be approved by an administrator?')), - - ('member_posting_only', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _("""Restrict posting privilege to list members? - (<i>member_posting_only</i>)"""), - - _("""Use this option if you want to restrict posting to list - members. If you want list members to be able to post, plus a - handful of other posters, see the <i> posters </i> setting - below.""")), - - ('posters', mm_cfg.EmailList, (5, WIDTH), 1, - _('''Addresses of members accepted for posting to this list - without implicit approval requirement. (See "Restrict ... to list - members" for whether or not this is in addition to allowing - posting by list members'''), - - _("""Adding entries here will have one of two effects, according - to whether another option restricts posting to members. - - <ul> - <li>If <i>member_posting_only</i> is 'yes', then entries - added here will have posting privilege in addition to list - members. - - <li>If <i>member_posting_only</i> is 'no', then <em>only</em> - the posters listed here will be able to post without admin - approval. - - </ul>""")), - - _("Spam-specific posting filters"), - - ('require_explicit_destination', mm_cfg.Radio, - (_('No'), _('Yes')), 0, - _("""Must posts have list named in destination (to, cc) field - (or be among the acceptable alias names, specified below)?"""), - - _("""Many (in fact, most) spams do not explicitly name their - myriad destinations in the explicit destination addresses - in - fact often the To: field has a totally bogus address for - obfuscation. The constraint applies only to the stuff in the - address before the '@' sign, but still catches all such spams. - - <p>The cost is that the list will not accept unhindered any - postings relayed from other addresses, unless - - <ol> - <li>The relaying address has the same name, or - - <li>The relaying address name is included on the options that - specifies acceptable aliases for the list. - - </ol>""")), - - ('acceptable_aliases', mm_cfg.Text, (4, WIDTH), 0, - _("""Alias names (regexps) which qualify as explicit to or cc - destination names for this list."""), - - _("""Alternate addresses that are acceptable when - `require_explicit_destination' is enabled. This option takes a - list of regular expressions, one per line, which is matched - against every recipient address in the message. The matching is - performed with Python's re.match() function, meaning they are - anchored to the start of the string. - - <p>For backwards compatibility with Mailman 1.1, if the regexp - does not contain an `@', then the pattern is matched against just - the local part of the recipient address. If that match fails, or - if the pattern does contain an `@', then the pattern is matched - against the entire recipient address. - - <p>Matching against the local part is deprecated; in a future - release, the pattern will always be matched against the entire - recipient address.""")), - - ('max_num_recipients', mm_cfg.Number, 5, 0, - _('Ceiling on acceptable number of recipients for a posting.'), - - _('''If a posting has this number, or more, of recipients, it is - held for admin approval. Use 0 for no ceiling.''')), - - ('forbidden_posters', mm_cfg.EmailList, (5, WIDTH), 1, - _('Addresses whose postings are always held for approval.'), - _('''Email addresses whose posts should always be held for - approval, no matter what other options you have set. See also - the subsequent option which applies to arbitrary content of - arbitrary headers.''')), - - ('bounce_matching_headers', mm_cfg.Text, (6, WIDTH), 0, - _('Hold posts with header value matching a specified regexp.'), - _("""Use this option to prohibit posts according to specific - header values. The target value is a regular-expression for - matching against the specified header. The match is done - disregarding letter case. Lines beginning with '#' are ignored - as comments. - - <p>For example:<pre>to: .*@public.com </pre> says to hold all - postings with a <em>To:</em> mail header containing '@public.com' - anywhere among the addresses. - - <p>Note that leading whitespace is trimmed from the regexp. This - can be circumvented in a number of ways, e.g. by escaping or - bracketing it. - - <p> See also the <em>forbidden_posters</em> option for a related - mechanism.""")), - - ('anonymous_list', mm_cfg.Radio, (_('No'), _('Yes')), 0, - _("""Hide the sender of a message, replacing it with the list - address (Removes From, Sender and Reply-To fields)""")), - ] - - config_info['nondigest'] = [ - _("Policies concerning immediately delivered list traffic."), + + # + # Web API support via administrative categories + # + def GetConfigCategories(self): + class CategoryDict(UserDict): + def __init__(self): + UserDict.__init__(self) + self.keysinorder = mm_cfg.ADMIN_CATEGORIES[:] + def keys(self): + return self.keysinorder + def items(self): + items = [] + for k in mm_cfg.ADMIN_CATEGORIES: + items.append((k, self.data[k])) + return items + def values(self): + values = [] + for k in mm_cfg.ADMIN_CATEGORIES: + values.append(self.data[k]) + return values - ('nondigestable', mm_cfg.Toggle, (_('No'), _('Yes')), 1, - _("""Can subscribers choose to receive mail immediately, rather - than in batched digests?""")), + categories = CategoryDict() + # Only one level of mixin inheritance allowed + for gui in self._gui: + k, v = gui.GetConfigCategory() + categories[k] = (v, gui) + return categories - ('msg_header', mm_cfg.Text, (4, WIDTH), 0, - _('Header added to mail sent to regular list members'), - _('''Text prepended to the top of every immediately-delivery - message. ''') + Utils.maketext('headfoot.html', - raw=1, mlist=self)), - - ('msg_footer', mm_cfg.Text, (4, WIDTH), 0, - _('Footer added to mail sent to regular list members'), - _('''Text appended to the bottom of every immediately-delivery - message. ''') + Utils.maketext('headfoot.html', - raw=1, mlist=self)), - ] + def GetConfigSubCategories(self, category): + for gui in self._gui: + if hasattr(gui, 'GetConfigSubCategories'): + # Return the first one that knows about the given subcategory + subcat = gui.GetConfigSubCategories(category) + if subcat is not None: + return subcat + return None - config_info['bounce'] = Bouncer.GetConfigInfo(self) - return config_info + def GetConfigInfo(self): + info = {} + for gui in self._gui: + try: + key = gui.GetConfigCategory()[0] + value = gui.GetConfigInfo(self) + except AttributeError: + pass + else: + info[key] = value + return info + + # + # List creation + # def Create(self, name, admin, crypted_password): if Utils.list_exists(name): raise Errors.MMListAlreadyExistsError, name @@ -897,6 +388,10 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, self.CheckValues() self.Save() + + # + # Database and filesystem I/O + # def __save(self, dict): # Marshal this dictionary to file, and rotate the old version to a # backup file. The dictionary must contain only builtin objects. We @@ -944,8 +439,9 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, # copy all public attributes to marshalable dictionary dict = {} for key, value in self.__dict__.items(): - if key[0] <> '_': - dict[key] = value + if key[0] == '_' or type(value) is MethodType: + continue + dict[key] = value # Make config.db unreadable by `other', as it contains all the # list members' passwords (in clear text). omask = os.umask(007) @@ -1014,6 +510,10 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, self.CheckValues() self.CheckVersion(dict) + + # + # Sanity checks + # def CheckVersion(self, stored_state): """Migrate prior version's state to new structure, if changed.""" if (self.data_version >= mm_cfg.DATA_FILE_VERSION and @@ -1042,23 +542,19 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, if not self._ready: raise Errors.MMListNotReadyError + + # + # Membership management front-ends and assertion checks + # def AddMember(self, name, password, digest=0, remote=None, lang=None): - self.IsListInitialized() - # normalize the name, it could be of the form - # - # <person@place.com> User Name - # person@place.com (User Name) - # etc - # - name = Utils.ParseAddrs(name) - # Remove spaces... it's a common thing for people to add... - name = EMPTYSTRING.join(name.split()) - # lower case only the domain part - name = Utils.LCDomain(name) - + # Add a new member, after sanity checking the address, and based on + # the mailing list's policies. + fullname, emailaddr = parseaddr(name) + # Lowercase the domain part + emailaddr = Utils.LCDomain(emailaddr) # Validate the e-mail address to some degree. Utils.ValidateEmail(name) - if self.IsMember(name): + if self.isMember(name): raise Errors.MMAlreadyAMember if name == self.GetListEmail().lower(): # Trying to subscribe the list to itself! @@ -1073,25 +569,25 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, lang = self.preferred_language if self.subscribe_policy == 0: - # no confirmation or approval necessary: - self.ApprovedAddMember(name, password, digest, lang) + # No confirmation or approval is necessary + self.ApprovedAddMember(emailaddr, fullname, password, digest, lang) elif self.subscribe_policy == 1 or self.subscribe_policy == 3: # User confirmation required cookie = Pending.new(Pending.SUBSCRIPTION, - name, password, digest, lang) + emailaddr, fullname, password, digest, lang) if remote is not None: - by = " " + remote - remote = _(" from %(remote)s") + by = ' ' + remote + remote = _(' from %(remote)s') else: - by = "" - remote = "" - recipient = self.GetMemberAdminEmail(name) + by = '' + remote = '' + recipient = self.GetMemberAdminEmail(emailaddr) realname = self.real_name confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1), cookie) text = Utils.maketext( 'verify.txt', - {'email' : name, + {'email' : emailaddr, 'listaddr' : self.GetListEmail(), 'listname' : realname, 'cookie' : cookie, @@ -1106,19 +602,180 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, text) msg['Reply-To'] = self.GetRequestEmail() msg.send(self) - if recipient != name: - who = "%s (%s)" % (name, recipient.split('@')[0]) - else: who = name + if recipient <> emailaddr: + who = "%s (%s)" % (emailaddr, recipient.split('@')[0]) + else: + who = name syslog('subscribe', '%s: pending %s %s', self.internal_name(), who, by) raise Errors.MMSubscribeNeedsConfirmation else: - # subscription approval is required. add this entry to the admin + # Subscription approval is required. Add this entry to the admin # requests database. - self.HoldSubscription(name, password, digest, lang) + self.HoldSubscription(emailaddr, fullname, password, digest, lang) raise Errors.MMNeedApproval, _( 'subscriptions to %(realname)s require administrator approval') + def ApprovedAddMember(self, name, password, digest, + ack=None, admin_notif=None, lang=None): + # A convenience interface on ApprovedAddMembers (plural) + res = self.ApprovedAddMembers([name], [password], + digest, ack, admin_notif, lang) + # There should be exactly one (key, value) pair in the returned dict, + # extract the possible exception value + assert len(res) == 1 + res = res.values()[0] + if res is None: + # User was added successfully + return + else: + # Split up the exception list and reraise it here + e, v = res + raise e, v + + def ApprovedAddMembers(self, names, passwords, digest, + ack=None, admin_notif=None, lang=None): + """Subscribe members in list `names'. + + Passwords can be supplied in the passwords list. If an empty + password is encountered, a random one is generated and used. + + Returns a dict where the keys are addresses that were tried + subscribed, and the corresponding values are either two-element + tuple containing the first exception type and value that was + raised when trying to add that address, or `None' to indicate + that no exception was raised. + + """ + if ack is None: + ack = self.send_welcome_msg + if admin_notif is None: + admin_notif = self.admin_notify_mchanges + if lang is None: + lang = self.preferred_language + + # Make sure our passwords vector is the same size as our names vector + if type(passwords) is not ListType: + # TypeError? -- For now, ignore whatever value(s) we were given + passwords = [None] * len(names) + lenpws = len(passwords) + lennames = len(names) + if lenpws < lennames: + passwords.extend([None] * (lennames - lenpws)) + + result = {} + fullnames = {} + for name, pw in zip(names, passwords): + # Dig out the full name and the email address, lowercase the + # domain part, and validate + fullname, emailaddr = parseaddr(name) + emailaddr = Utils.LCDomain(emailaddr) + fullnames[name] = fullname, emailaddr + try: + Utils.ValidateEmail(emailaddr) + except (Errors.MMBadEmailError, Errors.MMHostileAddress), e: + # We don't really need the traceback object for the exception. + result[name] = e + # WIBNI we could `continue' within `try' constructs... + if result.has_key(name): + continue + if self.isMember(emailaddr): + result[name] = [Errors.MMAlreadyAMember, name] + continue + # Make sure we set a moderately "good" password + if not pw: + pw = Utils.MakeRandomPassword() + self.addNewMember(emailaddr, realname=fullname, digest=digest, + password=pw, language=lang) + self.setMemberOption(name, mm_cfg.DisableMime, + 1 - self.mime_is_default_digest) + result[name] = None + + # Now send and log results + if not result: + return + + if digest: + kind = ' (digest)' + else: + kind = '' + + for name in result.keys(): + if result[name] is None: + fullname, emailaddr = fullnames[name] + syslog('subscribe', '%s: new%s %s (%s)', + self.internal_name(), kind, emailaddr, fullname) + if ack: + self.SendSubscribeAck( + emailaddr, self.getMemberPassword(emailaddr), digest) + if admin_notif: + adminaddr = self.GetAdminEmail() + realname = self.real_name + subject = _('%(realname)s subscription notification') + text = Utils.maketext( + "adminsubscribeack.txt", + {"listname" : self.real_name, + "member" : name, + }, lang=lang, mlist=self) + msg = Message.UserNotification( + self.owner, mm_cfg.MAILMAN_OWNER, subject, text) + msg.send(self) + return result + + def ApprovedDeleteMember(self, name, whence=None, + admin_notif=None, userack=1): + if admin_notif is None: + admin_notif = self.admin_notify_mchanges + + # Delete a member, for which we know the approval has been made + fullname, emailaddr = parseaddr(name) + if not self.isMember(emailaddr): + raise Errors.MMNoSuchUserError + # Remove the member + self.removeMember(emailaddr) + # And send an acknowledgement to the user... + if userack and self.goodbye_msg and len(self.goodbye_msg): + self.SendUnsubscribeAck(name) + # ...and to the administrator + if admin_notif: + realname = self.real_name + subject = _('%(realname)s unsubscribe notification') + text = Utils.maketext( + 'adminunsubscribeack.txt', + {'member' : name, + 'listname': self.real_name, + }, mlist=self) + msg = Message.UserNotification(self.owner, + mm_cfg.MAILMAN_OWNER, + subject, text) + msg.send(self) + if whence: + whence = "; %s" % whence + else: + whence = "" + syslog('subscribe', '%s: deleted %s%s', + self.internal_name(), name, whence) + + def ChangeMemberName(self, addr, name, globally): + self.setMemberName(addr, name) + if not globally: + return + for listname in Utils.list_names(): + # Don't bother with ourselves + if listname == self.internal_name(): + continue + mlist = MailList(listname, lock=0) + if mlist.host_name <> self.host_name: + continue + if not mlist.isMember(oldaddr): + continue + mlist.Lock() + try: + mlist.setMemberName(addr, name) + mlist.Save() + finally: + mlist.Unlock() + def ChangeMemberAddress(self, oldaddr, newaddr, globally): # Changing a member address consists of verifying the new address, # making sure the new address isn't already a member, and optionally @@ -1127,7 +784,10 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, # Most of these checks are copied from AddMember newaddr = Utils.LCDomain(newaddr) Utils.ValidateEmail(newaddr) - if self.IsMember(newaddr): + # Raise an exception if this email address is already a member of the + # list, but only if the new address is the same case-wise as the old + # address. + if newaddr == oldaddr and self.isMember(newaddr): raise Errors.MMAlreadyAMember if newaddr == self.GetListEmail().lower(): raise Errors.MMBadEmailError @@ -1155,6 +815,34 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, msg['Reply-To'] = self.GetRequestEmail() msg.send(self) + def ApprovedChangeMemberAddress(self, oldaddr, newaddr, globally): + # Change the membership for the current list first. We don't lock and + # save ourself since we assume that the list is already locked. + self.changeMemberAddress(oldaddr, newaddr) + # If globally is true, then we also include every list for which + # oldaddr is a member. + if not globally: + return + for listname in Utils.list_names(): + # Don't bother with ourselves + if listname == self.internal_name(): + continue + mlist = MailList(listname, lock=0) + if mlist.host_name <> self.host_name: + continue + if not mlist.isMember(oldaddr): + continue + mlist.Lock() + try: + mlist.changeMemberAddress(oldaddr, newaddr) + mlist.Save() + finally: + mlist.Unlock() + + + # + # Confirmation processing + # def ProcessConfirmation(self, cookie): data = Pending.confirm(cookie) if data is None: @@ -1179,7 +867,7 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, elif op == Pending.UNSUBSCRIPTION: addr = data[0] # Can raise MMNoSuchUserError if they unsub'd via other means - self.DeleteMember(addr, whence='web confirmation') + self.ApprovedDeleteMember(addr, whence='web confirmation') return op, addr elif op == Pending.CHANGE_OF_ADDRESS: oldaddr, newaddr, globally = data @@ -1217,227 +905,10 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, msg['Reply-To'] = self.GetRequestEmail() msg.send(self) - def ApprovedAddMember(self, name, password, digest, - ack=None, admin_notif=None, lang=None): - res = self.ApprovedAddMembers([name], [password], - digest, ack, admin_notif, lang) - # There should be exactly one (key, value) pair in the returned dict, - # extract the possible exception value - res = res.values()[0] - if res is None: - # User was added successfully - return - else: - # Split up the exception list and reraise it here - e, v = res - raise e, v - - def ApprovedAddMembers(self, names, passwords, digest, - ack=None, admin_notif=None, lang=None): - """Subscribe members in list `names'. - - Passwords can be supplied in the passwords list. If an empty - password is encountered, a random one is generated and used. - - Returns a dict where the keys are addresses that were tried - subscribed, and the corresponding values are either two-element - tuple containing the first exception type and value that was - raised when trying to add that address, or `None' to indicate - that no exception was raised. - - """ - if ack is None: - if self.send_welcome_msg: - ack = 1 - else: - ack = 0 - if admin_notif is None: - if self.admin_notify_mchanges: - admin_notif = 1 - else: - admin_notif = 0 - if type(passwords) is not ListType: - # Type error -- ignore whatever value(s) we were given - passwords = [None] * len(names) - lenpws = len(passwords) - lennames = len(names) - if lenpws < lennames: - passwords.extend([None] * (lennames - lenpws)) - result = {} - dirty = 0 - for i in range(lennames): - try: - # normalize the name, it could be of the form - # - # <person@place.com> User Name - # person@place.com (User Name) - # etc - # - name = Utils.ParseAddrs(names[i]) - Utils.ValidateEmail(name) - name = Utils.LCDomain(name) - except (Errors.MMBadEmailError, Errors.MMHostileAddress): - # We don't really need the traceback object for the exception, - # and as using it in the wrong way prevents garbage collection - # from working smoothly, we strip it away - result[name] = sys.exc_info()[:2] - # WIBNI we could `continue' within `try' constructs... - if result.has_key(name): - continue - if self.IsMember(name): - result[name] = [Errors.MMAlreadyAMember, name] - continue - self.__AddMember(name, digest) - if lang is None: - lang = self.preferred_language - self.SetPreferredLanguage(name, lang) - self.SetUserOption(name, mm_cfg.DisableMime, - 1 - self.mime_is_default_digest, - save_list=0) - # Make sure we set a "good" password - password = passwords[i] - if not password: - password = Utils.MakeRandomPassword() - self.passwords[name.lower()] = password - # An address has been added successfully, make sure the - # list config is saved later on - dirty = 1 - result[name] = None - - if dirty: - self.Save() - if digest: - kind = " (D)" - else: - kind = "" - for name in result.keys(): - if result[name] is None: - syslog('subscribe', '%s: new%s %s', - self.internal_name(), kind, name) - if ack: - self.SendSubscribeAck( - name, - self.passwords[name.lower()], - digest) - if admin_notif: - adminaddr = self.GetAdminEmail() - realname = self.real_name - subject = _('%(realname)s subscription notification') - text = Utils.maketext( - "adminsubscribeack.txt", - {"listname" : self.real_name, - "member" : name, - }, lang=lang, mlist=self) - msg = Message.UserNotification( - self.owner, mm_cfg.MAILMAN_OWNER, subject, text) - msg.send(self) - return result - - def DeleteMember(self, name, whence=None, admin_notif=None, userack=1): - self.IsListInitialized() - # FindMatchingAddresses *should* never return more than 1 address. - # However, should log this, just to make sure. - aliases = Utils.FindMatchingAddresses(name, self.members, - self.digest_members) - if not len(aliases): - raise Errors.MMNoSuchUserError - - def DoActualRemoval(alias, me=self): - kind = "(unfound)" - try: - del me.passwords[alias] - except KeyError: - pass - if me.user_options.has_key(alias): - del me.user_options[alias] - try: - del me.members[alias] - kind = "regular" - except KeyError: - pass - try: - del me.digest_members[alias] - kind = "digest" - except KeyError: - pass - if me.language.has_key(alias): - del me.language[alias] - - map(DoActualRemoval, aliases) - if userack and self.goodbye_msg and len(self.goodbye_msg): - self.SendUnsubscribeAck(name) - self.ClearBounceInfo(name) - self.Save() - if admin_notif is None: - if self.admin_notify_mchanges: - admin_notif = 1 - else: - admin_notif = 0 - if admin_notif: - realname = self.real_name - subject = _('%(realname)s unsubscribe notification') - text = Utils.maketext( - 'adminunsubscribeack.txt', - {'member' : name, - 'listname': self.real_name, - }, mlist=self) - msg = Message.UserNotification(self.owner, - mm_cfg.MAILMAN_OWNER, - subject, text) - msg.send(self) - if whence: - whence = "; %s" % whence - else: - whence = "" - syslog('subscribe', '%s: deleted %s%s', - self.internal_name(), name, whence) - - def ApprovedChangeMemberAddress(self, oldaddr, newaddr, globally): - # Get the user's current options and password. Ugly hack: if a user's - # options would have been zero, then Mailman saves room by deleting - # the entry for the user from the user_options dictionary. Note that - # /really/ it would be better if GetUserOption() and SetUserOption() - # supported an interface to get the entire option value. - flags = self.user_options.get(oldaddr, 0) - password = self.passwords[oldaddr] - digest = oldaddr in self.GetDigestMembers() - lang = self.GetPreferredLanguage(oldaddr) - self.ApprovedAddMember(newaddr, password, digest, - ack=1, admin_notif=1, lang=lang) - # hack the account flags - if not flags: - try: - del self.user_options[newaddr] - except KeyError: - pass - else: - self.user_options[newaddr] = flags - # Delete the old address - self.DeleteMember(oldaddr, userack=1) - # Now, if the globally flag was set, then try to do this for every - # mailing list that had the oldaddr as a member. - if not globally: - return - for listname in Utils.list_names(): - # Don't bother with ourselves - if listname == self.internal_name(): - continue - mlist = MailList(listname, lock=0) - if mlist.host_name <> self.host_name: - continue - if not mlist.IsMember(oldaddr): - continue - mlist.Lock() - try: - mlist.ApprovedChangeMemberAddress(oldaddr, newaddr, 0) - mlist.Save() - finally: - mlist.Unlock() - - def IsMember(self, address): - return len(Utils.FindMatchingAddresses(address, self.members, - self.digest_members)) - + + # + # Miscellaneous stuff + # def HasExplicitDest(self, msg): """True if list name or any acceptable_alias is included among the to or cc addrs.""" @@ -1542,37 +1013,10 @@ bad regexp in bounce_matching_header line: %s return line return 0 - def Locked(self): - return self.__lock.locked() - - def Lock(self, timeout=0): - self.__lock.lock(timeout) - # Must reload our database for consistency. Watch out for lists that - # don't exist. - try: - self.Load() - except Errors.MMUnknownListError: - self.Unlock() - raise - - def Unlock(self): - self.__lock.unlock(unconditionally=1) - - def __repr__(self): - if self.Locked(): - status = " (locked)" - else: - status = "" - return ("<%s.%s %s%s at %s>" % - (self.__module__, self.__class__.__name__, - `self._internal_name`, status, hex(id(self))[2:])) - - def internal_name(self): - return self._internal_name - - def fullpath(self): - return self._full_path - + + # + # Multilingual (i18n) support + # def SetPreferredLanguage(self, name, lang): assert lang in self.GetAvailableLanguages() lcname = name.lower() @@ -1582,7 +1026,9 @@ bad regexp in bounce_matching_header line: %s if self.language.has_key(lcname): del self.language[lcname] - def GetPreferredLanguage(self, name): + def GetPreferredLanguage(self, name=None): + if name is None: + return self.preferred_language lcname = name.lower() return self.language.get(lcname, self.preferred_language) |
