#! /usr/bin/python

"""
Grep through a mail log, keeping a history of lines with corresponding mail ID's
"""

import anydbm
import sys
import os
import re

class mailgrep:
    def __init__(self, file=None, fd=None,
                        min_history=4096,
                        verbose=0,
                        matches=[],
                        regex=None,
                        exclude=[],
                        maxlines=2147483647,
                        writelinefunction=sys.stdout.write):
        self.min_history = min_history;
        self.dict0 = {};
        self.dict1 = {};
        self.verbose = verbose
        self.matches=[]
        self.exclude=exclude
        self.matchedkeys0={}
        self.matchedkeys1={}
        self.maxlines=maxlines;
        for thing in matches:
            self.matches.append(thing.lower())
        if regex:
            self.regex=re.compile(regex)
            self.pattern=regex
        else:
            self.regex=None
            self.pattern="(none)"
        self.writelinefunction = writelinefunction
        if file:
            self.fd = open(file,'r') 
        else:
            self.fd = fd

    def __del__(self):
        # self.fd.close() ... well, but only if we opened it
        pass

    def remember_line(self,key,line):
        """Append the line to the list for the key, rotating to the "old" dictionary if necessary """
        if not self.dict0.has_key(key):
            self.dict0[key]=[];
        self.dict0[key].append(line);
        if len(self.dict0) >= self.min_history:
            del self.dict1;
            del self.matchedkeys1;
            self.dict1=self.dict0
            self.matchedkeys1=self.matchedkeys0
            self.dict0={}
            self.matchedkeys0={}

    # 1Lb4tI-0003Uy-1B
    def eximid(self,line):
        """Pull the exim mail id out of the line"""
        bits = line.split(" ")
        # postfix
        if len(bits)>6 and bits[4].startswith("postfix/"):
            return bits[5]
        # exim
        for part in bits:
            if len(part)<16: continue
            if part.find(":") >= 0: continue
            if part.find("[") >= 0: continue
            if part.find("-") >= 0: return part
        # bland default    
        return line
    
    def match(self,line):
        """See if this line matches what we are looking for (e.g. the domain name)"""
        for anexclude in self.exclude:
            if line.find(anexclude) != -1: return False
        if self.regex:
            if self.regex.search(line): return self.pattern
        for amatch in self.matches:
            if line.lower().find(amatch) != -1: return amatch
        for amatch in self.matchedkeys0.keys():
            if line.find(amatch) != -1: return amatch
        for amatch in self.matchedkeys1.keys():
            if line.find(amatch) != -1: return amatch
        return False

    def run(self):
        """ Reads newer lines from the named file, writes them using the
        writelinefunction, and returns the number of lines processed """
        count_lines = 0
        while True:
            line = self.fd.readline()
            if not line:
                break  # eof
            match = self.match(line);
            key = self.eximid(line)
            if match:
                if key:
                    if self.verbose: prefix=key+": " 
                    else: prefix=''
                    if self.dict1.has_key(key):
                        for mline in self.dict1[key]:
                            self.writelinefunction(prefix+mline)
                            count_lines += 1
                            if count_lines > self.maxlines: return count_lines
                        del self.dict1[key]
                    if self.dict0.has_key(key):
                        for mline in self.dict0[key]:
                            self.writelinefunction(prefix+mline)
                            count_lines += 1
                            if count_lines > self.maxlines: return count_lines
                        del self.dict0[key]
                self.matchedkeys0[key]=True;
                if self.verbose: prefix=match+": " 
                else: prefix=''
                self.writelinefunction(prefix+line)
                count_lines += 1
                if count_lines > self.maxlines: return count_lines
            else:
                self.remember_line(key,line)
        return count_lines
    
if __name__ == '__main__':

    # options ..
    usage="usage: %prog [options] regexp [file ...]\n"+__doc__+"\n"
    import optparse
    parser=optparse.OptionParser(usage)
    group = optparse.OptionGroup(parser, "Input options", "What we look for in the input")
    group.add_option("-n","--number",dest="number", action="store", default=2147483647, type='int',
                       help="number of lines of output to print")
    group.add_option("-c","--context",dest="history_context", action="store", default=4096, type='int',
                       help="maximum number of lines of history context to keep")
    group.add_option("-r","--reverse",dest="reverse", action="store_true", default=False,
                       help="show the tail of the file")
    group.add_option("-x","--exclude",dest="exclude", action="append", default=[],
                       help="pattern matches to exclude")
    group.add_option("-v","--verbose",dest="verbose", action="store_true", default=False,
                       help="print junk")
    parser.add_option_group(group)
    (options,args) = parser.parse_args()

    # don't moan about being piped to head ...
    import signal
    try: signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    except: pass
    try: signal.signal(signal.SIGINT, signal.SIG_DFL)
    except: pass
    if len(args)<1:
        parser.print_help()
        sys.exit(1)

    regex=args[0]
    # go for it
    if len(args)==1: args=[True,None]
    if len(options.exclude)==0:
        exclude=[' cwd=']   # multiple deltions cause havoc
    else:
        exclude=options.exclude
    for file in args[1:]:
        if not file:
            if sys.stdin.isatty():
                file="/var/log/exim_mainlog";
            else:
                fd=sys.stdin;
        if file:
            if options.reverse:
                import tac
                fd=tac.tac(file)
            elif file.find(".bz2") != -1:
                fd=os.popen("bzcat "+file,'r')
            elif file.find(".gz") != -1:
                fd=os.popen("zcat "+file,'r')
            else:
                fd=open(file,'r')
        x=mailgrep(fd=fd,
            regex=regex,
            verbose=options.verbose,
            exclude=exclude,
            maxlines=options.number,
            min_history=options.history_context);
        x.run();
        del x
    sys.exit(0)

