#!/usr/bin/python
#
# Copyright (C) 2003 Brenda Bell
#
#    spamfilter.py - used to filter email received via a secondary MX
#    that does not support user-defined filtering.
#
#    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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# HISTORY
#
# 27 Jun, 2003  Changed script to write to syslog only when blacklist
#               processing has been triggerred
#
# SYNOPSIS
#
#     cat file-name | spamfilter.py
#
# DESCRIPTION
#
#     This script identifies email that is redirected to its final destination
#     through a backup mail server.  Its intended use is as a qmail filter;
#     hence, it reads the message from stdin and returns 0 when the email is
#     determined to be from a blacklisted sender.
#
#     Example scenario:
#
#         example.com uses a backup mail server at zoneedit.com.
#         A known spammer attempts to connect to example.com and is refused due
#         to firewall rules, tcprules, etc.
#         The spammer then tries to connect to the backup mail server and succeeds.
#         The backup mail server accepts the email, detects that the primary MX at
#         example.com is available and redirects the email to example.com, thereby
#         circumventing the rules established to deny access to the spammer.
#
#     How it works:
#
#         ZoneEdit adds a well structured header that is guaranteed to contain the
#         sender's real IP address.  The sender's IP address is matched against a
#         list of denied IP and/or network addresses. If a match is found, the sender
#         has been blacklisted and the email should be bounced.
#
#         The script can be installed as a bouncesaying command for qmail as follows:
#
#             bouncesaying "This email address no longer accepts mail" spamfilter.py
#
# RETURN CODES
#
#     0    The sender is blacklisted; the email should be bounced
#     1    The sender is not blacklisted; the email should be delived
#
# CUSTOMIZATION
#
#     1)  Provide a custom implementation of the getDeniedAddresses function.  How you
#         implement this function is up to you as long as it returns a list of IP
#         addresses in one of the following formats:
#
#             n.n.n.n
#             n.n.n.n/nm
#
#         where nm is the number of bits to be allocated to the network portion of
#         the IP address.
#
#         Examples:
#
#             192.168.1.64    prevents the delivery of email from 192.168.1.64
#
#             192.168.1.0/24  prevents the delivery of email from any IP address in
#                             the 192.168.1.0 network; e.g., from 192.168.1.0 through
#                             192.168.1.255
#
#     2)  Provide a custom implementation of getSearchString function.  How you
#         implement this function is up to you as long as it returns a string that can
#         be used to accurately identify redirected mail; e.g., when this string appears
#         in any Received header, the script will continue to validate the email against
#         the blacklist.
#
# DEPENDENCIES
#
#     ipaddr.py downloadable as of Jun 27, 2003 from
#     ftp://ftp.cendio.se/pub/playground/python/ipaddr-1.1.tar.gz
#
# CAVEATS
#
#     The script assumes that the Received: header written by the local MX is immediately
#     followed by a header written by the forwarding MX and that the header written by
#     the forwarding MX contains the real IP address of the sending server.
#
#     The script assumes that the sender's IP address appears in the header as [n.n.n.n].
#
# VERSION
#
#     1.1, released Jun 27, 2003
#
# REPORTING BUGS
#
#     Report bugs to b311b-bugs-spamfilter@theotherbell.com
#

import sys
import string
from syslog import *
import email
from ipaddr import *

def getAllowedAddresses():
    return [
        '203.7.0.0/17'         #includes linuxchix.org
        ]

def getDeniedAddresses():
    return [
         '64.248.0.0/13'       #korea
        ,'66.151.41.0/24'      #opmnetwork.net
        ,'66.151.42.0/24'      #opmnetwork.net
        ,'202.0.0.0/8'         #apnic
#        ,'203.0.0.0/8'         #apnic
        ,'205.244.71.0/24'     #concentric.net
        ,'207.88.0.0/16'       #xo.com
        ,'209.220.162.138/32'  #sweepsclub.com
        ,'211.0.0.0/8'         #apnic
        ,'216.82.72.149'       #e-xpedient.com
        ]

def getSearchString():
    return '(HELO ' + 'mx2.zoneedit.com' + ')'

# parse stdin into a Message
oMessage = email.message_from_file(sys.stdin)

lDeniedAddresses = getDeniedAddresses()
searchString = getSearchString()

# iterate through the Received: headers
lReceivedHeaders = oMessage.get_all('Received')
for thisHeader in lReceivedHeaders[:]:
    # did this email come through zoneedit?
    cameFromBackupMailServer = string.find(thisHeader,searchString)
    if cameFromBackupMailServer != -1:
        # the next Received header will identify the sender
        idxOfNextHeader = lReceivedHeaders.index(thisHeader) + 1
        sendersHeader = lReceivedHeaders.pop(idxOfNextHeader)
        haveSendersIP = string.find(sendersHeader,'[')
        if haveSendersIP == -1: # cant find ip address; should never happen
            continue
        # get the original sender's ip
        sendersIP = sendersHeader[haveSendersIP+1:string.find(sendersHeader,']')]
        # check the sender against the list of denied addresses
        for deniedAddress in lDeniedAddresses[:]:
            # compute the ip address and netmask parts
            hasNetmask = string.find(deniedAddress,'/')
            if -1 != hasNetmask:
                deniedIP = ipaddr(deniedAddress[:hasNetmask])
                deniedNetmask=deniedAddress[hasNetmask+1:]
            else:
                deniedIP = ipaddr(deniedAddress)
                deniedNetmask = 32
            matchNetmask = netmask(deniedNetmask, DEMAND_NONE)
            # compute the network address for the senders ip and lDeniedAddressesed ip
            deniedNetwork = network(deniedIP.ip_str(), matchNetmask.netmask_str(), DEMAND_NONE)
            sendersNetwork = network(sendersIP, matchNetmask.netmask_str(), DEMAND_NONE)
            # if the two networks match, we need to bounce the email
            if deniedNetwork.network_str() == sendersNetwork.network_str():
                syslog(LOG_WARNING | LOG_MAIL,'Bounced blacklisted email from ' + sendersIP)
                sys.exit(0)
        syslog(LOG_INFO | LOG_MAIL,'Accepted email from ' + sendersIP)
sys.exit(1)
