IMailTransportAgentDelivery.deliver() returns a dictionary just like
[mailman.git] / src / mailman / mta / bulk.py
blob21bbd171316acc0c046ced07249f6e4d85732a3b
1 # Copyright (C) 2009 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Module stuff."""
20 from __future__ import absolute_import, unicode_literals
22 __metaclass__ = type
23 __all__ = [
24 'BulkDelivery',
28 import logging
29 import smtplib
31 from itertools import chain
33 from zope.interface import implements
35 from mailman.config import config
36 from mailman.interfaces.mta import IMailTransportAgentDelivery
37 from mailman.mta.connection import Connection
40 log = logging.getLogger('mailman.smtp')
42 # A mapping of top-level domains to bucket numbers. The zeroth bucket is
43 # reserved for everything else. At one time, these were the most common
44 # domains.
45 CHUNKMAP = dict(
46 com=1,
47 net=2,
48 org=2,
49 edu=3,
50 us=3,
51 ca=3,
56 class BulkDelivery:
57 """Deliver messages to the MTA in as few sessions as possible."""
59 implements(IMailTransportAgentDelivery)
61 def __init__(self, max_recipients=None):
62 """Create a bulk deliverer.
64 :param max_recipients: The maximum number of recipients per delivery
65 chunk. None, zero or less means to group all recipients into one
66 big chunk.
67 :type max_recipients: integer
68 """
69 self._max_recipients = (max_recipients
70 if max_recipients is not None
71 else 0)
72 self._connection = Connection(
73 config.mta.smtp_host, int(config.mta.smtp_port),
74 self._max_recipients)
76 def chunkify(self, recipients):
77 """Split a set of recipients into chunks.
79 The `max_recipients` argument given to the constructor specifies the
80 maximum number of recipients in each chunk.
82 :param recipients: The set of recipient email addresses
83 :type recipients: sequence of email address strings
84 :return: A list of chunks, where each chunk is a set containing no
85 more than `max_recipients` number of addresses. The chunk can
86 contain fewer, and no packing is guaranteed.
87 :rtype: list of sets of strings
88 """
89 if self._max_recipients <= 0:
90 yield set(recipients)
91 return
92 # This algorithm was originally suggested by Chuq Von Rospach. Start
93 # by splitting the recipient addresses into top-level domain buckets,
94 # using the "most common" domains. Everything else ends up in the
95 # zeroth bucket.
96 by_bucket = {}
97 for address in recipients:
98 localpart, at, domain = address.partition('@')
99 domain_parts = domain.split('.')
100 bucket_number = CHUNKMAP.get(domain_parts[-1], 0)
101 by_bucket.setdefault(bucket_number, set()).add(address)
102 # Fill chunks by sorting the tld values by length.
103 chunk = set()
104 for tld_chunk in sorted(by_bucket.values(), key=len, reverse=True):
105 while tld_chunk:
106 chunk.add(tld_chunk.pop())
107 if len(chunk) == self._max_recipients:
108 yield chunk
109 chunk = set()
110 # Every tld bucket starts a new chunk, but only if non-empty
111 if len(chunk) > 0:
112 yield chunk
113 chunk = set()
114 # Be sure to include the last chunk, but only if it's non-empty.
115 if len(chunk) > 0:
116 yield chunk
118 def deliver(self, mlist, msg, msgdata):
119 """See `IMailTransportAgentDelivery`."""
120 recipients = msgdata.get('recipients')
121 if recipients is None:
122 return
123 # Blow away any existing Sender and Errors-To headers and substitute
124 # our own. Our interpretation of RFC 5322 $3.6.2 is that Mailman is
125 # the "agent responsible for actual transmission of the message"
126 # because what we send to list members is different than what the
127 # original author sent. RFC 2076 says Errors-To is "non-standard,
128 # discouraged" but we include it for historical purposes.
129 del msg['sender']
130 del msg['errors-to']
131 # The message metadata can override the calculation of the sender, but
132 # otherwise it falls to the list's -bounces robot. If this message is
133 # not intended for any specific mailing list, the site owner's address
134 # is used.
135 sender = msgdata.get('sender')
136 if sender is None:
137 sender = (config.mailman.site_owner
138 if mlist is None
139 else mlist.bounces_address)
140 msg['Sender'] = sender
141 msg['Errors-To'] = sender
142 message_id = msg['message-id']
143 for recipients in self.chunkify(msgdata['recipients']):
144 try:
145 refused = self._connection.sendmail(
146 sender, recipients, msg.as_string())
147 except smtplib.SMTPRecipientsRefused as error:
148 log.error('%s recipients refused: %s', message_id, error)
149 refused = error.recipients
150 return refused