Update of /cvsroot/mailman/mailman3/src/mailman/smtp
In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv31133/src/mailman/smtp

Added Files:
      Tag: 3.0
        __init__.py bounceproc.py chunkify.py connection.py 
        decorate.py pool.py postfix.py verp.py 
Log Message:
Initial checkin of Mailman3 source

--- NEW FILE: __init__.py ---
# Copyright (C) 2004 Barry A. Warsaw
#
# 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., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.

from mailman.smtp.pool import ConnectionPool
from mailman.smtp.verp import VERPMailer
#from mailman.smtp.bulk import BulkMailer

_pool = None
_verp = None
_bulk = None



def send(mlist, msg, msgdata):
    # Decide whether to bulk send or verp send this message
    if msgdata.get('personalize') or mlist.personalize:
        _verp.send(mlist, msg, msgdata)
    else:
        _bulk.send(mlist, msg, msgdata)



def startup():
    global _pool, _verp, _bulk
    _pool = ConnectionPool()
    _pool.start()
    _verp = VERPMailer(_pool)
#    _bulk = BulkMailer(_pool)


def shutdown():
    _pool.close()

--- NEW FILE: bounceproc.py ---
# Copyright (C) 2004 Barry A. Warsaw
#
# 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., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.

import os
import sets
import errno
import logging
import datetime
import threading

from mailman.config import configuration
from mailman.workthread import WorkThread

log = logging.getLogger('mailman.bounces')



class BounceProc(object):
    def __init__(self):
        self._bouncefp = None
        self._seqno = 0
        self._count = 0
        self._bouncefp = None
        self._rlock = threading.RLock()
        self._stop = threading.Event()
        self._procthread = BounceProcessorThread(self._stop, self)
        self._procthread.start()

    def _open(self):
        filename = 'bounces.%05d.%05d.open' % (os.getpid(), self._seqno)
        self._seqno += 1
        path = os.path.join(configuration.dirs.bouncedata, filename)
        self._bouncefp = open(path, 'w')
        self._count = 0

    def close(self):
        self._close()
        self._stop.set()

    def _close(self):
        if self._bouncefp is None:
            return
        # Closing should flip the events file to .closed so that the
        # processing thread will process the bounces, the next time its
        # started up.
        self._rlock.acquire()
        try:
            self._bouncefp.flush()
            self._bouncefp.close()
            oldname = self._bouncefp.name
            newname = os.path.splitext(oldname)[0] + '.closed'
            os.rename(oldname, newname)
        finally:
            self._rlock.release()
        return newname

    def register(self, addresses, date=None):
        if self._bouncefp is None:
            self._open()
        if date is None:
            date = datetime.date.now()
        self._rlock.acquire()
        try:
            for address in addresses:
                print >> self._bouncefp, address, date.isoformat()
                self._count += 1
        finally:
            self._rlock.release()

    def flip(self):
        self._rlock.acquire()
        try:
            if not self._count:
                return None
            newname = self.close()
        finally:
            self._rlock.release()
        self._open()
        return newname



class BounceProcessorThread(WorkThread):
    def __init__(self, event, bounceEvents):
        WorkThread.__init__(self, event, log)
        self._bounceEvents = bounceEvents
        self._files = []
        # First scan the event dir for existing .close files, and add them to
        # the queue for processing.
        for f in os.listdir(configuration.dirs.bouncedata):
            path = os.path.join(configuration.dirs.bouncedata, f)
            if path.endswith('.closed'):
                self._files.append(path)

    def _takefile(self, file):
        if file is None:
            return None
        newfile = os.path.splitext(file)[0] + '.taken'
        try:
            os.rename(file, newfile)
            return file
        except OSError, e:
            if e.errno <> errno.ENOENT: raise
            return None

    def _work(self):
        # Process one file at a time.  If self._files is empty, flip the event
        # log to get another file to work on.
        file = None
        while self._files and not file:
            file = self._takefile(self._files.pop())
        if file is None:
            file = self._takefile(self._bounceEvents.flip())
        if file is None:
            # Nothing to do
            return
        # Now process all the events in the file.  We only process one bounce
        # per address per day.
        s = sets.Set()
        fp = open(file)
        try:
            for line in fp:
                address, date = line.split()
                date = datetime.date(*[int(x) for x in date.split('-')])
                if (address, date) in s:
                    # We already registered a bounce for this combination
                    continue
                else:
                    #db.register(address, date)
                    log.debug('Bounce registered: %s at %s', address, date)
                    s.add((address, date))
        finally:
            fp.close()

--- NEW FILE: chunkify.py ---
# Copyright (C) 2004 Barry A. Warsaw
#
# 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., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.

"""Chunkify a list of recipients.

The algorithm is as follows:

 - sort the addresses by reverse domain
 - fill those into max_recips buckets

I can imagine all manner of other algorithms for chunking, but we're
essentially playing a guessing game as to what's most efficient for the MTA
we're talking to.  WIBNI we didn't even have to do this?
"""

from mailman.config import configuration



def chunkify(recips):
    recipchunks = []
    chunk = []
    chunksize = 0
    revrecips = sort(recips)
    while revrecips:
        chunk.append(revrecips.pop())
        chunksize += 1
        if chunksize == configuration.smtp.maxrecips:
            recipchunks.append(chunk)
            chunk = []
            chunksize = 0
    if chunk:
        recipchunks.append(chunk)
    return recipchunks



def sort(recips):
    revrecips = []
    for address in recips:
        localpart, domain = address.split('@', 1)
        parts = domain.split('.')
        parts.reverse()
        revrecips.append((tuple(parts), address))
        revrecips.sort()
    for parts, address in revrecips:
        yield address

--- NEW FILE: connection.py ---
# Copyright (C) 2004 Barry A. Warsaw
#
# 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., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.

"""Connection objects."""

import smtplib



class Connection(object):
    """The representation of one re-connectable connection."""

    def __init__(self, host, port, maxsessions):
        self._host = host
        self._port = port
        self._maxsessions = maxsessions
        self._numsessions = None
        self._conn = None

    def _connect(self):
        self._conn = smtplib.SMTP()
        self._conn.connect(self._host, self._port)
        self._numsessions = self._maxsessions

    def send(self, envsender, recips, msgtext):
        if self._conn is None:
            self._connect()
        try:
            results = self._conn.sendmail(envsender, recips, msgtext)
        except Exception:
            # For safety, close this connection.  The next send attempt will
            # automatically re-open it.  Pass the exception on up.
            self.close()
            raise
        # This session has been successfully completed.
        if self._numsessions is not None:
            self._numsessions -= 1
            if self._numsessions <= 0:
                self.close()
        return results

    def close(self):
        # Only quit the underlying connection if it actually is connected
        if self._conn:
            try:
                self._conn.quit()
            except smtplib.SMTPException:
                pass
            self._conn = None

--- NEW FILE: decorate.py ---
# Copyright (C) 2004 Barry A. Warsaw
#
# 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., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.

"""Decorate the message with the appropriate header and footers."""

LATIN_1_15 = ('iso-8859-1', 'iso-8859-15')



def compatible(mcset, lcset):
    # If the list's preferred charset is us-ascii, we can always safely add
    # the header/footer to a plain text message since all email charsets
    # Mailman supports are strict supersets of us-ascii -- no, UTF-16 emails
    # are not supported.
    if lcset == 'us-ascii':
        return True
    if mcset == lcset:
        return True
    # Latin 1 and Latin 9 are basically compatible
    if lcset in LATIN_1_15 and mcset in LATIN_1_15:
        return True
    # For now, nothing else is compatible.
    return False



def decorate(mlist, msg, msgdata):
    if msgdata.get('nodecorate'):
        return
    if msgdata.get('isdigest') and not mlist.shared_decoration:
        header = mlist.digest_header
        footer = mlist.digest_footer
    else:
        header = mlist.msg_header
        header = mlist.msg_footer
    # Escape hatch if there's nothing to do
    if not header and not footer:
        return
    # Attach the headers and footers in a MIME-compliant way.
    #
    # If the messages's content type is text/plain and the character sets are
    # compatible, we can add these parts by concatenation to the payload.
    mcset = msg.get_param('charset', 'us-ascii').lower()
    # XXX get the list's preferred character set
    lcset = 'us-ascii'
    msgtype = msg.get_type('text/plain')
    if not msg.is_multipart() and msgtype == 'text/plain' and \
           msg.get('content-transfer-coding', '').lower() <> 'base64' and \
           compatible(mcset, lcset):
        payload = msg.get_payload()
        frontsep = endsep = ''
        if header and not header.endswith('\n'):
            frontsep = '\n'
        if footer and not payload.endswith('\n'):
            endsep = '\n'
        payload = header + frontsep + payload + endsep + footer
        msg.set_payload(payload)
        return
    # For now, we don't add the headers and footers for any other combination.

--- NEW FILE: pool.py ---
# Copyright (C) 2004 Barry A. Warsaw
#
# 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., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.

"""Connection pools.

You can specify any number of outgoing smtp servers that Mailman can send
messages out on, and the number of simultaneous connections per server.
Mailman will maintain a pool of connection objects per server and re-use these
for among the delivery threads that you can also specify.
"""

import os
import socket
import smtplib
import logging
import datetime
import threading
from Queue import Queue

from mailman.config import configuration
from mailman.smtp.bounceproc import BounceProc
from mailman.smtp.connection import Connection
from mailman.queues.switchboard import Switchboard

log = logging.getLogger('mailman.smtp')



class ConnectionPool(object):
    def __init__(self):
        __pychecker__ = 'unusednames=i'

        self._queue = Queue()
        # Create a connection objects, one for every connection we're allowed
        # to make to each server.  They'll each block on a job queue, which
        # we'll fill for each message to be sent.
        self._threads = []
        for host, port, conns, sessions in configuration.smtp.server:
            for i in range(conns):
                conn = Connection(host, port, sessions)
                t = ConnectionThread(self._queue, conn)
                self._threads.append(t)

    def start(self):
        for t in self._threads:
            t.start()

    def close(self):
        # Send the "end job" marker to all threads, then join them
        for t in self._threads:
            self._queue.put(None)
        for t in self._threads:
            t.join()

    def send(self, envsender, recips, text, msg, msgdata):
        self._queue.put((envsender, recips, text, msg, msgdata))



class ConnectionThread(threading.Thread):
    def __init__(self, queue, conn):
        threading.Thread.__init__(self)
        self._queue = queue
        self._conn = conn
        retrydir = os.path.join(configuration.dirs.qdir, 'retry')
        self._retryq = Switchboard(retrydir)
        self._bounces = BounceProc()

    def run(self):
        while True:
            # This will block until there is work to do
            job = self._queue.get()
            if job is None:
                # We're shutting down
                break
            envsender, recips, text, msg, msgdata = job
            try:
                refused = self._conn.send(envsender, recips, text)
            except smtplib.SMTPRecipientsRefused, e:
                log.error('SMTPRecipientRefused: %s', msg['message-id'])
                refused = e.recipients
                log.info('refused recipients: %s', refused)
            except (socket.error, smtplib.SMTPException, IOError), e:
                log.error('other mta error: %s', e)
                # If the exception had an associated error code, use it,
                # otherwise, fake it with a non-triggering exception code
                refused = {}
                errcode = getattr(e, 'smtp_code', -1)
                errmsg = getattr(e, 'smtp_error', 'ignore')
                for r in recips:
                    refused[r] = (errcode, errmsg)
            # Now go through all the refused recipients.  Log permanent
            # failures in the bounce events file.  Collect temporary failures
            # so that the message can be retried.
            tempfailures = []
            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
                    self._bounces.register(recip)
                else:
                    # Deal with persistent transient failures by queuing them
                    # up for retry later.
                    tempfailures.append(recip)
            # If there are any temporary failures, retry this message
            if tempfailures:
                self._retryq.enqueue(msg, msgdata,
                                     recips=tempfailures,
                                     last_recipcnt=len(recips),
                                     last_attempt=datetime.datetime.now())

--- NEW FILE: postfix.py ---
# Copyright (C) 2004 Barry A Warsaw
#
# 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., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.

"""Postfix-specific support."""

import os

from mailman.config import configuration
from mailman.database import dbmgr



def genaliases(fp):
    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.
"""
    domains = dbmgr.lists.domains()
    if not domains:
        return
    print >> fp, '# VIRTUAL DOMAINS START'
    for domain in domains:
        print >> fp, domain, '\tIGNORE'
    print >> fp, '# VIRTUAL DOMAINS END'
    print >> fp
    print >> fp, '# LOOP ADDRESSES START'
    for domain in domains:
        print >> fp, '[EMAIL PROTECTED]' % (
            configuration.smtp.loop_address,
            domain,
            os.path.join(configuration.dirs.vardir,
                         configuration.smtp.loop_address + '.txt'))
    print >> fp, '# LOOP ADDRESSES END'
    print >> fp
    for listid in dbmgr.lists.listIds():
        listname, domain = listid.split('.', 1)
        # The trailing slash tells Postfix this is a maildir alias
        maildir = os.path.join(configuration.dirs.maildir, listid) + '/'
        print >> fp, '# STANZA START:', listid
        print '[EMAIL PROTECTED]' % (listname, domain, maildir)
        for command in configuration.smtp.command:
            print '[EMAIL PROTECTED]' % (listname, command, domain, maildir)
        print >> fp, '# STANZA END'
        print >> fp
    # Now run the postmap command on the virtual file
    os.system(configuration.smtp.alias_command + ' ' + fp.name)

--- NEW FILE: verp.py ---
# Copyright (C) 2004 Barry A. Warsaw
#
# 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., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.

import datetime
import threading

from mailman.smtp.chunkify import sort
from mailman.smtp.decorate import decorate
from mailman.text.safedict import safedict
from mailman.text.strings import mstring

ENVSENDER = '[EMAIL PROTECTED]'



class VERPMailer:
    def __init__(self, conn):
        self._conn = conn

    def send(self, mlist, msg, msgdata):
        # There's no point in chunking the recipients for VERP delivery since
        # each recipient gets sent in a new SMTP transaction.
        #
        # See if this is an unshunted message for which there were some
        # undelivered recipients.
        recips = msgdata.get('undelivered')
        if not recips:
            recips = msgdata.get('recips')
        if not recips:
            return
        # Prepare the message by personalizing certain parts of the headers,
        # and adding the decoration.  Then flatten the message, making it
        # ready for mail merging in the inner loop.
        del msg['to']
        msg['To'] = '${mm:recipient}'
        decorate(mlist, msg, msgdata)
        rawtext = mstring(msg.as_string())
        # The substitution dict, with list-specific keys
        listname = mlist.listname
        d = safedict(
            {'mm:public_name': mlist.public_name,
             'mm:listname': listname,
             'mm:domain': mlist.domain,
             'mm:listinfo': 'http://%s/listinfo/%s' % (mydomain, listname),
             })
        # Get a connection.  This will block if there are no connections left.
        for recip in recips:
            # Calculate member specific keys for the dict
            d.update({
                'mm:member_page': 'http://%s/options/%s' % (mydomain, recip),
                'mm:recipient': recip,
                })
            # Calculate the VERP'd sender
            localpart, domain = recip.split('@', 1)
            envsender = ENVSENDER % (localpart, domain, mydomain)
            text = rawtext % d
            self._conn.send(envsender, [recip], text, msg, msgdata)


_______________________________________________
Mailman-checkins mailing list
[EMAIL PROTECTED]
Unsubscribe: http://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org

Reply via email to