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