Merge branch 'mid' into 'master'
[mailman.git] / src / mailman / mta / base.py
blobceb833a3d931a1072d7de78fc6b7054e6630558a
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 """Base delivery class."""
20 import copy
21 import socket
22 import logging
23 import smtplib
25 from lazr.config import as_boolean
26 from mailman.config import config
27 from mailman.interfaces.mta import IMailTransportAgentDelivery
28 from mailman.mta.connection import as_SecureMode, Connection
29 from public import public
30 from zope.interface import implementer
33 log = logging.getLogger('mailman.smtp')
36 @public
37 @implementer(IMailTransportAgentDelivery)
38 class BaseDelivery:
39 """Base delivery class."""
41 def __init__(self):
42 """Create a basic deliverer."""
43 self._connection = Connection(
44 config.mta.smtp_host, int(config.mta.smtp_port),
45 int(config.mta.max_sessions_per_connection),
46 config.mta.smtp_user if config.mta.smtp_user else None,
47 config.mta.smtp_pass if config.mta.smtp_pass else None,
48 as_SecureMode(config.mta.smtp_secure_mode),
49 as_boolean(config.mta.smtp_verify_cert),
50 as_boolean(config.mta.smtp_verify_hostname),
53 def _deliver_to_recipients(self, mlist, msg, msgdata, recipients):
54 """Low-level delivery to a set of recipients.
56 :param mlist: The mailing list being delivered to.
57 :type mlist: `IMailingList`
58 :param msg: The original message being delivered.
59 :type msg: `Message`
60 :param msgdata: Additional message metadata for this delivery.
61 :type msgdata: dictionary
62 :param recipients: The recipients of this message.
63 :type recipients: sequence
64 :return: delivery failures as defined by `smtplib.SMTP.sendmail`
65 :rtype: dictionary
66 """
67 # Do the actual sending.
68 sender = self._get_sender(mlist, msg, msgdata)
69 message_id = msg['message-id']
70 # Since the recipients can be a set or a list, sort the recipients by
71 # email address for predictability and testability.
72 try:
73 refused = self._connection.sendmail(
74 sender, sorted(recipients), msg)
75 except smtplib.SMTPRecipientsRefused as error:
76 log.error('%s recipients refused: %s', message_id, error)
77 refused = error.recipients
78 except smtplib.SMTPResponseException as error:
79 log.error('%s response exception: %s', message_id, error)
80 refused = dict(
81 # recipient -> (code, error)
82 (recipient, (error.smtp_code, error.smtp_error))
83 for recipient in recipients)
84 except (socket.error, IOError, smtplib.SMTPException) as error:
85 # MTA not responding, or other socket problems, or any other
86 # kind of SMTPException. In that case, nothing got delivered,
87 # so treat this as a temporary failure. We use error code 444
88 # for this (temporary, unspecified failure, cf RFC 5321).
89 log.error('%s low level smtp error: %s', message_id, error)
90 error = str(error)
91 refused = dict(
92 # recipient -> (code, error)
93 (recipient, (444, error))
94 for recipient in recipients)
95 return refused
97 def _get_sender(self, mlist, msg, msgdata):
98 """Return the envelope sender to use.
100 The message metadata can override the calculation of the sender, but
101 otherwise it falls to the list's -bounces robot. If this message is
102 not intended for any specific mailing list, the site owner's address
103 is used.
105 :param mlist: The mailing list being delivered to.
106 :type mlist: `IMailingList`
107 :param msg: The original message being delivered.
108 :type msg: `Message`
109 :param msgdata: Additional message metadata for this delivery.
110 :type msgdata: dictionary
111 :return: The envelope sender.
112 :rtype: string
114 sender = msgdata.get('sender')
115 if sender is None:
116 return (config.mailman.site_owner
117 if mlist is None
118 else mlist.bounces_address)
119 return sender
122 @public
123 class IndividualDelivery(BaseDelivery):
124 """Deliver a unique individual message to each recipient.
126 This is a framework delivery mechanism. By using mixins, registration,
127 and subclassing you can customize this delivery class to do any
128 combination of VERP, full personalization, individualized header/footer
129 decoration and even full mail merging.
131 The core concept here is that for each recipient, the deliver() method
132 iterates over the list of registered callbacks, each of which have a
133 chance to modify the message before final delivery.
136 def __init__(self):
137 """See `BaseDelivery`."""
138 super().__init__()
139 self.callbacks = []
141 def deliver(self, mlist, msg, msgdata):
142 """See `IMailTransportAgentDelivery`.
144 Craft a unique message for every recipient. Encode the recipient's
145 delivery address in the return envelope so there can be no ambiguity
146 in bounce processing.
148 refused = {}
149 recipients = msgdata.get('recipients', set())
150 for recipient in recipients:
151 log.debug('IndividualDelivery to: %s', recipient)
152 # Make a copy of the original messages and operator on it, since
153 # we're going to munge it repeatedly for each recipient.
154 message_copy = copy.deepcopy(msg)
155 msgdata_copy = msgdata.copy()
156 # Squirrel the current recipient away in the message metadata.
157 # That way the subclass's _get_sender() override can encode the
158 # recipient address in the sender, e.g. for VERP.
159 msgdata_copy['recipient'] = recipient
160 # See if the recipient is a member of the mailing list, and if so,
161 # squirrel this information away for use by other modules, such as
162 # the header/footer decorator. XXX 2012-03-05 this is probably
163 # highly inefficient on the database.
164 member = mlist.members.get_member(recipient)
165 msgdata_copy['member'] = member
166 for callback in self.callbacks:
167 callback(mlist, message_copy, msgdata_copy)
168 status = self._deliver_to_recipients(
169 mlist, message_copy, msgdata_copy, [recipient])
170 refused.update(status)
171 return refused