#!/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: 1.10 $"[11:-2]

usage = """
Maildrop service startup file

Usage: maildrop.py [options]

Options:

  - d

    Debug mode: All output will be written to the terminal

  - h

    Maildrop home (Must be specified!)

  - s

    SMTP host to be used (Must be specified!)

  - p

    SMTP port to be used (defaults to 25)

  - i

    Polling interval in seconds
    
  - b

    Drop only x mails in one smtp-connection (defaults to 0 = all mails at once)
"""

import getopt, smtplib, os, sys, time, rfc822

DEBUG = 0
MAILDROP_INTERVAL = 120
SMTP_PORT = 25
MAILDROP_BATCH = 0
FATAL_ERROR_CODES = ('500', '501', '502', '503', '504', '550', '551', '553')
MaildropError = 'Maildrop Error'


try:
    if sys.version.split()[0] < '2.1':
        raise  MaildropError, 'Invalid python version %s' % sys.version.split[0]

    if len( sys.argv ) < 3:
        print usage

    opts, args = getopt.getopt( sys.argv[1:], 'h:i:s:d:b:p:' )

    for o_key, o_val in opts:
        if o_key == '-h':
            MAILDROP_HOME = o_val
            if not os.path.isdir( MAILDROP_HOME ):
                raise MaildropError, 'Invalid maildrop home "%s"' % o_val

            MAILDROP_SPOOL = os.path.join( MAILDROP_HOME, 'spool' )
            if not os.path.isdir( MAILDROP_SPOOL ):
                os.mkdir( MAILDROP_SPOOL )

            log_home = os.path.join( MAILDROP_HOME, 'var' )
            if not os.path.isdir( log_home ):
                os.mkdir( log_home )
            LOG_FILE = os.path.join( log_home, 'maildrop.log' )

        if o_key == '-s':
            SMTP_HOST = o_val
            
        if o_key == '-p':
            try:
                SMTP_PORT = int(o_val)
            except ValueError:
                SMTP_PORT = 25
                
        if o_key == '-i':
            try:
                MAILDROP_INTERVAL = int( o_val )
            except:
                msg = 'Invalid Maildrop interval "%s"' % str( o_val )
                raise MaildropError, msg

        if o_key == '-b':
            try:
                MAILDROP_BATCH = int(o_val)
            except ValueError:
                MAILDROP_BATCH = 0

        if o_key == '-d':
            if o_val not in (0, '0', 'False'):
                DEBUG = 1

    try:
        mail_server = smtplib.SMTP( SMTP_HOST, SMTP_PORT )
        mail_server.quit()
    except:
        msg = 'Invalid SMTP server "%s:%d"' % (SMTP_HOST, SMTP_PORT)
        raise MaildropError, msg



except SystemExit: sys.exit( 0 )
except:
    print usage
    print
    print "%s: %s" % ( sys.exc_type, sys.exc_value )
    print
    sys.exit( 1 )

try: 
    ppid, pid = os.getppid(), os.getpid()
    if ppid == 1:
        ppid = pid
except: 
    pass # getpid not supported
else:
    pid_home = os.path.join( MAILDROP_HOME, 'var' )
    if not os.path.isdir( pid_home ):
        os.mkdir( pid_home )

    pidfile_path = os.path.join( pid_home, 'maildrop.pid' )
    pid_file = open( pidfile_path, 'w' )
    pid_file.write( '%s %s' % ( pid, ppid ) )
    pid_file.close()

if DEBUG:
    # PID still might not exist so it's inside a try
    try:
        print 'Maildrop started with PID %s' % str( pid )
    except:
        print 'Maildrop started with unknown PID'
                    
while 1:
    # Are there any files in the spool directory?
    to_be_sent = []
    all_files = os.listdir( MAILDROP_SPOOL )

    if len( all_files ) > 0:
        for file_name in all_files:
            f_name, f_ext = os.path.splitext( file_name )

            # Exclude lock files
            if f_ext == '.lck':
                continue

            # Exclude files that have locked files
            if f_name + '.lck' in all_files:
                continue

            # Exclude directories
            file_path = os.path.join( MAILDROP_SPOOL, file_name )
            if os.path.isdir( file_path ):
                continue

            # 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:' ):
                continue

            # Parse and handle content (mail it out)
            mail_dict = {}
            mail_dict['file_path'] = file_path
            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

            to_be_sent.append( mail_dict )

        if len( to_be_sent ) > 0:
            # Open the log file
            time_stamp = time.strftime( '%Y/%m/%d %H:%M:%S' )
            log_file = open( 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 )
                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

                for mail_dict in to_be_sent[0:batch]:
                    # 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( mail_dict.get( '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( mail_dict.get( '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()

                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 )
