#!/usr/bin/env python2
"""
    Rip an email box apart and do stuff with the contents,
    Includes forwarding the messages to a specified email address
    and removing spam or postmaster bounces

    Copyright  Afrihost.com (PTY) Ltd 2004
    Original file by: Ed Pascoe <ed@pascoe.co.za>
"""
__author__  = "Ed Pascoe <ed@pascoe.co.za>"
__credits__ = "Afrihost.com -- The best webhosting. Ever."


import os.path, sys
import optparse
import time

import mailbox,smtplib,socket
import anydbm

if __name__=='__main__': sys.path.append(os.path.dirname(os.path.realpath(sys.argv[0]))+'/../../pylib')

class mboxrip:
    "Rips the mailbox apart and forwards to relevant owners. Create a new instance each time"
    __smtpSock=None
    __smtpErrorCount=0
    __smtpErrorLimit=3
    __duplock=None
    dupfile=None
    
    def __init__(self,filename,options):
        self.filename=filename
        self.options=options

        self.__dupOpen()

    def __dupOpen(self):
        """Creates a duplicate lock file"""
        if self.__duplock: return self.__duplock

        if self.options.nodups: #We keep a duplicate file around incase the run is interupted part of the way through
            self.dupfile=self.filename+".duplock"
            self.__duplock = anydbm.open(self.dupfile, 'c')
            print "using lockfile",self.dupfile
            return self.__duplock
        else: return None

    def isdup(self,msgid):
        """Check to see if the message is a duplicate, if not it adds it to the duplicate DB,
           Autochecks to see if dups checking is enabled, and will safely ignore calls where needed
        """
        if not self.options.nodups: return False #checking disabled
        if not msgid: return False #Msgid is blank, can't check
        msgid=str(msgid)
        if self.__duplock.has_key(msgid): return True
        else: return False

    def dupwrite(self,msgid):
        """Check to see if the message is a duplicate, if not it adds it to the duplicate DB,
           Autochecks to see if dups checking is enabled, and will safely ignore calls where needed
        """
        if not self.options.nodups: return True #checking disabled, pretend it worked
        if not msgid: return True #Msgid is blank, can't check
        self.__duplock[str(msgid)]=str(msgid)
        #self.__duplock.sync()
        return True
        
    def smtpsock(self):
        """Returns an smtp connection. Auto connects and bail on excess erros"""
        if self.__smtpSock : return self.__smtpSock
        self.__smtpErrorCount = self.__smtpErrorCount + 1
        if self.__smtpErrorCount  > self.__smtpErrorLimit:
            raise Exception,"Too many errors to continue. Try waiting a awhile and then try again"
        try: 
             self.__smtpSock = smtplib.SMTP(self.options.smtp)
             return self.__smtpSock
        except: 
            if self.__smtpErrorCount == self.__smtpErrorLimit:
                raise #Out of retries. Some thing is really wrong
            return self.smtpsock() #Otherwise just retry
        
    def isjunk(self,msg):
        """Is this a bounce or a spam message? Safe to call if junk testing disabled"""
        if not self.options.wipe:  return False #Junk testing disabled

        try: replyto=msg.getfirstmatchingheader("Reply-to")[0].strip().split(':',2)[1].strip()
        except IndexError: replyto=""
        try:
            mailfrom=msg.getfirstmatchingheader("From")[0].strip().split(':',2)[1].strip()
            mailfrom=mailfrom.lower()
            if mailfrom.find("postmaster@")> -1: 
                print "Bounce"
                return True
            if mailfrom.find("notexist@") > -1:
                print "Bounce"
                return True
            if mailfrom.find("daemon@") > -1:
                print "Bounce"
                return True
            if mailfrom.find("notify@") > -1:
                print "Bounce"
                return True
            if mailfrom.find("spam ") > -1:
                print "Bounce"
                return True
            if mailfrom.find("nobody@") > -1:
                print "Bounce"
                return True
            if replyto.find("nobody@") > -1:
                print "Bounce"
                return True
        #
        except IndexError: 
            print "Index Error"
            return 1

        return 0

    def sendmessage(self,mailfrom,to,email,unixfrom):
        if self.options.test: return True #Pretend the message was sent
        if self.options.inplace: #Assuming we are editing in place
            f=open(self.filename+".replace","a")
            f.write(unixfrom)
            f.write(email)
            f.close()
            return True

        server=self.smtpsock()
        try: server.sendmail(mailfrom, to, email)
        except smtplib.SMTPSenderRefused: 
            return True #Bad to address, ignore
        except (socket.error,IOError,smtplib.SMTPDataError),e:  #May be a tempory problem?
            e=str(e)
            if e.find("virus") > -1: 
                print "Virus, skipping messages"
                return True #Skip virus messages
            sys.stderr.write("IO Error on smtp (%s), sleeping\n" % e)
            time.sleep(5)
            self.__smtpErrorCount=self.__smtpErrorCount+1
            if self.__smtpErrorCount > self.__smtpErrorLimit:
                raise  #Too many errors, game over

    def run(self):
        fp=open(self.filename,"r")
        box=mailbox.UnixMailbox(fp)
        msgid={}
        count=0
        while(1):
            msg=box.next()
            if not msg: break
            count=count+1

            #Find from address
            if self.options.mailfrom:
                mailfrom=self.options.mailfrom
            else:
                try:mailfrom=msg.getfirstmatchingheader("From")[0].strip().split(':',2)[1].strip()
                except IndexError: 
                    print count,"No from address"
                    continue
            #Find the message id
            try:id=msg.getfirstmatchingheader("Message-ID")[0].strip().split(':',2)[1].strip()
            except IndexError:  id=None
            if not id:
                print count,"No msgid, skipping!!!"
                continue #Skip dup
            if self.isdup(id): 
                print count,id,"DUP, skipping!!!"
                continue #Skip dup
            if self.isjunk(msg): 
                print count,id,"SPAM, skipping!!!"
                self.dupwrite(id)
                continue #Skip Spam
            if self.options.mailto: to=self.options.mailto
            else:
                try:to=msg.getfirstmatchingheader("Envelope-to")[0]
                except IndexError: 
                    print count,id,"no envelope, skipping!!!"
                    self.dupwrite(id)
                    continue #No envelope-to means its cpanel cruft
            email=str(msg)+"\n"+msg.fp.read()
            self.sendmessage(mailfrom,to,email,msg.unixfrom)
            #
            #If we get here the message has been sent successfully
            print count,id,"Sent"
            sys.stdout.flush()
            self.dupwrite(id)
        print "Finished." 
        if self.options.nodups:
            print "The file %s contains the message id's of all entries sent.  You may want to remove it" % (self.dupfile)
        if self.options.inplace:
            print "The replacement mailbox is ready, please execute the following"
            print "     cat %s > %s\n     rm %s" % (self.filename+".replace", self.filename, self.filename+".replace")

if __name__=='__main__':
    usage="Usage: %prog [<options>] [mailboxes]" +"\n"+__doc__+"\n"
    parser=optparse.OptionParser(usage)
    parser.add_option("-d","--nodups", dest="nodups", action="store_false", default=1, help="Do Not Remove duplicate mail ID's")
    parser.add_option("--host", dest="smtp", type="string", default="127.0.0.1", help="SMTP server to use (default 127.0.0.1)")
    parser.add_option("--nowipe", dest="wipe", action="store_false", default=1, help="Donot remove all email bounces and spam messages")
    parser.add_option("--test", dest="test", type="string", help="Do not attempt to email message, just pretend to")
    parser.add_option("--from", dest="mailfrom", type="string", help="SMTP server to use (default 127.0.0.1)")
    parser.add_option("--to", dest="mailto", type="string", help="Rather than resending the mesasge send it to this address")
    parser.add_option("--inplace", dest="inplace", action="store_true", default=0, help="Implies --wipe, removes all spam and bounce messages")
    (options,args) = parser.parse_args()

    if options.inplace: options.wipe=1
    for box in args:
        m=mboxrip(box,options)
        m.run()

