summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/mta/base.py67
-rw-r--r--src/mailman/mta/bulk.py13
-rw-r--r--src/mailman/mta/personalized.py44
-rw-r--r--src/mailman/mta/verp.py99
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'