"""Determine whether this message should be held for approval.
This modules tests only for hold situations, such as messages that are too
large, messages that have potential administrivia, etc. Definitive approvals
or denials are handled by a different module.
If no determination can be made (i.e. none of the hold criteria matches), then
we do nothing. If the message must be held for approval, then the hold
database is updated and any administrator notification messages are sent.
Finally an exception is raised to let the pipeline machinery know that further
message handling should stop.
"""
import email
from email.MIMEText import MIMEText
from email.MIMEMessage import MIMEMessage
import email.Utils
from types import ClassType
from Mailman import mm_cfg
from Mailman import Utils
from Mailman import Errors
from Mailman import Message
from Mailman import i18n
from Mailman import Pending
from Mailman.Logging.Syslog import syslog
def _(s):
return s
class ForbiddenPoster(Errors.HoldMessage):
reason = _('Sender is explicitly forbidden')
rejection = _('You are forbidden from posting messages to this list.')
class ModeratedPost(Errors.HoldMessage):
reason = _('Post to moderated list')
rejection = _('Your message was deemed inappropriate by the moderator.')
class NonMemberPost(Errors.HoldMessage):
reason = _('Post by non-member to a members-only list')
rejection = _('Non-members are not allowed to post messages to this list.')
class NotExplicitlyAllowed(Errors.HoldMessage):
reason = _('Posting to a restricted list by sender requires approval')
rejection = _('This list is restricted; your message was not approved.')
class TooManyRecipients(Errors.HoldMessage):
reason = _('Too many recipients to the message')
rejection = _('Please trim the recipient list; it is too long.')
class ImplicitDestination(Errors.HoldMessage):
reason = _('Message has implicit destination')
rejection = _('''Blind carbon copies or other implicit destinations are
not allowed. Try reposting your message by explicitly including the list
address in the To: or Cc: fields.''')
class Administrivia(Errors.HoldMessage):
reason = _('Message may contain administrivia')
def rejection_notice(self, mlist):
listurl = mlist.GetScriptURL('listinfo', absolute=1)
request = mlist.GetRequestEmail()
return _("""Please do *not* post administrative requests to the mailing
list. If you wish to subscribe, visit %(listurl)s or send a message with the
word `help' in it to the request address, %(request)s, for further
instructions.""")
class SuspiciousHeaders(Errors.HoldMessage):
reason = _('Message has a suspicious header')
rejection = _('Your message had a suspicious header.')
class MessageTooBig(Errors.HoldMessage):
def __init__(self, msgsize, limit):
self.__msgsize = msgsize
self.__limit = limit
def reason_notice(self):
size = self.__msgsize
limit = self.__limit
return _('''Message body is too big: %(size)d bytes with a limit of
%(limit)d KB''')
def rejection_notice(self, mlist):
kb = self.__limit
return _('''Your message was too big; please trim it to less than
%(kb)d KB in size.''')
class ModeratedNewsgroup(ModeratedPost):
reason = _('Posting to a moderated newsgroup')
_ = i18n._
def ackp(msg):
ack = msg.get('x-ack', '').lower()
precedence = msg.get('precedence', '').lower()
if ack <> 'yes' and precedence in ('bulk', 'junk', 'list'):
return 0
return 1
def process(mlist, msg, msgdata):
if msgdata.get('approved'):
return
listname = mlist.internal_name()
adminaddr = listname + '-admin'
sender = msg.get_sender()
if not sender or sender[:len(listname)+6] == adminaddr:
sender = msg.get_sender(use_envelope=0)
if mlist.administrivia and Utils.is_administrivia(msg):
hold_for_approval(mlist, msg, msgdata, Administrivia)
if mlist.max_num_recipients > 0:
recips = email.Utils.getaddresses(msg.get_all('to', []) +
msg.get_all('cc', []))
if len(recips) >= mlist.max_num_recipients:
hold_for_approval(mlist, msg, msgdata, TooManyRecipients)
if mlist.require_explicit_destination and \
not mlist.HasExplicitDest(msg) and \
not msgdata.get('fromusenet'):
hold_for_approval(mlist, msg, msgdata, ImplicitDestination)
if mlist.bounce_matching_headers:
triggered = mlist.hasMatchingHeader(msg)
if triggered:
hold_for_approval(mlist, msg, msgdata, SuspiciousHeaders)
if mlist.max_message_size > 0:
bodylen = 0
for line in email.Iterators.body_line_iterator(msg):
bodylen += len(line)
if bodylen/1024.0 > mlist.max_message_size:
hold_for_approval(mlist, msg, msgdata,
MessageTooBig(bodylen, mlist.max_message_size))
if mlist.news_moderation == 2:
hold_for_approval(mlist, msg, msgdata, ModeratedNewsgroup)
def hold_for_approval(mlist, msg, msgdata, exc):
if type(exc) is ClassType:
exc = exc()
listname = mlist.real_name
sender = msgdata.get('sender', msg.get_sender())
message_id = msg.get('message-id', 'n/a')
owneraddr = mlist.GetOwnerEmail()
adminaddr = mlist.GetBouncesEmail()
requestaddr = mlist.GetRequestEmail()
reason = Utils.wrap(exc.reason_notice())
msgdata['rejection_notice'] = Utils.wrap(exc.rejection_notice(mlist))
id = mlist.HoldMessage(msg, reason, msgdata)
d = {'listname' : listname,
'hostname' : mlist.host_name,
'reason' : _(reason),
'sender' : sender,
'subject' : msg.get('subject', _('(no subject)')),
'admindb_url': mlist.GetScriptURL('admindb', absolute=1),
}
fromusenet = msgdata.get('fromusenet')
cookie = Pending.new(Pending.HELD_MESSAGE, id)
if not fromusenet and ackp(msg) and mlist.respond_to_post_requests and \
mlist.autorespondToSender(sender):
d['confirmurl'] = '%s/%s' % (mlist.GetScriptURL('confirm', absolute=1),
cookie)
lang = msgdata.get('lang', mlist.getMemberLanguage(sender))
subject = _('Your message to %(listname)s awaits moderator approval')
text = Utils.maketext('postheld.txt', d, lang=lang, mlist=mlist)
nmsg = Message.UserNotification(sender, adminaddr, subject, text, lang)
nmsg.send(mlist)
if mlist.admin_immed_notify:
otranslation = i18n.get_translation()
i18n.set_language(mlist.preferred_language)
try:
lang = mlist.preferred_language
charset = Utils.GetCharSet(lang)
usersubject = msg.get('subject', _('(no subject)'))
d['reason'] = _(reason)
d['subject'] = usersubject
subject = _('%(listname)s post from %(sender)s requires approval')
nmsg = Message.UserNotification(owneraddr, owneraddr, subject,
lang=lang)
nmsg.set_type('multipart/mixed')
text = MIMEText(
Utils.maketext('postauth.txt', d, raw=1, mlist=mlist),
_charset=charset)
dmsg = MIMEText(Utils.wrap(_("""\
If you reply to this message, keeping the Subject: header intact, Mailman will
discard the held message. Do this if the message is spam. If you reply to
this message and include an Approved: header with the list password in it, the
message will be approved for posting to the list. The Approved: header can
also appear in the first line of the body of the reply.""")),
_charset=Utils.GetCharSet(lang))
dmsg['Subject'] = 'confirm ' + cookie
dmsg['Sender'] = requestaddr
dmsg['From'] = requestaddr
nmsg.attach(text)
nmsg.attach(MIMEMessage(msg))
nmsg.attach(MIMEMessage(dmsg))
nmsg.send(mlist, **{'tomoderators': 1})
finally:
i18n.set_translation(otranslation)
syslog('vette', '%s post from %s held, message-id=%s: %s',
listname, sender, message_id, reason)
raise exc