#! /usr/bin/python

# Written by Radovan Garabik <garabik@melkor.dnp.fmph.uniba.sk>.
# For new versions, look at http://melkor.dnp.fmph.uniba.sk/~garabik/pycmail.html

import sys, os, os.path, pwd, string, StringIO, re, time, traceback
import pprint, getopt
import rfc822, socket


class MailDestiny:

    def __init__(self, destinyname=""):
        self.destinyname = destinyname

    def name(self):
        return "generic mail destiny"


class DevNull(MailDestiny):
    "send mail to /dev/null"

    def __init__(self):
        pass

    def getfd(self):
        self.fd = open("/dev/null", "a")
        return self.fd

    def closefd(self):
        self.fd.close()

    def name(self):
        return "/dev/null"


class Pipe(MailDestiny):
    "send mail to another program - headers included"

    def getfd(self):
        self.fd = os.popen(self.destinyname, "w")
        return self.fd

    def closefd(self):
        self.fd.close()

    def name(self):
        return "Pipe to "+self.destinyname


class Forward(MailDestiny):
    "forward mail to another address"

    def getfd(self):
        self.fd = os.popen(sendmailbin+" -i "+self.destinyname, "w")
        return self.fd

    def closefd(self):
        self.fd.close()

    def name(self):
        return "Forward to "+self.destinyname


class MailDir(MailDestiny):
    "deliver mail to a maildir directory"

    def filename(self):
        return `long(time.time())`[:-1]+"."+`os.getpid()`+"."+socket.gethostname()

    def getfd(self):
        self.name = self.filename()
        while os.path.isfile(self.name):
            time.sleep(2)
        try:
            os.makedirs (self.destinyname+"/tmp", 0700)
            os.makedirs (self.destinyname+"/cur", 0700)
            os.makedirs (self.destinyname+"/new", 0700)
        except:
            pass
        os.umask(077)
        self.fd=open(self.destinyname+"/tmp/"+self.name,"a")
        return self.fd

    def closefd(self):
        self.fd.close()
        os.link(self.destinyname+"/tmp/"+self.name, self.destinyname+"/new/"+self.name)
        os.unlink(self.destinyname+"/tmp/"+self.name)

    def name(self):
        return "MailDir "+self.destinyname


class MailBox(MailDestiny):
    "deliver mail to BSD mail folder - needs lockfile (from procmail package) for locking to work!"

    def lock(self):
        if self.destinyname == defaultbox:
            os.system(lockfilebin+" -ml")
        else:
            os.system(lockfilebin+" "+self.destinyname+".lock")

    def unlock(self):
        if self.destinyname == defaultbox:
            os.system(lockfilebin+" -mu")
        else:
            os.unlink(self.destinyname+".lock")

    def locked(self):
        return os.path.isfile(self.destinyname+".lock")

    def filename(self):
        return self.destinyname

    def getfd(self):
        self.lock()
        self.name = self.filename()
        os.umask(077)
        self.fd=open(self.name,"a")
        return self.fd

    def closefd(self):
        self.fd.close()
        self.unlock()

    def name(self):
        return "MailBox "+self.destinyname


def Debug(text="Debugging...", level=2):
    "print text if debuglevel >= level"
    if DEBUGLEVEL >=level:
        print 'DEBUG:', text


def Append(*dest):
    "add to the mail destination"
    global DESTINATION
    for i in dest:
        DESTINATION.append(i)

def Set(*dest):
    "set the mail destination"
    global DESTINATION
    DESTINATION = []
    for i in dest:
        DESTINATION.append(i)

def Junk():
    Set(DevNull())

def SetDefault():
    Set(default)



def SendMail(recipient, sender=None, subject=None, text="pycmail test mail"):
    "send mail"
    "to do: specify eventual additional headers as parameters"
    if DEBUGLEVEL >= 2:
        Debug("sending mail to %s, Subject: %s" % (recipient, subject), 2)
        return
    if sender == None:
        sender = USERNAME
    sm = os.popen(sendmailbin+" -t", "w")
    sm.write("From: %s\n" % sender)
    sm.write("To: %s\n" % recipient)
    if subject:
        sm.write("Subject: %s\n" % subject)
    sm.write("\n")
    sm.write(text)
    sm.close()


def Reply(recipient=None, sender=None, subject=None, text="pycmail test reply"):
    "reply to the current mail"
    "to do: specify eventual additional headers as parameters"
    if subject == None:
        subject = "Re: "+SUBJECT
    if recipient == None:
        recipient = mailmsg.getaddr('Reply-To:')[1] or ADDR_FROM[1]
    if recipient <> None:
        SendMail(recipient, sender = sender, subject=subject, text=text)


def Bounce(text="Mail bounced."):
    "bounce mail. This only prints the text to stdout, which causes MTA to bounce the message"
    Debug("Bouncing mail.", 2)
    print text


def Stop():
    raise "stop_of_pycmailrc"


def Matches(s, regexp, flags=''):
    return Contains(s, regexp, reg=1, flags=flags)

def Contains(s, sub, case=None, rex=0, flags=None):
    "return true if sub occurs in s"
    "if rex is 1, do it as regular expression, else just substring"
    "if searching substrings, default case=0, if regexps, case=1"
    if rex:
        if case == None:
            case = 1
        if flags == None:
            flags = re.M
        else:
            flags = flags+re.M
        if case:
            f = flags
        else:
            f = flags+re.I
        return re.search(sub, s or "", re.M+flags)
    else:    
        if case == None:
            case = 0
        if case:
            return string.count(s or "", sub)
        else:
            return string.count(string.lower(s or ""), string.lower(sub))

def InHeader(hname, sub, case=None, rex=0, flags=None):
    "return true if sub is in header with name hname"
    return Contains(mailmsg.getheader(hname), sub, case, rex, flags)

def untuple(list):
    "change list of tuples into tuple of lists"
    return map(lambda x: x[0], list), map(lambda x: x[1], list)




USERNAME = pwd.getpwuid(os.getuid())[0]
USERHOME = pwd.getpwuid(os.getuid())[5]

DEBUGLEVEL = 0

try:
    optlist, args = getopt.getopt(sys.argv[1:], "d:", ["debuglevel="])
except:
    optlist, args = [], []

for i in optlist:
    if i[0] in ['-d', '--debuglevel']:
        DEBUGLEVEL = string.atoi(i[1])

if args:
    user_pycmailrc = os.path.expanduser(args[0])
else:
    user_pycmailrc = USERHOME+"/.pycmailrc"


defaultbox = "/var/spool/mail/"+USERNAME
default = MailBox(defaultbox)
bufsize = 4096
bodysize = 1000

sendmailbin = "/usr/sbin/sendmail"
lockfilebin = "/usr/bin/lockfile"

SetDefault()

try:
    if os.path.isfile("/etc/pycmailrc"):
        execfile("/etc/pycmailrc")


    stdin = sys.stdin
    msg = ""

    while 1:
        l = stdin.readline()
        msg = msg+l
        if l == '\n' or l == "":
            message_read = 0
            if l == "":
                # message contains only headers - a bit patological case,
                # but we handle it anyway
                BODY = ""
                msg = msg + '\n'  # so that we do not corrupt next message
                message_read = 1
            break

    if not message_read:
        to_read = bufsize*(1+(len(msg)+bodysize)/bufsize)-(len(msg)+bodysize)
        BODY = stdin.read(to_read)
        msg = msg + BODY

    msgIO = StringIO.StringIO(msg)
    mailmsg = rfc822.Message(msgIO)
    FROM = mailmsg.getheader('From') or ""
    TO = mailmsg.getheader('To') or ""
    CC = mailmsg.getheader('Cc') or ""
    SUBJECT = mailmsg.getheader('Subject') or ""
    ADDR_FROM = mailmsg.getaddr('From')
    ADDR_TO = mailmsg.getaddr('To')
    ADDR_CC = mailmsg.getaddr('Cc')
    NAMELIST_TO, ADDRLIST_TO = untuple(mailmsg.getaddrlist('To'))
    NAMELIST_CC, ADDRLIST_CC = untuple(mailmsg.getaddrlist('Cc'))
    msgIO.close()

    Debug("From: %s\nTo %s\nCC: %s\nSubject: %s" % (FROM, TO, CC, SUBJECT), 4)
    Debug("Message headers:\n"+pprint.pformat(mailmsg.headers), 6)
    Debug("Message body:\n"+BODY, 8)


    if os.path.isfile(user_pycmailrc):
        try:
            execfile(user_pycmailrc)
        except "stop_of_pycmailrc":  # used to simulate a goto command
            pass 
        except: # there is some error in .pycmailrc....
            SetDefault() # delivery to system default mailbox
            if DEBUGLEVEL == 0:
                pass
            elif DEBUGLEVEL >= 1:
                traceback.print_exc()


    if DESTINATION == []:
        SetDefault()

    destfd = []
    for i in DESTINATION:
        if DEBUGLEVEL < 2:
            fd = i.getfd()
            fd.write(msg)
            destfd.append(fd)
        else:
            Debug("destination: "+i.name(), 2)


    if not message_read:
        while 1:
            msg = stdin.read(bufsize)
            if msg == "": break
            if DEBUGLEVEL < 2:
                for i in destfd:
                    i.write(msg)
            else:
                Debug("getting next chunk", 4)
                Debug("next chunk of body: \n%s\n" % msg, 10)

finally:
    for i in DESTINATION:
        try:
            if DEBUGLEVEL in [0,1]:
                i.closefd()
        except:
            if DEBUGLEVEL == 0:
                pass
            elif DEBUGLEVEL >= 1:
                traceback.print_exc()

Debug("end of run", 4)
