Пример #1
0
 def __sendAdminBounceNotice(self, member, msg):
     # BAW: This is a bit kludgey, but we're not providing as much
     # information in the new admin bounce notices as we used to (some of
     # 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,
             "addr": member,
             "negative": "",
             "did": _("disabled"),
             "but": "",
             "reenable": "",
             "owneraddr": siteowner,
         },
         mlist=self,
     )
     subject = _("Bounce action notification")
     umsg = Message.UserNotification(self.GetOwnerEmail(), siteowner, subject, lang=self.preferred_language)
     # BAW: Be sure you set the type before trying to attach, or you'll get
     # a MultipartConversionError.
     umsg.set_type("multipart/mixed")
     umsg.attach(MIMEText(text, _charset=Utils.GetCharSet(self.preferred_language)))
     if isinstance(msg, StringType):
         umsg.attach(MIMEText(msg))
     else:
         umsg.attach(MIMEMessage(msg))
     umsg.send(self)
Пример #2
0
 def __sendAdminBounceNotice(self, member, msg, did=_('disabled')):
     # BAW: This is a bit kludgey, but we're not providing as much
     # information in the new admin bounce notices as we used to (some of
     # 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,
         'addr': member,
         'negative': '',
         'did': did,
         'but': '',
         'reenable': '',
         'owneraddr': siteowner,
     },
                           mlist=self)
     subject = _('Bounce action notification')
     umsg = Message.UserNotification(self.GetOwnerEmail(),
                                     siteowner,
                                     subject,
                                     lang=self.preferred_language)
     # BAW: Be sure you set the type before trying to attach, or you'll get
     # a MultipartConversionError.
     umsg.set_type('multipart/mixed')
     umsg.attach(
         MIMEText(text, _charset=Utils.GetCharSet(self.preferred_language)))
     if isinstance(msg, StringType):
         umsg.attach(MIMEText(msg))
     else:
         umsg.attach(MIMEMessage(msg))
     umsg.send(self)
Пример #3
0
def _addvirtual(mlist, fp):
    listname = mlist.internal_name()
    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')
    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.
    fp.seek(0, 2)
    if not fp.tell():
        print >> fp, """\
# This file is generated by Mailman, and is kept in sync with the binary hash
# file virtual-mailman.db.  YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you
# know what you're doing, and can keep the two files properly in sync.  If you
# screw it up, you're on your own.
#
# Note that you should already have this virtual domain set up properly in
# your Postfix installation.  See README.POSTFIX for details.

# LOOP ADDRESSES START
%s\t%s
# LOOP ADDRESSES END
""" % (loopaddr, loopdest)
    # The text file entries get a little extra info
    print >> fp, '# STANZA START:', listname
    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
    # Finish the text file stanza
    print >> fp, '# STANZA END:', listname
    print >> fp
Пример #4
0
def _addlist(mlist, fp):
    # Set up the mailman-loop address
    loopaddr = Utils.ParseEmail(Utils.get_site_email(extra='loop'))[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.
    fp.seek(0, 2)
    if not fp.tell():
        print >> fp, """\
# This file is generated by Mailman, and is kept in sync with the
# binary hash file aliases.db.  YOU SHOULD NOT MANUALLY EDIT THIS FILE
# unless you know what you're doing, and can keep the two files properly
# in sync.  If you screw it up, you're on your own.
"""
        print >> fp, '# The ultimate loop stopper address'
        print >> fp, '%s: %s' % (loopaddr, loopmbox)
        print >> fp
    # Bootstrapping.  bin/genaliases must be run before any lists are created,
    # but if no lists exist yet then mlist is None.  The whole point of the
    # exercise is to get the minimal aliases.db file into existance.
    if mlist is None:
        return
    listname = mlist.internal_name()
    fieldsz = len(listname) + len('-unsubscribe')
    # The text file entries get a little extra info
    print >> fp, '# STANZA START:', listname
    print >> fp, '# CREATED:', time.ctime(time.time())
    # Now add all the standard alias entries
    for k, v in makealiases(listname):
        # Format the text file nicely
        print >> fp, k + ':', ((fieldsz - len(k)) * ' ') + v
    # Finish the text file stanza
    print >> fp, '# STANZA END:', listname
    print >> fp
Пример #5
0
def _addlist(mlist, fp):
    # Set up the mailman-loop address
    loopaddr = Utils.ParseEmail(Utils.get_site_email(extra='loop'))[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.
    fp.seek(0, 2)
    if not fp.tell():
        print >> fp, """\
# This file is generated by Mailman, and is kept in sync with the
# binary hash file aliases.db.  YOU SHOULD NOT MANUALLY EDIT THIS FILE
# unless you know what you're doing, and can keep the two files properly
# in sync.  If you screw it up, you're on your own.
"""
        print >> fp, '# The ultimate loop stopper address'
        print >> fp, '%s: %s' % (loopaddr, loopmbox)
        print >> fp
    # Bootstrapping.  bin/genaliases must be run before any lists are created,
    # but if no lists exist yet then mlist is None.  The whole point of the
    # exercise is to get the minimal aliases.db file into existance.
    if mlist is None:
        return
    listname = mlist.internal_name()
    fieldsz = len(listname) + len('-unsubscribe')
    # The text file entries get a little extra info
    print >> fp, '# STANZA START:', listname
    print >> fp, '# CREATED:', time.ctime(time.time())
    # Now add all the standard alias entries
    for k, v in makealiases(listname):
        # Format the text file nicely
        print >> fp, k + ':', ((fieldsz - len(k)) * ' ') + v
    # Finish the text file stanza
    print >> fp, '# STANZA END:', listname
    print >> fp
Пример #6
0
    def create(self, email):
        if self.exists:
            raise ListAlreadyExists

        langs = [mm_cfg.DEFAULT_SERVER_LANGUAGE]
        pw = Utils.MakeRandomPassword()
        pw_hashed = Utils.sha_new(pw).hexdigest()
        urlhost = mm_cfg.DEFAULT_URL_HOST
        host_name = mm_cfg.DEFAULT_EMAIL_HOST
        web_page_url = mm_cfg.DEFAULT_URL_PATTERN % urlhost

        # TODO: Add some atomicity. We should roll back changes using
        #       a try/else if something (like MTA alias update) fails
        #       before the function terminates.

        try:
            oldmask = os.umask(002)
            self.mlist.Create(self.name, email, pw_hashed, langs=langs,
                              emailhost=host_name, urlhost=urlhost)
            self.mlist.preferred_language = langs[0]

            # Reply-To set to list address
            self.mlist.reply_goes_to_list = 2
            self.mlist.reply_to_address = "%s@%s" % (self.list, self.domain)

            # Allow messages from listname@domain
            self.mlist.acceptable_aliases = "%s@%s\n" % (self.list, self.domain)

            self.mlist.subject_prefix = "[%s] " % (self.list)
            self.mlist.msg_footer = ""
            self.mlist.subscribe_policy = 2 # Confirm and approve
            self.mlist.max_message_size = 20480 # 20M

            self.mlist.Save()

        finally:
            os.umask(oldmask)
            self.mlist.Unlock()

        if mm_cfg.MTA:
            modname = 'Mailman.MTA.' + mm_cfg.MTA
            __import__(modname)
            sys.modules[modname].create(self.mlist)

        siteowner = Utils.get_site_email(self.mlist.host_name, 'owner')
        text = Utils.maketext(
            'newlist.txt',
            {'listname'    : self.name,
             'password'    : pw,
             'admin_url'   : self.mlist.GetScriptURL('admin', absolute=1),
             'listinfo_url': self.mlist.GetScriptURL('listinfo', absolute=1),
             'requestaddr' : self.mlist.GetRequestEmail(),
             'siteowner'   : siteowner,
             }, mlist=self.mlist)


        msg = Message.UserNotification(email, siteowner, 'Your new mailing list: %s' % self.name,
                                        text, self.mlist.preferred_language)
        msg.send(self.mlist)
def openidreg_overview(lang, msg=''):
    # Present the general listinfo overview
    hostname = Utils.get_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()
#    doc.set_language(lang)
    doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)

    legend = _(" OpenID Registeration for Systers Mailing Lists")
    doc.SetTitle(legend)

    table = Table(border=0, width="100%")
    table.AddRow([Center(Header(2, legend))])
    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
                      bgcolor=mm_cfg.WEB_HEADER_COLOR)

    # Skip any mailing lists that isn't advertised.

    if msg:
        greeting = FontAttr(msg, color="ff5060", size="+1")
    else:
        greeting = FontAttr(_('Welcome!'), size='+2')

    welcome = [greeting]
    mailmanlink = Link(mm_cfg.MAILMAN_URL, _('Mailman')).Format()
    

    # set up some local variables
    adj = msg and _('right') or ''
    siteowner = Utils.get_site_email()
    welcome.extend(
        (_(''' This is the Systers OpenID registeration form . To enable your systers account fill in the following entries.
        <p>or Go back to the listinfo page if already using it '''),
         Link(Utils.ScriptURL('listinfo'),
              _('the mailing lists overview page')),
         _(''' <p>If you are having trouble using the lists, please contact '''),
         Link('mailto:' + siteowner, siteowner),
         '.<p>',
         FormatOpenIDLogin(),
         '<p>'))
    
         
    table.AddRow([apply(Container, welcome)])
    table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)

    

    doc.AddItem(table)
    doc.AddItem('<hr>')
    doc.AddItem(MailmanLogo())
    print doc.Format()
Пример #8
0
def create(mlist, cgi=False, nolock=False, quiet=False):
    if mlist is None:
        return
    listname = mlist.internal_name()
    fieldsz = len(listname) + len('-unsubscribe')
    if cgi:
        # If a list is being created via the CGI, the best we can do is send
        # an email message to mailman-owner requesting that the proper aliases
        # be installed.
        sfp = StringIO()
        if not quiet:
            print(_("""
The mailing list `%(listname)s' has been created via the through-the-web
interface.  In order to complete the activation of this mailing list, the
proper /etc/aliases (or equivalent) file must be updated.  The program
`newaliases' may also have to be run.

Here are the entries for the /etc/aliases file:
"""),
                  file=sfp)
        outfp = sfp
    else:
        if not quiet:
            print(
                C_("""\
To finish creating your mailing list, you must edit your /etc/aliases (or
equivalent) file by adding the following lines, and possibly running the
`newaliases' program:
"""))
        print(C_("""\
## %(listname)s mailing list"""))
        outfp = sys.stdout
    # Common path
    for k, v in makealiases(listname):
        print(k + ':', ((fieldsz - len(k)) * ' '), v, file=outfp)
    # If we're using the command line interface, we're done.  For ttw, we need
    # to actually send the message to mailman-owner now.
    if not cgi:
        print(file=outfp)
        return
    # Send the message to the site -owner so someone can do something about
    # this request.
    siteowner = Utils.get_site_email(extra='owner')
    # Should this be sent in the site list's preferred language?
    msg = Message.UserNotification(
        siteowner, siteowner,
        _('Mailing list creation request for list %(listname)s'),
        sfp.getvalue(), mm_cfg.DEFAULT_SERVER_LANGUAGE)
    msg.send(mlist)
Пример #9
0
    def notify_owner (self):
        """Send an email to the owner of the list of successful creation."""

        siteowner = Utils.get_site_email(self.ml.host_name, 'owner')
        text = Utils.maketext(
            'newlist.txt',
            {'listname'    : self.ln,
             'password'    : '',
             'admin_url'   : self.ml.GetScriptURL('admin', absolute=1),
             'listinfo_url': self.ml.GetScriptURL('listinfo', absolute=1),
             'requestaddr' : self.ml.GetRequestEmail(),
             'siteowner'   : siteowner,
             }, mlist=self.ml)
        msg = Message.UserNotification(
            self.owner, siteowner, 'Your new mailing list: %s' % self.ln,
            text, self.ml.preferred_language)
        msg.send(self.ml)
Пример #10
0
def remove(mlist, cgi=False):
    listname = mlist.internal_name()
    fieldsz = len(listname) + len('-unsubscribe')
    if cgi:
        # If a list is being removed via the CGI, the best we can do is send
        # an email message to mailman-owner requesting that the appropriate
        # aliases be deleted.
        sfp = StringIO()
        print(_("""\
The mailing list `%(listname)s' has been removed via the through-the-web
interface.  In order to complete the de-activation of this mailing list, the
appropriate /etc/aliases (or equivalent) file must be updated.  The program
`newaliases' may also have to be run.

Here are the entries in the /etc/aliases file that should be removed:
"""),
              file=sfp)
        outfp = sfp
    else:
        print(
            C_("""
To finish removing your mailing list, you must edit your /etc/aliases (or
equivalent) file by removing the following lines, and possibly running the
`newaliases' program:

## %(listname)s mailing list"""))
        outfp = sys.stdout
    # Common path
    for k, v in makealiases(listname):
        print(k + ':', ((fieldsz - len(k)) * ' '), v, file=outfp)
    # If we're using the command line interface, we're done.  For ttw, we need
    # to actually send the message to mailman-owner now.
    if not cgi:
        print(file=outfp)
        return
    siteowner = Utils.get_site_email(extra='owner')
    # Should this be sent in the site list's preferred language?
    msg = Message.UserNotification(
        siteowner, siteowner,
        _('Mailing list removal request for list %(listname)s'),
        sfp.getvalue(), mm_cfg.DEFAULT_SERVER_LANGUAGE)
    msg['Date'] = email.utils.formatdate(localtime=1)
    outq = get_switchboard(mm_cfg.OUTQUEUE_DIR)
    outq.enqueue(msg, recips=[siteowner], nodecorate=1)
Пример #11
0
 def __init__(self, mlist, subject=None, text=None, tomoderators=1):
     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')
     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
     del self['to']
     self['To'] = mlist.GetOwnerEmail()
     self._sender = sender
     # User notifications are normally sent with Precedence: bulk.  This
     # is appropriate as they can be backscatter of rejected spam.
     # Owner notifications are not backscatter and are perhaps more
     # important than 'bulk' so give them Precedence: list by default.
     # (LP: #1313146)
     self['Precedence'] = 'list'
Пример #12
0
def _addvirtual(mlist, fp):
    listname = mlist.internal_name()
    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')
    loopdest = Utils.ParseEmail(loopaddr)[0]
    if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN:
        loopdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
    # Seek to the end of the text file, but if it's empty write the standard
    # disclaimer, and the loop catch address.
    fp.seek(0, 2)
    if not fp.tell():
        print >> fp, """\
# This file is generated by Mailman, and is kept in sync with the binary hash
# file virtual-mailman.db.  YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you
# know what you're doing, and can keep the two files properly in sync.  If you
# screw it up, you're on your own.
#
# Note that you should already have this virtual domain set up properly in
# your Postfix installation.  See README.POSTFIX for details.

# LOOP ADDRESSES START
%s\t%s
# LOOP ADDRESSES END
""" % (loopaddr, loopdest)
    # The text file entries get a little extra info
    print >> fp, '# STANZA START:', listname
    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)
        if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN:
            localaddr = '%s@%s' % (k, mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN)
        else:
            localaddr = k
        # Format the text file nicely
        print >> fp, fqdnaddr, ((fieldsz - len(k)) * ' '), localaddr
    # Finish the text file stanza
    print >> fp, '# STANZA END:', listname
    print >> fp
Пример #13
0
def _check_for_virtual_loopaddr(mlist, filename):
    loopaddr = Utils.get_site_email(mlist.host_name, extra='loop')
    loopdest = Utils.ParseEmail(loopaddr)[0]
    infp = open(filename)
    omask = os.umask(007)
    try:
        outfp = open(filename + '.tmp', 'w')
    finally:
        os.umask(omask)
    try:
        # Find the start of the loop address block
        while True:
            line = infp.readline()
            if not line:
                break
            outfp.write(line)
            if line.startswith('# LOOP ADDRESSES START'):
                break
        # Now see if our domain has already been written
        while True:
            line = infp.readline()
            if not line:
                break
            if line.startswith('# LOOP ADDRESSES END'):
                # It hasn't
                print >> outfp, '%s\t%s' % (loopaddr, loopdest)
                outfp.write(line)
                break
            elif line.startswith(loopaddr):
                # We just found it
                outfp.write(line)
                break
            else:
                # This isn't our loop address, so spit it out and continue
                outfp.write(line)
        outfp.writelines(infp.readlines())
    finally:
        infp.close()
        outfp.close()
    os.rename(filename + '.tmp', filename)
Пример #14
0
def _check_for_virtual_loopaddr(mlist, filename):
    loopaddr = Utils.get_site_email(mlist.host_name, extra='loop')
    loopdest = Utils.ParseEmail(loopaddr)[0]
    infp = open(filename)
    omask = os.umask(007)
    try:
        outfp = open(filename + '.tmp', 'w')
    finally:
        os.umask(omask)
    try:
        # Find the start of the loop address block
        while True:
            line = infp.readline()
            if not line:
                break
            outfp.write(line)
            if line.startswith('# LOOP ADDRESSES START'):
                break
        # Now see if our domain has already been written
        while True:
            line = infp.readline()
            if not line:
                break
            if line.startswith('# LOOP ADDRESSES END'):
                # It hasn't
                print >> outfp, '%s\t%s' % (loopaddr, loopdest)
                outfp.write(line)
                break
            elif line.startswith(loopaddr):
                # We just found it
                outfp.write(line)
                break
            else:
                # This isn't our loop address, so spit it out and continue
                outfp.write(line)
        outfp.writelines(infp.readlines())
    finally:
        infp.close()
        outfp.close()
    os.rename(filename + '.tmp', filename)
Пример #15
0
                       dir)

    title = _('Mailing list deletion results')
    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)
    if not problems:
        table.AddRow([
            _('''You have successfully deleted the mailing list
    <b>%(listname)s</b>.''')
        ])
    else:
        sitelist = Utils.get_site_email(mlist.host_name)
        table.AddRow([
            _('''There were some problems deleting the mailing list
        <b>%(listname)s</b>.  Contact your site administrator at %(sitelist)s
        for details.''')
        ])
    doc.AddItem(table)
    doc.AddItem('<hr>')
    doc.AddItem(
        _('Return to the ') +
        Link(Utils.ScriptURL('listinfo'), _('general list overview')).Format())
    doc.AddItem(
        _('<br>Return to the ') + Link(Utils.ScriptURL(
            'admin'), _('administrative list overview')).Format())
    doc.AddItem(MailmanLogo())
Пример #16
0
def _check_for_virtual_loopaddr(mlist, filename):
    loopaddr = Utils.get_site_email(mlist.host_name, extra='loop')
    loopdest = Utils.ParseEmail(loopaddr)[0]
    siteaddr = Utils.get_site_email(mlist.host_name)
    sitedest = Utils.ParseEmail(siteaddr)[0]
    siteowneraddr = Utils.get_site_email(mlist.host_name, extra='owner')
    siteownerdest = Utils.ParseEmail(siteowneraddr)[0]
    if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN:
        loopdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
        sitedest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
        siteownerdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
    # If the site list's host_name is a virtual domain, adding the list and
    # owner addresses to the SITE ADDRESSES will duplicate the entries in the
    # stanza for the list.  Postfix doesn't like dups so we try to comment them
    # here, but only for the actual site list domain.
    if (MailList(mm_cfg.MAILMAN_SITE_LIST, lock=False).host_name.lower() ==
            mlist.host_name.lower()):
        siteaddr = '#' + siteaddr
        siteowneraddr = '#' + siteowneraddr
    infp = open(filename)
    omask = os.umask(007)
    try:
        outfp = open(filename + '.tmp', 'w')
    finally:
        os.umask(omask)
    try:
        # Find the start of the loop address block
        while True:
            line = infp.readline()
            if not line:
                break
            outfp.write(line)
            if line.startswith('# LOOP ADDRESSES START'):
                break
        # Now see if our domain has already been written
        while True:
            line = infp.readline()
            if not line:
                break
            if line.startswith('# LOOP ADDRESSES END'):
                # It hasn't
                print >> outfp, '%s\t%s' % (loopaddr, loopdest)
                outfp.write(line)
                break
            elif line.startswith(loopaddr):
                # We just found it
                outfp.write(line)
                break
            else:
                # This isn't our loop address, so spit it out and continue
                outfp.write(line)
        # Now do it all again for the site list address. It must follow the
        # loop addresses.
        while True:
            line = infp.readline()
            if not line:
                break
            outfp.write(line)
            if line.startswith('# SITE ADDRESSES START'):
                break
        # Now see if our domain has already been written
        while True:
            line = infp.readline()
            if not line:
                break
            if line.startswith('# SITE ADDRESSES END'):
                # It hasn't
                print >> outfp, '%s\t%s' % (siteaddr, sitedest)
                print >> outfp, '%s\t%s' % (siteowneraddr, siteownerdest)
                outfp.write(line)
                break
            elif line.startswith(siteaddr) or line.startswith('#' + siteaddr):
                # We just found it
                outfp.write(line)
                break
            else:
                # This isn't our loop address, so spit it out and continue
                outfp.write(line)
        outfp.writelines(infp.readlines())
    finally:
        infp.close()
        outfp.close()
    os.rename(filename + '.tmp', filename)
Пример #17
0
class MaildirRunner(Runner):
    # This class is much different than most runners because it pulls files
    # of a different format than what scripts/post and friends leaves.  The
    # files this runner reads are just single message files as dropped into
    # the directory by the MTA.  This runner will read the file, and enqueue
    # it in the expected qfiles directory for normal processing.
    def __init__(self, slice=None, numslices=1):
        # Don't call the base class constructor, but build enough of the
        # underlying attributes to use the base class's implementation.
        self._stop = 0
        self._dir = os.path.join(mm_cfg.MAILDIR_DIR, 'new')
        self._cur = os.path.join(mm_cfg.MAILDIR_DIR, 'cur')
        self._parser = Parser(Message)

    def _oneloop(self):
        # Refresh this each time through the list.  BAW: could be too
        # expensive.
        listnames = Utils.list_names()
        # Cruise through all the files currently in the new/ directory
        try:
            files = os.listdir(self._dir)
        except OSError, e:
            if e.errno <> errno.ENOENT: raise
            # Nothing's been delivered yet
            return 0
        for file in files:
            srcname = os.path.join(self._dir, file)
            dstname = os.path.join(self._cur, file + ':1,P')
            xdstname = os.path.join(self._cur, file + ':1,X')
            try:
                os.rename(srcname, dstname)
            except OSError, e:
                if e.errno == errno.ENOENT:
                    # Some other MaildirRunner beat us to it
                    continue
                syslog('error', 'Could not rename maildir file: %s', srcname)
                raise
            # Now open, read, parse, and enqueue this message
            try:
                fp = open(dstname)
                try:
                    msg = self._parser.parse(fp)
                finally:
                    fp.close()
                # Now we need to figure out which queue of which list this
                # message was destined for.  See verp_bounce() in
                # BounceRunner.py for why we do things this way.
                vals = []
                for header in ('delivered-to', 'envelope-to', 'apparently-to'):
                    vals.extend(msg.get_all(header, []))
                for field in vals:
                    to = parseaddr(field)[1]
                    if not to:
                        continue
                    mo = lre.match(to)
                    if not mo:
                        # This isn't an address we care about
                        continue
                    listname, subq = mo.group('listname', 'subq')
                    if listname in listnames:
                        break
                else:
                    # As far as we can tell, this message isn't destined for
                    # any list on the system.  What to do?
                    syslog('error', 'Message apparently not for any list: %s',
                           xdstname)
                    os.rename(dstname, xdstname)
                    continue
                # BAW: blech, hardcoded
                msgdata = {'listname': listname}
                # -admin is deprecated
                if subq in ('bounces', 'admin'):
                    queue = get_switchboard(mm_cfg.BOUNCEQUEUE_DIR)
                elif subq == 'confirm':
                    msgdata['toconfirm'] = 1
                    queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
                elif subq in ('join', 'subscribe'):
                    msgdata['tojoin'] = 1
                    queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
                elif subq in ('leave', 'unsubscribe'):
                    msgdata['toleave'] = 1
                    queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
                elif subq == 'owner':
                    msgdata.update({
                        'toowner': 1,
                        'envsender': Utils.get_site_email(extra='bounces'),
                        'pipeline': mm_cfg.OWNER_PIPELINE,
                        })
                    queue = get_switchboard(mm_cfg.INQUEUE_DIR)
                elif subq is None:
                    msgdata['tolist'] = 1
                    queue = get_switchboard(mm_cfg.INQUEUE_DIR)
                elif subq == 'request':
                    msgdata['torequest'] = 1
                    queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
                else:
                    syslog('error', 'Unknown sub-queue: %s', subq)
                    os.rename(dstname, xdstname)
                    continue
                queue.enqueue(msg, msgdata)
                os.unlink(dstname)
            except Exception, e:
                os.rename(dstname, xdstname)
                syslog('error', str(e))
Пример #18
0
def listinfo_overview(msg=''):
    # Present the general listinfo overview
    hostname = Utils.get_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()
    doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)

    legend = _("%(hostname)s Mailing Lists")
    doc.SetTitle(legend)

    table = Table(border=0, width="100%")
    table.AddRow([Center(Header(2, legend))])
    table.AddCellInfo(table.GetCurrentRowIndex(),
                      0,
                      colspan=2,
                      bgcolor=mm_cfg.WEB_HEADER_COLOR)

    # Skip any mailing lists that isn't advertised.
    advertised = []
    listnames = Utils.list_names()
    listnames.sort()

    for name in listnames:
        mlist = MailList.MailList(name, lock=0)
        if mlist.advertised:
            if mm_cfg.VIRTUAL_HOST_OVERVIEW and (
                    mlist.web_page_url.find('/%s/' % hostname) == -1
                    and mlist.web_page_url.find('/%s:' % hostname) == -1):
                # List is for different identity of this host - skip it.
                continue
            else:
                advertised.append(
                    (mlist.GetScriptURL('listinfo'), mlist.real_name,
                     Utils.websafe(mlist.description)))
    if msg:
        greeting = FontAttr(msg, color="ff5060", size="+1")
    else:
        greeting = FontAttr(_('Welcome!'), size='+2')

    welcome = [greeting]
    mailmanlink = Link(mm_cfg.MAILMAN_URL, _('Mailman')).Format()
    if not advertised:
        welcome.extend(
            _('''<p>There currently are no publicly-advertised
            %(mailmanlink)s mailing lists on %(hostname)s.'''))
    else:
        welcome.append(
            _('''<p>Below is a listing of all the public mailing lists on
            %(hostname)s.  Click on a list name to get more information about
            the list, or to subscribe, unsubscribe, and change the preferences
            on your subscription.'''))

    # set up some local variables
    adj = msg and _('right') or ''
    siteowner = Utils.get_site_email()
    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
        list name appended.
        <p>List administrators, you can visit '''),
         Link(Utils.ScriptURL('admin'), _('the list admin overview page')),
         _(''' to find the management interface for your list.
         <p>If you are having trouble using the lists, please contact '''),
         Link('mailto:' + siteowner, siteowner), '.<p>'))

    table.AddRow([apply(Container, welcome)])
    table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)

    if advertised:
        table.AddRow(['&nbsp;', '&nbsp;'])
        table.AddRow([
            Bold(FontAttr(_('List'), size='+2')),
            Bold(FontAttr(_('Description'), size='+2'))
        ])
        highlight = 1
        for url, real_name, description in advertised:
            table.AddRow([
                Link(url, Bold(real_name)), description
                or Italic(_('[no description available]'))
            ])
            if highlight and mm_cfg.WEB_HIGHLIGHT_COLOR:
                table.AddRowInfo(table.GetCurrentRowIndex(),
                                 bgcolor=mm_cfg.WEB_HIGHLIGHT_COLOR)
            highlight = not highlight

    doc.AddItem(table)
    doc.AddItem('<hr>')
    doc.AddItem(MailmanLogo())
    print doc.Format()
Пример #19
0
 def _dispose(self, mlist, msg, msgdata):
     # Make sure we have the most up-to-date state
     mlist.Load()
     outq = get_switchboard(mm_cfg.OUTQUEUE_DIR)
     # There are a few possibilities here:
     #
     # - the message could have been VERP'd in which case, we know exactly
     #   who the message was destined for.  That make our job easy.
     # - the message could have been originally destined for a list owner,
     #   but a list owner address itself bounced.  That's bad, and for now
     #   we'll simply attempt to deliver the message to the site list
     #   owner.
     #   Note that this means that automated bounce processing doesn't work
     #   for the site list.  Because we can't reliably tell to what address
     #   a non-VERP'd bounce was originally sent, we have to treat all
     #   bounces sent to the site list as potential list owner bounces.
     # - the list owner could have set list-bounces (or list-admin) as the
     #   owner address.  That's really bad as it results in a loop of ever
     #   growing unrecognized bounce messages.  We detect this based on the
     #   fact that this message itself will be from the site bounces
     #   address.  We then send this to the site list owner instead.
     # Notices to list-owner have their envelope sender and From: set to
     # the site-bounces address.  Check if this is this a bounce for a
     # message to a list owner, coming to site-bounces, or a looping
     # message sent directly to the -bounces address.  We have to do these
     # cases separately, because sending to site-owner will reset the
     # envelope sender.
     # Is this a site list bounce?
     if (mlist.internal_name().lower() ==
             mm_cfg.MAILMAN_SITE_LIST.lower()):
         # 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
         # get stuck in a bounce loop.
         outq.enqueue(msg, msgdata,
                      recips=mlist.owner,
                      envsender=Utils.get_site_email(extra='loop'),
                      nodecorate=1,
                      )
         return
     # Is this a possible looping message sent directly to a list-bounces
     # address other than the site list?
     # Check From: because unix_from might be VERP'd.
     # Also, check the From: that Message.OwnerNotification uses.
     if (msg.get('from') ==
             Utils.get_site_email(mlist.host_name, 'bounces')):
         # Just send it to the sitelist-owner address.  If that bounces
         # we'll handle it above.
         outq.enqueue(msg, msgdata,
                      recips=[Utils.get_site_email(extra='owner')],
                      envsender=Utils.get_site_email(extra='loop'),
                      nodecorate=1,
                      )
         return
     # List isn't doing bounce processing?
     if not mlist.bounce_processing:
         return
     # Try VERP detection first, since it's quick and easy
     addrs = verp_bounce(mlist, msg)
     if addrs:
         # We have an address, but check if the message is non-fatal.
         if BouncerAPI.ScanMessages(mlist, msg) is BouncerAPI.Stop:
             return
     else:
         # See if this was a probe message.
         token = verp_probe(mlist, msg)
         if token:
             self._probe_bounce(mlist, token)
             return
         # That didn't give us anything useful, so try the old fashion
         # bounce matching modules.
         addrs = BouncerAPI.ScanMessages(mlist, msg)
         if addrs is BouncerAPI.Stop:
             # This is a recognized, non-fatal notice. Ignore it.
             return
     # If that still didn't return us any useful addresses, then send it on
     # or discard it.
     addrs = filter(None, addrs)
     if not addrs:
         syslog('bounce',
                '%s: bounce message w/no discernable addresses: %s',
                mlist.internal_name(),
                msg.get('message-id', 'n/a'))
         maybe_forward(mlist, msg)
         return
     # BAW: It's possible that there are None's in the list of addresses,
     # although I'm unsure how that could happen.  Possibly ScanMessages()
     # can let None's sneak through.  In any event, this will kill them.
     # addrs = filter(None, addrs)
     # MAS above filter moved up so we don't try to queue an empty list.
     self._queue_bounces(mlist.internal_name(), addrs, msg)
Пример #20
0
def process(mlist, msg, msgdata):
    recips = msgdata.get('recips')
    if not recips:
        # Nobody to deliver to!
        return
    # Calculate the non-VERP envelope sender.
    envsender = msgdata.get('envsender')
    if envsender is None:
        if mlist:
            envsender = mlist.GetBouncesEmail()
        else:
            envsender = Utils.get_site_email(extra='bounces')
    # 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
    # separately.  If we're not VERPing, then we'll chunkify based on
    # SMTP_MAX_RCPTS.  Note that most MTAs have a limit on the number of
    # recipients they'll swallow in a single transaction.
    deliveryfunc = None
    if ('personalize' not in msgdata or msgdata['personalize']) and (
           msgdata.get('verp') or mlist.personalize):
        chunks = [[recip] for recip in recips]
        msgdata['personalize'] = 1
        deliveryfunc = verpdeliver
    elif mm_cfg.SMTP_MAX_RCPTS <= 0:
        chunks = [recips]
    else:
        chunks = chunkify(recips, mm_cfg.SMTP_MAX_RCPTS)
    # See if this is an unshunted message for which some were undelivered
    if 'undelivered' in msgdata:
        chunks = msgdata['undelivered']
    # If we're doing bulk delivery, then we can stitch up the message now.
    if deliveryfunc is None:
        # Be sure never to decorate the message more than once!
        if not msgdata.get('decorated'):
            Decorate.process(mlist, msg, msgdata)
            msgdata['decorated'] = True
        deliveryfunc = bulkdeliver
    refused = {}
    t0 = time.time()
    # Open the initial connection
    origrecips = msgdata['recips']
    # MAS: get the message sender now for logging.  If we're using 'sender'
    # and not 'from', bulkdeliver changes it for bounce processing.  If we're
    # VERPing, it doesn't matter because bulkdeliver is working on a copy, but
    # otherwise msg gets changed.  If the list is anonymous, the original
    # sender is long gone, but Cleanse.py has logged it.
    origsender = msgdata.get('original_sender', msg.get_sender())
    # `undelivered' is a copy of chunks that we pop from to do deliveries.
    # This seems like a good tradeoff between robustness and resource
    # utilization.  If delivery really fails (i.e. qfiles/shunt type
    # failures), then we'll pick up where we left off with `undelivered'.
    # This means at worst, the last chunk for which delivery was attempted
    # could get duplicates but not every one, and no recips should miss the
    # message.
    conn = Connection()
    try:
        msgdata['undelivered'] = chunks
        while chunks:
            chunk = chunks.pop()
            msgdata['recips'] = chunk
            try:
                deliveryfunc(mlist, msg, msgdata, envsender, refused, conn)
            except Exception:
                # If /anything/ goes wrong, push the last chunk back on the
                # undelivered list and re-raise the exception.  We don't know
                # how many of the last chunk might receive the message, so at
                # worst, everyone in this chunk will get a duplicate.  Sigh.
                chunks.append(chunk)
                raise
        del msgdata['undelivered']
    finally:
        conn.quit()
        msgdata['recips'] = origrecips
    # Log the successful post
    t1 = time.time()
    d = MsgSafeDict(msg, {'time'    : t1-t0,
                          # BAW: Urg.  This seems inefficient.
                          'size'    : len(msg.as_string()),
                          '#recips' : len(recips),
                          '#refused': len(refused),
                          'listname': mlist.internal_name(),
                          'sender'  : origsender,
                          })
    # We have to use the copy() method because extended call syntax requires a
    # concrete dictionary object; it does not allow a generic mapping.  It's
    # still worthwhile doing the interpolation in syslog() because it'll catch
    # any catastrophic exceptions due to bogus format strings.
    if mm_cfg.SMTP_LOG_EVERY_MESSAGE:
        syslog.write_ex(mm_cfg.SMTP_LOG_EVERY_MESSAGE[0],
                        mm_cfg.SMTP_LOG_EVERY_MESSAGE[1], kws=d)

    if refused:
        if mm_cfg.SMTP_LOG_REFUSED:
            syslog.write_ex(mm_cfg.SMTP_LOG_REFUSED[0],
                            mm_cfg.SMTP_LOG_REFUSED[1], kws=d)

    elif msgdata.get('tolist'):
        # Log the successful post, but only if it really was a post to the
        # mailing list.  Don't log sends to the -owner, or -admin addrs.
        # -request addrs should never get here.  BAW: it may be useful to log
        # the other messages, but in that case, we should probably have a
        # separate configuration variable to control that.
        if mm_cfg.SMTP_LOG_SUCCESS:
            syslog.write_ex(mm_cfg.SMTP_LOG_SUCCESS[0],
                            mm_cfg.SMTP_LOG_SUCCESS[1], kws=d)

    # Process any failed deliveries.
    tempfailures = []
    permfailures = []
    for recip, (code, smtpmsg) in list(refused.items()):
        # DRUMS is an internet draft, but it says:
        #
        #    [RFC-821] incorrectly listed the error where an SMTP server
        #    exhausts its implementation limit on the number of RCPT commands
        #    ("too many recipients") as having reply code 552.  The correct
        #    reply code for this condition is 452. Clients SHOULD treat a 552
        #    code in this case as a temporary, rather than permanent failure
        #    so the logic below works.
        #
        if code >= 500 and code != 552:
            # A permanent failure
            permfailures.append(recip)
        else:
            # Deal with persistent transient failures by queuing them up for
            # future delivery.  TBD: this could generate lots of log entries!
            tempfailures.append(recip)
        if mm_cfg.SMTP_LOG_EACH_FAILURE:
            d.update({'recipient': recip,
                      'failcode' : code,
                      'failmsg'  : smtpmsg})
            syslog.write_ex(mm_cfg.SMTP_LOG_EACH_FAILURE[0],
                            mm_cfg.SMTP_LOG_EACH_FAILURE[1], kws=d)
    # Return the results
    if tempfailures or permfailures:
        raise Errors.SomeRecipientsFailed(tempfailures, permfailures)
Пример #21
0
def _addvirtual(mlist, fp):
    listname = mlist.internal_name()
    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')
    loopdest = Utils.ParseEmail(loopaddr)[0]
    # And the site list posting address.
    siteaddr = Utils.get_site_email(mlist.host_name)
    sitedest = Utils.ParseEmail(siteaddr)[0]
    # And the site list -owner address.
    siteowneraddr = Utils.get_site_email(mlist.host_name, extra='owner')
    siteownerdest = Utils.ParseEmail(siteowneraddr)[0]
    if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN:
        loopdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
        sitedest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
        siteownerdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
    # If the site list's host_name is a virtual domain, adding the list and
    # owner addresses to the SITE ADDRESSES will duplicate the entries in the
    # stanza for the list.  Postfix doesn't like dups so we try to comment them
    # here, but only for the actual site list domain.
    if (MailList(mm_cfg.MAILMAN_SITE_LIST, lock=False).host_name.lower() ==
            hostname.lower()):
        siteaddr = '#' + siteaddr
        siteowneraddr = '#' + siteowneraddr
    # Seek to the end of the text file, but if it's empty write the standard
    # disclaimer, and the loop catch address and site address.
    fp.seek(0, 2)
    if not fp.tell():
        print >> fp, """\
# This file is generated by Mailman, and is kept in sync with the binary hash
# file virtual-mailman.db.  YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you
# know what you're doing, and can keep the two files properly in sync.  If you
# screw it up, you're on your own.
#
# Note that you should already have this virtual domain set up properly in
# your Postfix installation.  See README.POSTFIX for details.

# LOOP ADDRESSES START
%s\t%s
# LOOP ADDRESSES END

# We also add the site list address in each virtual domain as that address
# is exposed on admin and listinfo overviews, and we add the site list-owner
# address as it is exposed in the list created email notice.

# SITE ADDRESSES START
%s\t%s
%s\t%s
# SITE ADDRESSES END
""" % (loopaddr, loopdest, siteaddr, sitedest, siteowneraddr, siteownerdest)
    # The text file entries get a little extra info
    print >> fp, '# STANZA START:', listname
    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)
        if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN:
            localaddr = '%s@%s' % (k, mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN)
        else:
            localaddr = k
        # Format the text file nicely
        print >> fp, fqdnaddr, ((fieldsz - len(k)) * ' '), localaddr
    # Finish the text file stanza
    print >> fp, '# STANZA END:', listname
    print >> fp
Пример #22
0
def listinfo_overview(msg=''):
    # Present the general listinfo overview
    hostname = Utils.get_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()
    doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)

    legend = _("%(hostname)s Mailing Lists")
    doc.SetTitle(legend)

    table = Table(border=0, width="100%")
    table.AddRow([Center(Header(2, legend))])
    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
                      bgcolor=mm_cfg.WEB_HEADER_COLOR)

    # Skip any mailing lists that isn't advertised.
    advertised = []
    listnames = Utils.list_names()
    listnames.sort()

    for name in listnames:
        mlist = MailList.MailList(name, lock=0)
        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.
                continue
            else:
                advertised.append((mlist.GetScriptURL('listinfo'),
                                   mlist.real_name,
                                   mlist.description))
    if msg:
        greeting = FontAttr(msg, color="ff5060", size="+1")
    else:
        greeting = FontAttr(_('Welcome!'), size='+2')

    welcome = [greeting]
    mailmanlink = Link(mm_cfg.MAILMAN_URL, _('Mailman')).Format()
    if not advertised:
        welcome.extend(
            _('''<p>There currently are no publicly-advertised
            %(mailmanlink)s mailing lists on %(hostname)s.'''))
    else:
        welcome.append(
            _('''<p>Below is a listing of all the public mailing lists on
            %(hostname)s.  Click on a list name to get more information about
            the list, or to subscribe, unsubscribe, and change the preferences
            on your subscription.'''))

    # set up some local variables
    adj = msg and _('right') or ''
    siteowner = Utils.get_site_email()
    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
        list name appended.
        <p>List administrators, you can visit '''),
         Link(Utils.ScriptURL('admin'),
              _('the list admin overview page')),
         _(''' to find the management interface for your list.
         <p>If you are having trouble using the lists, please contact '''),
         Link('mailto:' + siteowner, siteowner),
         '.<p>'))
    
    welcome.extend(
        (_('''Users who are subscribed to one of the Mailing List can use OpenID to login with common credentials,
        to access all their subscribed List.
        <p> To register OpenID  '''),
        Link(Utils.ScriptURL('openidreg'),
              _('Click Here')),
        _(''' to use OpenID for Systers'''),
        '.<p>'))   
    table.AddRow([apply(Container, welcome)])
    table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)

    if advertised:
        table.AddRow(['&nbsp;', '&nbsp;'])
        table.AddRow([Bold(FontAttr(_('List'), size='+2')),
                      Bold(FontAttr(_('Description'), size='+2'))
                      ])
        highlight = 1
        for url, real_name, description in advertised:
            table.AddRow(
                [Link(url, Bold(real_name)),
                      description or Italic(_('[no description available]'))])
            if highlight and mm_cfg.WEB_HIGHLIGHT_COLOR:
                table.AddRowInfo(table.GetCurrentRowIndex(),
                                 bgcolor=mm_cfg.WEB_HIGHLIGHT_COLOR)
            highlight = not highlight

    doc.AddItem(table)
    doc.AddItem('<hr>')
    doc.AddItem(MailmanLogo())
    print doc.Format()
Пример #23
0
def _addvirtual(mlist, fp):
    listname = mlist.internal_name()
    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')
    loopdest = Utils.ParseEmail(loopaddr)[0]
    # And the site list posting address.
    siteaddr = Utils.get_site_email(mlist.host_name)
    sitedest = Utils.ParseEmail(siteaddr)[0]
    # And the site list -owner address.
    siteowneraddr = Utils.get_site_email(mlist.host_name, extra='owner')
    siteownerdest = Utils.ParseEmail(siteowneraddr)[0]
    if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN:
        loopdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
        sitedest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
        siteownerdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
    # If the site list's host_name is a virtual domain, adding the list and
    # owner addresses to the SITE ADDRESSES will duplicate the entries in the
    # stanza for the list.  Postfix doesn't like dups so we try to comment them
    # here, but only for the actual site list domain.
    if (MailList(mm_cfg.MAILMAN_SITE_LIST,
                 lock=False).host_name.lower() == hostname.lower()):
        siteaddr = '#' + siteaddr
        siteowneraddr = '#' + siteowneraddr
    # Seek to the end of the text file, but if it's empty write the standard
    # disclaimer, and the loop catch address and site address.
    fp.seek(0, 2)
    if not fp.tell():
        print >> fp, """\
# This file is generated by Mailman, and is kept in sync with the binary hash
# file virtual-mailman.db.  YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you
# know what you're doing, and can keep the two files properly in sync.  If you
# screw it up, you're on your own.
#
# Note that you should already have this virtual domain set up properly in
# your Postfix installation.  See README.POSTFIX for details.

# LOOP ADDRESSES START
%s\t%s
# LOOP ADDRESSES END

# We also add the site list address in each virtual domain as that address
# is exposed on admin and listinfo overviews, and we add the site list-owner
# address as it is exposed in the list created email notice.

# SITE ADDRESSES START
%s\t%s
%s\t%s
# SITE ADDRESSES END
""" % (loopaddr, loopdest, siteaddr, sitedest, siteowneraddr, siteownerdest)
    # The text file entries get a little extra info
    print >> fp, '# STANZA START:', listname
    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)
        if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN:
            localaddr = '%s@%s' % (k, mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN)
        else:
            localaddr = k
        # Format the text file nicely
        print >> fp, fqdnaddr, ((fieldsz - len(k)) * ' '), localaddr
    # Finish the text file stanza
    print >> fp, '# STANZA END:', listname
    print >> fp
Пример #24
0
def _check_for_virtual_loopaddr(mlist, filename):
    loopaddr = Utils.get_site_email(mlist.host_name, extra='loop')
    loopdest = Utils.ParseEmail(loopaddr)[0]
    siteaddr = Utils.get_site_email(mlist.host_name)
    sitedest = Utils.ParseEmail(siteaddr)[0]
    siteowneraddr = Utils.get_site_email(mlist.host_name, extra='owner')
    siteownerdest = Utils.ParseEmail(siteowneraddr)[0]
    if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN:
        loopdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
        sitedest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
        siteownerdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN
    # If the site list's host_name is a virtual domain, adding the list and
    # owner addresses to the SITE ADDRESSES will duplicate the entries in the
    # stanza for the list.  Postfix doesn't like dups so we try to comment them
    # here, but only for the actual site list domain.
    if (MailList(mm_cfg.MAILMAN_SITE_LIST,
                 lock=False).host_name.lower() == mlist.host_name.lower()):
        siteaddr = '#' + siteaddr
        siteowneraddr = '#' + siteowneraddr
    infp = open(filename)
    omask = os.umask(007)
    try:
        outfp = open(filename + '.tmp', 'w')
    finally:
        os.umask(omask)
    try:
        # Find the start of the loop address block
        while True:
            line = infp.readline()
            if not line:
                break
            outfp.write(line)
            if line.startswith('# LOOP ADDRESSES START'):
                break
        # Now see if our domain has already been written
        while True:
            line = infp.readline()
            if not line:
                break
            if line.startswith('# LOOP ADDRESSES END'):
                # It hasn't
                print >> outfp, '%s\t%s' % (loopaddr, loopdest)
                outfp.write(line)
                break
            elif line.startswith(loopaddr):
                # We just found it
                outfp.write(line)
                break
            else:
                # This isn't our loop address, so spit it out and continue
                outfp.write(line)
        # Now do it all again for the site list address. It must follow the
        # loop addresses.
        while True:
            line = infp.readline()
            if not line:
                break
            outfp.write(line)
            if line.startswith('# SITE ADDRESSES START'):
                break
        # Now see if our domain has already been written
        while True:
            line = infp.readline()
            if not line:
                break
            if line.startswith('# SITE ADDRESSES END'):
                # It hasn't
                print >> outfp, '%s\t%s' % (siteaddr, sitedest)
                print >> outfp, '%s\t%s' % (siteowneraddr, siteownerdest)
                outfp.write(line)
                break
            elif line.startswith(siteaddr) or line.startswith('#' + siteaddr):
                # We just found it
                outfp.write(line)
                break
            else:
                # This isn't our loop address, so spit it out and continue
                outfp.write(line)
        outfp.writelines(infp.readlines())
    finally:
        infp.close()
        outfp.close()
    os.rename(filename + '.tmp', filename)
Пример #25
0
	textSend = 1
	tmp = '#  FILE: %s  #' % fileNameNew
	text.append('#' * len(tmp))
	text.append(tmp)
	text.append('#' * len(tmp))
	text.append('')

	linesLeft = showLines	#  e-mail first linesLeft of log files
	for line in fileinput.input(fileNameNew):
		if linesLeft == 0:
			text.append('[... truncated ...]')
			break
		linesLeft = linesLeft - 1
		line = string.rstrip(line)
		text.append(line)
	text.append('')

#  send message if we've actually found anything
if textSend:
	text = string.join(text, '\n') + '\n'
	siteowner = Utils.get_site_email()
	Utils.SendTextToUser(
		'Mailman Log Report -- %s' % time.ctime(time.time()),
		text, siteowner, siteowner)

#  compress any log-files we made
if hasattr(mm_cfg, 'COMPRESS_LOGFILES_WITH') and mm_cfg.COMPRESS_LOGFILES_WITH:
	for file in newLogfiles:
		os.system(mm_cfg.COMPRESS_LOGFILES_WITH % file)
Пример #26
0
                problems += 1
                syslog('error',
                       'directory %s not deleted due to permission problems',
                       dir)

    title = _('Mailing list deletion results')
    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)
    if not problems:
        table.AddRow([_('''You have successfully deleted the mailing list
    <b>%(listname)s</b>.''')])
    else:
        sitelist = Utils.get_site_email(mlist.host_name)
        table.AddRow([_('''There were some problems deleting the mailing list
        <b>%(listname)s</b>.  Contact your site administrator at %(sitelist)s
        for details.''')])
    doc.AddItem(table)
    doc.AddItem('<hr>')
    doc.AddItem(_('Return to the ') +
                Link(Utils.ScriptURL('listinfo'),
                     _('general list overview')).Format())
    doc.AddItem(_('<br>Return to the ') +
                Link(Utils.ScriptURL('admin'),
                     _('administrative list overview')).Format())
    doc.AddItem(MailmanLogo())



        pass  #  permission denied, DOH!

    textSend = 1
    tmp = '#  FILE: %s  #' % fileNameNew
    text.append('#' * len(tmp))
    text.append(tmp)
    text.append('#' * len(tmp))
    text.append('')

    linesLeft = showLines  #  e-mail first linesLeft of log files
    for line in fileinput.input(fileNameNew):
        if linesLeft == 0:
            text.append('[... truncated ...]')
            break
        linesLeft = linesLeft - 1
        line = string.rstrip(line)
        text.append(line)
    text.append('')

#  send message if we've actually found anything
if textSend:
    text = string.join(text, '\n') + '\n'
    siteowner = Utils.get_site_email()
    Utils.SendTextToUser('Mailman Log Report -- %s' % time.ctime(time.time()),
                         text, siteowner, siteowner)

#  compress any log-files we made
if hasattr(mm_cfg, 'COMPRESS_LOGFILES_WITH') and mm_cfg.COMPRESS_LOGFILES_WITH:
    for file in newLogfiles:
        os.system(mm_cfg.COMPRESS_LOGFILES_WITH % file)
 def _oneloop(self):
     # Refresh this each time through the list.  BAW: could be too
     # expensive.
     listnames = Utils.list_names()
     # Cruise through all the files currently in the new/ directory
     try:
         files = os.listdir(self._dir)
     except OSError as e:
         if e.errno != errno.ENOENT: raise
         # Nothing's been delivered yet
         return 0
     for file in files:
         srcname = os.path.join(self._dir, file)
         dstname = os.path.join(self._cur, file + ':1,P')
         xdstname = os.path.join(self._cur, file + ':1,X')
         try:
             os.rename(srcname, dstname)
         except OSError as e:
             if e.errno == errno.ENOENT:
                 # Some other MaildirRunner beat us to it
                 continue
             syslog('error', 'Could not rename maildir file: %s', srcname)
             raise
         # Now open, read, parse, and enqueue this message
         try:
             fp = open(dstname)
             try:
                 msg = self._parser.parse(fp)
             finally:
                 fp.close()
             # Now we need to figure out which queue of which list this
             # message was destined for.  See verp_bounce() in
             # BounceRunner.py for why we do things this way.
             vals = []
             for header in ('delivered-to', 'envelope-to', 'apparently-to'):
                 vals.extend(msg.get_all(header, []))
             for field in vals:
                 to = parseaddr(field)[1]
                 if not to:
                     continue
                 mo = lre.match(to)
                 if not mo:
                     # This isn't an address we care about
                     continue
                 listname, subq = mo.group('listname', 'subq')
                 if listname in listnames:
                     break
             else:
                 # As far as we can tell, this message isn't destined for
                 # any list on the system.  What to do?
                 syslog('error', 'Message apparently not for any list: %s',
                        xdstname)
                 os.rename(dstname, xdstname)
                 continue
             # BAW: blech, hardcoded
             msgdata = {'listname': listname}
             # -admin is deprecated
             if subq in ('bounces', 'admin'):
                 queue = get_switchboard(mm_cfg.BOUNCEQUEUE_DIR)
             elif subq == 'confirm':
                 msgdata['toconfirm'] = 1
                 queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
             elif subq in ('join', 'subscribe'):
                 msgdata['tojoin'] = 1
                 queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
             elif subq in ('leave', 'unsubscribe'):
                 msgdata['toleave'] = 1
                 queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
             elif subq == 'owner':
                 msgdata.update({
                     'toowner': 1,
                     'envsender': Utils.get_site_email(extra='bounces'),
                     'pipeline': mm_cfg.OWNER_PIPELINE,
                     })
                 queue = get_switchboard(mm_cfg.INQUEUE_DIR)
             elif subq is None:
                 msgdata['tolist'] = 1
                 queue = get_switchboard(mm_cfg.INQUEUE_DIR)
             elif subq == 'request':
                 msgdata['torequest'] = 1
                 queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
             else:
                 syslog('error', 'Unknown sub-queue: %s', subq)
                 os.rename(dstname, xdstname)
                 continue
             queue.enqueue(msg, msgdata)
             os.unlink(dstname)
         except Exception as e:
             os.rename(dstname, xdstname)
             syslog('error', str(e))
Пример #29
0
def process(mlist, msg, msgdata):
    recips = msgdata.get('recips')
    if not recips:
        # Nobody to deliver to!
        return
    # Calculate the non-VERP envelope sender.
    envsender = msgdata.get('envsender')
    if envsender is None:
        if mlist:
            envsender = mlist.GetBouncesEmail()
        else:
            envsender = Utils.get_site_email(extra='bounces')
    # 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
    # separately.  If we're not VERPing, then we'll chunkify based on
    # SMTP_MAX_RCPTS.  Note that most MTAs have a limit on the number of
    # recipients they'll swallow in a single transaction.
    deliveryfunc = None
    if (not msgdata.has_key('personalize') or msgdata['personalize']) and (
           msgdata.get('verp') or mlist.personalize):
        chunks = [[recip] for recip in recips]
        msgdata['personalize'] = 1
        deliveryfunc = verpdeliver
    elif mm_cfg.SMTP_MAX_RCPTS <= 0:
        chunks = [recips]
    else:
        chunks = chunkify(recips, mm_cfg.SMTP_MAX_RCPTS)
    # See if this is an unshunted message for which some were undelivered
    if msgdata.has_key('undelivered'):
        chunks = msgdata['undelivered']
    # If we're doing bulk delivery, then we can stitch up the message now.
    if deliveryfunc is None:
        # Be sure never to decorate the message more than once!
        if not msgdata.get('decorated'):
            Decorate.process(mlist, msg, msgdata)
            msgdata['decorated'] = True
        deliveryfunc = bulkdeliver
    refused = {}
    t0 = time.time()
    # Open the initial connection
    origrecips = msgdata['recips']
    # MAS: get the message sender now for logging.  If we're using 'sender'
    # and not 'from', bulkdeliver changes it for bounce processing.  If we're
    # VERPing, it doesn't matter because bulkdeliver is working on a copy, but
    # otherwise msg gets changed.  If the list is anonymous, the original
    # sender is long gone, but Cleanse.py has logged it.
    origsender = msgdata.get('original_sender', msg.get_sender())
    # `undelivered' is a copy of chunks that we pop from to do deliveries.
    # This seems like a good tradeoff between robustness and resource
    # utilization.  If delivery really fails (i.e. qfiles/shunt type
    # failures), then we'll pick up where we left off with `undelivered'.
    # This means at worst, the last chunk for which delivery was attempted
    # could get duplicates but not every one, and no recips should miss the
    # message.
    conn = Connection()
    try:
        msgdata['undelivered'] = chunks
        while chunks:
            chunk = chunks.pop()
            msgdata['recips'] = chunk
            try:
                deliveryfunc(mlist, msg, msgdata, envsender, refused, conn)
            except Exception:
                # If /anything/ goes wrong, push the last chunk back on the
                # undelivered list and re-raise the exception.  We don't know
                # how many of the last chunk might receive the message, so at
                # worst, everyone in this chunk will get a duplicate.  Sigh.
                chunks.append(chunk)
                raise
        del msgdata['undelivered']
    finally:
        conn.quit()
        msgdata['recips'] = origrecips
    # Log the successful post
    t1 = time.time()
    d = MsgSafeDict(msg, {'time'    : t1-t0,
                          # BAW: Urg.  This seems inefficient.
                          'size'    : len(msg.as_string()),
                          '#recips' : len(recips),
                          '#refused': len(refused),
                          'listname': mlist.internal_name(),
                          'sender'  : origsender,
                          })
    # We have to use the copy() method because extended call syntax requires a
    # concrete dictionary object; it does not allow a generic mapping.  It's
    # still worthwhile doing the interpolation in syslog() because it'll catch
    # any catastrophic exceptions due to bogus format strings.
    if mm_cfg.SMTP_LOG_EVERY_MESSAGE:
        syslog.write_ex(mm_cfg.SMTP_LOG_EVERY_MESSAGE[0],
                        mm_cfg.SMTP_LOG_EVERY_MESSAGE[1], kws=d)

    if refused:
        if mm_cfg.SMTP_LOG_REFUSED:
            syslog.write_ex(mm_cfg.SMTP_LOG_REFUSED[0],
                            mm_cfg.SMTP_LOG_REFUSED[1], kws=d)

    elif msgdata.get('tolist'):
        # Log the successful post, but only if it really was a post to the
        # mailing list.  Don't log sends to the -owner, or -admin addrs.
        # -request addrs should never get here.  BAW: it may be useful to log
        # the other messages, but in that case, we should probably have a
        # separate configuration variable to control that.
        if mm_cfg.SMTP_LOG_SUCCESS:
            syslog.write_ex(mm_cfg.SMTP_LOG_SUCCESS[0],
                            mm_cfg.SMTP_LOG_SUCCESS[1], kws=d)

    # Process any failed deliveries.
    tempfailures = []
    permfailures = []
    for recip, (code, smtpmsg) in refused.items():
        # DRUMS is an internet draft, but it says:
        #
        #    [RFC-821] incorrectly listed the error where an SMTP server
        #    exhausts its implementation limit on the number of RCPT commands
        #    ("too many recipients") as having reply code 552.  The correct
        #    reply code for this condition is 452. Clients SHOULD treat a 552
        #    code in this case as a temporary, rather than permanent failure
        #    so the logic below works.
        #
        if code >= 500 and code <> 552:
            # A permanent failure
            permfailures.append(recip)
        else:
            # Deal with persistent transient failures by queuing them up for
            # future delivery.  TBD: this could generate lots of log entries!
            tempfailures.append(recip)
        if mm_cfg.SMTP_LOG_EACH_FAILURE:
            d.update({'recipient': recip,
                      'failcode' : code,
                      'failmsg'  : smtpmsg})
            syslog.write_ex(mm_cfg.SMTP_LOG_EACH_FAILURE[0],
                            mm_cfg.SMTP_LOG_EACH_FAILURE[1], kws=d)
    # Return the results
    if tempfailures or permfailures:
        raise Errors.SomeRecipientsFailed(tempfailures, permfailures)
Пример #30
0
 def _dispose(self, mlist, msg, msgdata):
     # Make sure we have the most up-to-date state
     mlist.Load()
     outq = get_switchboard(mm_cfg.OUTQUEUE_DIR)
     # There are a few possibilities here:
     #
     # - the message could have been VERP'd in which case, we know exactly
     #   who the message was destined for.  That make our job easy.
     # - the message could have been originally destined for a list owner,
     #   but a list owner address itself bounced.  That's bad, and for now
     #   we'll simply attempt to deliver the message to the site list
     #   owner.
     #   Note that this means that automated bounce processing doesn't work
     #   for the site list.  Because we can't reliably tell to what address
     #   a non-VERP'd bounce was originally sent, we have to treat all
     #   bounces sent to the site list as potential list owner bounces.
     # - the list owner could have set list-bounces (or list-admin) as the
     #   owner address.  That's really bad as it results in a loop of ever
     #   growing unrecognized bounce messages.  We detect this based on the
     #   fact that this message itself will be from the site bounces
     #   address.  We then send this to the site list owner instead.
     # Notices to list-owner have their envelope sender and From: set to
     # the site-bounces address.  Check if this is this a bounce for a
     # message to a list owner, coming to site-bounces, or a looping
     # message sent directly to the -bounces address.  We have to do these
     # cases separately, because sending to site-owner will reset the
     # envelope sender.
     # Is this a site list bounce?
     if (mlist.internal_name().lower() == mm_cfg.MAILMAN_SITE_LIST.lower()):
         # 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
         # get stuck in a bounce loop.
         outq.enqueue(
             msg,
             msgdata,
             recips=mlist.owner,
             envsender=Utils.get_site_email(extra='loop'),
             nodecorate=1,
         )
         return
     # Is this a possible looping message sent directly to a list-bounces
     # address other than the site list?
     # Check From: because unix_from might be VERP'd.
     # Also, check the From: that Message.OwnerNotification uses.
     if (msg.get('from') == Utils.get_site_email(mlist.host_name,
                                                 'bounces')):
         # Just send it to the sitelist-owner address.  If that bounces
         # we'll handle it above.
         outq.enqueue(
             msg,
             msgdata,
             recips=[Utils.get_site_email(extra='owner')],
             envsender=Utils.get_site_email(extra='loop'),
             nodecorate=1,
         )
         return
     # List isn't doing bounce processing?
     if not mlist.bounce_processing:
         return
     # Try VERP detection first, since it's quick and easy
     addrs = verp_bounce(mlist, msg)
     if addrs:
         # We have an address, but check if the message is non-fatal.
         if BouncerAPI.ScanMessages(mlist, msg) is BouncerAPI.Stop:
             return
     else:
         # See if this was a probe message.
         token = verp_probe(mlist, msg)
         if token:
             self._probe_bounce(mlist, token)
             return
         # That didn't give us anything useful, so try the old fashion
         # bounce matching modules.
         addrs = BouncerAPI.ScanMessages(mlist, msg)
         if addrs is BouncerAPI.Stop:
             # This is a recognized, non-fatal notice. Ignore it.
             return
     # If that still didn't return us any useful addresses, then send it on
     # or discard it.
     addrs = filter(None, addrs)
     if not addrs:
         syslog('bounce',
                '%s: bounce message w/no discernable addresses: %s',
                mlist.internal_name(), msg.get('message-id', 'n/a'))
         maybe_forward(mlist, msg)
         return
     # BAW: It's possible that there are None's in the list of addresses,
     # although I'm unsure how that could happen.  Possibly ScanMessages()
     # can let None's sneak through.  In any event, this will kill them.
     # addrs = filter(None, addrs)
     # MAS above filter moved up so we don't try to queue an empty list.
     self._queue_bounces(mlist.internal_name(), addrs, msg)
Пример #31
0
def process_request(doc, cgidata, mlist):
    password = cgidata.getfirst('password', '').strip()
    try:
        delarchives = int(cgidata.getfirst('delarchives', '0'))
    except ValueError:
        delarchives = 0

    # Removing a list is limited to the list-creator (a.k.a. list-destroyer),
    # the list-admin, or the site-admin.  Don't use WebAuthenticate here
    # because we want to be sure the actual typed password is valid, not some
    # password sitting in a cookie.
    if mlist.Authenticate(
        (mm_cfg.AuthCreator, mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin),
            password) == mm_cfg.UnAuthorized:
        request_deletion(
            doc, mlist,
            _('You are not authorized to delete this mailing list'))
        return

    # Do the MTA-specific list deletion tasks
    if mm_cfg.MTA:
        modname = 'Mailman.MTA.' + mm_cfg.MTA
        __import__(modname)
        sys.modules[modname].remove(mlist, cgi=1)

    REMOVABLES = ['lists/%s']

    if delarchives:
        REMOVABLES.extend([
            'archives/private/%s',
            'archives/private/%s.mbox',
            'archives/public/%s',
            'archives/public/%s.mbox',
        ])

    problems = 0
    listname = mlist.internal_name()
    for dirtmpl in REMOVABLES:
        dir = os.path.join(mm_cfg.VAR_PREFIX, dirtmpl % listname)
        if os.path.islink(dir):
            try:
                os.unlink(dir)
            except OSError as e:
                if e.errno not in (errno.EACCES, errno.EPERM): raise
                problems += 1
                syslog('error',
                       'link %s not deleted due to permission problems', dir)
        elif os.path.isdir(dir):
            try:
                shutil.rmtree(dir)
            except OSError as e:
                if e.errno not in (errno.EACCES, errno.EPERM): raise
                problems += 1
                syslog('error',
                       'directory %s not deleted due to permission problems',
                       dir)

    title = _('Mailing list deletion results')
    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)
    if not problems:
        table.AddRow([
            _('''You have successfully deleted the mailing list
    <b>%(listname)s</b>.''')
        ])
    else:
        sitelist = Utils.get_site_email(mlist.host_name)
        table.AddRow([
            _('''There were some problems deleting the mailing list
        <b>%(listname)s</b>.  Contact your site administrator at %(sitelist)s
        for details.''')
        ])
    doc.AddItem(table)
    doc.AddItem('<hr>')
    doc.AddItem(
        _('Return to the ') +
        Link(Utils.ScriptURL('listinfo'), _('general list overview')).Format())
    doc.AddItem(
        _('<br>Return to the ') + Link(Utils.ScriptURL(
            'admin'), _('administrative list overview')).Format())
    doc.AddItem(MailmanLogo())