# Copyright (C) 1998-2006 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.
"""Do more detailed spam detection.
This module hard codes site wide spam detection. By hacking the
KNOWN_SPAMMERS variable, you can set up more regular expression matches
against message headers. If spam is detected the message is discarded
immediately.
TBD: This needs to be made more configurable and robust.
"""
import re
from cStringIO import StringIO
from email.Generator import Generator
from Mailman import mm_cfg
from Mailman import Errors
from Mailman import i18n
from Mailman.Handlers.Hold import hold_for_approval
try:
True, False
except NameError:
True = 1
False = 0
# First, play footsie with _ so that the following are marked as translated,
# but aren't actually translated until we need the text later on.
def _(s):
return s
class SpamDetected(Errors.DiscardMessage):
"""The message contains known spam"""
class HeaderMatchHold(Errors.HoldMessage):
reason = _('The message headers matched a filter rule')
# And reset the translator
_ = i18n._
class Tee:
def __init__(self, outfp_a, outfp_b):
self._outfp_a = outfp_a
self._outfp_b = outfp_b
def write(self, s):
self._outfp_a.write(s)
self._outfp_b.write(s)
# Class to capture the headers separate from the message body
class HeaderGenerator(Generator):
def __init__(self, outfp, mangle_from_=True, maxheaderlen=78):
Generator.__init__(self, outfp, mangle_from_, maxheaderlen)
self._headertxt = ''
def _write_headers(self, msg):
sfp = StringIO()
oldfp = self._fp
self._fp = Tee(oldfp, sfp)
try:
Generator._write_headers(self, msg)
finally:
self._fp = oldfp
self._headertxt = sfp.getvalue()
def header_text(self):
return self._headertxt
def process(mlist, msg, msgdata):
if msgdata.get('approved'):
return
# First do site hard coded header spam checks
for header, regex in mm_cfg.KNOWN_SPAMMERS:
cre = re.compile(regex, re.IGNORECASE)
for value in msg.get_all(header, []):
mo = cre.search(value)
if mo:
# we've detected spam, so throw the message away
raise SpamDetected
# Before we go to header_filter_rules, we exclude internally generated
# owner notification from checking, because 1) we collect headers from
# all the attachments but this will cause matching the filter rule again,
# and 2) list owners may want to check header name / value pair like
# 'Precedence: bulk' which is also generated by mailman. Both will
# cause loop of holding owner notification messages if the action is
# set to 'hold'.
if msgdata.get('toowner') and msg.get('x-list-administrivia') == 'yes':
return
# Now do header_filter_rules
# TK: Collect headers in sub-parts because attachment filename
# extension may be a clue to possible virus/spam.
headers = ''
for p in msg.walk():
g = HeaderGenerator(StringIO())
g.flatten(p)
headers += g.header_text()
# Now reshape headers (remove extra CR and connect multiline).
headers = re.sub('\n+', '\n', headers)
headers = re.sub('\n\s', ' ', headers)
for patterns, action, empty in mlist.header_filter_rules:
if action == mm_cfg.DEFER:
continue
for pattern in patterns.splitlines():
if pattern.startswith('#'):
continue
if re.search(pattern, headers, re.IGNORECASE|re.MULTILINE):
if action == mm_cfg.DISCARD:
raise Errors.DiscardMessage
if action == mm_cfg.REJECT:
raise Errors.RejectMessage(
_('Message rejected by filter rule match'))
if action == mm_cfg.HOLD:
hold_for_approval(mlist, msg, msgdata, HeaderMatchHold)
if action == mm_cfg.ACCEPT:
return