1# Copyright (C) 1998-2018 by the Free Software Foundation, Inc.
2#
3# This program is free software; you can redistribute it and/or
4# modify it under the terms of the GNU General Public License
5# as published by the Free Software Foundation; either version 2
6# of the License, or (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16# USA.
17
18"""Determine whether this message should be held for approval.
19
20This modules tests only for hold situations, such as messages that are too
21large, messages that have potential administrivia, etc.  Definitive approvals
22or denials are handled by a different module.
23
24If no determination can be made (i.e. none of the hold criteria matches), then
25we do nothing.  If the message must be held for approval, then the hold
26database is updated and any administrator notification messages are sent.
27Finally an exception is raised to let the pipeline machinery know that further
28message handling should stop.
29"""
30
31import email
32from email.MIMEText import MIMEText
33from email.MIMEMessage import MIMEMessage
34import email.Utils
35from types import ClassType
36
37from Mailman import mm_cfg
38from Mailman import Utils
39from Mailman import Errors
40from Mailman import Message
41from Mailman import i18n
42from Mailman import Pending
43from Mailman.Logging.Syslog import syslog
44
45# First, play footsie with _ so that the following are marked as translated,
46# but aren't actually translated until we need the text later on.
47def _(s):
48    return s
49
50
51
52class ForbiddenPoster(Errors.HoldMessage):
53    reason = _('Sender is explicitly forbidden')
54    rejection = _('You are forbidden from posting messages to this list.')
55
56class ModeratedPost(Errors.HoldMessage):
57    reason = _('Post to moderated list')
58    rejection = _('Your message was deemed inappropriate by the moderator.')
59
60class NonMemberPost(Errors.HoldMessage):
61    reason = _('Post by non-member to a members-only list')
62    rejection = _('Non-members are not allowed to post messages to this list.')
63
64class NotExplicitlyAllowed(Errors.HoldMessage):
65    reason = _('Posting to a restricted list by sender requires approval')
66    rejection = _('This list is restricted; your message was not approved.')
67
68class TooManyRecipients(Errors.HoldMessage):
69    reason = _('Too many recipients to the message')
70    rejection = _('Please trim the recipient list; it is too long.')
71
72class ImplicitDestination(Errors.HoldMessage):
73    reason = _('Message has implicit destination')
74    rejection = _('''Blind carbon copies or other implicit destinations are
75not allowed.  Try reposting your message by explicitly including the list
76address in the To: or Cc: fields.''')
77
78class Administrivia(Errors.HoldMessage):
79    reason = _('Message may contain administrivia')
80
81    def rejection_notice(self, mlist):
82        listurl = mlist.GetScriptURL('listinfo', absolute=1)
83        request = mlist.GetRequestEmail()
84        return _("""Please do *not* post administrative requests to the mailing
85list.  If you wish to subscribe, visit %(listurl)s or send a message with the
86word `help' in it to the request address, %(request)s, for further
87instructions.""")
88
89class SuspiciousHeaders(Errors.HoldMessage):
90   reason = _('Message has a suspicious header')
91   rejection = _('Your message had a suspicious header.')
92
93class MessageTooBig(Errors.HoldMessage):
94    def __init__(self, msgsize, limit):
95        self.__msgsize = msgsize
96        self.__limit = limit
97
98    def reason_notice(self):
99        size = self.__msgsize
100        limit = self.__limit
101        return _('''Message body is too big: %(size)d bytes with a limit of
102%(limit)d KB''')
103
104    def rejection_notice(self, mlist):
105        kb = self.__limit
106        return _('''Your message was too big; please trim it to less than
107%(kb)d KB in size.''')
108
109class ModeratedNewsgroup(ModeratedPost):
110    reason = _('Posting to a moderated newsgroup')
111
112
113
114# And reset the translator
115_ = i18n._
116
117
118
119def ackp(msg):
120    ack = msg.get('x-ack', '').lower()
121    precedence = msg.get('precedence', '').lower()
122    if ack <> 'yes' and precedence in ('bulk', 'junk', 'list'):
123        return 0
124    return 1
125
126
127
128def process(mlist, msg, msgdata):
129    if msgdata.get('approved'):
130        return
131    # Get the sender of the message
132    listname = mlist.internal_name()
133    adminaddr = listname + '-admin'
134    sender = msg.get_sender()
135    # Special case an ugly sendmail feature: If there exists an alias of the
136    # form "owner-foo: bar" and sendmail receives mail for address "foo",
137    # sendmail will change the envelope sender of the message to "bar" before
138    # delivering.  This feature does not appear to be configurable.  *Boggle*.
139    if not sender or sender[:len(listname)+6] == adminaddr:
140        sender = msg.get_sender(use_envelope=0)
141    #
142    # Possible administrivia?
143    if mlist.administrivia and Utils.is_administrivia(msg):
144        hold_for_approval(mlist, msg, msgdata, Administrivia)
145        # no return
146    #
147    # Are there too many recipients to the message?
148    if mlist.max_num_recipients > 0:
149        # figure out how many recipients there are
150        recips = email.Utils.getaddresses(msg.get_all('to', []) +
151                                          msg.get_all('cc', []))
152        if len(recips) >= mlist.max_num_recipients:
153            hold_for_approval(mlist, msg, msgdata, TooManyRecipients)
154            # no return
155    #
156    # Implicit destination?  Note that message originating from the Usenet
157    # side of the world should never be checked for implicit destination.
158    if mlist.require_explicit_destination and \
159           not mlist.HasExplicitDest(msg) and \
160           not msgdata.get('fromusenet'):
161        # then
162        hold_for_approval(mlist, msg, msgdata, ImplicitDestination)
163        # no return
164    #
165    # Suspicious headers?
166    if mlist.bounce_matching_headers:
167        triggered = mlist.hasMatchingHeader(msg)
168        if triggered:
169            # TBD: Darn - can't include the matching line for the admin
170            # message because the info would also go to the sender
171            hold_for_approval(mlist, msg, msgdata, SuspiciousHeaders)
172            # no return
173    #
174    # Is the message too big?
175    if mlist.max_message_size > 0:
176        bodylen = 0
177        for line in email.Iterators.body_line_iterator(msg):
178            bodylen += len(line)
179        for part in msg.walk():
180            if part.preamble:
181                bodylen += len(part.preamble)
182            if part.epilogue:
183                bodylen += len(part.epilogue)
184        if bodylen/1024.0 > mlist.max_message_size:
185            hold_for_approval(mlist, msg, msgdata,
186                              MessageTooBig(bodylen, mlist.max_message_size))
187            # no return
188    #
189    # Are we gatewaying to a moderated newsgroup and is this list the
190    # moderator's address for the group?
191    if mlist.gateway_to_news and mlist.news_moderation == 2:
192        hold_for_approval(mlist, msg, msgdata, ModeratedNewsgroup)
193
194
195
196def hold_for_approval(mlist, msg, msgdata, exc):
197    # BAW: This should really be tied into the email confirmation system so
198    # that the message can be approved or denied via email as well as the
199    # web.
200    #
201    # XXX We use the weird type(type) construct below because in Python 2.1,
202    # type is a function not a type and so can't be used as the second
203    # argument in isinstance().  However, in Python 2.5, exceptions are
204    # new-style classes and so are not of ClassType.
205    if isinstance(exc, ClassType) or isinstance(exc, type(type)):
206        # Go ahead and instantiate it now.
207        exc = exc()
208    listname = mlist.real_name
209    sender = msgdata.get('sender', msg.get_sender())
210    usersubject = msg.get('subject')
211    charset = Utils.GetCharSet(mlist.preferred_language)
212    if usersubject:
213        usersubject = Utils.oneline(usersubject, charset)
214    else:
215        usersubject = _('(no subject)')
216    message_id = msg.get('message-id', 'n/a')
217    owneraddr = mlist.GetOwnerEmail()
218    adminaddr = mlist.GetBouncesEmail()
219    requestaddr = mlist.GetRequestEmail()
220    # We need to send both the reason and the rejection notice through the
221    # translator again, because of the games we play above
222    reason = Utils.wrap(exc.reason_notice())
223    if isinstance(exc, NonMemberPost) and mlist.nonmember_rejection_notice:
224        msgdata['rejection_notice'] = Utils.wrap(
225                                  mlist.nonmember_rejection_notice.replace(
226                                      '%(listowner)s', owneraddr))
227    else:
228        msgdata['rejection_notice'] = Utils.wrap(exc.rejection_notice(mlist))
229    id = mlist.HoldMessage(msg, reason, msgdata)
230    # Now we need to craft and send a message to the list admin so they can
231    # deal with the held message.
232    d = {'listname'   : listname,
233         'hostname'   : mlist.host_name,
234         'reason'     : _(reason),
235         'sender'     : sender,
236         'subject'    : usersubject,
237         'admindb_url': mlist.GetScriptURL('admindb', absolute=1),
238         }
239    # We may want to send a notification to the original sender too
240    fromusenet = msgdata.get('fromusenet')
241    # Since we're sending two messages, which may potentially be in different
242    # languages (the user's preferred and the list's preferred for the admin),
243    # we need to play some i18n games here.  Since the current language
244    # context ought to be set up for the user, let's craft his message first.
245    cookie = mlist.pend_new(Pending.HELD_MESSAGE, id)
246    if not fromusenet and ackp(msg) and mlist.respond_to_post_requests and \
247           mlist.autorespondToSender(sender, mlist.getMemberLanguage(sender)):
248        # Get a confirmation cookie
249        d['confirmurl'] = '%s/%s' % (mlist.GetScriptURL('confirm', absolute=1),
250                                     cookie)
251        lang = msgdata.get('lang', mlist.getMemberLanguage(sender))
252        subject = _('Your message to %(listname)s awaits moderator approval')
253        text = Utils.maketext('postheld.txt', d, lang=lang, mlist=mlist)
254        nmsg = Message.UserNotification(sender, owneraddr, subject, text, lang)
255        nmsg.send(mlist)
256    # Now the message for the list owners.  Be sure to include the list
257    # moderators in this message.  This one should appear to come from
258    # <list>-owner since we really don't need to do bounce processing on it.
259    if mlist.admin_immed_notify:
260        # Now let's temporarily set the language context to that which the
261        # admin is expecting.
262        otranslation = i18n.get_translation()
263        i18n.set_language(mlist.preferred_language)
264        try:
265            lang = mlist.preferred_language
266            charset = Utils.GetCharSet(lang)
267            # We need to regenerate or re-translate a few values in d
268            d['reason'] = _(reason)
269            d['subject'] = usersubject
270            # craft the admin notification message and deliver it
271            subject = _('%(listname)s post from %(sender)s requires approval')
272            nmsg = Message.UserNotification(owneraddr, owneraddr, subject,
273                                            lang=lang)
274            nmsg.set_type('multipart/mixed')
275            text = MIMEText(
276                Utils.maketext('postauth.txt', d, raw=1, mlist=mlist),
277                _charset=charset)
278            dmsg = MIMEText(Utils.wrap(_("""\
279If you reply to this message, keeping the Subject: header intact, Mailman will
280discard the held message.  Do this if the message is spam.  If you reply to
281this message and include an Approved: header with the list password in it, the
282message will be approved for posting to the list.  The Approved: header can
283also appear in the first line of the body of the reply.""")),
284                            _charset=Utils.GetCharSet(lang))
285            dmsg['Subject'] = 'confirm ' + cookie
286            dmsg['Sender'] = requestaddr
287            dmsg['From'] = requestaddr
288            dmsg['Date'] = email.Utils.formatdate(localtime=True)
289            dmsg['Message-ID'] = Utils.unique_message_id(mlist)
290            nmsg.attach(text)
291            nmsg.attach(MIMEMessage(msg))
292            nmsg.attach(MIMEMessage(dmsg))
293            nmsg.send(mlist, **{'tomoderators': 1})
294        finally:
295            i18n.set_translation(otranslation)
296    # Log the held message
297    syslog('vette', '%s post from %s held, message-id=%s: %s',
298           listname, sender, message_id, reason)
299    # raise the specific MessageHeld exception to exit out of the message
300    # delivery pipeline
301    raise exc
302