#!/usr/bin/env python3
"""
    Manage the exim mailq
 
    Copyright  Afrihost.com (PTY) Ltd 2005
    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, os
import optparse
import re, smtplib
import random
import locale
if __name__=='__main__': sys.path.append(os.path.dirname(os.path.realpath(sys.argv[0]))+'/../pylib')

def compat_open(filename,mode,encoding=None,errors=None):
    # compatible open for older python
    try:
       return open(filename,mode,encoding=encoding,errors=errors)
    except TypeError:
       return open(filename,mode)
 
class EximManage:
    spool = "/var/spool/exim/input"
    msglist="List of Exim messages"
    exim="/usr/sbin/exim"
    def __init__(self,options={}):
        self.options=options
        if self.options.inputqueue:
            self.spool="/var/spool/exim.in/input"
            self.exim="/usr/sbin/exim -DSPOOL=/var/spool/exim.in" 
        self.build() #Build a list of messages

    def dwalk(self,path):
        for f in os.listdir(path):
            fp=os.path.join(path,f)
            if os.path.isdir(fp): 
                self.dwalk(fp) #Walk down through all the sub directories
            else:
                self.msghandle(f,fp)
                #self.msglist[f]=

    def msghandle(self,f,fp):
        # reId=re.compile('(......-......-..)-(.)')
        # To accommodate longer mail ID - Ryan
        reId=re.compile(r'(\w{1,6})-(\w{1,8})(\w{1,3})-(\w{1,4})')
        m=reId.search(f)
        if m:
            if m.group(1) in self.msglist:
                if m.group(2)=='H':
                    self.msglist[m.group(1)]={'path':fp} #We really just want the headers
            else:
                # This returns ID's before the '-' - Ryan
                # self.msglist[m.group(1)]={'path':fp}
                # This returns the full match - Ryan
                self.msglist[m.group(0)]={'path':fp}
        
    def __buildmsgstats(self,msg):
        reNum=re.compile(r'^(\d+)$')

        if self.msglist[msg]['path'][-1] != 'H':
            #print msg,"is missing a header file:",self.msglist[msg]
            del self.msglist[msg]
            return
        if not os.path.exists(self.msglist[msg]['path']): 
            del self.msglist[msg] #Must have been processed already
            return
        f=compat_open(self.msglist[msg]['path'],"r", encoding='utf-8', errors='ignore')
        f.readline() #1st line is just the message id 1D8DJq-0006gH-DP-H
        f.readline() #2nd line is Owner info (I think) mailnull 47 12
        mfrom=f.readline().strip() #3rd line is a from address <Bob.whatsit@example.com>
        if len(mfrom) > 2:
            if mfrom[0]=="<": mfrom=mfrom[1:]
            if mfrom[-1]==">": mfrom=mfrom[:-1]
        self.msglist[msg]['from']=mfrom

        self.msglist[msg]['timestamp']=f.readline().split()[0] #Recived timestmp  1110183026 0
        while 1:
            line=f.readline()
            if not line: break
            if line[0] != "-": break
            data=line.strip()[1:] #Chop the initial "-" sign off
            if len(data) == 1: data.append(1) #For puge flags like --deliver_firsttime
            self.msglist[msg][data[0]]=data[1]
        df=self.msglist[msg]['path']
        df=df[:-1] + "D"
        try: self.msglist[msg]['size']=os.path.getsize(df)
        except os.error: self.msglist[msg]['size'] = 0 #Datafile missing
        if 'from' not in self.msglist[msg]:
           self.msglist[msg]['from']='<>' #Must be a bounce 
        tocount=0
        self.msglist[msg]['tocount']=tocount
        while 1:
            line=f.readline()
            if not line: break
            line=line.strip()
            #print "Testing",msg,self.msglist[msg]['from'],line
            m=reNum.search(line)
            if m: 
                tocount=int(m.group(1))
                break
        self.msglist[msg]['tocount']=tocount
        to=[]
        while 1:  #Build list of destination email addresses
            line=f.readline()
            if not line: break
            line=line.strip()
            if len(line)==0: break
            to.append(line) 
        self.msglist[msg]['to']=to

    def build(self):
        """Walks through all the email messages and builds a list, auto called by constructor"""
        self.msglist={}
        self.dwalk(self.spool)
        for i in list(self.msglist.keys()):
            self.__buildmsgstats(i)
        
    def stats(self):
        fcount={}
        fmsgcount={}
        fsize={}
        fmsgids={}
        print("%-40s %6s %12s   %4s  %s" % ("from-address","addrs", "bytes", "msgs", "sample id"));

        for i in list(self.msglist.keys()):
            m=self.msglist[i]
            addr = m['from']
            try:
                if addr in fmsgids:
                    fmsgids[addr].append(i)
                else:
                    fmsgids[addr]=[i,]

                if addr in fmsgcount:
                    fmsgcount[addr]=fmsgcount[addr] + 1;
                else:
                    fmsgcount[addr]= 1;

                if addr in fcount:
                    fcount[addr]=fcount[addr] + m['tocount']
                else:
                    fcount[addr]= m['tocount']

                if addr in fsize:
                    fsize[addr]=fsize[addr] + (m['size'] * m['tocount'])
                else:
                    fsize[addr]= m['size'] * m['tocount']

            except KeyError: 
                print("Failure on ",m)
                raise
        klist=list(fcount.keys())
        klist.sort(key=lambda a:fsize[a])
        for k in klist:
            c="%5.0f" % float(fcount[k])
            s="%12.0f" % float(fsize[k])
            random.shuffle(fmsgids[k])
            idstring=fmsgids[k][0]  # message ids
            print("%-40s %6s %12s   %4d  %s" % (k,c,s, fmsgcount[k], idstring))

    def summary(self):
        print("%i messages in queue" % (len(self.msglist)))

    def delete_mail(self,matchfunc, mfrom):
        """Remove all messages from/to/or about a given address"""
        mfrom=mfrom.lower()
        if self.options.silent: silent="2>/dev/null > /dev/null"
        else: silent=""

        for i in list(self.msglist.keys()):
            if matchfunc(self.msglist[i],mfrom):
                os.system(self.exim +" -Mrm '%s' %s" % (i,silent))

    def show(self,matchfunc,match):
        for i in list(self.msglist.keys()):
            if matchfunc(self.msglist[i], match):
                print(i)

    def match_from(self,msglist_item,mfrom):
        """ Given self.msglist[i] check if the given it matches the given from-address """
        regex_from=re.compile('^' + mfrom.lower() + '$')
        if regex_from.search( msglist_item['from'].lower() ) :
            return True;
        return False
        
    def match_to(self,msglist_item,mto):
        """ Given self.msglist[i] check if the given it matches the given to-address """
        regex_to=re.compile('^' + mto.lower() + '$')
        found=None
        for em in msglist_item['to']:
            if regex_to.search( em.lower() ) :
                return True
        return False

    def match_email(self,msglist_item,email):
        """ Given self.msglist[i] check if the given it matches the given address in to or from """
        return self.match_from(msglist_item,email) or self.match_to(msglist_item,email)

    def forward(self,mailto,id):
        nonum=re.compile(r'^\d+\S\s+')

        msg=[]
        f = compat_open(self.msglist[id]['path'],"r", encoding='utf-8', errors='ignore')
        while 1: #Skip initial junk until we pass the first blank line. Everything after that is header
            line=f.readline()
            if len(line.strip())==0: break
        while 1:
            line=f.readline()
            if len(line.strip())==0: break
            msg.append(nonum.sub('',line))
        f.close()
        msg.append('') #Blank line between header and body
        df=self.msglist[id]['path'][:-1] + "D"
        f = compat_open(df,"r", encoding='utf-8', errors='ignore')
        null = f.readline() #First line is just a msg id
        data=f.readlines()
        f.close()
        msg=msg+data
        server = smtplib.SMTP('localhost')
        #server.set_debuglevel(1)
        server.sendmail(mailto, mailto, ''.join(msg)) #Use from and to as same address, snooping could be embarassing if we get caught otherwise
        server.quit() 
 
if __name__=='__main__':

    # don't moan about being piped to head ...
    import signal
    try: signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    except: pass

    usage="Usage: %prog [<options>]\n"+__doc__+"\n"
    parser=optparse.OptionParser(usage)
    parser.add_option("--from",  dest="mfrom", action="store_true", default=0,  help="list all message From: addresses listed on command line eg: exivi `eximanage --from bob@example.com`")
    parser.add_option("--to",    dest="mto",   action="store_true", default=0,  help="list all message To: addresses listed on command line eg: exivi `eximanage --to bob@example.com`")
    parser.add_option("--email", dest="email", action="store_true", default=0,  help="list all messages that refer to given email address, can contain regexp  eg: exim -Mf  `eximanage --from .*@example.com`")

    parser.add_option("--dfrom", dest="dfrom",  action="store_true", default=0, help="Delete all message from addresses listed on command line")
    parser.add_option("--dto",   dest="dto",    action="store_true", default=0, help="Delete all messages to a given addresson command line eg: exivi `eximanage --to bob@example.com`")
    parser.add_option("--demail",dest="demail", action="store_true", default=0, help="Delete all messages that refer to given email address, can contain regexp  eg: eximanage --dfrom .*@example.com   Be careful eximanage --dfrom '.*' would be quite thick!!! ")

    parser.add_option("-q","--silent", dest="silent", action="store_true", default=0,  help="Keep message down to a minimum")
    parser.add_option("--summary", dest="summary", action="store_true", default=0, help="Display message counts by domain")
    parser.add_option("--stats", dest="stats", action="store_true", default=0, help="Display message queue stats")
    parser.add_option("--forward","--fwd", dest="forward", type="string", help="Forward given msg ids to given email address")
    parser.add_option("--inq", dest="inputqueue", action="store_true", help="Use the input spambox queue instead")
    (options,args) = parser.parse_args()

    locale.setlocale(locale.LC_ALL,'en_US.UTF-8') #So we get comma separators on thousands

    if options.summary:  #Just show the queue size and exit quick, rather than processing the entire queue
        spooldir='';
        if options.inputqueue: spooldir='-DSPOOL=/var/spool/exim.in'
        outputfilter = "grep -o '@[^>]*' | tr -d '@' | sort | uniq -c | sort -rn"
        os.system("exim %s -bp | %s" % (spooldir, outputfilter))
        sys.exit(0)
        #m.summary()

    if not options.stats and not args:  #Just show the queue size and exit quick, rather than processing the entire queue
       if options.inputqueue:
           os.system("exim -DSPOOL=/var/spool/exim.in -bpc")
       else:
           os.system("exim -bpc")
       sys.exit(0)
       #m.summary()

    m=EximManage(options)

    if options.mfrom:
        for f in args:
            m.show(m.match_from,f)

    if options.mto:
        for f in args:
            m.show(m.match_to,f)

    if options.email:
        for f in args:
            m.show(m.match_email,f)

    if options.forward:
        for f in args:
            m.forward(options.forward,f)

    if options.demail:
        for f in args:
            m.delete_mail(m.match_email,f)

    if options.dfrom:
        for f in args:
            m.delete_mail(m.match_from,f)

    if options.dto:
        for f in args:
            m.delete_mail(m.match_to,f)

    if options.stats:
        m.stats()


