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