#!/usr/bin/env python

#####################################################################
#
# maildrop  A daemon to handle mail delivery for the MaildropHost
#
# This software is governed by a license. See
# LICENSE.txt for the terms of this license.
#
#####################################################################
__version__ = "$Revision: 1313 $"[11:-2]

import smtplib
import os
import sys
import time
import rfc822
import imp

FATAL_ERROR_CODES = ('500', '501', '502', '503', '504', '550', '551', '553')
MaildropError = 'Maildrop Error'

def usage():
    print 'Usage: maildrop.py /path/to/config.py'


def mainloop():                   
    while 1:
        # Are there any files in the spool directory?
        to_be_sent = []
        all_files = []

        for spool in MAILDROP_SPOOLS:
            all_files.extend([os.path.join(spool, x) for x in os.listdir(spool)])

        # Remove lock files
        clean_files = [x for x in all_files 
                         if not x.endswith('.lck')]

        # Remove files that were locked by the previously removed lock files
        clean_files = [x for x in clean_files 
                         if not '%s.lck' % x in all_files]

        # Remove directories
        to_be_sent = [x for x in clean_files if not os.path.isdir(x)]
        
        if len(to_be_sent) > 0:
            # Open the log file
            time_stamp = time.strftime('%Y/%m/%d %H:%M:%S')
            log_file = open(MAILDROP_LOG_FILE, 'a')
            msg = '\n### Started at %s...' % time_stamp
            log_file.write(msg)
            if DEBUG: print msg
    
            while len(to_be_sent) > 0:
                if (MAILDROP_BATCH == 0) or (MAILDROP_BATCH > len(to_be_sent)):
                    batch = len(to_be_sent)
                else:
                    batch = MAILDROP_BATCH
                    
                # Send mail
                try:
                    smtp_server = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
                    #smtp_server.set_debuglevel(1)
                    smtp_server.ehlo()
                except smtplib.SMTPConnectError:
                    # SMTP server did not respond. Log it and stop processing.
                    time_stamp = time.strftime('%Y/%m/%d %H:%M:%S')
                    err_msg = '!!!!! Connection error at %s' % time_stamp
                    finish_msg = '### Finished at %s' % time_stamp
                    log_file.write(err_msg)
                    if DEBUG: print err_msg
                    log_file.write(finish_msg)
                    if DEBUG: print finish_msg
                    log_file.close()
                    break
    
                if MAILDROP_TLS > 0:
                    if (MAILDROP_TLS > 1 and 
                         not smtp_server.has_extn('starttls')):
                        # Problem: TLS is required but the server does not 
                        # offer it We stop processing here.
                        time_stamp = time.strftime('%Y/%m/%d %H:%M:%S')
                        err_msg = '!!!!! TLS unavailable at %s' % time_stamp
                        finish_msg = '### Finished at %s' % time_stamp
                        log_file.write(err_msg)
                        if DEBUG: print err_msg
                        log_file.write(finish_msg)
                        if DEBUG: print finish_msg
                        log_file.close()
                        break
    
                    smtp_server.starttls()

                    # We have to say Hello again after starting TLS
                    smtp_server.ehlo()
    
                if MAILDROP_LOGIN != '' and MAILDROP_PASSWORD != '':
                    # Login is required to send mail
                    if not smtp_server.has_extn('auth'):
                        # The server does not offer authentication but we want it
                        # We stop processing here.
                        time_stamp = time.strftime('%Y/%m/%d %H:%M:%S')
                        err_msg = '!!!!! Authentication unavailable at %s' % time_stamp
                        finish_msg = '### Finished at %s' % time_stamp
                        log_file.write(err_msg)
                        if DEBUG: print err_msg
                        log_file.write(finish_msg)
                        if DEBUG: print finish_msg
                        log_file.close()
                        break
    
                    try:
                        smtp_server.login(MAILDROP_LOGIN, MAILDROP_PASSWORD)
                    except smtplib.SMTPAuthenticationError:
                        # Authentication with the given credentials fails.
                        # We stop processing here.
                        time_stamp = time.strftime('%Y/%m/%d %H:%M:%S')
                        err_msg = '!!!!! Authentication failed at %s' % time_stamp
                        finish_msg = '### Finished at %s' % time_stamp
                        log_file.write(err_msg)
                        if DEBUG: print err_msg
                        log_file.write(finish_msg)
                        if DEBUG: print finish_msg
                        log_file.close()
                        break
    
                for file_path in to_be_sent[0:batch]:
                    mail_dict = read_mail(file_path)
                    if not mail_dict: 
                        continue

                    # Create mail and send it off
                    h_from = mail_dict.get('From')
                    h_to = mail_dict.get('To')
                    h_to_list = []
                    for item in rfc822.AddressList(h_to):
                        h_to_list.append(item[1])
                    h_body = mail_dict.get('body')
    
                    try:
                        smtp_server.sendmail(h_from, h_to_list, h_body)
                        stat = 'OK'
                        os.remove(file_path)
                    except smtplib.SMTPRecipientsRefused, e:
                        for (addr, error) in e.recipients.items():
                             stat = 'FATAL: ', str(e) 
                             if str(error[0]) in FATAL_ERROR_CODES:
                                 os.remove(file_path)
                                 break
                    except smtplib.SMTPException, e:
                        stat = 'BAD: ', str(e)
    
                    mail_msg = '\n%s\t %s' % (stat, h_to)
                    log_file.write(mail_msg)
                    if DEBUG: print mail_msg
                    log_file.flush()
                    if WAIT_INTERVAL:
                        time.sleep(WAIT_INTERVAL)
    
                to_be_sent = to_be_sent[batch:]
                smtp_server.quit()
    
            time_stamp = time.strftime('%Y/%m/%d %H:%M:%S')
            finish_msg = '\n### Finished at %s\n' % time_stamp
            log_file.write(finish_msg)
            if DEBUG: print finish_msg
            log_file.close()
            
        time.sleep(MAILDROP_INTERVAL)


def read_mail(file_path):
    """ Reads in a mail from a file, returns a dictionary with keys
    for headers and body 
    """    
    # Read in file
    file_handle = open(file_path, 'r')
    file_contents = file_handle.read()
    file_handle.close()
    
    # Is this a real mail turd?
    if not file_contents.startswith('##To:'):
        return
    
    # Parse and handle content (mail it out)
    mail_dict = {}
    file_lines = file_contents.split('\n')
    
    for i in range(len(file_lines)):
        if file_lines[i].startswith('##'):
            header_line = file_lines[i][2:]
            header_key, header_val = header_line.split(':', 1)
            mail_dict[header_key] = header_val
        else:
            mail_dict['body'] = '\n'.join(file_lines[i:])
            break

    return mail_dict
    

def write_pid(pidfile_path, pid):
    """ Write the daemon pid to our pid file """
    pid_file = open(pidfile_path, 'w')
    pid_file.write(str(pid))
    pid_file.close()


if __name__ == "__main__":
    try:
        if len(sys.argv) < 2:
            usage()
            sys.exit(1)
    
        # IT IS EXPECTED THAT THE MODULE NAME IS 'config'
        c_dir, c_file = os.path.split(sys.argv[1])
        c_module = os.path.splitext(c_file)[0]
    
        if c_module != 'config':
            raise MaildropError, 'Invalid configuration file "%s"' % o_val
    
        sys.path.append(c_dir)
    
        try:
            handle, path, description = imp.find_module(c_module)
            module = imp.load_module(c_module, handle, path, description)
        except ImportError:
            raise MaildropError, 'Cannot load config from "%s"' % c_module
    
        from config import MAILDROP_HOME
        from config import MAILDROP_INTERVAL
        from config import MAILDROP_BATCH
        from config import MAILDROP_TLS
        from config import MAILDROP_LOGIN
        from config import MAILDROP_PASSWORD
        from config import SMTP_HOST
        from config import SMTP_PORT
        from config import DEBUG
        from config import WAIT_INTERVAL

        try:
            from config import MAILDROP_SPOOL
            MAILDROP_SPOOLS = [x.strip() for x in MAILDROP_SPOOL.split(';')]
        except ImportError:
            MAILDROP_SPOOLS = [os.path.join(MAILDROP_HOME, 'spool')]
        try:
            from config import MAILDROP_VAR
        except ImportError:
            MAILDROP_VAR = os.path.join(MAILDROP_HOME, 'var')
        try:
            from config import MAILDROP_LOG_FILE
        except ImportError:
            MAILDROP_LOG_FILE = os.path.join(MAILDROP_VAR, 'maildrop.log')
        try:
            from config import MAILDROP_PID_FILE
        except ImportError:
            MAILDROP_PID_FILE = os.path.join(MAILDROP_VAR, 'maildrop.pid')

        for spool in MAILDROP_SPOOLS:
            if not os.path.isdir(spool):
                os.makedirs(spool)
    
        if not os.path.isdir(MAILDROP_VAR):
            os.makedirs(MAILDROP_VAR)
    
        try:
            mail_server = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
    
            if MAILDROP_TLS > 1:
                mail_server.starttls()
    
            if MAILDROP_LOGIN != '' and MAILDROP_PASSWORD != '':
                mail_server.login(MAILDROP_LOGIN, MAILDROP_PASSWORD)
    
            mail_server.quit()
        except:
            if DEBUG: import traceback; traceback.print_exc()
            msg = 'Invalid SMTP server "%s:%d"' % (SMTP_HOST, SMTP_PORT)
            raise MaildropError, msg

    except SystemExit: 
        sys.exit(0)
    except:
        usage()
        print 
        print 'An error occured, aborting...'
        print "%s: %s" % (sys.exc_type, sys.exc_value)
        print
        sys.exit(1)
    

    if not DEBUG:
        # Do the Unix double-fork magic; see Stevens's book "Advanced
        # Programming in the UNIX Environment" (Addison-Wesley) for details
        # In DEBUG mode we do not fork/detach from the terminal!
        try:
            pid = os.fork()
            if pid > 0:
                # Exit first parent
                sys.exit(0)
        except OSError, e:
            print >>sys.stderr, "fork #1 failed: %d (%s)" % (
                e.errno, e.strerror)
            sys.exit(1)

        # Decouple from parent environment
        os.chdir("/")
        os.setsid()
        os.umask(0)

        # Do second fork
        try:
            pid = os.fork()
            if pid > 0:
                # Exit from second parent; print eventual PID before exiting
                if DEBUG:
                    print "maildrop daemon PID %d" % pid
                write_pid(MAILDROP_PID_FILE, pid)
                sys.exit(0)
        except OSError, e:
            print >>sys.stderr, "fork #2 failed: %d (%s)" % (
                e.errno, e.strerror)
            sys.exit(1)
    else:
        print '*****          Starting in DEBUG mode           *****'
        print '***** All log messages are shown on the console *****'
        sys.stdout.flush()

    # Start the daemon main loop
    mainloop()

