# Copyright (C) 2009-2025 by the Free Software Foundation, Inc.
#
# This file is part of GNU Mailman.
#
# GNU Mailman is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# GNU Mailman.  If not, see <https://www.gnu.org/licenses/>.

"""Base delivery class."""

import copy
import socket
import logging
import smtplib

from lazr.config import as_boolean
from mailman.config import config
from mailman.interfaces.mta import IMailTransportAgentDelivery
from mailman.mta.connection import as_SecureMode, Connection
from public import public
from zope.interface import implementer


log = logging.getLogger('mailman.smtp')


@public
@implementer(IMailTransportAgentDelivery)
class BaseDelivery:
    """Base delivery class."""

    def __init__(self):
        """Create a basic deliverer."""
        self._connection = Connection(
            config.mta.smtp_host, int(config.mta.smtp_port),
            int(config.mta.max_sessions_per_connection),
            config.mta.smtp_user if config.mta.smtp_user else None,
            config.mta.smtp_pass if config.mta.smtp_pass else None,
            as_SecureMode(config.mta.smtp_secure_mode),
            as_boolean(config.mta.smtp_verify_cert),
            as_boolean(config.mta.smtp_verify_hostname),
            )

    def _deliver_to_recipients(self, mlist, msg, msgdata, recipients):
        """Low-level delivery to a set of recipients.

        :param mlist: The mailing list being delivered to.
        :type mlist: `IMailingList`
        :param msg: The original message being delivered.
        :type msg: `Message`
        :param msgdata: Additional message metadata for this delivery.
        :type msgdata: dictionary
        :param recipients: The recipients of this message.
        :type recipients: sequence
        :return: delivery failures as defined by `smtplib.SMTP.sendmail`
        :rtype: dictionary
        """
        # Do the actual sending.
        sender = self._get_sender(mlist, msg, msgdata)
        message_id = msg['message-id']
        # Since the recipients can be a set or a list, sort the recipients by
        # email address for predictability and testability.
        try:
            refused = self._connection.sendmail(
                sender, sorted(recipients), msg)
        except smtplib.SMTPRecipientsRefused as error:
            log.error('%s recipients refused: %s', message_id, error)
            refused = error.recipients
        except smtplib.SMTPResponseException as error:
            log.error('%s response exception: %s', message_id, error)
            refused = dict(
                # recipient -> (code, error)
                (recipient, (error.smtp_code, error.smtp_error))
                for recipient in recipients)
        except (socket.error, IOError, smtplib.SMTPException) as error:
            # MTA not responding, or other socket problems, or any other
            # kind of SMTPException.  In that case, nothing got delivered,
            # so treat this as a temporary failure.  We use error code 444
            # for this (temporary, unspecified failure, cf RFC 5321).
            log.error('%s low level smtp error: %s', message_id, error)
            error = str(error)
            refused = dict(
                # recipient -> (code, error)
                (recipient, (444, error))
                for recipient in recipients)
        return refused

    def _get_sender(self, mlist, msg, msgdata):
        """Return the envelope sender to use.

        The message metadata can override the calculation of the sender, but
        otherwise it falls to the list's -bounces robot.  If this message is
        not intended for any specific mailing list, the site owner's address
        is used.

        :param mlist: The mailing list being delivered to.
        :type mlist: `IMailingList`
        :param msg: The original message being delivered.
        :type msg: `Message`
        :param msgdata: Additional message metadata for this delivery.
        :type msgdata: dictionary
        :return: The envelope sender.
        :rtype: string
        """
        sender = msgdata.get('sender')
        if sender is None:
            return (config.mailman.site_owner
                    if mlist is None
                    else mlist.bounces_address)
        return sender


@public
class IndividualDelivery(BaseDelivery):
    """Deliver a unique individual message to each recipient.

    This is a framework delivery mechanism.  By using mixins, registration,
    and subclassing you can customize this delivery class to do any
    combination of VERP, full personalization, individualized header/footer
    decoration and even full mail merging.

    The core concept here is that for each recipient, the deliver() method
    iterates over the list of registered callbacks, each of which have a
    chance to modify the message before final delivery.
    """

    def __init__(self):
        """See `BaseDelivery`."""
        super().__init__()
        self.callbacks = []

    def deliver(self, mlist, msg, msgdata):
        """See `IMailTransportAgentDelivery`.

        Craft a unique message for every recipient.  Encode the recipient's
        delivery address in the return envelope so there can be no ambiguity
        in bounce processing.
        """
        refused = {}
        recipients = msgdata.get('recipients', set())
        for recipient in recipients:
            log.debug('IndividualDelivery to: %s', recipient)
            # Make a copy of the original messages and operator on it, since
            # we're going to munge it repeatedly for each recipient.
            message_copy = copy.deepcopy(msg)
            msgdata_copy = msgdata.copy()
            # Squirrel the current recipient away in the message metadata.
            # That way the subclass's _get_sender() override can encode the
            # recipient address in the sender, e.g. for VERP.
            msgdata_copy['recipient'] = recipient
            # See if the recipient is a member of the mailing list, and if so,
            # squirrel this information away for use by other modules, such as
            # the header/footer decorator.  XXX 2012-03-05 this is probably
            # highly inefficient on the database.
            member = mlist.members.get_member(recipient)
            msgdata_copy['member'] = member
            for callback in self.callbacks:
                callback(mlist, message_copy, msgdata_copy)
            status = self._deliver_to_recipients(
                mlist, message_copy, msgdata_copy, [recipient])
            refused.update(status)
        return refused
