diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/mta/base.py | 67 | ||||
| -rw-r--r-- | src/mailman/mta/bulk.py | 13 | ||||
| -rw-r--r-- | src/mailman/mta/personalized.py | 44 | ||||
| -rw-r--r-- | src/mailman/mta/verp.py | 99 |
4 files changed, 157 insertions, 66 deletions
diff --git a/src/mailman/mta/base.py b/src/mailman/mta/base.py index 5942386a6..48a3cd8f4 100644 --- a/src/mailman/mta/base.py +++ b/src/mailman/mta/base.py @@ -22,9 +22,11 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ 'BaseDelivery', + 'IndividualDelivery', ] +import copy import logging import smtplib @@ -44,20 +46,11 @@ class BaseDelivery: implements(IMailTransportAgentDelivery) - def __init__(self, max_recipients=None): - """Create a basic deliverer. - - :param max_recipients: The maximum number of recipients per delivery - chunk. None, zero or less means to group all recipients into one - big chunk. - :type max_recipients: integer - """ - self._max_recipients = (max_recipients - if max_recipients is not None - else 0) + def __init__(self): + """Create a basic deliverer.""" self._connection = Connection( config.mta.smtp_host, int(config.mta.smtp_port), - self._max_recipients) + int(config.mta.max_sessions_per_connection)) def _deliver_to_recipients(self, mlist, msg, msgdata, sender, recipients): @@ -125,3 +118,53 @@ class BaseDelivery: if mlist is None else mlist.bounces_address) return sender + + + +class IndividualDelivery(BaseDelivery): + """Deliver a unique individual message to each recipient. + + This is a framework delivery mechanism. By using mixins, registration, + and subclassing you can customize this delivery class to do any + combination of VERP, full personalization, individualized header/footer + decoration and even full mail merging. + + The core concept here is that for each recipient, the deliver() method + iterates over the list of registered callbacks, each of which have a + chance to modify the message before final delivery. + """ + + def __init__(self): + """See `BaseDelivery`.""" + # + super(IndividualDelivery, self).__init__() + self.callbacks = [] + + def deliver(self, mlist, msg, msgdata): + """See `IMailTransportAgentDelivery`. + + Craft a unique message for every recipient. Encode the recipient's + delivery address in the return envelope so there can be no ambiguity + in bounce processing. + """ + recipients = msgdata.get('recipients') + if not recipients: + # Could be None, could be an empty sequence. + return + refused = {} + for recipient in recipients: + # Make a copy of the original messages and operator on it, since + # we're going to munge it repeatedly for each recipient. + message_copy = copy.deepcopy(msg) + msgdata_copy = msgdata.copy() + # Squirrel the current recipient away in the message metadata. + # That way the subclass's _get_sender() override can encode the + # recipient address in the sender, e.g. for VERP. + msgdata_copy['recipient'] = recipient + sender = self._get_sender(mlist, message_copy, msgdata_copy) + for callback in self.callbacks: + callback(mlist, message_copy, msgdata_copy) + status = self._deliver_to_recipients( + mlist, message_copy, msgdata_copy, sender, [recipient]) + refused.update(status) + return refused diff --git a/src/mailman/mta/bulk.py b/src/mailman/mta/bulk.py index e516594d4..3247331d3 100644 --- a/src/mailman/mta/bulk.py +++ b/src/mailman/mta/bulk.py @@ -45,6 +45,19 @@ CHUNKMAP = dict( class BulkDelivery(BaseDelivery): """Deliver messages to the MSA in as few sessions as possible.""" + def __init__(self, max_recipients=None): + """See `BaseDelivery`. + + :param max_recipients: The maximum number of recipients per delivery + chunk. None, zero or less means to group all recipients into one + big chunk. + :type max_recipients: integer + """ + super(BulkDelivery, self).__init__() + self._max_recipients = (max_recipients + if max_recipients is not None + else 0) + def chunkify(self, recipients): """Split a set of recipients into chunks. diff --git a/src/mailman/mta/personalized.py b/src/mailman/mta/personalized.py index 99d939096..3c8eba25a 100644 --- a/src/mailman/mta/personalized.py +++ b/src/mailman/mta/personalized.py @@ -22,6 +22,7 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ 'PersonalizedDelivery', + 'PersonalizedMixin', ] @@ -34,17 +35,26 @@ from mailman.mta.verp import VERPDelivery -class PersonalizedDelivery(VERPDelivery): - """Personalize the message's To header.""" +class PersonalizedMixin: + """Personalize the message's To header. - def _deliver_to_recipients(self, mlist, msg, msgdata, - sender, recipients): - """See `BaseDelivery`.""" - # This module only works with VERP delivery. - assert len(recipients) == 1, 'Single recipient is required' - # Try to find the real name for the recipient email address, if the - # address is associated with a user registered with Mailman. - recipient = recipients[0] + This is a mixin class, providing the basic functionality for header + personalization. The methods it provides are intended to be called from a + concrete base class. + """ + + def personalize_to(self, msg, recipient): + """Modify the To header to contain the recipient. + + The To header contents is replaced with the recipient's address, and + if the recipient is a user registered with Mailman, the recipient's + real name too. + + :param msg: The message to modify. + :type msg: `email.message.Message` + :param recipient: The recipient's email address. + :type recipient: string + """ user_manager = getUtility(IUserManager) user = user_manager.get_user(recipient) if user is None: @@ -55,5 +65,19 @@ class PersonalizedDelivery(VERPDelivery): # encoded for email transport. name = Header(user.real_name).encode() msg.replace_header('To', formataddr((name, recipient))) + + +class PersonalizedDelivery(VERPDelivery, PersonalizedMixin): + """Personalize the message's To header.""" + + def _deliver_to_recipients(self, mlist, msg, msgdata, + sender, recipients): + """See `BaseDelivery`.""" + # This module only works with VERP delivery. + assert len(recipients) == 1, 'Single recipient is required' + # Try to find the real name for the recipient email address, if the + # address is associated with a user registered with Mailman. + recipient = recipients[0] + self.personalize_to(msg, recipient) return super(PersonalizedDelivery, self)._deliver_to_recipients( mlist, msg, msgdata, sender, recipients) diff --git a/src/mailman/mta/verp.py b/src/mailman/mta/verp.py index a8cc9d937..a46c42cff 100644 --- a/src/mailman/mta/verp.py +++ b/src/mailman/mta/verp.py @@ -22,15 +22,15 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ 'VERPDelivery', + 'VERPMixin', ] -import copy import logging from mailman.config import config from mailman.email.utils import split_email -from mailman.mta.base import BaseDelivery +from mailman.mta.base import IndividualDelivery from mailman.utilities.string import expand @@ -39,49 +39,60 @@ log = logging.getLogger('mailman.smtp') -class VERPDelivery(BaseDelivery): - """Deliver a unique message to the MSA for each recipient.""" +class VERPMixin: + """Mixin for VERP functionality. - def deliver(self, mlist, msg, msgdata): - """See `IMailTransportAgentDelivery`. + This works by overriding the base class's _get_sender() method to return + the VERP'd envelope sender. It expects the individual recipient's address + to be squirreled away in the message metadata. + """ + def _get_sender(self, mlist, msg, msgdata): + """Return the recipient's address VERP encoded in the sender. - Craft a unique message for every recipient. Encode the recipient's - delivery address in the return envelope so there can be no ambiguity - in bounce processing. + :param mlist: The mailing list being delivered to. + :type mlist: `IMailingList` + :param msg: The original message being delivered. + :type msg: `Message` + :param msgdata: Additional message metadata for this delivery. + :type msgdata: dictionary """ - recipients = msgdata.get('recipients') - if not recipients: - # Could be None, could be an empty sequence. - return - sender = self._get_sender(mlist, msg, msgdata) + recipient = msgdata['recipient'] + sender = super(VERPMixin, self)._get_sender(mlist, msg, msgdata) sender_mailbox, sender_domain = split_email(sender) - refused = {} - for recipient in recipients: - # Make a copy of the original messages and operator on it, since - # we're going to munge it repeatedly for each recipient. - message_copy = copy.deepcopy(msg) - # Encode the recipient's address for VERP. - recipient_mailbox, recipient_domain = split_email(recipient) - if recipient_domain 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 - verp_sender = '{0}@{1}'.format( - expand(config.mta.verp_format, dict( - bounces=sender_mailbox, - local=recipient_mailbox, - domain=DOT.join(recipient_domain))), - DOT.join(sender_domain)) - # 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 message_copy['x-mailman-copy'] - if recipient in msgdata.get('add-dup-header', {}): - message_copy['X-Mailman-Copy'] = 'yes' - recipient_refused = self._deliver_to_recipients( - mlist, msg, msgdata, verp_sender, [recipient]) - refused.update(recipient_refused) - return refused + # Encode the recipient's address for VERP. + recipient_mailbox, recipient_domain = split_email(recipient) + if recipient_domain 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) + return sender + return '{0}@{1}'.format( + expand(config.mta.verp_format, dict( + bounces=sender_mailbox, + local=recipient_mailbox, + domain=DOT.join(recipient_domain))), + DOT.join(sender_domain)) + + + +class VERPDelivery(VERPMixin, IndividualDelivery): + """Deliver a unique message to the MSA for each recipient.""" + + def __init__(self): + """See `IndividualDelivery`.""" + super(VERPDelivery, self).__init__() + self.callbacks.append(self.avoid_duplicates) + + def avoid_duplicates(self, mlist, msg, msgdata): + """Flag the message for duplicate avoidance. + + 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. + """ + recipient = msgdata['recipient'] + del msg['x-mailman-copy'] + if recipient in msgdata.get('add-dup-header', {}): + msg['X-Mailman-Copy'] = 'yes' |
