summaryrefslogtreecommitdiff
path: root/mailman/pipeline/smtp_direct.py
diff options
context:
space:
mode:
authorBarry Warsaw2009-01-06 19:55:59 -0500
committerBarry Warsaw2009-01-06 19:55:59 -0500
commit89f5f76ed31d6ca2faf8e2a783a37e9009b03413 (patch)
treef1023f64501a49917674f5bcd78927aa5cee08ef /mailman/pipeline/smtp_direct.py
parent37c255b7b0c1b8ea10c8d24a44c8586de86ffcc6 (diff)
downloadmailman-89f5f76ed31d6ca2faf8e2a783a37e9009b03413.tar.gz
mailman-89f5f76ed31d6ca2faf8e2a783a37e9009b03413.tar.zst
mailman-89f5f76ed31d6ca2faf8e2a783a37e9009b03413.zip
Diffstat (limited to 'mailman/pipeline/smtp_direct.py')
-rw-r--r--mailman/pipeline/smtp_direct.py419
1 files changed, 0 insertions, 419 deletions
diff --git a/mailman/pipeline/smtp_direct.py b/mailman/pipeline/smtp_direct.py
deleted file mode 100644
index b82fb9227..000000000
--- a/mailman/pipeline/smtp_direct.py
+++ /dev/null
@@ -1,419 +0,0 @@
-# Copyright (C) 1998-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman 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 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman 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
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Local SMTP direct drop-off.
-
-This module delivers messages via SMTP to a locally specified daemon. This
-should be compatible with any modern SMTP server. It is expected that the MTA
-handles all final delivery. We have to play tricks so that the list object
-isn't locked while delivery occurs synchronously.
-
-Note: This file only handles single threaded delivery. See SMTPThreaded.py
-for a threaded implementation.
-"""
-
-__metaclass__ = type
-__all__ = [
- 'SMTPDirect',
- ]
-
-
-import copy
-import time
-import socket
-import logging
-import smtplib
-
-from email.Charset import Charset
-from email.Header import Header
-from email.Utils import formataddr
-from string import Template
-from zope.interface import implements
-
-from mailman import Utils
-from mailman.config import config
-from mailman.core import errors
-from mailman.i18n import _
-from mailman.interfaces.handler import IHandler
-from mailman.interfaces.mailinglist import Personalization
-
-
-DOT = '.'
-COMMA = ','
-log = logging.getLogger('mailman.smtp')
-
-
-
-# Manage a connection to the SMTP server
-class Connection:
- def __init__(self):
- self.__conn = None
-
- def __connect(self):
- self.__conn = smtplib.SMTP()
- host = config.mta.smtp_host
- port = int(config.mta.smtp_port)
- log.debug('Connecting to %s:%s', host, port)
- self.__conn.connect(host, port)
- self.__numsessions = int(config.mta.max_sessions_per_connection)
-
- def sendmail(self, envsender, recips, msgtext):
- if self.__conn is None:
- self.__connect()
- try:
- results = self.__conn.sendmail(envsender, recips, msgtext)
- except smtplib.SMTPException:
- # For safety, close this connection. The next send attempt will
- # automatically re-open it. Pass the exception on up.
- self.quit()
- raise
- # This session has been successfully completed.
- self.__numsessions -= 1
- # By testing exactly for equality to 0, we automatically handle the
- # case for SMTP_MAX_SESSIONS_PER_CONNECTION <= 0 meaning never close
- # the connection. We won't worry about wraparound <wink>.
- if self.__numsessions == 0:
- self.quit()
- return results
-
- def quit(self):
- if self.__conn is None:
- return
- try:
- self.__conn.quit()
- except smtplib.SMTPException:
- pass
- self.__conn = None
-
-
-
-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.bounces_address
- else:
- envsender = Utils.get_site_noreply()
- # Time to split up the recipient list. If we're personalizing or VERPing
- # then each chunk will have exactly one recipient. We'll then hand craft
- # an envelope sender and stitch a message together in memory for each one
- # 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 <> Personalization.none):
- chunks = [[recip] for recip in recips]
- msgdata['personalize'] = 1
- deliveryfunc = verpdeliver
- elif int(config.mta.max_recipients) <= 0:
- chunks = [recips]
- else:
- chunks = chunkify(recips, int(config.mta.max_recipients))
- # 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'):
- handler = config.handlers['decorate']
- handler.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()
- substitutions = dict(
- msgid = msg.get('message-id', 'n/a'),
- listname = mlist.fqdn_listname,
- sender = origsender,
- recip = len(recips),
- size = msg.original_size,
- time = t1 - t0,
- refused = len(refused),
- smtpcode = 'n/a',
- smtpmsg = 'n/a',
- )
- # Log this message.
- template = config.logging.smtp.every
- if template != 'no':
- template = Template(template)
- log.info('%s', template.safe_substitute(substitutions))
- if refused:
- template = config.logging.smtp.refused
- if template != 'no':
- template = Template(template)
- log.info('%s', template.safe_substitute(substitutions))
- else:
- # Log the successful post, but if it was not destined to the mailing
- # list (e.g. to the owner or admin), print the actual recipients
- # instead of just the number.
- if not msgdata.get('tolist'):
- recips = msg.get_all('to', [])
- recips.extend(msg.get_all('cc', []))
- substitutions['recips'] = COMMA.join(recips)
- template = config.logging.smtp.success
- if template != 'no':
- template = Template(template)
- log.info('%s', template.safe_substitute(substitutions))
- # 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)
- template = config.logging.smtp.failure
- if template != 'no':
- substitutions.update(
- recip = recip,
- smtpcode = code,
- smtpmsg = smtpmsg,
- )
- template = Template(template)
- log.info('%s', template.safe_substitute(substitutions))
- # Return the results
- if tempfailures or permfailures:
- raise errors.SomeRecipientsFailed(tempfailures, permfailures)
-
-
-
-def chunkify(recips, chunksize):
- # First do a simple sort on top level domain. It probably doesn't buy us
- # much to try to sort on MX record -- that's the MTA's job. We're just
- # trying to avoid getting a max recips error. Split the chunks along
- # these lines (as suggested originally by Chuq Von Rospach and slightly
- # elaborated by BAW).
- chunkmap = {'com': 1,
- 'net': 2,
- 'org': 2,
- 'edu': 3,
- 'us' : 3,
- 'ca' : 3,
- }
- buckets = {}
- for r in recips:
- tld = None
- i = r.rfind('.')
- if i >= 0:
- tld = r[i+1:]
- bin = chunkmap.get(tld, 0)
- bucket = buckets.get(bin, [])
- bucket.append(r)
- buckets[bin] = bucket
- # Now start filling the chunks
- chunks = []
- currentchunk = []
- chunklen = 0
- for bin in buckets.values():
- for r in bin:
- currentchunk.append(r)
- chunklen = chunklen + 1
- if chunklen >= chunksize:
- chunks.append(currentchunk)
- currentchunk = []
- chunklen = 0
- if currentchunk:
- chunks.append(currentchunk)
- currentchunk = []
- chunklen = 0
- return chunks
-
-
-
-def verpdeliver(mlist, msg, msgdata, envsender, failures, conn):
- handler = config.handlers['decorate']
- for recip in msgdata['recips']:
- # We now need to stitch together the message with its header and
- # footer. If we're VERPIng, we have to calculate the envelope sender
- # for each recipient. Note that the list of recipients must be of
- # length 1.
- #
- # BAW: ezmlm includes the message number in the envelope, used when
- # sending a notification to the user telling her how many messages
- # they missed due to bouncing. Neat idea.
- msgdata['recips'] = [recip]
- # Make a copy of the message and decorate + delivery that
- msgcopy = copy.deepcopy(msg)
- handler.process(mlist, msgcopy, msgdata)
- # Calculate the envelope sender, which we may be VERPing
- if msgdata.get('verp'):
- bmailbox, bdomain = Utils.ParseEmail(envsender)
- rmailbox, rdomain = Utils.ParseEmail(recip)
- if rdomain is None:
- # The recipient address is not fully-qualified. We can't
- # deliver it to this person, nor can we craft a valid verp
- # header. I don't think there's much we can do except ignore
- # this recipient.
- log.info('Skipping VERP delivery to unqual recip: %s', recip)
- continue
- envsender = Template(config.mta.verp_format).safe_substitute(
- bounces=bmailbox, mailbox=rmailbox,
- host=DOT.join(rdomain)) + '@' + DOT.join(bdomain)
- if mlist.personalize == Personalization.full:
- # When fully personalizing, we want the To address to point to the
- # recipient, not to the mailing list
- del msgcopy['to']
- name = None
- if mlist.isMember(recip):
- name = mlist.getMemberName(recip)
- if name:
- # Convert the name to an email-safe representation. If the
- # name is a byte string, convert it first to Unicode, given
- # the character set of the member's language, replacing bad
- # characters for which we can do nothing about. Once we have
- # the name as Unicode, we can create a Header instance for it
- # so that it's properly encoded for email transport.
- charset = Utils.GetCharSet(mlist.getMemberLanguage(recip))
- if charset == 'us-ascii':
- # Since Header already tries both us-ascii and utf-8,
- # let's add something a bit more useful.
- charset = 'iso-8859-1'
- charset = Charset(charset)
- codec = charset.input_codec or 'ascii'
- if not isinstance(name, unicode):
- name = unicode(name, codec, 'replace')
- name = Header(name, charset).encode()
- msgcopy['To'] = formataddr((name, recip))
- else:
- msgcopy['To'] = recip
- # We can flag the mail as a duplicate for each member, if they've
- # already received this message, as calculated by Message-ID. See
- # AvoidDuplicates.py for details.
- del msgcopy['x-mailman-copy']
- if msgdata.get('add-dup-header', {}).has_key(recip):
- msgcopy['X-Mailman-Copy'] = 'yes'
- # For the final delivery stage, we can just bulk deliver to a party of
- # one. ;)
- bulkdeliver(mlist, msgcopy, msgdata, envsender, failures, conn)
-
-
-
-def bulkdeliver(mlist, msg, msgdata, envsender, failures, conn):
- # Do some final cleanup of the message header. Start by blowing away
- # any the Sender: and Errors-To: headers so remote MTAs won't be
- # tempted to delivery bounces there instead of our envelope sender
- #
- # BAW An interpretation of RFCs 2822 and 2076 could argue for not touching
- # the Sender header at all. Brad Knowles points out that MTAs tend to
- # wipe existing Return-Path headers, and old MTAs may still honor
- # Errors-To while new ones will at worst ignore the header.
- del msg['sender']
- del msg['errors-to']
- msg['Sender'] = envsender
- msg['Errors-To'] = envsender
- # Get the plain, flattened text of the message, sans unixfrom
- msgtext = msg.as_string()
- refused = {}
- recips = msgdata['recips']
- msgid = msg['message-id']
- try:
- # Send the message
- refused = conn.sendmail(envsender, recips, msgtext)
- except smtplib.SMTPRecipientsRefused, e:
- log.error('%s recipients refused: %s', msgid, e)
- refused = e.recipients
- except smtplib.SMTPResponseException, e:
- log.error('%s SMTP session failure: %s, %s',
- msgid, e.smtp_code, e.smtp_error)
- # If this was a permanent failure, don't add the recipients to the
- # refused, because we don't want them to be added to failures.
- # Otherwise, if the MTA rejects the message because of the message
- # content (e.g. it's spam, virii, or has syntactic problems), then
- # this will end up registering a bounce score for every recipient.
- # Definitely /not/ what we want.
- if e.smtp_code < 500 or e.smtp_code == 552:
- # It's a temporary failure
- for r in recips:
- refused[r] = (e.smtp_code, e.smtp_error)
- except (socket.error, IOError, smtplib.SMTPException), e:
- # MTA not responding, or other socket problems, or any other kind of
- # SMTPException. In that case, nothing got delivered, so treat this
- # as a temporary failure.
- log.error('%s low level smtp error: %s', msgid, e)
- error = str(e)
- for r in recips:
- refused[r] = (-1, error)
- failures.update(refused)
-
-
-
-class SMTPDirect:
- """SMTP delivery."""
-
- implements(IHandler)
-
- name = 'smtp-direct'
- description = _('SMTP delivery.')
-
- def process(self, mlist, msg, msgdata):
- """See `IHandler`."""
- process(mlist, msg, msgdata)