#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This program is free software. It comes without any warranty, to
# the extent permitted by applicable law. You can redistribute it
# and/or modify it under the terms of the Do What The Fuck You Want
# To Public License, Version 2, as published by Sam Hocevar. See
# http://sam.zoy.org/wtfpl/COPYING for more details.


_NAME = 'pmailq'
_HELP = "[OPTIONS] [ list | parse | del ]"
_DESC = "%s postfix mail queue manager" % _NAME
_VERSION = '0.3'
_AUTHOR = 'Emmanuel Bouthenot <kolter@openics.org>'


MAILQ = "postqueue -p"
DELQ = "postsuper -d"


from optparse import OptionParser, OptionGroup # needs python >= 2.3
import sys, os, subprocess, fcntl, select, fnmatch

class Proc:

    def run(self, command):
        proc = subprocess.Popen(command, bufsize=1, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
        proc.stdin.close()
        outfile = proc.stdout
        outfd = outfile.fileno()
        errfile = proc.stderr
        errfd = errfile.fileno()

        # avoid deadlocks
        self.set_no_block(outfd)
        self.set_no_block(errfd)

        outdata = errdata = ''
        outeof = erreof = False

        while True:
            # wait for activity
            ready = select.select([outfd, errfd], [], [])
            if outfd in ready[0]:
                outchunk = outfile.read()
                if outchunk == '':
                    outeof = True
                outdata = outdata + outchunk
            if errfd in ready[0]:
                errchunk = errfile.read()
                if errchunk == '':
                    erreof = True
                errdata = errdata + errchunk
            if outeof and erreof:
                break
            # give a little time for buffers to fill
            select.select([],[],[],.1)

        err = proc.wait()

        return err, outdata, errdata

    def set_no_block(self, fd):
        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
        try:
            fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NDELAY)
        except AttributeError:            
            fcntl.fcntl(fd, fcntl.F_SETFL, fl | fcntl.FNDELAY)

class Mailqueue:

    def __init__(self):
        self.mailqueue = []
        self.filters = {
            'email'   : None,
            'msg'     : None,
            'lowsize' : 0,
            'upsize'  : 0,
            'active'  : False,
            'hold'    : False
            }
        self.parse()


    def add_filter(self, key, value):
        self.filters[key] = value
    

    def parse(self):
        p_ret, p_stdout, p_stderr = Proc().run(MAILQ)
        
        # mail system down ?
        if p_ret != 0:
            sys.stderr.write ("ERR : %s\n" % ", ".join(p_stderr.strip().split("\n")))
            sys.exit (-1)
        
        buffer = p_stdout.strip().split('\n');
        # checking empty mail queue        
        if len(buffer)>0 and buffer[0].strip() == "Mail queue is empty":
            sys.stderr.write ("INFO : %s\n" % buffer[0].strip())
            return None
        
        # skip first and last line
        buffer = "\n".join(buffer[1:-1]).strip()
        for block in buffer.split("\n\n"):
            lines = block.split("\n")

            headers = lines[0].split(' ')
            # squeeze repeated spaces
            while '' in headers:
                headers.remove('')
        
            queue = []
            dest = []
            info = ""
            for expl in lines[1:]:
                expl = expl.strip()
        
                if expl.startswith("(") and expl.endswith(")"):
                    if info == "":
                        info = expl[1:len(expl)-1]
            
                    if dest != []:
                        queue.append({ "info" : info , "dest" : dest })
                        dest = []
                        info = expl[1:len(expl)-1]                
                else:
                    dest.append(expl.lower())

            if dest != []:
                queue.append({ "info" : info , "dest" : dest })
        
            self.mailqueue.append({
                "id" : headers[0].rstrip("*!"),
                "active" : headers[0].endswith("*"),
                "hold" : headers[0].endswith("!"),
                "size" : headers[1],
                "date" : " ".join(headers[2:5]),
                "queue" : queue
                })


    def check(self, size, active, hold, dest, infos):
        if self.filters['email'] != None:
            match = False
            for e in dest:
                if fnmatch.fnmatch(e.lower(), self.filters['email'].lower()):
                    match = True
            if not match:
                return False
        if self.filters['msg'] != None:
            match = False
            for i in infos:
                if fnmatch.fnmatch(i.lower(), self.filters['msg'].lower()):
                    match = True
            if not match:
                return False
        if self.filters['active'] and not active:
            return False
        if self.filters['hold'] and not hold:
            return False
        if self.filters['lowsize'] != 0 and int(size) > self.filters['lowsize']:
            return False
        if self.filters['upsize'] != 0 and int(size) < self.filters['upsize']:
            return False

        return True


    def cmd_list(self):
        for m in self.mailqueue:
            out = "%s\n" % m['id'] 
            out += "  -date: %s\n" % m['date']
            out += "  -size: %s\n" % m['size']
            out += "  -active: %s\n" % str(m['active'])
            out += "  -hold: %s\n" % str(m['hold'])
            out += "  -to:\n"
            to = []
            i = []
            for n in m['queue']:
                i.append(n['info'])
                to += n['dest']
                out += "    + %s : [%s]\n" % (",".join(n['dest']), n['info'])

            if self.check(m['size'], m['active'], m['hold'], to, i):
                print out

        
    def cmd_parse(self):
        for m in self.mailqueue:
            e = []
            i = []
            for n in m['queue']:
                i.append(n['info'])
                for o in n['dest']:
                    e.append(o)
                
                if self.check(m['size'], m['active'], m['hold'], e, i):
                    print "%s|%s|%s|%d|%d|%s" % (m['id'], m['date'], m['size'], int(m['active']), int(m['hold']),  ",".join(n['dest']))


    def cmd_del(self):
        for m in self.mailqueue:
            e = []
            i=[]
            for n in m['queue']:
                i.append(n['info'])
                for o in n['dest']:
                    e.append(o)
                if self.check(m['size'], m['active'], m['hold'], e, i):
                    proc = popen2.Popen3("%s %s" % (DELQ, m['id']), True)
                    p_ret = proc.wait()
                    if p_ret != 0:
                        print "deleting %s [FAILED] (%s)" % (m['id'], "".join(proc.childerr.readlines()).strip())
                    else:
                        print "deleting %s [OK]" % m['id']

def main():
    usage = "%prog " + _HELP
    desc = _DESC
    parser = OptionParser(usage=usage, description=desc, version=("%s %s" % (_NAME, _VERSION)))

    opts = OptionGroup(parser, "filters")
    opts.add_option("-e", "--email", dest="email", type="string", metavar="PATTERN", help="select entries in queue with email matching PATTERN")
    parser.set_defaults(email=None)
    opts.add_option("-m", "--msg", dest="msg", type="string", metavar="PATTERN", help="select entries in queue with error message matching PATTERN")
    parser.set_defaults(msg=None)
    opts.add_option("-l", "--size-lower", dest="lowsize", type="int", metavar="SIZE", help="select entries in queue with size lower than SIZE bytes")
    parser.set_defaults(lowsize=0)
    opts.add_option("-u", "--size-upper", dest="upsize", type="int", metavar="SIZE", help="select entries in queue with size upper than SIZE bytes")
    parser.set_defaults(upsize=0)
    opts.add_option("-a", "--active", dest="active", default=False, action="store_true", help="select 'active' entries in queue (default: no)")
    opts.add_option("-o", "--hold", dest="hold", default=False, action="store_true", help="select 'on hold' entries in queue (default: no)")
    parser.add_option_group(opts)

    (options, args) = parser.parse_args()

    m = Mailqueue()
    m.add_filter("email", options.email)
    m.add_filter("msg", options.msg)
    m.add_filter("lowsize", options.lowsize)
    m.add_filter("upsize", options.upsize)
    m.add_filter("active", options.active)
    m.add_filter("hold", options.hold)

    if args == []:
        m.cmd_list()
    elif args[0] == "list":
        m.cmd_list()
    elif args[0] == "parse":
        m.cmd_parse()
    elif args[0] == "del":
        m.cmd_del()
    else:
        print "%s %s" % (_NAME, _HELP)


if __name__ == "__main__":
    main()

