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)
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
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/>.
20 from __future__
import absolute_import
, unicode_literals
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
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
67 :type max_recipients: integer
69 self
._max
_recipients
= (max_recipients
70 if max_recipients
is not None
72 self
._connection
= Connection(
73 config
.mta
.smtp_host
, int(config
.mta
.smtp_port
),
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
89 if self
._max
_recipients
<= 0:
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
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.
104 for tld_chunk
in sorted(by_bucket
.values(), key
=len, reverse
=True):
106 chunk
.add(tld_chunk
.pop())
107 if len(chunk
) == self
._max
_recipients
:
110 # Every tld bucket starts a new chunk, but only if non-empty
114 # Be sure to include the last chunk, but only if it's non-empty.
118 def deliver(self
, mlist
, msg
, msgdata
):
119 """See `IMailTransportAgentDelivery`."""
120 recipients
= msgdata
.get('recipients')
121 if recipients
is None:
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.
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
135 sender
= msgdata
.get('sender')
137 sender
= (config
.mailman
.site_owner
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']):
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