1# Copyright (C) 2009-2020 by the Free Software Foundation, Inc.
2#
3# This file is part of GNU Mailman.
4#
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.
9#
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.
14#
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/>.
17
18"""Base delivery class."""
19
20import copy
21import socket
22import logging
23import smtplib
24
25from lazr.config import as_boolean
26from mailman.config import config
27from mailman.interfaces.mta import IMailTransportAgentDelivery
28from mailman.mta.connection import Connection, as_SecureMode
29from public import public
30from zope.interface import implementer
31
32
33log = logging.getLogger('mailman.smtp')
34
35
36@public
37@implementer(IMailTransportAgentDelivery)
38class BaseDelivery:
39    """Base delivery class."""
40
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),
51            )
52
53    def _deliver_to_recipients(self, mlist, msg, msgdata, recipients):
54        """Low-level delivery to a set of recipients.
55
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.as_string())
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
96
97    def _get_sender(self, mlist, msg, msgdata):
98        """Return the envelope sender to use.
99
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.
104
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
113        """
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
120
121
122@public
123class IndividualDelivery(BaseDelivery):
124    """Deliver a unique individual message to each recipient.
125
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.
130
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.
134    """
135
136    def __init__(self):
137        """See `BaseDelivery`."""
138        super().__init__()
139        self.callbacks = []
140
141    def deliver(self, mlist, msg, msgdata):
142        """See `IMailTransportAgentDelivery`.
143
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.
147        """
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
172