diff options
| author | bwarsaw | 2006-07-08 17:58:13 +0000 |
|---|---|---|
| committer | bwarsaw | 2006-07-08 17:58:13 +0000 |
| commit | f321ff8f419284c32f7eea4e06c83212bccef6b0 (patch) | |
| tree | 7e1d1e1a1b8b81a48d86afb5c47eb039529993ac | |
| parent | 7a94dcd001240e0c06cc4b50017b8bfd097d9ff4 (diff) | |
| download | mailman-f321ff8f419284c32f7eea4e06c83212bccef6b0.tar.gz mailman-f321ff8f419284c32f7eea4e06c83212bccef6b0.tar.zst mailman-f321ff8f419284c32f7eea4e06c83212bccef6b0.zip | |
33 files changed, 370 insertions, 468 deletions
diff --git a/Mailman/Archiver/Archiver.py b/Mailman/Archiver/Archiver.py index 0089ee4dc..8c8dcd238 100644 --- a/Mailman/Archiver/Archiver.py +++ b/Mailman/Archiver/Archiver.py @@ -12,8 +12,8 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + # USA. """Mixin class for putting new messages in the right place for archival. @@ -30,12 +30,12 @@ import traceback from cStringIO import StringIO -from Mailman import Site +from Mailman import Mailbox from Mailman import Utils from Mailman import mm_cfg -from Mailman import Mailbox -from Mailman.i18n import _ from Mailman.SafeDict import SafeDict +from Mailman.configuration import config +from Mailman.i18n import _ log = logging.getLogger('mailman.error') @@ -125,7 +125,9 @@ class Archiver: os.umask(omask) def archive_dir(self): - return Site.get_archpath(self.internal_name()) + # Return the private archive directory + return os.path.join(config.PRIVATE_ARCHIVE_FILE_DIR, + self.fqdn_listname) def ArchiveFileName(self): """The mbox name where messages are left for archive construction.""" @@ -225,7 +227,8 @@ class Archiver: if mm_cfg.ARCHIVE_TO_MBOX == -1: # Archiving is completely disabled, don't require the skeleton. return - pubdir = Site.get_archpath(self.internal_name(), public=True) + pubdir = os.path.join(config.PUBLIC_ARCHIVE_FILE_DIR, + self.fqdn_listname) privdir = self.archive_dir() pubmbox = pubdir + '.mbox' privmbox = privdir + '.mbox' diff --git a/Mailman/Bouncer.py b/Mailman/Bouncer.py index 99774b398..1a5d10a8a 100644 --- a/Mailman/Bouncer.py +++ b/Mailman/Bouncer.py @@ -186,7 +186,6 @@ class Bouncer: # it was of dubious value). However, we'll provide empty, strange, or # meaningless strings for the unused %()s fields so that the language # translators don't have to provide new templates. - siteowner = Utils.get_site_email(self.host_name) text = Utils.maketext( 'bounce.txt', {'listname' : self.real_name, @@ -195,11 +194,12 @@ class Bouncer: 'did' : _('disabled'), 'but' : '', 'reenable' : '', - 'owneraddr': siteowner, + 'owneraddr': self.GetNoReplyEmail(), }, mlist=self) subject = _('Bounce action notification') umsg = Message.UserNotification(self.GetOwnerEmail(), - siteowner, subject, + self.GetNoReplyEmail(), + subject, lang=self.preferred_language) # BAW: Be sure you set the type before trying to attach, or you'll get # a MultipartConversionError. diff --git a/Mailman/Cgi/admin.py b/Mailman/Cgi/admin.py index aa1cfc846..3ba876340 100644 --- a/Mailman/Cgi/admin.py +++ b/Mailman/Cgi/admin.py @@ -205,7 +205,7 @@ def admin_overview(msg=''): # # This page should be displayed in the server's default language, which # should have already been set. - hostname = Utils.get_domain() + hostname = Utils.get_request_domain() legend = _('%(hostname)s mailing lists - Admin Links') # The html `document' doc = Document() @@ -222,11 +222,10 @@ def admin_overview(msg=''): listnames.sort() for name in listnames: - mlist = MailList.MailList(name, lock=0) + mlist = MailList.MailList(name, lock=False) if mlist.advertised: - if mm_cfg.VIRTUAL_HOST_OVERVIEW and \ - mlist.web_page_url.find(hostname) == -1: - # List is for different identity of this host - skip it. + if hostname not in mlist.web_page_url: + # This list is situated in a different virtual domain continue else: advertised.append((mlist.GetScriptURL('admin'), @@ -255,7 +254,7 @@ def admin_overview(msg=''): ]) creatorurl = Utils.ScriptURL('create') - mailman_owner = Utils.get_site_email() + mailman_owner = Utils.get_site_noreply() extra = msg and _('right ') or '' welcome.extend([ _('''To visit the administrators configuration page for an @@ -408,9 +407,7 @@ def show_results(mlist, doc, category, subcat, cgidata): otherlinks.AddItem(Link(mlist.GetBaseArchiveURL(), _('Go to list archives')).Format() + '<br> <br>') - # We do not allow through-the-web deletion of the site list! - if mm_cfg.OWNERS_CAN_DELETE_THEIR_OWN_LISTS and \ - mlist.internal_name() <> mm_cfg.MAILMAN_SITE_LIST: + if mm_cfg.OWNERS_CAN_DELETE_THEIR_OWN_LISTS: otherlinks.AddItem(Link(mlist.GetScriptURL('rmlist'), _('Delete this mailing list')).Format() + _(' (requires confirmation)<br> <br>')) diff --git a/Mailman/Cgi/create.py b/Mailman/Cgi/create.py index 767400569..da66c2f7e 100644 --- a/Mailman/Cgi/create.py +++ b/Mailman/Cgi/create.py @@ -25,16 +25,16 @@ import signal import logging from Mailman import Errors -from Mailman import i18n from Mailman import MailList from Mailman import Message -from Mailman import mm_cfg - +from Mailman import i18n +from Mailman.configuration import config from Mailman.htmlformat import * # Set up i18n _ = i18n._ -i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) +i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) +__i18n_templates__ = True log = logging.getLogger('mailman.error') @@ -42,7 +42,7 @@ log = logging.getLogger('mailman.error') def main(): doc = Document() - doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) + doc.set_language(config.DEFAULT_SERVER_LANGUAGE) cgidata = cgi.FieldStorage() parts = Utils.GetPathPieces() @@ -75,42 +75,35 @@ def main(): def process_request(doc, cgidata): - # Lowercase the listname since this is treated as the "internal" name. + # Lowercase the listname since this is treated as the 'internal' name. listname = cgidata.getvalue('listname', '').strip().lower() owner = cgidata.getvalue('owner', '').strip() try: - autogen = int(cgidata.getvalue('autogen', '0')) + autogen = bool(cgidata.getvalue('autogen', '0')) except ValueError: - autogen = 0 + autogen = False try: - notify = int(cgidata.getvalue('notify', '0')) + notify = bool(cgidata.getvalue('notify', '0')) except ValueError: - notify = 0 + notify = False try: - moderate = int(cgidata.getvalue('moderate', - mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION)) + moderate = bool(cgidata.getvalue('moderate', + config.DEFAULT_DEFAULT_MEMBER_MODERATION)) except ValueError: - moderate = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION + moderate = config.DEFAULT_DEFAULT_MEMBER_MODERATION password = cgidata.getvalue('password', '').strip() confirm = cgidata.getvalue('confirm', '').strip() auth = cgidata.getvalue('auth', '').strip() - langs = cgidata.getvalue('langs', [mm_cfg.DEFAULT_SERVER_LANGUAGE]) + langs = cgidata.getvalue('langs', [config.DEFAULT_SERVER_LANGUAGE]) if not isinstance(langs, list): langs = [langs] - # Sanity check + # Sanity checks safelistname = Utils.websafe(listname) if '@' in listname: request_creation(doc, cgidata, - _('List name must not include "@": %(safelistname)s')) - return - if Utils.list_exists(listname): - # BAW: should we tell them the list already exists? This could be - # used to mine/guess the existance of non-advertised lists. Then - # again, that can be done in other ways already, so oh well. - request_creation(doc, cgidata, - _('List already exists: %(safelistname)s')) + _('List name must not include "@": $safelistname')) return if not listname: request_creation(doc, cgidata, @@ -120,17 +113,16 @@ def process_request(doc, cgidata): request_creation(doc, cgidata, _('You forgot to specify the list owner')) return - if autogen: if password or confirm: request_creation( doc, cgidata, - _('''Leave the initial password (and confirmation) fields + _("""Leave the initial password (and confirmation) fields blank if you want Mailman to autogenerate the list - passwords.''')) + passwords.""")) return password = confirm = Utils.MakeRandomPassword( - mm_cfg.ADMIN_PASSWORD_LENGTH) + config.ADMIN_PASSWORD_LENGTH) else: if password <> confirm: request_creation(doc, cgidata, @@ -147,9 +139,9 @@ def process_request(doc, cgidata): return # The authorization password must be non-empty, and it must match either # the list creation password or the site admin password - ok = 0 + ok = False if auth: - ok = Utils.check_global_password(auth, 0) + ok = Utils.check_global_password(auth, False) if not ok: ok = Utils.check_global_password(auth) if not ok: @@ -157,27 +149,26 @@ def process_request(doc, cgidata): doc, cgidata, _('You are not authorized to create new mailing lists')) return - # Make sure the web hostname matches one of our virtual domains - hostname = Utils.get_domain() - if mm_cfg.VIRTUAL_HOST_OVERVIEW and \ - not mm_cfg.VIRTUAL_HOSTS.has_key(hostname): - safehostname = Utils.websafe(hostname) + # Make sure the url host name matches one of our virtual domains. Then + # calculate the list's posting address. + url_host = Utils.get_request_domain() + email_host = config.get_email_host(url_host) + if not email_host: + safehostname = Utils.websafe(email_host) request_creation(doc, cgidata, - _('Unknown virtual host: %(safehostname)s')) + _('Unknown virtual host: $safehostname')) return - emailhost = mm_cfg.VIRTUAL_HOSTS.get(hostname, mm_cfg.DEFAULT_EMAIL_HOST) + fqdn_listname = '%s@%s' % (listname, email_host) # We've got all the data we need, so go ahead and try to create the list - # See admin.py for why we need to set up the signal handler. mlist = MailList.MailList() - - def sigterm_handler(signum, frame, mlist=mlist): + # See admin.py for why we need to set up the signal handler. + def sigterm_handler(signum, frame): # Make sure the list gets unlocked... mlist.Unlock() # ...and ensure we exit, otherwise race conditions could cause us to # enter MailList.Save() while we're in the unlocked state, and that # could be bad! sys.exit(0) - try: # Install the emergency shutdown signal handler signal.signal(signal.SIGTERM, sigterm_handler) @@ -189,33 +180,29 @@ def process_request(doc, cgidata): oldmask = os.umask(002) try: try: - mlist.Create(listname, owner, pw, langs, emailhost) + mlist.Create(fqdn_listname, owner, pw, langs) finally: os.umask(oldmask) except Errors.EmailAddressError, s: - request_creation(doc, cgidata, - _('Bad owner email address: %(s)s')) + request_creation(doc, cgidata, _('Bad owner email address: $s')) return except Errors.MMListAlreadyExistsError: + safelistname = Utils.websafe(listname) request_creation(doc, cgidata, - _('List already exists: %(listname)s')) + _('List already exists: $safelistname')) return except Errors.BadListNameError, s: - request_creation(doc, cgidata, - _('Illegal list name: %(s)s')) + request_creation(doc, cgidata, _('Illegal list name: $s')) return except Errors.MMListError: request_creation( doc, cgidata, - _('''Some unknown error occurred while creating the list. - Please contact the site administrator for assistance.''')) + _("""Some unknown error occurred while creating the list. + Please contact the site administrator for assistance.""")) return - # Initialize the host_name and web_page_url attributes, based on # virtual hosting settings and the request environment variables. mlist.default_member_moderation = moderate - mlist.web_page_url = mm_cfg.DEFAULT_URL_PATTERN % hostname - mlist.host_name = emailhost mlist.Save() finally: # Now be sure to unlock the list. It's okay if we get a signal here @@ -223,34 +210,31 @@ def process_request(doc, cgidata): # unlocking is unconditional, so it's not an error if we unlock while # we're already unlocked. mlist.Unlock() - # Now do the MTA-specific list creation tasks - if mm_cfg.MTA: - modname = 'Mailman.MTA.' + mm_cfg.MTA + if config.MTA: + modname = 'Mailman.MTA.' + config.MTA __import__(modname) - sys.modules[modname].create(mlist, cgi=1) - + sys.modules[modname].create(mlist, cgi=True) # And send the notice to the list owner. if notify: - siteowner = Utils.get_site_email(mlist.host_name, 'owner') + siteowner = mlist.GetNoReplyEmail() text = Utils.maketext( 'newlist.txt', {'listname' : listname, 'password' : password, - 'admin_url' : mlist.GetScriptURL('admin', absolute=1), - 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1), + 'admin_url' : mlist.GetScriptURL('admin', absolute=True), + 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=True), 'requestaddr' : mlist.GetRequestEmail(), 'siteowner' : siteowner, }, mlist=mlist) msg = Message.UserNotification( owner, siteowner, - _('Your new mailing list: %(listname)s'), + _('Your new mailing list: $listname'), text, mlist.preferred_language) msg.send(mlist) - # Success! - listinfo_url = mlist.GetScriptURL('listinfo', absolute=1) - admin_url = mlist.GetScriptURL('admin', absolute=1) + listinfo_url = mlist.GetScriptURL('listinfo', absolute=True) + admin_url = mlist.GetScriptURL('admin', absolute=True) create_url = Utils.ScriptURL('create') title = _('Mailing list creation results') @@ -258,10 +242,10 @@ def process_request(doc, cgidata): table = Table(border=0, width='100%') table.AddRow([Center(Bold(FontAttr(title, size='+1')))]) table.AddCellInfo(table.GetCurrentRowIndex(), 0, - bgcolor=mm_cfg.WEB_HEADER_COLOR) - table.AddRow([_('''You have successfully created the mailing list - <b>%(listname)s</b> and notification has been sent to the list owner - <b>%(owner)s</b>. You can now:''')]) + bgcolor=config.WEB_HEADER_COLOR) + table.AddRow([_("""You have successfully created the mailing list + <b>$listname</b> and notification has been sent to the list owner + <b>$owner</b>. You can now:""")]) ullist = UnorderedList() ullist.AddItem(Link(listinfo_url, _("Visit the list's info page"))) ullist.AddItem(Link(admin_url, _("Visit the list's admin page"))) @@ -281,14 +265,14 @@ dummy = Dummy() def request_creation(doc, cgidata=dummy, errmsg=None): # What virtual domain are we using? - hostname = Utils.get_domain() + hostname = Utils.get_request_domain() # Set up the document - title = _('Create a %(hostname)s Mailing List') + title = _('Create a $hostname Mailing List') doc.SetTitle(title) table = Table(border=0, width='100%') table.AddRow([Center(Bold(FontAttr(title, size='+1')))]) table.AddCellInfo(table.GetCurrentRowIndex(), 0, - bgcolor=mm_cfg.WEB_HEADER_COLOR) + bgcolor=config.WEB_HEADER_COLOR) # Add any error message if errmsg: table.AddRow([Header(3, Bold( @@ -315,7 +299,7 @@ def request_creation(doc, cgidata=dummy, errmsg=None): password can also be used for authentication. """)]) # Build the form for the necessary input - GREY = mm_cfg.WEB_ADMINITEM_COLOR + GREY = config.WEB_ADMINITEM_COLOR form = Form(Utils.ScriptURL('create')) ftable = Table(border=0, cols='2', width='100%', cellspacing=3, cellpadding=4) @@ -336,9 +320,9 @@ def request_creation(doc, cgidata=dummy, errmsg=None): ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY) try: - autogen = int(cgidata.getvalue('autogen', '0')) + autogen = bool(cgidata.getvalue('autogen', '0')) except ValueError: - autogen = 0 + autogen = False ftable.AddRow([Label(_('Auto-generate initial list password?')), RadioButtonArray('autogen', (_('No'), _('Yes')), checked=autogen, @@ -359,14 +343,14 @@ def request_creation(doc, cgidata=dummy, errmsg=None): ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY) try: - notify = int(cgidata.getvalue('notify', '1')) + notify = bool(cgidata.getvalue('notify', '1')) except ValueError: - notify = 1 + notify = True try: - moderate = int(cgidata.getvalue('moderate', - mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION)) + moderate = bool(cgidata.getvalue('moderate', + config.DEFAULT_DEFAULT_MEMBER_MODERATION)) except ValueError: - moderate = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION + moderate = config.DEFAULT_DEFAULT_MEMBER_MODERATION ftable.AddRow([Center(Italic(_('List Characteristics')))]) ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2) @@ -383,7 +367,7 @@ def request_creation(doc, cgidata=dummy, errmsg=None): # Create the table of initially supported languages, sorted on the long # name of the language. revmap = {} - for key, (name, charset) in mm_cfg.LC_DESCRIPTIONS.items(): + for key, (name, charset) in config.LC_DESCRIPTIONS.items(): revmap[_(name)] = key langnames = revmap.keys() langnames.sort() @@ -391,7 +375,7 @@ def request_creation(doc, cgidata=dummy, errmsg=None): for name in langnames: langs.append(revmap[name]) try: - langi = langs.index(mm_cfg.DEFAULT_SERVER_LANGUAGE) + langi = langs.index(config.DEFAULT_SERVER_LANGUAGE) except ValueError: # Someone must have deleted the servers's preferred language. Could # be other trouble lurking! @@ -400,11 +384,11 @@ def request_creation(doc, cgidata=dummy, errmsg=None): # invocations. checked = [0] * len(langs) checked[langi] = 1 - deflang = _(Utils.GetLanguageDescr(mm_cfg.DEFAULT_SERVER_LANGUAGE)) + deflang = _(Utils.GetLanguageDescr(config.DEFAULT_SERVER_LANGUAGE)) ftable.AddRow([Label(_( - '''Initial list of supported languages. <p>Note that if you do not + """Initial list of supported languages. <p>Note that if you do not select at least one initial language, the list will use the server - default language of %(deflang)s''')), + default language of $deflang""")), CheckBoxArray('langs', [_(Utils.GetLanguageDescr(L)) for L in langs], checked=checked, diff --git a/Mailman/Cgi/listinfo.py b/Mailman/Cgi/listinfo.py index fc96f5d29..d8281ccea 100644 --- a/Mailman/Cgi/listinfo.py +++ b/Mailman/Cgi/listinfo.py @@ -66,7 +66,7 @@ def main(): def listinfo_overview(msg=''): # Present the general listinfo overview - hostname = Utils.get_domain() + hostname = Utils.get_request_domain() # Set up the document and assign it the correct language. The only one we # know about at the moment is the server's default. doc = Document() @@ -86,11 +86,10 @@ def listinfo_overview(msg=''): listnames.sort() for name in listnames: - mlist = MailList.MailList(name, lock=0) + mlist = MailList.MailList(name, lock=False) if mlist.advertised: - if mm_cfg.VIRTUAL_HOST_OVERVIEW and \ - mlist.web_page_url.find(hostname) == -1: - # List is for different identity of this host - skip it. + if hostname not in mlist.web_page_url: + # This list is situated in a different virtual domain continue else: advertised.append((mlist.GetScriptURL('listinfo'), @@ -116,7 +115,7 @@ def listinfo_overview(msg=''): # set up some local variables adj = msg and _('right') or '' - siteowner = Utils.get_site_email() + siteowner = Utils.get_site_noreply() welcome.extend( (_(''' To visit the general information page for an unadvertised list, open a URL similar to this one, but with a '/' and the %(adj)s diff --git a/Mailman/Cgi/rmlist.py b/Mailman/Cgi/rmlist.py index 78a3e0a12..e10e77b6e 100644 --- a/Mailman/Cgi/rmlist.py +++ b/Mailman/Cgi/rmlist.py @@ -168,7 +168,7 @@ def process_request(doc, cgidata, mlist): table.AddRow([_('''You have successfully deleted the mailing list <b>%(listname)s</b>.''')]) else: - sitelist = Utils.get_site_email(mlist.host_name) + sitelist = mlist.GetNoReplyEmail() table.AddRow([_('''There were some problems deleting the mailing list <b>%(listname)s</b>. Contact your site administrator at %(sitelist)s for details.''')]) diff --git a/Mailman/Commands/cmd_lists.py b/Mailman/Commands/cmd_lists.py index ff5dd355a..b137efc57 100644 --- a/Mailman/Commands/cmd_lists.py +++ b/Mailman/Commands/cmd_lists.py @@ -1,4 +1,4 @@ -# Copyright (C) 2002 by the Free Software Foundation, Inc. +# Copyright (C) 2002-2006 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -12,7 +12,8 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. """ lists @@ -53,10 +54,8 @@ def process(res, args): # We can mention this list if you already know about it if not xlist.advertised and xlist is not mlist: continue - # Skip the list if it isn't in the same virtual domain. BAW: should a - # message to the site list include everything regardless of domain? - if mm_cfg.VIRTUAL_HOST_OVERVIEW and \ - xlist.host_name <> mlist.host_name: + # Skip the list if it isn't in the same virtual domain. + if xlist.host_name <> mlist.host_name: continue realname = xlist.real_name description = xlist.description or _('n/a') diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in index d8248a3cf..e843bc249 100644 --- a/Mailman/Defaults.py.in +++ b/Mailman/Defaults.py.in @@ -58,29 +58,21 @@ MAILMAN_URL = 'http://www.gnu.org/software/mailman/index.html' #MAILMAN_URL = 'http://www.list.org/' #MAILMAN_URL = 'http://mailman.sf.net/' -# Mailman needs to know about (at least) two fully-qualified domain names -# (fqdn); 1) the hostname used in your urls, and 2) the hostname used in email -# addresses for your domain. For example, if people visit your Mailman system -# with "http://www.dom.ain/mailman" then your url fqdn is "www.dom.ain", and -# if people send mail to your system via "yourlist@dom.ain" then your email -# fqdn is "dom.ain". DEFAULT_URL_HOST controls the former, and -# DEFAULT_EMAIL_HOST controls the latter. Mailman also needs to know how to -# map from one to the other (this is especially important if you're running -# with virtual domains). You use "add_virtualhost(urlfqdn, emailfqdn)" to add -# new mappings. -# -# If you don't need to change DEFAULT_EMAIL_HOST and DEFAULT_URL_HOST in your -# mm_cfg.py, then you're done; the default mapping is added automatically. If -# however you change either variable in your mm_cfg.py, then be sure to also -# include the following: -# -# add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST) -# -# because otherwise the default mappings won't be correct. -DEFAULT_EMAIL_HOST = '@MAILHOST@' -DEFAULT_URL_HOST = '@URLHOST@' DEFAULT_URL_PATTERN = 'http://%s/mailman/' +# This address is used as the from address whenever a message comes from some +# entity to which there is no natural reply recipient. Set this to a real +# human or to /dev/null. It will be appended with the hostname of the list +# involved or the DEFAULT_EMAIL_HOST if none is available. Address must not +# bounce and it must not point to a Mailman process. +NO_REPLY_ADDRESS = 'noreply' + +# This address is the "site owner" address. Certain messages which must be +# delivered to a human, but which can't be delivered to a list owner (e.g. a +# bounce from a list owner), will be sent to this address. It should point to +# a human. +SITE_OWNER_ADDRESS = 'changeme@example.com' + # DEFAULT_HOST_NAME has been replaced with DEFAULT_EMAIL_HOST, however some # sites may have the former in their mm_cfg.py files. If so, we'll believe # that, otherwise we'll believe DEFAULT_EMAIL_HOST. Same for DEFAULT_URL. @@ -88,7 +80,6 @@ DEFAULT_HOST_NAME = None DEFAULT_URL = None HOME_PAGE = 'index.html' -MAILMAN_SITE_LIST = 'mailman' # Normally when a site administrator authenticates to a web page with the site # password, they get a cookie which authorizes them as the list admin. It @@ -109,36 +100,29 @@ HTML_TO_PLAIN_TEXT_COMMAND = '/usr/bin/lynx -dump %(filename)s' # Virtual domains ##### -# Set up your virtual host mappings here. This is primarily used for the -# thru-the-web list creation, so its effects are currently fairly limited. -# Use add_virtualhost() call to add new mappings. The keys are strings as -# determined by Utils.get_domain(), the values are as appropriate for -# DEFAULT_HOST_NAME. -VIRTUAL_HOSTS = {} - -# When set to Yes, the listinfo and admin overviews of lists on the machine -# will be confined to only those lists whose web_page_url configuration option -# host is included within the URL by which the page is visited - only those -# "on the virtual host". When set to No, all advertised (i.e. public) lists -# are included in the overview. -VIRTUAL_HOST_OVERVIEW = On - - -# Helper function; use this in your mm_cfg.py files. If optional emailhost is -# omitted it defaults to urlhost with the first name stripped off, e.g. +# Mailman needs to know about at least one domain, called the 'site default +# domain'. If you run only one domain with Mailman, this will generally be +# calculated automatically when you configured Mailman. You can always change +# this in your mailman.cfg file. You can also add additional virtual domains +# in your mailman.cfg file to enable multiple virtual domains. Every mailing +# list will be situated in exactly one virtual domain. # -# add_virtualhost('www.dom.ain') -# VIRTUAL_HOST['www.dom.ain'] -# ==> 'dom.ain' +# For Mailman's purposes, a virtual domain associates an email host name with +# a web host name. These may be the same, but often they are different, and +# the list is always referred to by its fully-qualified posting address. For +# example, if you created 'mylist' in the example.com domain, people can post +# to your list via 'mylist@example.com'. They may refer to the web pages via +# 'www.example.com'. So your email host name is 'example.com' and your web +# host name is 'www.example.com'. # -def add_virtualhost(urlhost, emailhost=None): - DOT = '.' - if emailhost is None: - emailhost = DOT.join(urlhost.split(DOT)[1:]) - VIRTUAL_HOSTS[urlhost.lower()] = emailhost.lower() - -# And set the default -add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST) +# To add a virtual domain, put a call to add_domain(email_host, url_host) in +# your mailman.cfg file. If no add_domain() calls are found, Mailman will +# automatically add a virtual domain for the following defaults. However if +# you explicit add domains, you will need to add these defaults as well. +# +# These defaults will be filled in by configure. +DEFAULT_EMAIL_HOST = '@MAILHOST@' +DEFAULT_URL_HOST = '@URLHOST@' # Note that you will want to run bin/fix_url.py to change the domain of an # existing list. bin/fix_url.py must be run within the bin/withlist script, diff --git a/Mailman/Deliverer.py b/Mailman/Deliverer.py index e1007e135..8f93bf48e 100644 --- a/Mailman/Deliverer.py +++ b/Mailman/Deliverer.py @@ -88,7 +88,7 @@ your membership administrative address, %(addr)s.''')) msg.send(self, verp=mm_cfg.VERP_PERSONALIZED_DELIVERIES) def MailUserPassword(self, user): - listfullname = '%s@%s' % (self.real_name, self.host_name) + listfullname = self.fqdn_listname requestaddr = self.GetRequestEmail() # find the lowercased version of the user's address adminaddr = self.GetBouncesEmail() diff --git a/Mailman/Errors.py b/Mailman/Errors.py index a0aacf72a..a0fdd8c52 100644 --- a/Mailman/Errors.py +++ b/Mailman/Errors.py @@ -12,15 +12,22 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. +"""Mailman errors.""" -"""Shared Mailman errors and messages.""" + + +# Base class for all exceptions raised in Mailman (XXX except legacy string +# exceptions). +class MailmanException(Exception): + pass -# exceptions for problems related to opening a list -class MMListError(Exception): pass +# Exceptions for problems related to opening a list +class MMListError(MailmanException): pass class MMUnknownListError(MMListError): pass class MMCorruptListDatabaseError(MMListError): pass class MMListNotReadyError(MMListError): pass @@ -28,12 +35,12 @@ class MMListAlreadyExistsError(MMListError): pass class BadListNameError(MMListError): pass # Membership exceptions -class MMMemberError(Exception): pass +class MMMemberError(MailmanException): pass class MMBadUserError(MMMemberError): pass class MMAlreadyAMember(MMMemberError): pass # "New" style membership exceptions (new w/ MM2.1) -class MemberError(Exception): pass +class MemberError(MailmanException): pass class NotAMemberError(MemberError): pass class AlreadyReceivingDigests(MemberError): pass class AlreadyReceivingRegularDeliveries(MemberError): pass @@ -43,7 +50,7 @@ class MembershipIsBanned(MemberError): pass # Exception hierarchy for various authentication failures, can be # raised from functions in SecurityManager.py -class MMAuthenticationError(Exception): pass +class MMAuthenticationError(MailmanException): pass class MMBadPasswordError(MMAuthenticationError): pass class MMPasswordsMustMatch(MMAuthenticationError): pass class MMCookieError(MMAuthenticationError): pass @@ -69,10 +76,12 @@ FORBIDDEN_SENDER_MSG = "Forbidden sender" # New style class based exceptions. All the above errors should eventually be # converted. -class MailmanError(Exception): - """Base class for all Mailman exceptions.""" +class MailmanError(MailmanException): + """Base class for all Mailman errors.""" pass +class BadDomainSpecificationError(MailmanError): + """The specification of a virtual domain is invalid or duplicated.""" class MMLoopingPost(MailmanError): """Post already went through this list!""" diff --git a/Mailman/Handlers/SMTPDirect.py b/Mailman/Handlers/SMTPDirect.py index 60fb52359..b71902e2d 100644 --- a/Mailman/Handlers/SMTPDirect.py +++ b/Mailman/Handlers/SMTPDirect.py @@ -105,7 +105,7 @@ def process(mlist, msg, msgdata): if mlist: envsender = mlist.GetBouncesEmail() else: - envsender = Utils.get_site_email(extra='bounces') + envsender = Utils.get_site_noreply() # Time to split up the recipient list. If we're personalizing or VERPing # then each chunk will have exactly one recipient. We'll then hand craft # an envelope sender and stitch a message together in memory for each one diff --git a/Mailman/MTA/Manual.py b/Mailman/MTA/Manual.py index cb25e9fba..4a3d6aec4 100644 --- a/Mailman/MTA/Manual.py +++ b/Mailman/MTA/Manual.py @@ -85,9 +85,7 @@ equivalent) file by adding the following lines, and possibly running the if not cgi: print >> outfp return - # Send the message to the site -owner so someone can do something about - # this request. - siteowner = Utils.get_site_email(extra='owner') + siteowner = Utils.get_site_noreply() # Should this be sent in the site list's preferred language? msg = Message.UserNotification( siteowner, siteowner, @@ -130,7 +128,7 @@ equivalent) file by removing the following lines, and possibly running the if not cgi: print >> outfp return - siteowner = Utils.get_site_email(extra='owner') + siteowner = Utils.get_site_noreply() # Should this be sent in the site list's preferred language? msg = Message.UserNotification( siteowner, siteowner, diff --git a/Mailman/MTA/Postfix.py b/Mailman/MTA/Postfix.py index 46143c549..4301321e7 100644 --- a/Mailman/MTA/Postfix.py +++ b/Mailman/MTA/Postfix.py @@ -78,7 +78,7 @@ def clear(): def _addlist(mlist, fp): # Set up the mailman-loop address - loopaddr = Utils.ParseEmail(Utils.get_site_email(extra='loop'))[0] + loopaddr = Utils.ParseEmail(Utils.get_site_noreply())[0] loopmbox = os.path.join(mm_cfg.DATA_DIR, 'owner-bounces.mbox') # Seek to the end of the text file, but if it's empty write the standard # disclaimer, and the loop catch address. @@ -118,7 +118,7 @@ def _addvirtual(mlist, fp): fieldsz = len(listname) + len('-unsubscribe') hostname = mlist.host_name # Set up the mailman-loop address - loopaddr = Utils.get_site_email(mlist.host_name, extra='loop') + loopaddr = mlist.GetNoReplyEmail() loopdest = Utils.ParseEmail(loopaddr)[0] # Seek to the end of the text file, but if it's empty write the standard # disclaimer, and the loop catch address. @@ -142,9 +142,8 @@ def _addvirtual(mlist, fp): print >> fp, '# CREATED:', time.ctime(time.time()) # Now add all the standard alias entries for k, v in makealiases(listname): - fqdnaddr = '%s@%s' % (k, hostname) # Format the text file nicely - print >> fp, fqdnaddr, ((fieldsz - len(k)) * ' '), k + print >> fp, mlist.fqdn_listname, ((fieldsz - len(k)) * ' '), k # Finish the text file stanza print >> fp, '# STANZA END:', listname print >> fp @@ -153,7 +152,7 @@ def _addvirtual(mlist, fp): # Blech. def _check_for_virtual_loopaddr(mlist, filename): - loopaddr = Utils.get_site_email(mlist.host_name, extra='loop') + loopaddr = mlist.GetNoReplyEmail() loopdest = Utils.ParseEmail(loopaddr)[0] infp = open(filename) omask = os.umask(007) diff --git a/Mailman/MailList.py b/Mailman/MailList.py index ecf449246..069a0b397 100644 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -68,7 +68,6 @@ from Mailman import Gui from Mailman import i18n from Mailman import MemberAdaptor from Mailman import Message -from Mailman import Site from Mailman.OldStyleMemberships import OldStyleMemberships _ = i18n._ @@ -183,9 +182,13 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, def fullpath(self): return self._full_path + @property + def fqdn_listname(self): + return '%s@%s' % (self._internal_name, self.host_name) + def getListAddress(self, extra=None): if extra is None: - return '%s@%s' % (self.internal_name(), self.host_name) + return self.fqdn_listname return '%s-%s@%s' % (self.internal_name(), extra, self.host_name) # For backwards compatibility @@ -195,6 +198,9 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, def GetOwnerEmail(self): return self.getListAddress('owner') + def GetNoReplyEmail(self): + return '%s@%s' % (config.NO_REPLY_ADDRESS, self.host_name) + def GetRequestEmail(self, cookie=''): if config.VERP_CONFIRMATIONS and cookie: return self.GetConfirmEmail(cookie) @@ -273,7 +279,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, lifetime=config.LIST_LOCK_LIFETIME) self._internal_name = name if name: - self._full_path = Site.get_listpath(name) + self._full_path = os.path.join(config.LIST_DATA_DIR, name) else: self._full_path = '' # Only one level of mixin inheritance allowed @@ -466,34 +472,41 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # # List creation # - def Create(self, name, admin, crypted_password, - langs=None, emailhost=None): - if Utils.list_exists(name): - raise Errors.MMListAlreadyExistsError, name - # Validate what will be the list's posting address. If that's - # invalid, we don't want to create the mailing list. The hostname - # part doesn't really matter, since that better already be valid. - # However, most scripts already catch MMBadEmailError as exceptions on - # the admin's email address, so transform the exception. - if emailhost is None: - emailhost = config.DEFAULT_EMAIL_HOST - postingaddr = '%s@%s' % (name, emailhost) + def Create(self, fqdn_listname, admin_email, crypted_password, langs=None): + # Validate the list's posting address, which should be fqdn_listname. + # If that's invalid, do not create any of the mailing list artifacts + # (the subdir in lists/ and the subdirs in archives/public and + # archives/private. Most scripts already catch MMBadEmailError as + # exceptions on the admin's email address, so transform the exception. + if '@' not in fqdn_listname: + raise Errors.BadListNameError(fqdn_listname) + listname, email_host = fqdn_listname.split('@', 1) + if email_host not in config.domains: + raise Errors.BadDomainSpecificationError(email_host) try: - Utils.ValidateEmail(postingaddr) + Utils.ValidateEmail(fqdn_listname) except Errors.MMBadEmailError: - raise Errors.BadListNameError, postingaddr + raise Errors.BadListNameError(fqdn_listname) + # See if the mailing list already exists. + if Utils.list_exists(fqdn_listname): + raise Errors.MMListAlreadyExistsError(fqdn_listname) # Validate the admin's email address - Utils.ValidateEmail(admin) - self._internal_name = name - self._full_path = Site.get_listpath(name, create=1) + Utils.ValidateEmail(admin_email) + self._internal_name = listname + self._full_path = os.path.join(config.LIST_DATA_DIR, fqdn_listname) + Utils.makedirs(self._full_path) # Don't use Lock() since that tries to load the non-existant config.pck self.__lock.lock() - self.InitVars(name, admin, crypted_password) + self.InitVars(listname, admin_email, crypted_password) self.CheckValues() if langs is None: self.available_languages = [self.preferred_language] else: self.available_languages = langs + # Set the various host names + self.host_name = email_host + url_host = config.domains[email_host] + self.web_page_url = config.DEFAULT_URL_PATTERN % url_host @@ -1350,7 +1363,6 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, addresses in the recipient headers. """ # This is the list's full address. - listfullname = '%s@%s' % (self.internal_name(), self.host_name) recips = [] # Check all recipient addresses against the list's explicit addresses, # specifically To: Cc: and Resent-to: @@ -1367,7 +1379,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, if (# TBD: backwards compatibility: deprecated localpart == self.internal_name() or # exact match against the complete list address - addr == listfullname): + addr == self.fqdn_listname): return True recips.append((addr, localpart)) # Helper function used to match a pattern against an address. @@ -1480,7 +1492,7 @@ bad regexp in bounce_matching_header line: %s text = Utils.maketext( 'nomoretoday.txt', {'sender' : sender, - 'listname': '%s@%s' % (self.real_name, self.host_name), + 'listname': self.fqdn_listname, 'num' : count, 'owneremail': self.GetOwnerEmail(), }, diff --git a/Mailman/Message.py b/Mailman/Message.py index 147ac073d..acad25680 100644 --- a/Mailman/Message.py +++ b/Mailman/Message.py @@ -259,13 +259,11 @@ class UserNotification(Message): class OwnerNotification(UserNotification): """Like user notifications, but this message goes to the list owners.""" - def __init__(self, mlist, subject=None, text=None, tomoderators=1): + def __init__(self, mlist, subject=None, text=None, tomoderators=True): recips = mlist.owner[:] if tomoderators: recips.extend(mlist.moderator) - # We have to set the owner to the site's -bounces address, otherwise - # we'll get a mail loop if an owner's address bounces. - sender = Utils.get_site_email(mlist.host_name, 'bounces') + sender = config.SITE_OWNER_ADDRESS lang = mlist.preferred_language UserNotification.__init__(self, recips, sender, subject, text, lang) # Hack the To header to look like it's going to the -owner address diff --git a/Mailman/Queue/BounceRunner.py b/Mailman/Queue/BounceRunner.py index 93bee2062..9e1a34bf5 100644 --- a/Mailman/Queue/BounceRunner.py +++ b/Mailman/Queue/BounceRunner.py @@ -177,16 +177,17 @@ class BounceRunner(Runner, BounceMixin): # we'll simply log the problem and attempt to deliver the message to # the site owner. # - # All messages to list-owner@vdom.ain have their envelope sender set - # to site-owner@dom.ain (no virtual domain). Is this a bounce for a + # All messages sent to list owners have their sender set to the site + # owner address. That way, if a list owner address bounces, at least + # some human has a chance to deal with it. Is this a bounce for a # message to a list owner, coming to the site owner? - if msg.get('to', '') == Utils.get_site_email(extra='owner'): + if msg.get('to', '') == config.SITE_OWNER_ADDRESS: # Send it on to the site owners, but craft the envelope sender to - # be the -loop detection address, so if /they/ bounce, we won't + # be the noreply address, so if the site owner bounce, we won't # get stuck in a bounce loop. outq.enqueue(msg, msgdata, - recips=[Utils.get_site_email()], - envsender=Utils.get_site_email(extra='loop'), + recips=[config.SITE_OWNER_ADDRESS], + envsender=config.NO_REPLY_ADDRESS, ) # List isn't doing bounce processing? if not mlist.bounce_processing: diff --git a/Mailman/Queue/IncomingRunner.py b/Mailman/Queue/IncomingRunner.py index 332c9f129..efd7072d8 100644 --- a/Mailman/Queue/IncomingRunner.py +++ b/Mailman/Queue/IncomingRunner.py @@ -115,6 +115,8 @@ class IncomingRunner(Runner): QDIR = config.INQUEUE_DIR def _dispose(self, mlist, msg, msgdata): + if msgdata.get('envsender') is None: + msg['envsender'] = mlist.GetNoReplyEmail() # Try to get the list lock. try: mlist.Lock(timeout=config.LIST_LOCK_TIMEOUT) diff --git a/Mailman/Queue/MaildirRunner.py b/Mailman/Queue/MaildirRunner.py index 9e5e08be5..e253f3506 100644 --- a/Mailman/Queue/MaildirRunner.py +++ b/Mailman/Queue/MaildirRunner.py @@ -162,8 +162,8 @@ class MaildirRunner(Runner): queue = get_switchboard(config.CMDQUEUE_DIR) elif subq == 'owner': msgdata.update({ - 'toowner': 1, - 'envsender': Utils.get_site_email(extra='bounces'), + 'toowner': True, + 'envsender': config.SITE_OWNER_ADDRESS, 'pipeline': config.OWNER_PIPELINE, }) queue = get_switchboard(config.INQUEUE_DIR) diff --git a/Mailman/Queue/Runner.py b/Mailman/Queue/Runner.py index 95cb3fd43..bdb811a3a 100644 --- a/Mailman/Queue/Runner.py +++ b/Mailman/Queue/Runner.py @@ -132,8 +132,6 @@ class Runner: # # Find out which mailing list this message is destined for. listname = msgdata.get('listname') - if not listname: - listname = config.MAILMAN_SITE_LIST mlist = self._open_list(listname) if not mlist: log.error('Dequeuing message destined for missing list: %s', diff --git a/Mailman/Site.py b/Mailman/Site.py index 72becb224..e69de29bb 100644 --- a/Mailman/Site.py +++ b/Mailman/Site.py @@ -1,108 +0,0 @@ -# Copyright (C) 2002-2006 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, -# USA. - -"""Provide some customization for site-wide behavior. - -This should be considered experimental for Mailman 2.1. The default -implementation should work for standard Mailman. -""" - -import os -import errno - -from Mailman.configuration import config - - - -def _makedir(path): - try: - omask = os.umask(0) - try: - os.makedirs(path, 02775) - finally: - os.umask(omask) - except OSError, e: - # Ignore the exceptions if the directory already exists - if e.errno <> errno.EEXIST: - raise - - - -# BAW: We don't really support domain<>None yet. This will be added in a -# future version. By default, Mailman will never pass in a domain argument. -def get_listpath(listname, domain=None, create=0): - """Return the file system path to the list directory for the named list. - - If domain is given, it is the virtual domain for the named list. The - default is to not distinguish list paths on the basis of virtual domains. - - If the create flag is true, then this method should create the path - hierarchy if necessary. If the create flag is false, then this function - should not attempt to create the path heirarchy (and in fact the absence - of the path might be significant). - """ - path = os.path.join(config.LIST_DATA_DIR, listname) - if create: - _makedir(path) - return path - - - -# BAW: We don't really support domain<>None yet. This will be added in a -# future version. By default, Mailman will never pass in a domain argument. -def get_archpath(listname, domain=None, create=False, public=False): - """Return the file system path to the list's archive directory for the - named list in the named virtual domain. - - If domain is given, it is the virtual domain for the named list. The - default is to not distinguish list paths on the basis of virtual domains. - - If the create flag is true, then this method should create the path - hierarchy if necessary. If the create flag is false, then this function - should not attempt to create the path heirarchy (and in fact the absence - of the path might be significant). - - If public is true, then the path points to the public archive path (which - is usually a symlink instead of a directory). - """ - if public: - subdir = config.PUBLIC_ARCHIVE_FILE_DIR - else: - subdir = config.PRIVATE_ARCHIVE_FILE_DIR - path = os.path.join(subdir, listname) - if create: - _makedir(path) - return path - - - -# BAW: We don't really support domain<>None yet. This will be added in a -# future version. By default, Mailman will never pass in a domain argument. -def get_listnames(domain=None): - """Return the names of all the known lists for the given domain. - - If domain is given, it is the virtual domain for the named list. The - default is to not distinguish list paths on the basis of virtual domains. - """ - # Import this here to avoid circular imports - from Mailman.Utils import list_exists - # We don't currently support separate virtual domain directories - got = [] - for fn in os.listdir(config.LIST_DATA_DIR): - if list_exists(fn): - got.append(fn) - return got diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 5e594be8d..b190e2ded 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -40,7 +40,6 @@ from email.Errors import HeaderParseError from string import ascii_letters, digits, whitespace from Mailman import Errors -from Mailman import Site from Mailman.SafeDict import SafeDict from Mailman.configuration import config @@ -68,7 +67,7 @@ def list_exists(listname): # # The former two are for 2.1alpha3 and beyond, while the latter two are # for all earlier versions. - basepath = Site.get_listpath(listname) + basepath = os.path.join(config.LIST_DATA_DIR, listname) for ext in ('.pck', '.pck.last', '.db', '.db.last'): dbfile = os.path.join(basepath, 'config' + ext) if os.path.exists(dbfile): @@ -78,8 +77,11 @@ def list_exists(listname): def list_names(): """Return the names of all lists in default list directory.""" - # We don't currently support separate listings of virtual domains - return Site.get_listnames() + got = set() + for fn in os.listdir(config.LIST_DATA_DIR): + if list_exists(fn): + got.add(fn) + return got @@ -227,7 +229,7 @@ def ScriptURL(target, web_page_url=None, absolute=False): absolute - a flag which if set, generates an absolute url """ if web_page_url is None: - web_page_url = config.DEFAULT_URL_PATTERN % get_domain() + web_page_url = config.DEFAULT_URL_PATTERN % get_request_domain() if web_page_url[-1] <> '/': web_page_url = web_page_url + '/' fullpath = os.environ.get('REQUEST_URI') @@ -389,7 +391,7 @@ def get_global_password(siteadmin=True): def check_global_password(response, siteadmin=True): challenge = get_global_password(siteadmin) if challenge is None: - return None + return False return challenge == sha.new(response).hexdigest() @@ -651,6 +653,21 @@ def reap(kids, func=None, once=False): if once: break + + +def makedirs(path): + try: + omask = os.umask(0) + try: + os.makedirs(path, 02775) + finally: + os.umask(omask) + except OSError, e: + # Ignore the exceptions if the directory already exists + if e.errno <> errno.EEXIST: + raise + + def GetLanguageDescr(lang): return config.LC_DESCRIPTIONS[lang][0] @@ -664,29 +681,17 @@ def IsLanguage(lang): -def get_domain(): +def get_request_domain(): host = os.environ.get('HTTP_HOST', os.environ.get('SERVER_NAME')) port = os.environ.get('SERVER_PORT') # Strip off the port if there is one if port and host.endswith(':' + port): host = host[:-len(port)-1] - if config.VIRTUAL_HOST_OVERVIEW and host: - return host.lower() - else: - # See the note in Defaults.py concerning DEFAULT_URL - # vs. DEFAULT_URL_HOST. - hostname = ((config.DEFAULT_URL - and urlparse.urlparse(config.DEFAULT_URL)[1]) - or config.DEFAULT_URL_HOST) - return hostname.lower() + return host.lower() -def get_site_email(hostname=None, extra=None): - if hostname is None: - hostname = config.VIRTUAL_HOSTS.get(get_domain(), get_domain()) - if extra is None: - return '%s@%s' % (config.MAILMAN_SITE_LIST, hostname) - return '%s-%s@%s' % (config.MAILMAN_SITE_LIST, extra, hostname) +def get_site_noreply(): + return '%s@%s' % (config.NO_REPLY_ADDRESS, config.DEFAULT_EMAIL_HOST) @@ -700,9 +705,8 @@ def get_site_email(hostname=None, extra=None): _serial = 0 def unique_message_id(mlist): global _serial - msgid = '<mailman.%d.%d.%d.%s@%s>' % ( - _serial, time.time(), os.getpid(), - mlist.internal_name(), mlist.host_name) + msgid = '<mailman.%d.%d.%d.%s>' % ( + _serial, time.time(), os.getpid(), mlist.fqdn_listname) _serial += 1 return msgid diff --git a/Mailman/Version.py b/Mailman/Version.py index 91fb82e65..7a61ba545 100644 --- a/Mailman/Version.py +++ b/Mailman/Version.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2005 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2006 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -12,10 +12,11 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. # Mailman version -VERSION = "2.2.0a0" +VERSION = "2.2.0a1" # And as a hex number in the manner of PY_VERSION_HEX ALPHA = 0xa @@ -30,7 +31,7 @@ MINOR_REV = 2 MICRO_REV = 0 REL_LEVEL = ALPHA # at most 15 beta releases! -REL_SERIAL = 0 +REL_SERIAL = 1 HEX_VERSION = ((MAJOR_REV << 24) | (MINOR_REV << 16) | (MICRO_REV << 8) | (REL_LEVEL << 4) | (REL_SERIAL << 0)) @@ -49,4 +50,3 @@ REQUESTS_FILE_SCHEMA_VERSION = 1 # Printable version string used by command line scripts MAILMAN_VERSION = 'GNU Mailman ' + VERSION - diff --git a/Mailman/bin/add_members.py b/Mailman/bin/add_members.py index 0adcda518..932e4f77d 100644 --- a/Mailman/bin/add_members.py +++ b/Mailman/bin/add_members.py @@ -202,7 +202,7 @@ def main(): if admin_notify: subject = _('$mlist.real_name subscription notification') msg = Message.UserNotification( - mlist.owner, Utils.get_site_email(), subject, s.getvalue(), + mlist.owner, mlist.GetNoReplyEmail(), subject, s.getvalue(), mlist.preferred_language) msg.send(mlist) diff --git a/Mailman/bin/change_pw.py b/Mailman/bin/change_pw.py index afa932160..6c2beaf13 100644 --- a/Mailman/bin/change_pw.py +++ b/Mailman/bin/change_pw.py @@ -155,7 +155,7 @@ def main(): hostname = mlist.host_name adminurl = mlist.GetScriptURL('admin', absolute=True) msg = Message.UserNotification( - mlist.owner[:], Utils.get_site_email(), + mlist.owner[:], mlist.GetNoReplyEmail(), _('Your new $listname list password'), _('''\ The site administrator at $hostname has changed the password for your diff --git a/Mailman/bin/list_lists.py b/Mailman/bin/list_lists.py index 0feac2845..f4a193f18 100644 --- a/Mailman/bin/list_lists.py +++ b/Mailman/bin/list_lists.py @@ -43,11 +43,17 @@ List only those mailing lists that are publicly advertised""")) default=False, action='store_true', help=_("""\ Displays only the list name, with no description.""")) - parser.add_option('-V', '--virtual-host-overview', - default=None, type='string', dest='vhost', + parser.add_option('-d', '--domain', + default=[], type='string', action='append', + dest='domains', help=_("""\ +List only those mailing lists that match the given virtual domain, which may +be either the email host or the url host name. Multiple -d options may be +given.""")) + parser.add_option('-f', '--full', + default=False, action='store_true', help=_("""\ -List only those mailing lists that are homed to the given virtual domain. -This only works if the VIRTUAL_HOST_OVERVIEW variable is set.""")) +Print the full list name, including the posting address. This option is +ignored when -b is given.""")) parser.add_option('-C', '--config', help=_('Alternative configuration file to use')) opts, args = parser.parse_args() @@ -72,12 +78,18 @@ def main(): mlist = MailList.MailList(n, lock=False) if opts.advertised and not mlist.advertised: continue - if opts.vhost and config.VIRTUAL_HOST_OVERVIEW and \ - opts.vhost.find(mlist.web_page_url) == -1 and \ - mlist.web_page_url.find(opts.vhost) == -1: - continue - mlists.append(mlist) - longest = max(len(mlist.real_name), longest) + if opts.domains: + for domain in opts.domains: + if domain in mlist.web_page_url or domain == mlist.host_name: + mlists.append(mlist) + break + else: + mlists.append(mlist) + if opts.full: + name = mlist.internal_name() + else: + name = mlist.real_name + longest = max(len(name), longest) if not mlists and not opts.bare: print _('No matching mailing lists found') @@ -92,8 +104,12 @@ def main(): if opts.bare: print mlist.internal_name() else: + if opts.full: + name = mlist.internal_name() + else: + name = mlist.real_name description = mlist.description or _('[no description available]') - print ' ', format % (mlist.real_name, description) + print ' ', format % (name, description) diff --git a/Mailman/bin/mailmanctl.py b/Mailman/bin/mailmanctl.py index 0c83e3324..e3a3ec866 100644 --- a/Mailman/bin/mailmanctl.py +++ b/Mailman/bin/mailmanctl.py @@ -275,16 +275,6 @@ def start_all_runners(): -def check_for_site_list(): - sitelistname = config.MAILMAN_SITE_LIST - try: - sitelist = MailList(sitelistname, lock=False) - except Errors.MMUnknownListError: - print >> sys.stderr, _('Site list is missing: $sitelistname') - elog.error('Site list is missing: %s', config.MAILMAN_SITE_LIST) - sys.exit(1) - - def check_privs(): # If we're running as root (uid == 0), coerce the uid and gid to that # which Mailman was configured for, and refuse to run if we didn't coerce @@ -342,8 +332,6 @@ def main(): print _('Re-opening all log files') kill_watcher(signal.SIGHUP) elif command == 'start': - # First, complain loudly if there's no site list. - check_for_site_list() # Here's the scoop on the processes we're about to create. We'll need # one for each qrunner, and one for a master child process watcher / # lock refresher process. diff --git a/Mailman/bin/newlist.py b/Mailman/bin/newlist.py index 0f62f5373..3136ef425 100644 --- a/Mailman/bin/newlist.py +++ b/Mailman/bin/newlist.py @@ -38,52 +38,20 @@ __i18n_templates__ = True def parseargs(): parser = optparse.OptionParser(version=Version.MAILMAN_VERSION, usage=_("""\ -%%prog [options] [listname [listadmin-addr [admin-password]]] +%prog [options] [listname [listadmin-addr [admin-password]]] -Create a new, unpopulated mailing list. +Create a new empty mailing list. You can specify as many of the arguments as you want on the command line: you will be prompted for the missing ones. -Every Mailman list has two parameters which define the default host name for -outgoing email, and the default URL for all web interfaces. When you -configured Mailman, certain defaults were calculated, but if you are running -multiple virtual Mailman sites, then the defaults may not be appropriate for -the list you are creating. +Every Mailman mailing list is situated in a domain, and Mailman supports +multiple virtual domains. 'listname' is required, and if it contains an '@', +it should specify the posting address for your mailing list and the right-hand +side of the email address must be an existing domain. -You also specify the domain to create your new list in by typing the command -like so: - - newlist --urlhost=www.mydom.ain mylist - -where `www.mydom.ain' should be the base hostname for the URL to this virtual -hosts's lists. E.g. with this setting people will view the general list -overviews at http://www.mydom.ain/mailman/listinfo. Also, www.mydom.ain -should be a key in the VIRTUAL_HOSTS mapping in mm_cfg.py/Defaults.py if -the email hostname to be automatically determined. - -If you want the email hostname to be different from the one looked up by the -VIRTUAL_HOSTS or if urlhost is not registered in VIRTUAL_HOSTS, you can specify -`emailhost' like so: - - newlist --urlhost=www.mydom.ain --emailhost=mydom.ain mylist - -where `mydom.ain' is the mail domain name. If you don't specify emailhost but -urlhost is not in the virtual host list, then mm_cfg.DEFAULT_EMAIL_HOST will -be used for the email interface. - -For backward compatibility, you can also specify the domain to create your -new list in by spelling the listname like so: - - mylist@www.mydom.ain - -where www.mydom.ain is used for `urlhost' but it will also be used for -`emailhost' if it is not found in the virtual host table. Note that -'--urlhost' and '--emailhost' have precedence to this notation. - -If you spell the list name as just `mylist', then the email hostname will be -taken from DEFAULT_EMAIL_HOST and the url will be taken from DEFAULT_URL (as -defined in your Defaults.py file or overridden by settings in mm_cfg.py). +If 'listname' does not have an '@', the list will be situated in the default +domain, which Mailman created when you configured the system. Note that listnames are forced to lowercase.""")) parser.add_option('-l', '--language', @@ -91,12 +59,6 @@ Note that listnames are forced to lowercase.""")) help=_("""\ Make the list's preferred language LANGUAGE, which must be a two letter language code.""")) - parser.add_option('-u', '--urlhost', - type='string', action='store', - help=_('The hostname for the web interface')) - parser.add_option('-e', '--emailhost', - type='string', action='store', - help=_('The hostname for the email server')) parser.add_option('-q', '--quiet', default=False, action='store_true', help=_("""\ @@ -140,17 +102,20 @@ def main(): listname = listname.lower() if '@' in listname: - # Note that --urlhost and --emailhost have precedence - listname, domain = listname.split('@', 1) - urlhost = opts.urlhost or domain - emailhost = opts.emailhost or config.VIRTUAL_HOSTS.get(domain, domain) - - urlhost = opts.urlhost or config.DEFAULT_URL_HOST - host_name = (opts.emailhost or - config.VIRTUAL_HOSTS.get(urlhost, config.DEFAULT_EMAIL_HOST)) - web_page_url = config.DEFAULT_URL_PATTERN % urlhost - - if Utils.list_exists(listname): + fqdn_listname = listname + listname, email_host = listname.split('@', 1) + url_host = config.domains.get(email_host) + if not url_host: + print >> sys.stderr, _('Undefined domain: $email_host') + sys.exit(1) + else: + email_host = config.DEFAULT_EMAIL_HOST + url_host = config.DEFAULT_URL_HOST + fqdn_listname = '%s@%s' % (listname, email_host) + web_page_url = config.DEFAULT_URL_PATTERN % url_host + # Even though MailList.Create() will check to make sure the list doesn't + # yet exist, do it now for better usability. + if Utils.list_exists(fqdn_listname): parser.print_help() print >> sys.stderr, _('List already exists: $listname') @@ -177,6 +142,9 @@ def main(): print >> sys.stderr, _('The list password cannot be empty') mlist = MailList.MailList() + # Assign the preferred language before Create() since that will use it to + # set available_languages. + mlist.preferred_language = opts.language try: pw = sha.new(listpasswd).hexdigest() # Guarantee that all newly created files have the proper permission. @@ -185,7 +153,7 @@ def main(): oldmask = os.umask(002) try: try: - mlist.Create(listname, owner_mail, pw) + mlist.Create(fqdn_listname, owner_mail, pw) except Errors.BadListNameError, s: parser.print_help() print >> sys.stderr, _('Illegal list name: $s') @@ -200,13 +168,6 @@ def main(): sys.exit(1) finally: os.umask(oldmask) - - # Assign domain-specific attributes - mlist.host_name = host_name - mlist.web_page_url = web_page_url - - # And assign the preferred language - mlist.preferred_language = opts.language mlist.Save() finally: mlist.Unlock() @@ -222,14 +183,13 @@ def main(): print _('Hit enter to notify $listname owner...'), sys.stdin.readline() if not opts.quiet: - siteowner = Utils.get_site_email(mlist.host_name, 'owner') d = dict( listname = listname, password = listpasswd, admin_url = mlist.GetScriptURL('admin', absolute=True), listinfo_url = mlist.GetScriptURL('listinfo', absolute=True), requestaddr = mlist.GetRequestEmail(), - siteowner = siteowner, + siteowner = mlist.GetNoReplyEmail(), ) text = Utils.maketext('newlist.txt', d, mlist=mlist) # Set the I18N language to the list's preferred language so the header @@ -239,7 +199,7 @@ def main(): i18n.set_language(mlist.preferred_language) try: msg = Message.UserNotification( - owner_mail, siteowner, + owner_mail, mlist.GetNoReplyEmail(), _('Your new mailing list: $listname'), text, mlist.preferred_language) msg.send(mlist) diff --git a/Mailman/bin/owner.py b/Mailman/bin/owner.py index 34f796ad3..f1eb7ce62 100644 --- a/Mailman/bin/owner.py +++ b/Mailman/bin/owner.py @@ -60,7 +60,6 @@ def main(): inq.enqueue(sys.stdin.read(), listname=listname, _plaintext=True, - envsender=Utils.get_site_email(extra='bounces'), pipeline=mm_cfg.OWNER_PIPELINE, toowner=True) diff --git a/Mailman/bin/rmlist.py b/Mailman/bin/rmlist.py index 9655327f0..861e79226 100644 --- a/Mailman/bin/rmlist.py +++ b/Mailman/bin/rmlist.py @@ -78,6 +78,9 @@ def main(): config.load(opts.config) listname = args[0].lower().strip() + if '@' not in listname: + listname = '%s@%s' % (listname, config.DEFAULT_EMAIL_HOST) + if not Utils.list_exists(listname): if not opts.archives: print >> sys.stderr, _( @@ -85,7 +88,7 @@ def main(): sys.exit(1) else: print _( - 'No such list: $listname. Removing its residual archives.') + 'No such list: ${listname}. Removing its residual archives.') if not opts.archives: print _('Not removing archives. Reinvoke with -a to remove them.') diff --git a/Mailman/bin/update.py b/Mailman/bin/update.py index ea74abfd6..36812dcf8 100644 --- a/Mailman/bin/update.py +++ b/Mailman/bin/update.py @@ -196,6 +196,23 @@ def move_language_templates(mlist): +def situate_list(listname): + # This turns the directory called 'listname' into a directory called + # 'listname@domain'. Start by finding out what the domain should be. + # A list's domain is its email host. + mlist = MailList.MailList(listname, lock=False) + fullname = mlist.fqdn_listname + oldpath = os.path.join(config.VAR_PREFIX, 'lists', listname) + newpath = os.path.join(config.VAR_PREFIX, 'lists', fullname) + if os.path.exists(newpath): + print >> sys.stderr, _('WARNING: could not situate list: $listname') + else: + os.rename(oldpath, newpath) + print _('situated list $listname to $fullname') + return fullname + + + def dolist(listname): mlist = MailList.MailList(listname, lock=False) try: @@ -672,10 +689,8 @@ Exiting.""") if not listnames: print _('no lists == nothing to do, exiting') return - # - # for people with web archiving, make sure the directories + # For people with web archiving, make sure the directories # in the archiving are set with proper perms for b6. - # if os.path.isdir("%s/public_html/archives" % config.PREFIX): print _("""\ fixing all the perms on your old html archives to work with b6 @@ -684,8 +699,12 @@ If your archives are big, this could take a minute or two...""") archive_path_fixer, "") print _('done') for listname in listnames: + # With 2.2.0a0, all list names grew an @domain suffix. If you find a + # list without that, move it now. + if not '@' in listname: + listname = situate_list(listname) print _('Updating mailing list: $listname') - errors = errors + dolist(listname) + errors += dolist(listname) print print _('Updating Usenet watermarks') wmfile = os.path.join(config.DATA_DIR, 'gate_watermarks') diff --git a/Mailman/configuration.py b/Mailman/configuration.py index cf3af03f3..1f1c52f19 100644 --- a/Mailman/configuration.py +++ b/Mailman/configuration.py @@ -21,12 +21,17 @@ import os import errno from Mailman import Defaults +from Mailman import Errors _missing = object() class Configuration(object): + def __init__(self): + self.domains = {} + self._reverse = None + def load(self, filename=None): # Load the configuration from the named file, or if not given, search # in VAR_PREFIX for an etc/mailman.cfg file. If that file is missing, @@ -39,10 +44,11 @@ class Configuration(object): filename = os.path.join(Defaults.VAR_PREFIX, 'etc', 'mailman.cfg') # Set up the execfile namespace ns = Defaults.__dict__.copy() - # Prune a few things + # Prune a few things, add a few things del ns['__file__'] del ns['__name__'] del ns['__doc__'] + ns['add_domain'] = self.add_domain # Attempt our first choice path = os.path.abspath(os.path.expanduser(filename)) try: @@ -95,7 +101,41 @@ class Configuration(object): self.CONFIG_FILE = os.path.join(etcdir, 'mailman.cfg') self.LOCK_FILE = os.path.join(lockdir, 'master-qrunner') # Now update our dict so attribute syntax just works + if 'add_domain' in ns: + del ns['add_domain'] self.__dict__.update(ns) + # Add the default domain if there are no virtual domains currently + # defined. + if not self.domains: + self.add_domain(self.DEFAULT_EMAIL_HOST, self.DEFAULT_URL_HOST) + + def add_domain(self, email_host, url_host): + """Add the definition of a virtual domain. + + email_host is the right-hand side of the posting email address, + e.g. 'example.com' in 'mylist@example.com'. url_host is the host name + part of the exposed web pages, e.g. 'www.example.com'.""" + if email_host in self.domains: + raise Errors.BadDomainSpecificationError( + 'Duplicate email host: %s' % email_host) + # Make sure there's only one mapping for the url_host + if url_host in self.domains.values(): + raise Errors.BadDomainSpecificationError( + 'Duplicate url host: %s' % url_host) + # We'll do the reverse mappings on-demand. There shouldn't be too + # many virtual hosts that it will really matter that much. + self.domains[email_host] = url_host + # Invalidate the reverse mapping cache + self._reverse = None + + # Given an email host name, the url host name can be looked up directly. + # This does the reverse mapping. + def get_email_host(self, url_host, default=None): + if self._reverse is None: + # XXX Can't use a generator comprehension until Python 2.4 is + # minimum requirement. + self._reverse = dict([(v, k) for k, v in self.domains.items()]) + return self._reverse.get(url_host, default) diff --git a/Mailman/i18n.py b/Mailman/i18n.py index 69f9801d1..4a32ce3e3 100644 --- a/Mailman/i18n.py +++ b/Mailman/i18n.py @@ -110,7 +110,7 @@ def _(s): if isinstance(v, unicode): d[k] = v.encode(charset, 'replace') # Are we using $-strings or %-strings? - if d.get('__i18n_templates__', False): + if use_templates: return Template(tns).safe_substitute(attrdict(d)) return tns % SafeDict(d) diff --git a/bin/withlist b/bin/withlist index 4209032d6..b6bc8911f 100644 --- a/bin/withlist +++ b/bin/withlist @@ -130,12 +130,7 @@ from Mailman import MailList from Mailman import Utils from Mailman import loginit from Mailman.i18n import _ - -try: - True, False -except NameError: - True = 1 - False = 0 +from Mailman.configuration import config # `m' will be the MailList object and `r' will be the results of the callable @@ -246,6 +241,9 @@ def main(): if all and not run: usage(1, _('--all requires --run')) + # XXX Allow for specifying the configuration file via -C/--config + config.load() + # The default for interact is 1 unless -r was given if interact is None: if run is None: |
