#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Benno MailArchiv
#
# Copyright 2018-2021 LWsystems GmbH
#
# http://www.lw-systems.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.
import sys
import os
import logging
import logging.handlers
import imaplib
import re
import fcntl
import argparse

from Benno.IMAP.Config import Config
from Benno.IMAP import Mailbox, ConnectionError
from Benno.IMAP.User import IMAPUser
from Benno.RSAkey import RSAkey

versionstring='benno-imapsync version 3.0.0'


class ImportLock:
    def __init__(self,inboxdir,imapuser,container):
        self.initialized = False
        self.lockfile = inboxdir + '/.' + ' '.join(container) +':' + imapuser+'.fetching'
        # check lock
        self.fp = open(self.lockfile, 'a')
        fcntl.lockf(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
        self.fp.close()
        if os.path.isfile(self.lockfile):
            os.unlink(self.lockfile)
        # lock
        self.fp = open(self.lockfile, 'w')
        self.fp.write(str(os.getpid()))
        self.fp.write("\n")
        self.fp.flush()
        fcntl.lockf(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
        self.initialized = True
        

    def unlock(self):
        if not self.initialized:
            return
        fcntl.lockf(self.fp, fcntl.LOCK_UN)
        # os.close(self.fp)
        if os.path.isfile(self.lockfile):
            os.unlink(self.lockfile)


    def errormsg(self,msg):
        self.fp = open(self.lockfile, 'a')
        self.fp.write(msg+"\n")
        self.fp.close()


################################################################################

########################################
# read RSA key to decrypt user passwords
def readKey(config):
    keyfile=config.get('DEFAULT','keyfile')
    if keyfile=='':
        logging.debug('Read key from STDIN')
        keylines=sys.stdin.readlines()
        privkey = ''.join(str(line) for line in keylines)
        if not privkey:
            sys.exit('No key from STDIN')
    else:
        logging.debug('Read key from file: %s' % keyfile)
        try:
            with open(keyfile,'r') as kf:
                privkey = kf.read()
        except Exception as e:
            sys.exit('Cannot read %s: %s' % (keyfile,e))
    return privkey


### MAIN ###
if __name__ == "__main__":
    configfile = os.getenv('CONFIGFILE','/etc/benno-imap/imapsync.conf')
    parser = argparse.ArgumentParser(description='load new emails via IMAP')
    parser.add_argument('-c', '--config', help='config file (/etc/benno-imap/imapsync.conf)', default='%s' % configfile)
    parser.add_argument('-f', '--logfile', help='logfile path (/var/log/benno/imapsync.log')
    parser.add_argument('-L', '--loglevel', default='ERROR', help='loglevel (default=ERROR, values: CRITICAL, ERROR, WARNING, INFO, DEBUG')
    parser.add_argument('-v', '--version', action='store_true')
    args = parser.parse_args()

    if args.version:
        print("%s" % versionstring)
        sys.exit(0)

    try:
        if not os.path.isfile(args.config):
            sys.stderr.write('Cannot open configfile %s\n\n' % args.config)
            parser.print_usage()
            sys.exit(1)
    except NameError as e:
        parser.print_usage()
        sys.exit(1)

    try:
        config = Config(args.config)
    except (FileNotFoundError, PermissionError):
        print('Cannot read configfile %s' % args.config)
        parser.print_usage()
        sys.exit(1)
    config.add_section('LOG')
    config.set('LOG','loglevel',args.loglevel.upper())

    logger=logging.getLogger()
    logger.setLevel(config.get('LOG','loglevel'))
    if args.logfile:
        config.set('LOG','logfile',args.logfile)
        handler=logging.handlers.TimedRotatingFileHandler(config.get('LOG','logfile'),when='midnight')
    else:
        handler=logging.StreamHandler()
    formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
    handler.setFormatter((formatter))
    logger.addHandler(handler)

    logger.info("Read file \"%s\"", args.config)

    imaplib._MAXLINE=int(config.get('DEFAULT','imap_maxlinelength'))
    logging.info("*** Start email import %s." % os.getpid())

    privkey=readKey(config)

    for account in IMAPUser.List(config):
        imappass = ''       # clear imap plaintext password
        imapuser  = account.imapuser
        imaphost  = account.imaphost
        inboxdir = config.get(imapuser, 'inboxdir')
        containerlist = account.archive
        containers = ','.join(containerlist)
        if not account.archive:
            logging.warning('User %s on %s has no container', imapuser, imaphost)
            continue

        if (account.status & 1) == 0:
            logging.info('%s|User %s on %s disabled.', containers, imapuser, imaphost)
            continue

        ssl = config.getboolean(account.id, 'ssl')
        extraHeader=config.get(account.id,'extraheader')
        skipfolders=config.get(account.id,'skipfolders')
        skiplist=re.split(';\s+|;\s?',skipfolders)

        try:
            imappass = RSAkey.decrypt(privkey,account.imappassword).decode()
        except ValueError as e:
            if (account.imappassword):
                logging.warning('%s|Skip import for %s on %s: password decryption failed', containers,imapuser,imaphost)
                logging.debug('  %s' % e)
            else:
                logging.warning('%s|Skip import for %s on %s: Password not set', containers,imapuser,imaphost)
            continue

        try:
            lock=ImportLock(inboxdir,imapuser,containers)
        except Exception as e:
            if e.errno == 2:
                logging.error('%s|Directory does not exist: %s/%s', containers,imapuser,inboxdir)
            else:
                logging.warning('%s|Skip import for %s: already running', containers,imapuser)
            continue

        logging.info("%s|Import %s from %s", containers,imapuser,imaphost)

        try:
            mailbox=Mailbox(logger, imaphost, imapuser, ssl)
            mailbox.login(imappass)
        except Exception as e:
            logging.error('%s|%s cannot login on %s: %s' % (containers, imapuser, imaphost, e))
            lock.errormsg('%s|%s cannot login on %s: %s' % (containers, imapuser, imaphost, e))
            continue

        try:
            folderList = mailbox.listFolder()
        except imaplib.IMAP4.error as e:
            # Catch o365 bug(?)
            logging.error('%s|Error list folders for %s on %s: %s' % (containers, imapuser, imaphost, e))
            continue

        for folder in folderList:
            if folder in skiplist:
                logging.info('%s|Skip folder %s/%s on %s', containers, imapuser, folder, imaphost)
                continue
            logging.debug('%s|Load folder %s/%s from %s', containers, imapuser, folder, imaphost)
            try:
                # status of current connection
                uidvalidity,uidnext=mailbox.getStatus(folder)
            except ConnectionError as e:
                logging.warning('%s|Cannot access folder: %s/%s on %s: %s' % (containers, imapuser, folder, imaphost, e))
                continue
            except Exception as e:
                logging.error('%s|Status error on: %s/%s on %s:%s' % (containers, imapuser, folder, imaphost, e))
                try:
                    mailbox.login(imappass)
                except Exception as e:
                    logging.error('"%s|%s" cannot re-login on %s: %s' % (containers,imapuser,imaphost,e))
                    continue
            try:
                fetchnext = account.imapstatus.getNext(folder)
            except KeyError:
                fetchnext=1
            if not account.imapstatus.checkValidity(folder,uidvalidity):
                logging.warning('%s|UIDVALIDITY of folder %s folder has changed to %s',containers,folder,uidvalidity)
                account.imapstatus.setValidity(folder,uidvalidity)
                fetchnext=1
            if account.imapstatus.hasNewMails(folder,uidnext):
                # new mails in folder
                logging.debug('%s|Load %s to %s from folder %s/%s on %s',containers,fetchnext,uidnext,imapuser,folder,imaphost)
                try:
                    # set X-REAL-RCPTTO header
                    if config.get(imapuser,'envelope'):
                        envelopeTo = account.imapuser
                    else:
                        envelopeTo = False

                    mailbox.fetchMails(inboxdir, folder, fetchnext, containerlist, envelopeTo, extraHeader)
                    account.imapstatus.setNext(folder, mailbox.cur_uid)
                except Exception as e:
                    logging.error('%s|Fail to fetch messages from %s/%s on %s: %s' % (containers,imapuser,folder,imaphost,e))
                    logging.debug('%s|%s' % (containers, e), exc_info=True)
                    print('IMAP ERROR: Cannot load message from %s/%s on %s' % (imapuser,folder,imaphost), file=sys.stderr)
                except ConnectionError as e:
                    logging.error('%s|Cannot access folder %s/%s on %s: %s' % (containers,imapuser,folder,imaphost,e))
            else:
                logging.debug('%s|No new emails in folder %s/%s on %s', containers,imapuser,folder,imaphost)
        mailbox.logout()
        account.updateIMAPStatus()
        lock.unlock()
    logging.info('*** Email import %s done.', os.getpid())

