aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman/Handlers/CalcRecips.py
blob: 51733ceeaf58c1a323fda0c402562b938a43ef48 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# 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)