# Copyright (C) 1998-2018 by the Free Software Foundation, Inc.
#
# This program 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 2
# of the License, or (at your option) any later version.
# 
# This program 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 this program; if not, write to the Free Software 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.

"""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 `recips' attribute of the message.  This attribute is used by the
SendmailDeliver and BulkDeliver modules.
"""

import email.Utils
from Mailman import mm_cfg
from Mailman import Utils
from Mailman import Message
from Mailman import Errors
from Mailman.MemberAdaptor import ENABLED
from Mailman.MailList import MailList
from Mailman.i18n import _
from Mailman.Logging.Syslog import syslog
from Mailman.Errors import MMUnknownListError

# Use set for sibling list recipient calculation
try:
    set
except NameError: # Python2.3
    from sets import Set as set



def process(mlist, msg, msgdata):
    # Short circuit if we've already calculated the recipients list,
    # regardless of whether the list is empty or not.
    if msgdata.has_key('recips'):
        return
    # Should the original sender should be included in the recipients list?
    include_sender = 1
    sender = msg.get_sender()
    try:
        if mlist.getMemberOption(sender, mm_cfg.DontReceiveOwnPosts):
            include_sender = 0
    except Errors.NotAMemberError:
        pass
    # 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)
    missing = []
    password = msg.get('urgent', missing)
    if password is not missing:
        if mlist.Authenticate((mm_cfg.AuthListPoster,
                               mm_cfg.AuthListModerator,
                               mm_cfg.AuthListAdmin),
                              password):
            recips = mlist.getMemberCPAddresses(mlist.getRegularMemberKeys() +
                                                mlist.getDigestMemberKeys())
            msgdata['recips'] = recips
            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.
            realname = mlist.real_name
            text = _("""\
Your urgent message to the %(realname)s mailing list was not authorized for
delivery.  The original message as received by Mailman is attached.
""")
            raise Errors.RejectMessage, Utils.wrap(text)
    # Calculate the regular recipients of the message
    recips = [mlist.getMemberCPAddress(m)
              for m in mlist.getRegularMemberKeys()
              if mlist.getDeliveryStatus(m) == ENABLED]
    # Remove the sender if they don't want to receive their own posts
    if not include_sender:
        try:
            recips.remove(mlist.getMemberCPAddress(sender))
        except (Errors.NotAMemberError, ValueError):
            # Sender does not want to get copies of their own messages (not
            # metoo), but delivery to their address is disabled (nomail).  Or
            # the sender is not a member of the mailing list.
            pass
    # Handle topic classifications
    do_topic_filters(mlist, msg, msgdata, recips)
    # Regular delivery exclude/include (if in/not_in To: or Cc:) lists
    recips = do_exclude(mlist, msg, msgdata, recips)
    recips = do_include(mlist, msg, msgdata, recips)
    # Bookkeeping
    msgdata['recips'] = recips



def do_topic_filters(mlist, msg, msgdata, recips):
    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')
    zaprecips = []
    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 recips:
            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.
                zaprecips.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 recips:
            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,
                                         mm_cfg.ReceiveNonmatchingTopics):
                # The user has interest in some topics, but elects not to
                # receive message that match no topics, so zap him.
                zaprecips.append(user)
            # Otherwise, the user wants non-matching messages.
    # Prune out the non-receiving users
    for user in zaprecips:
        recips.remove(user)


def do_exclude(mlist, msg, msgdata, recips):
    # regular_exclude_lists are the other mailing lists on this mailman
    # installation whose members are excluded from the regular (non-digest)
    # delivery of this list if those list addresses appear in To: or Cc:
    # headers.
    if not mlist.regular_exclude_lists:
        return recips
    recips = set(recips)
    destinations = email.Utils.getaddresses(msg.get_all('to', []) +
                                            msg.get_all('cc', []))
    destinations = [y.lower() for x,y in destinations]
    for listname in mlist.regular_exclude_lists:
        listname = listname.lower()
        if listname not in destinations:
            continue
        listlhs, hostname = listname.split('@')
        if listlhs == mlist.internal_name():
            syslog('error', 'Exclude list %s is a self reference.',
                    listname)
            continue
        try:
            slist = MailList(listlhs, lock=False)
        except MMUnknownListError:
            syslog('error', 'Exclude list %s not found.', listname)
            continue
        if not mm_cfg.ALLOW_CROSS_DOMAIN_SIBLING \
           and slist.host_name != hostname:
            syslog('error', 'Exclude list %s is not in the same domain.',
                    listname)
            continue
        if mlist.regular_exclude_ignore:
            for sender in msg.get_senders():
                if slist.isMember(sender):
                    break
                for sender in Utils.check_eq_domains(sender,
                                  slist.equivalent_domains):
                    if slist.isMember(sender):
                        break
                if slist.isMember(sender):
                    break
            else:
                continue
        srecips = set([slist.getMemberCPAddress(m)
                   for m in slist.getRegularMemberKeys()
                   if slist.getDeliveryStatus(m) == ENABLED])
        recips -= srecips
    return list(recips)


def do_include(mlist, msg, msgdata, recips):
    # regular_include_lists are the other mailing lists on this mailman
    # installation whose members are included in the regular (non-digest)
    # delivery if those list addresses don't appear in To: or Cc: headers.
    if not mlist.regular_include_lists:
        return recips
    recips = set(recips)
    destinations = email.Utils.getaddresses(msg.get_all('to', []) +
                                            msg.get_all('cc', []))
    destinations = [y.lower() for x,y in destinations]
    for listname in mlist.regular_include_lists:
        listname = listname.lower()
        if listname in destinations:
            continue
        listlhs, hostname = listname.split('@')
        if listlhs == mlist.internal_name():
            syslog('error', 'Include list %s is a self reference.',
                    listname)
            continue
        try:
            slist = MailList(listlhs, lock=False)
        except MMUnknownListError:
            syslog('error', 'Include list %s not found.', listname)
            continue
        if not mm_cfg.ALLOW_CROSS_DOMAIN_SIBLING \
           and slist.host_name != hostname:
            syslog('error', 'Include list %s is not in the same domain.',
                    listname)
            continue
        srecips = set([slist.getMemberCPAddress(m)
                   for m in slist.getRegularMemberKeys()
                   if slist.getDeliveryStatus(m) == ENABLED])
        recips |= srecips
    return list(recips)
