# Copyright (C) 1998-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/>.

"""Calculate the regular (i.e. non-digest) recipients of the message.

This module calculates the non-digest recipients for the message based on the
list's membership and configuration options.  It places the list of recipients
on the `recipients' attribute of the message.  This attribute is used by the
SendmailDeliver and BulkDeliver modules.
"""

from mailman.config import config
from mailman.core.i18n import _
from mailman.interfaces.handler import IHandler
from mailman.interfaces.member import DeliveryStatus
from mailman.interfaces.pipeline import RejectMessage
from mailman.utilities.string import wrap
from public import public
from zope.interface import implementer


@public
@implementer(IHandler)
class MemberRecipients:
    """Calculate the regular (i.e. non-digest) recipients of the message."""

    name = 'member-recipients'
    description = _('Calculate the regular recipients of the message.')

    def process(self, mlist, msg, msgdata):
        """See `IHandler`."""
        # Short circuit if we've already calculated the recipients list,
        # regardless of whether the list is empty or not.
        if 'recipients' in msgdata:
            return
        # Should the original sender should be included in the recipients list?
        include_sender = True
        member = mlist.members.get_member(msg.sender)
        if member and not member.receive_own_postings:
            include_sender = False
        # Support for urgent messages, which bypasses digests and disabled
        # delivery and forces an immediate delivery to all members Right Now.
        # We are specifically /not/ allowing the site admins password to work
        # here because we want to discourage the practice of sending the site
        # admin password through email in the clear. (see also Approve.py)
        #
        # XXX This is broken.
        missing = object()
        password = msg.get('urgent', missing)
        if password is not missing:
            if mlist.Authenticate((config.AuthListModerator,
                                   config.AuthListAdmin),
                                  password):
                recipients = mlist.getMemberCPAddresses(
                    mlist.getRegularMemberKeys() +
                    mlist.getDigestMemberKeys())
                msgdata['recipients'] = recipients
                return
            else:
                # Bad Urgent: password, so reject it instead of passing it on.
                # I think it's better that the sender know they screwed up
                # than to deliver it normally.
                text = _("""\
Your urgent message to the ${mlist.display_name} mailing list was not
authorized for delivery.  The original message as received by Mailman is
attached.
""")
                raise RejectMessage(wrap(text))
        # Calculate the regular recipients of the message
        recipients = set(member.address.email
                         for member in mlist.regular_members.members
                         if member.delivery_status == DeliveryStatus.enabled)
        # Remove the sender if they don't want to receive their own posts
        if not include_sender and member.address.email in recipients:
            recipients.remove(member.address.email)
        # Handle topic classifications
        # XXX: Disabled for now until we fix it properly
        #
        # do_topic_filters(mlist, msg, msgdata, recipients)
        #
        # Bookkeeping
        msgdata['recipients'] = recipients


def do_topic_filters(mlist, msg, msgdata, recipients):
    """Filter out recipients based on topics."""
    if not mlist.topics_enabled:
        # MAS: if topics are currently disabled for the list, send to all
        # regardless of ReceiveNonmatchingTopics
        return
    hits = msgdata.get('topichits')
    zap_recipients = []
    if hits:
        # The message hit some topics, so only deliver this message to those
        # who are interested in one of the hit topics.
        for user in recipients:
            utopics = mlist.getMemberTopics(user)
            if not utopics:
                # This user is not interested in any topics, so they get all
                # postings.
                continue
            # BAW: Slow, first-match, set intersection!
            for topic in utopics:
                if topic in hits:
                    # The user wants this message
                    break
            else:
                # The user was interested in topics, but not any of the ones
                # this message matched, so zap him.
                zap_recipients.append(user)
    else:
        # The semantics for a message that did not hit any of the pre-canned
        # topics is to troll through the membership list, looking for users
        # who selected at least one topic of interest, but turned on
        # ReceiveNonmatchingTopics.
        for user in recipients:
            if not mlist.getMemberTopics(user):
                # The user did not select any topics of interest, so he gets
                # this message by default.
                continue
            if not mlist.getMemberOption(
                    user, config.ReceiveNonmatchingTopics):
                # The user has interest in some topics, but elects not to
                # receive message that match no topics, so zap him.
                zap_recipients.append(user)
            # Otherwise, the user wants non-matching messages.
    # Prune out the non-receiving users
    for user in zap_recipients:
        recipients.remove(user)
