#!/usr/bin/env  python
"""
This script read all entries from a diary file and send the entries of
a week per mail to a receiver.

Please read the class documentation of L{SendWeekly} for more informations
about the format of the entries.

B{CVS Status Informations}::
  $Id: weekly.py,v 1.12 2006-10-06 16:36:50 carsten Exp $

B{Revision History}::
  $Log: weekly.py,v $
  Revision 1.12  2006-10-06 16:36:50  carsten
  - try to use local user as sender if no sender is given
  - version 0.5

  Revision 1.11  2005/08/03 19:28:25  carsten
  - version 0.4a
  - fix: wrong linefeed at one line in each email

  Revision 1.10  2005/04/13 17:23:08  carsten
  - version 0.4
  - add wildcards %B (first date in the week) and %E (last date in the week)
    to use in the subject

  Revision 1.9  2005/03/27 08:06:28  carsten
  - version 0.3
  - enhance documentation a little bit
  - add cc and bcc to commandline and mailheader
  - fix: mails uses CRLF as linebreak

  Revision 1.8  2005/02/09 19:53:04  carsten
  - replace nearly all "None"s with corresponding boolean expression

  Revision 1.7  2005/02/03 19:02:06  carsten
  - fix bug in class initialization

  Revision 1.6  2005/02/01 18:16:00  carsten
  - version 0.2c
  - add documentation for class variables
  - replace basestring by str to get better compatibility

@author:  Carsten Grohmann <mail (at) carstengrohmann (dot) de>
@license: GPL version 2
@version: 0.5
"""
__revision__  = "$Revision: 1.12 $"
__version__   = "0.5"
__date__      = "$Date: 2006-10-06 16:36:50 $"
__cvsid__     = "$Id: weekly.py,v 1.12 2006-10-06 16:36:50 carsten Exp $"
__author__    = "Carsten Grohmann <mail (at) carstengrohmann dot de>"
__copyright__ = "Carsten Grohmann (c) 2004-2006"

import getopt
import getpass
import re
import smtplib
import sys
import time

def checkiso(iso):
    """
    check the correctness of the given iso formated date
    return: True or False
    """
    if not isinstance(iso, str):
        try:
            iso="%d" % iso
        except:
            return False
    
    # check length
    if len(iso) != 8:
        return False
    
    _day   = int(iso[6:8])
    _month = int(iso[4:6])
    
    if ( (_day < 1) or (_day>31) ):
        return False
        
    if ( (_month < 1) or (_month>12) ):
        return False

    return True

def sec2iso(seconds=""):
    """
    converts the time in seconds since 1970 to the iso format YYYYMMDD
    @param seconds: time in seconds since 1970
    @return: iso format of the timestamp
    """
    # check input data
    if not seconds:
        seconds=time.time()
    
    _year  = int(time.localtime(seconds)[0])
    _month = int(time.localtime(seconds)[1])
    _day   = int(time.localtime(seconds)[2])
    return "%04d%02d%02d" % (_year, _month, _day)
    
def iso2sec(iso=""):
    """
    convert a iso formated date in seconds since 1970
    @param iso: date in iso format YYYYMMDD
    @return: date in seconds since 1970
    """
    # check input data
    if not iso:
        raise ValueError, "ERROR: No value given!"
    
    _day   = int(iso[6:8])
    _month = int(iso[4:6])
    _year  = int(iso[:4])
    return time.mktime((_year, _month, _day, 1, 1, 1, 0, 0, -1))

def iso2date(iso):
    """
    convert a date from the ISO-Format YYYYMMDD to the format DD.MM.YYYY
    @param iso:  date in ISO format
    @return:     date in european format DD.MM.YYYY
    """
    _day   = int(iso[6:8])
    _month = int(iso[4:6])
    _year  = int(iso[:4])
    return ( "%02d.%02d.%04d" % (_day, _month, _year) )

class SendWeekly:
    """
    Read all entries of a week from a diary file and send the entries as a
    mail to a receiver.
    
    Format of an entry::
        Every entry starts with the date on a single line. This line has a
        leading "-- " and a european styled date "DD.MM.YYYY".
        
        The text of the entry in the next lines can be free formated.
    
    Example diary entries::
      -- 30.12.2004
      Programming
      - today I finished a small script to make the ldap query 
        
      -- 31.12.2004
      on Call
      - fix a small cpu load issue
    
    @ivar mailbcopy:    BCC address of the mail sended by this script
    @ivar mailcopy:     CC address of the mail sended by this script
    @ivar diaryfile:    file with the diary entries
    @ivar mailsender:   Sender address of the mail sended by this script
    @ivar mailreceiver: Receiver address of the mail sended by this script
    @ivar mailserver:   SMTP server to send this mail
    @ivar mailsubject:  Subject of the weekly mail
    @ivar startingweek: First weekday of a week; valid values are sunday or
                        monday
    @ivar dayinweek:    One day in the week to process
    """
    # dictionary with the dates of all entries read from the diary file
    __dates={}
    
    # seconds per day
    __secondsperday = 60*60*24
    
    # regular expession to match starting line of a diary entry
    __re_dateline = re.compile("^-- ")
    
    # file with the diary entries
    diaryfile="./today.log"
    
    # list with the content of the diary file
    __message=[]
    
    # for smtp formated message created from the data of __message
    __SMTPMessage=[]

    # BCC address of the mail sended by this script
    mailbcopy=""
    
    # CC address of the mail sended by this script
    mailcopy=""
    
    # Sender address of the mail sended by this script
    mailsender=""
    
    # Receiver address of the mail sended by this script
    mailreceiver=""
    
    # SMTP server to send this mail
    mailserver="localhost"
    
    # Subject of the weekly mail
    mailsubject="weekly %B - %E"
    
    # First weekday of a week
    startingweek="monday"
    
    # One day in the week to process
    dayinweek=""
    
    # First entry of a week in the weekly file
    __firstday=""

    # Last entry of a week in the weekly file    
    __lastday=""
    
    def __init__(self, bcopy="", copy="", file="", sender="", receiver="",
        subject="", server="", dayinweek="", startingweek=""):
        """
        Initialize the class and set the important values
        @param bcopy:     bcc address of the email
        @param copy:      cc address of the email
        @param file:      diary file
        @param sender:    sender address of the email
        @param receiver:  receiver address of the email
        @param subject:   subject of the email
        @param server:    delivery smtp server
        @param dayinweek: date of the to processed week
        @param startingweek: first day of week (sunday or monday are only valid)
        """
        # initialize values
        self.mailsender=""
        self.mailreceiver=""
        self.dayinweek=""
       
        if file:      self.diaryfile    = file
        if bcopy:     self.mailbcopy    = bcopy;
        if copy:      self.mailcopy     = copy;
        if sender:    self.mailsender   = sender
        if receiver:  self.mailreceiver = receiver
        if subject:   self.mailsubject  = subject
        if server:    self.mailserver   = server
        if dayinweek: self.dayinweek    = dayinweek
        if ( (startingweek) and 
             ( (startingweek.lower() == 'monday') or 
               (startingweek.lower() == 'sunday') )
           ):
            self.startingweek=startingweek.lower()
    
    def createweekly(self):
        """
        create the weekly message from the diary entries stored in the
        dictionary
        @see: self.__dates and self.__message
        """
        self.__message=[]
        datelist=self.__dates.keys()
        datelist.sort()
        for _date in datelist:
            if (( _date >= self.__firstday) and (_date <= self.__lastday)):
                self.__message.extend(self.__dates[_date])
            elif _date > self.__lastday:
                break
        
    def createSMTPMessage(self):
        """
        create a full formated SMTP message with the header lines "From:",
        "To:", "Subject:" and optionally "cc:" and "bcc:"
        """
        if ( (not self.mailsender) or (not self.mailreceiver)):
             raise ValueError, \
                "Sender address or receiver address of the mail not set"
        _dummy=[]  
        _dummy.append('From: %s'    % self.mailsender)
        _dummy.append('To: %s'      % self.mailreceiver)
        if self.mailbcopy: _dummy.append('bcc: %s' % self.mailbcopy)
        if self.mailcopy:  _dummy.append('cc: %s'  % self.mailcopy)
        _dummy.append('Subject: %s' % self.createSubject(self.mailsubject))
        _dummy.append('\r\n')
        _dummy.extend(self.__message)
        
        # remove all whitespaces at end of each line
        _dummy = map(lambda line: line.rstrip(), _dummy)
        
        # join all lines to a single string
        self.__SMTPMessage="\r\n".join(_dummy)

    def createSubject(self, subject=""):
        """
        Replace the wildcards in the subject string with current values
        
        %B replaced by the date of the first entry
        
        %E replaced by the date of the last entry
        
        @note: The format of the dates is always YYYYMMDD.
        @param subject: Subject with wildcards
        @return: Subject with current values instead of wildcards
        """
        if (not subject): raise ValueError, "Subject not set."
        
        subject=subject.replace("%B", self.__firstday)
        subject=subject.replace("%E", self.__lastday)
        
        return subject
        
    def processAll(self):
        """
        Process all steps to send a email with the diary entries
        """
        self.readdiary()
        self.searchdays()
        self.createweekly()
        self.createSMTPMessage()
        self.sendSMTPMessage()
        
    def readdiary(self, filename=""):
        """
        read the diary file into the internal structures
        @param filename: filename of the diary file
        """
        
        if filename:
            self.diaryfile=filename
            
        # read diary log
        try:
            fd = open(self.diaryfile, 'r')
        except IOError:
            print "ERROR: Can't open %s." % self.diaryfile
            sys.exit(1)

        # store all diary entries in a dictionary
        _date="not_set"
        for line in fd:
            
            # remove linefeeds
            line=line.rstrip()
            if self.__re_dateline.match(line):
                _date=self.linedate2iso(line)
            if _date != "not_set":
                if self.__dates.has_key(_date):
                    self.__dates[_date].append(line)
                else:
                    self.__dates[_date]=[]
                    self.__dates[_date].append(line)
        # close file
        try:
            fd.close()
        except:
            print "ERROR: Can't close %s" % self.diaryfile
            sys.exit(1)
            
    def searchdays(self, date=""):
        """
        search the first and the last day of the week with the "date" inside
        @param date: date of one day in the searched week in iso format
        """
        if date:
            self.dayinweek=date
            
        # search first entry of the current week
        self.__firstday=self.searchfirstday(self.dayinweek);
        if (not self.__firstday):
            print "Can't find a diary entry of the specified week in \"%s\"." \
                % self.diaryfile
            sys.exit(1)
        
        # search last entry of the current week
        self.__lastday=self.searchlastday(self.dayinweek)
        if (not self.__lastday):
            raise ValueError, "Internat error in this script"

    def searchfirstday(self, date):
        """
        search the first entry of this week
        @param date: date of one day in the current week in YYYYMMDD
        @return:     date of the first entry of a week in iso format or None if
                     it failed
        """
        # convert date to seconds since 1970
        seconds=iso2sec(date)
        
        # get current weekday
        curweekday=time.localtime(seconds)[6]
        
        # get date of the first weekday in this week
        if self.startingweek.lower() == 'sunday':
            startdate = seconds - (curweekday + 1 ) * self.__secondsperday
        else:
            startdate = seconds - (curweekday ) * self.__secondsperday
        
        # search first date of this week in the date list
        for i in range(7):
            _date=sec2iso(startdate)
            if self.__dates.has_key(_date):
                return (_date)
                
            # increment (next) weekday
            startdate+=self.__secondsperday
            
        return(None)
  
    def searchlastday(self, date):
        """
        search the last entry of this week
        @param date: date of one day in the current week in YYYYMMDD
        @return:     date of the last entry of a week in iso format
        """
        # convert date to seconds since 1970
        seconds=iso2sec(date)
        
        # get current weekday
        curweekday=time.localtime(seconds)[6]
        
        # get date of the first weekday in this week
        if self.startingweek.lower() == 'sunday':
            startdate = seconds + (5 - curweekday) * self.__secondsperday
        else:
            startdate = seconds + (6 - curweekday) * self.__secondsperday
        
        # search first date of this week in the date list
        for i in range(7):
            _date=sec2iso(startdate)
            if self.__dates.has_key(_date):
                return (_date)
                
            # increment (next) weekday
            startdate-=self.__secondsperday
            
        return(None)
    
    def sendSMTPMessage(self, server=""):
        """
        send the preformated smtp message
        @param server:   smtp server
        @see: L{createSMTPMessage}
        """
        if server:
            self.mailserver=server
        
        if not self.__SMTPMessage:
            self.createSMTPMessage
        
        try:
            smtpserver=smtplib.SMTP(self.mailserver)
            smtpserver.sendmail(self.mailsender, self.mailreceiver,
                self.__SMTPMessage)
            smtpserver.quit()
        except smtplib.SMTPRecipientsRefused:
            print "ERROR: during sending mail to %s" % self.mailreceiver
            print "       All receivers rejected!"
            sys.exit(1)
        except smtplib.SMTPSenderRefused:
            print "ERROR: during sending mail from %s" % self.mailsender
            print "       Sender address not accept."
            sys.exit(1)
        except:
            print "ERROR: during sending mail to %s" % self.mailreceiver
            sys.exit(1)

    def linedate2iso(self, dateline):
        """
        convert a line starting with "-- DD.MM.YYYY" to the ISO-Format YYYYMMDD
        @return: date in iso format YYYYMMDD
        """
        # ignore the first part, split the other part on the dot
        # and concat it in reverse order
        _date  = int(dateline.split()[1].split('.')[0])
        _month = int(dateline.split()[1].split('.')[1])
        _year  = int(dateline.split()[1].split('.')[2])
        return( ("%04d%02d%02d" % (_year, _month, _date)) )
    
def usage(rval=0):
    """
    Print usage message and exit
    @param rval: return value of this script
    """
    print """%s Version %s
Usage: %s [Options]...

This script read all entries from a diary file and send the entries of the
specified week per email to a receiver.
If not date is specified with --thisweek, --lastweek or --date, the current
week is assumed.
It is only necessary to set either the sender address or the receiver address
of the email. The second address will be taken from the given one.

Options:
  --bcopy <address>     send a blind copy (bcc) to the address 
  --copy <address>      send a copy (cc) to the address
  --file <filename>     file with the diary entries
  --help                show this message
  --sender <address>    sender address of the email
  --receiver <address>  receiver address of the email
  --server <server>     delivery server of the email
  --subject <subject>   subject of the email
  --monday              first day of the week is monday
  --sunday              first day of the week is sunday
  --version             print some informations like version number, author,
                        licence etc about this script
  --thisweek            process the diary entries of this week
  --lastweek            process the diary entries of last week
  --date YYYYMMDD process the diary entries of the week with YYYYMMDD inside

Wildcards in the subject
  %%B replace by the date of the first entry in the week (YYYYMMDD format)
  %%E replace by the date of the last entry in the week (YYYYMMDD format)
""" % ("weekly.py", __version__, "weekly.py")
    sys.exit(rval)
    
def main():
    """
    main loop - get and check the command line options, creates and runs the
    class SendWeekly
    """
    # dictionary with the default configuration settings
    config={
        'bcopy'        : "",             # email bcc address
        'copy'         : "",             # email cc address
        'date'         : "",             # date inside the week to process
        'sender'       : "",             # sender email address
        'receiver'     : "",             # receiver email address
        'subject'      : 'weekly %B - %E',  # subject of the email
        'server'       : 'localhost',    # smtp server
        'file'         : './today.log',  # diary files
        'startingweek' : 'monday'        # weeks starts with this day
    }
    
    # get options
    if len(sys.argv) == 1:
        usage(1)
    
    try:
        optlist=getopt.getopt(sys.argv[1:], "", ["bcopy=", "copy=", "date=",
            "file=", "help", "lastweek", "monday", "receiver=", "sender=",
            "server=", "sunday","subject=", "thisweek",  "version"])[0]
    except getopt.GetoptError, msg:
        print "ERROR: Wrong program option: %s" %msg
        usage(1)
    except:
        print "ERROR: Unknown exception raised."
        print sys.exc_info()
        sys.exit(1)
    
    # process options
    for option,argument in optlist:
        if option == "--version":
            print "%s Version %s" % ("weekly.py", __version__)
            print "Author:      %s" % __author__
            print "Copyright:   %s" % __copyright__
            print "Homepage:    http://www.carstengrohmann.de"
            print "Last Changes %s" % __date__
            print "Licence:     GPL Version 2.0 or higher"
            print "Version      %s" % __version__
            sys.exit(1)
        elif option == "--bcopy":     config['bcopy']=argument
        elif option == "--copy":      config['copy']=argument
        elif option == "--file":      config['file']=argument
        elif option == "--help":      usage()
        elif option == "--sender":    config['sender']=argument
        elif option == "--server":    config['server']=argument
        elif option == "--subject":   config['subject']=argument
        elif option == "--receiver":  config['receiver']=argument
        elif option == "--thisweek":  config['date']=sec2iso(time.time())
        elif option == "--lastweek":  config['date']=sec2iso(
	                                              time.time()-(7*24*60*60))
        elif option == "--monday":    config['startingweek']='monday'
        elif option == "--sunday":    config['startingweek']='sunday'
            
    # check email addresses
    if ( (not config['sender']) and (not config['receiver']) ):
        print "ERROR: Sender address nor receiver address for the mail is set!"
        print "       Please use --sender or --receiver to set the correct"
        print "       values."
        sys.exit(1)
    
    # complete email addresses if one is set
    if ( (not config['sender']) and (config['receiver']) ):
        # try to get the username from the OS or use the receiver address
        current_user = None
        try:
	     current_user = getpass.getuser()
	except:
	    current_user = config['receiver']
	
        config['sender'] = current_user

    elif ( (config['sender']) and (not config['receiver']) ):
        config['receiver'] = config['sender']
        
    # checking date settings
    if not config['date']:
        config['date']=sec2iso(time.time())
        
    # check correctness of the format
    if not checkiso(config['date']):
        print "ERROR: Wrong date format %s. Please use only YYYYMMDD." \
            % config['date']
        sys.exit(1)

    # create the class with the settings from the command line
    sw=SendWeekly(bcopy=config['bcopy'],
                  copy=config['copy'],
                  file=config['file'],
                  sender=config['sender'],
                  server=config['server'],
                  subject=config['subject'],
                  receiver=config['receiver'],
                  dayinweek=config['date'],
                  startingweek=config['startingweek'])
    
    # process all in one step
    sw.processAll()
    
    print "Send weekly successfully."
    
if __name__ == '__main__':
    main()
