#!/usr/bin/env python2
"""
    Rip a maildir directory 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,re

import mailbox,smtplib,socket

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

class maildirsend:
    "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
    reFrom=re.compile(r'^Return-path:\s(.*?)\s*$') 
    reTo=re.compile(r'^Envelope-to:\s(.*?)\s*$') 
    reComma=re.compile(r'^\s+(.*?)\s*$')
    
    def __init__(self,options):
        self.options=options
        if self.options.dupfile: self.lockname=self.options.dupfile
        else: self.lockname="/root/db.sent"
        self.__dupOpen()
    def __dupOpen(self):
        """Creates a duplicate lock file"""
        print "In dupOpen"
        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.lockname
            self.__duplock={}
            if os.path.exists(self.dupfile):
                for line in open(self.dupfile,"r"):
                    line=line.strip()
                    if line: self.__duplock[line]=1
            else:
                f=open(self.dupfile,"w")
                f.close()
            print "using lockfile",self.dupfile
        else: return None
    def emailfile(self,fname):
            f=open(fname,"r")
            hasreturn=False
            hasto=False
            mfrom=""
            mto=""
            ato=[]
            comma=False
            for line in f:
                if comma:
                    m=self.reComma.search(line)
                    if not m:
                        print "Doh!!!!!",line
                        sys.exit(1)
                    line=m.group(1)
                    if line[-1]==",":
                        ato.append(line[:-1]) #Skip the trailing comma
                    else:
                        ato.append(line)
                        comma=False #No more commas
                m=self.reFrom.search(line)
                if m:
                    mfrom=m.group(1)
                    if mfrom=="<>": break
                    hasreturn=True
                else:
                    m=self.reTo.search(line)
                    if m:
                        hasto=True
                        mto=m.group(1)
                        if mto[-1]==",":
                            ato.append(mto[:-1])
                            comma=True #We have to join all the extra lines
                        else: ato.append(mto)
                if not comma:
                    if hasto and hasreturn: break
            f.close()
            if self.options.mailto:
                hasto=True 
                ato=[self.options.mailto,]
            if (hasto and hasreturn): #We skip bounces (from <>)
                print fname,"  ", mfrom,"  ----> ", ato
                sys.stdout.flush()
                self.smtpsend(self.options.mailfrom,ato,fname)
                self.dupwrite(fname)
    def directory(self,dirname):
        if os.path.exists(os.path.join(dirname,'cur')):
            self.directory(os.path.join(dirname,'cur'))
        if os.path.exists(os.path.join(dirname,'new')):
            self.directory(os.path.join(dirname,'new'))
        print "Processing",dirname
        for f in os.listdir(dirname):
            fname=os.path.join(dirname,f)
            if self.isdup(fname): continue #Skip dups
            if not os.path.isfile(fname): continue #files only 
            if fname.find("db.")==0: continue #Skip the database file
            self.emailfile(fname)
    def smtpsend(self,mailfrom,mailto,fname):
        """Sends the contents of file fname to <mailto> from <mailfrom>"""
        #Send the actual messages
        server=self.smtpsock()
        f=open(fname)
        if self.options.test:
            f.close()
            print "Test only, not sending %s from %s to %s" % (fname, mailfrom,mailto)
            return
        try: server.sendmail(mailfrom, mailto, f.read())
        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
        f.close()
    def isdup(self,msgid):
        """Check to see if the message is a duplicate, 
           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)
        f=open(self.dupfile,"a")
        f.write(str(msgid))
        f.write("\n")
        f.close()
        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

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("--test", dest="test", action='store_true',default=False, help="Do not attempt to email message, just pretend to")
    parser.add_option("--from", dest="mailfrom", type="string", default='bounce@afrihost.com',help="SMTP from header to use (bounce@afrihost.com)")
    parser.add_option("--dupfile", dest="dupfile", type="string", default='/root/maildirtool.db',help="Records messages here so we can rerun without sending dups  (/root/maildirtool.db)  ")
    parser.add_option("--to", dest="mailto", type="string", help="Rather than resending the mesasge send it to this address")
    (options,args) = parser.parse_args()

    m=maildirsend(options)
    for dirname in args:
        m.directory(dirname)
    print "Once you are happy with sending remove the duplicate file '%s'" % (options.dupfile)
                
