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)
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 <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
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
49 :type max_recipients: integer
52 self
._max
_recipients
= (max_recipients
53 if max_recipients
is not None
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
69 if self
._max
_recipients
<= 0:
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
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.
84 for tld_chunk
in sorted(by_bucket
.values(), key
=len, reverse
=True):
86 chunk
.add(tld_chunk
.pop())
87 if len(chunk
) == self
._max
_recipients
:
90 # Every tld bucket starts a new chunk, but only if non-empty
94 # Be sure to include the last chunk, but only if it's non-empty.
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
)
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
)