Merge branch 'weblate-gnu-mailman-mailman' into 'master'
[mailman.git] / src / mailman / mta / bulk.py
blob5ac143aec947166760f296ef77c4fd5220b161ef
1 # Copyright (C) 2009-2023 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 <https://www.gnu.org/licenses/>.
18 """Bulk message delivery."""
20 from mailman.mta.arc_signing import ARCSigningMixin
21 from mailman.mta.base import BaseDelivery
22 from mailman.mta.decorating import DecoratingMixin
23 from public import public
26 # A mapping of top-level domains to bucket numbers. The zeroth bucket is
27 # reserved for everything else. At one time, these were the most common
28 # domains.
29 CHUNKMAP = dict(
30 com=1,
31 net=2,
32 org=2,
33 edu=3,
34 us=3,
35 ca=3,
39 @public
40 class BulkDelivery(BaseDelivery, DecoratingMixin, ARCSigningMixin):
41 """Deliver messages to the MSA in as few sessions as possible."""
43 def __init__(self, max_recipients=None):
44 """See `BaseDelivery`.
46 :param max_recipients: The maximum number of recipients per delivery
47 chunk. None, zero or less means to group all recipients into one
48 big chunk.
49 :type max_recipients: integer
50 """
51 super().__init__()
52 self._max_recipients = (max_recipients
53 if max_recipients is not None
54 else 0)
56 def chunkify(self, recipients):
57 """Split a set of recipients into chunks.
59 The `max_recipients` argument given to the constructor specifies the
60 maximum number of recipients in each chunk.
62 :param recipients: The set of recipient email addresses
63 :type recipients: sequence of email address strings
64 :return: A list of chunks, where each chunk is a set containing no
65 more than `max_recipients` number of addresses. The chunk can
66 contain fewer, and no packing is guaranteed.
67 :rtype: list of sets of strings
68 """
69 if self._max_recipients <= 0:
70 yield set(recipients)
71 return
72 # This algorithm was originally suggested by Chuq Von Rospach. Start
73 # by splitting the recipient addresses into top-level domain buckets,
74 # using the "most common" domains. Everything else ends up in the
75 # zeroth bucket.
76 by_bucket = {}
77 for address in recipients:
78 localpart, at, domain = address.partition('@')
79 domain_parts = domain.split('.')
80 bucket_number = CHUNKMAP.get(domain_parts[-1], 0)
81 by_bucket.setdefault(bucket_number, set()).add(address)
82 # Fill chunks by sorting the tld values by length.
83 chunk = set()
84 for tld_chunk in sorted(by_bucket.values(), key=len, reverse=True):
85 while tld_chunk:
86 chunk.add(tld_chunk.pop())
87 if len(chunk) == self._max_recipients:
88 yield chunk
89 chunk = set()
90 # Every tld bucket starts a new chunk, but only if non-empty
91 if len(chunk) > 0:
92 yield chunk
93 chunk = set()
94 # Be sure to include the last chunk, but only if it's non-empty.
95 if len(chunk) > 0:
96 yield chunk
98 def deliver(self, mlist, msg, msgdata):
99 """See `IMailTransportAgentDelivery`."""
100 # Message needs to be decorated and arc signed.
101 self.decorate(mlist, msg, msgdata)
102 self.arc_sign(mlist, msg, msgdata)
103 refused = {}
104 for recipients in self.chunkify(msgdata.get('recipients', set())):
105 chunk_refused = self._deliver_to_recipients(
106 mlist, msg, msgdata, recipients)
107 refused.update(chunk_refused)
108 return refused