#!/usr/bin/env python3
#
# load_message.py - takes a single email or mbox formatted
# file on stdin or in a file and reads it into the database.
#

import os
import sys

from optparse import OptionParser
from configparser import ConfigParser

import psycopg2

from lib.storage import ArchivesParserStorage
from lib.mbox import MailboxBreakupParser
from lib.exception import IgnorableException
from lib.log import log, opstatus
from lib.varnish import VarnishPurger


def log_failed_message(listid, srctype, src, msg, err):
    try:
        msgid = msg.msgid
    except Exception:
        msgid = "<unknown>"
    log.error("Failed to load message (msgid %s) from %s, spec %s: %s" % (msgid, srctype, src, err))

    # We also put the data in the db. This happens in the main transaction
    # so if the whole script dies, it goes away...
    conn.cursor().execute("INSERT INTO loaderrors (listid, msgid, srctype, src, err) VALUES (%(listid)s, %(msgid)s, %(srctype)s, %(src)s, %(err)s)", {
        'listid': listid,
        'msgid': msgid,
        'srctype': srctype,
        'src': src,
        'err': str(str(err), 'us-ascii', 'replace'),
    })


if __name__ == "__main__":
    optparser = OptionParser()
    optparser.add_option('-l', '--list', dest='list', help='Name of list to load message for')
    optparser.add_option('-d', '--directory', dest='directory', help='Load all messages in directory')
    optparser.add_option('-m', '--mbox', dest='mbox', help='Load all messages in mbox')
    optparser.add_option('-i', '--interactive', dest='interactive', action='store_true', help='Prompt after each message')
    optparser.add_option('-v', '--verbose', dest='verbose', action='store_true', help='Verbose output')
    optparser.add_option('--force-date', dest='force_date', help='Override date (used for dates that can\'t be parsed)')
    optparser.add_option('--filter-msgid', dest='filter_msgid', help='Only process message with given msgid')

    (opt, args) = optparser.parse_args()

    if (len(args)):
        print("No bare arguments accepted")
        optparser.print_usage()
        sys.exit(1)

    if not opt.list:
        print("List must be specified")
        optparser.print_usage()
        sys.exit(1)

    if opt.directory and opt.mbox:
        print("Can't specify both directory and mbox!")
        optparser.print_usage()
        sys.exit(1)

    if opt.force_date and (opt.directory or opt.mbox) and not opt.filter_msgid:
        print("Can't use force_date with directory or mbox - only individual messages")
        optparser.print_usage()
        sys.exit(1)

    if opt.filter_msgid and not (opt.directory or opt.mbox):
        print("filter_msgid makes no sense without directory or mbox!")
        optparser.print_usage()
        sys.exit(1)

    log.set(opt.verbose)

    cfg = ConfigParser()
    cfg.read('%s/archives.ini' % os.path.realpath(os.path.dirname(sys.argv[0])))
    try:
        connstr = cfg.get('db', 'connstr')
    except Exception:
        connstr = 'need_connstr'

    conn = psycopg2.connect(connstr)
    curs = conn.cursor()

    # Take an advisory lock to force serialization.
    # We could do this "properly" by reordering operations and using ON CONFLICT,
    # but concurrency is not that important and this is easier...
    try:
        curs.execute("SET statement_timeout='30s'")
        curs.execute("SELECT pg_advisory_xact_lock(8059944559669076)")
    except Exception as e:
        print(("Failed to wait on advisory lock: %s" % e))
        sys.exit(1)

    # Get the listid we're working on
    curs.execute("SELECT listid FROM lists WHERE listname=%(list)s", {
        'list': opt.list
    })
    r = curs.fetchall()
    if len(r) != 1:
        log.error("List %s not found" % opt.list)
        conn.close()
        sys.exit(1)
    listid = r[0][0]

    purges = set()

    if opt.directory:
        # Parse all files in directory
        for x in os.listdir(opt.directory):
            log.status("Parsing file %s" % x)
            with open(os.path.join(opt.directory, x)) as f:
                ap = ArchivesParserStorage()
                ap.parse(f)
                if opt.filter_msgid and not ap.is_msgid(opt.filter_msgid):
                    continue
                try:
                    ap.analyze(date_override=opt.force_date)
                except IgnorableException as e:
                    log_failed_message(listid, "directory", os.path.join(opt.directory, x), ap, e)
                    opstatus.failed += 1
                    continue
                ap.store(conn, listid)
                purges.update(ap.purges)
            if opt.interactive:
                print("Interactive mode, committing transaction")
                conn.commit()
                print("Proceed to next message with Enter, or input a period (.) to stop processing")
                x = input()
                if x == '.':
                    print("Ok, aborting!")
                    break
                print("---------------------------------")
    elif opt.mbox:
        if not os.path.isfile(opt.mbox):
            print("File %s does not exist" % opt.mbox)
            sys.exit(1)
        mboxparser = MailboxBreakupParser(opt.mbox)
        while not mboxparser.EOF:
            ap = ArchivesParserStorage()
            msg = next(mboxparser)
            if not msg:
                break
            ap.parse(msg)
            if opt.filter_msgid and not ap.is_msgid(opt.filter_msgid):
                continue
            try:
                ap.analyze(date_override=opt.force_date)
            except IgnorableException as e:
                log_failed_message(listid, "mbox", opt.mbox, ap, e)
                opstatus.failed += 1
                continue
            ap.store(conn, listid)
            purges.update(ap.purges)
        if mboxparser.returncode():
            log.error("Failed to parse mbox:")
            log.error(mboxparser.stderr_output())
            sys.exit(1)
    else:
        # Parse single message on stdin
        ap = ArchivesParserStorage()
        ap.parse(sys.stdin.buffer)
        try:
            ap.analyze(date_override=opt.force_date)
        except IgnorableException as e:
            log_failed_message(listid, "stdin", "", ap, e)
            conn.close()
            sys.exit(1)
        ap.store(conn, listid)
        purges.update(ap.purges)
        if opstatus.stored:
            log.log("Stored message with message-id %s" % ap.msgid)

    conn.commit()
    conn.close()
    opstatus.print_status()

    VarnishPurger(cfg).purge(purges)

[((27, 4, 27, 100), 'lib.log.log.error', 'log.error', ({(27, 14, 27, 99): "('Failed to load message (msgid %s) from %s, spec %s: %s' % (msgid, srctype,\n    src, err))"}, {}), "('Failed to load message (msgid %s) from %s, spec %s: %s' % (msgid,\n    srctype, src, err))", False, 'from lib.log import log, opstatus\n'), ((41, 16, 41, 30), 'optparse.OptionParser', 'OptionParser', ({}, {}), '()', False, 'from optparse import OptionParser\n'), ((77, 4, 77, 24), 'lib.log.log.set', 'log.set', ({(77, 12, 77, 23): 'opt.verbose'}, {}), '(opt.verbose)', False, 'from lib.log import log, opstatus\n'), ((79, 10, 79, 24), 'configparser.ConfigParser', 'ConfigParser', ({}, {}), '()', False, 'from configparser import ConfigParser\n'), ((86, 11, 86, 36), 'psycopg2.connect', 'psycopg2.connect', ({(86, 28, 86, 35): 'connstr'}, {}), '(connstr)', False, 'import psycopg2\n'), ((180, 4, 180, 27), 'lib.log.opstatus.print_status', 'opstatus.print_status', ({}, {}), '()', False, 'from lib.log import log, opstatus\n'), ((55, 8, 55, 19), 'sys.exit', 'sys.exit', ({(55, 17, 55, 18): '(1)'}, {}), '(1)', False, 'import sys\n'), ((60, 8, 60, 19), 'sys.exit', 'sys.exit', ({(60, 17, 60, 18): '(1)'}, {}), '(1)', False, 'import sys\n'), ((65, 8, 65, 19), 'sys.exit', 'sys.exit', ({(65, 17, 65, 18): '(1)'}, {}), '(1)', False, 'import sys\n'), ((70, 8, 70, 19), 'sys.exit', 'sys.exit', ({(70, 17, 70, 18): '(1)'}, {}), '(1)', False, 'import sys\n'), ((75, 8, 75, 19), 'sys.exit', 'sys.exit', ({(75, 17, 75, 18): '(1)'}, {}), '(1)', False, 'import sys\n'), ((105, 8, 105, 49), 'lib.log.log.error', 'log.error', ({(105, 18, 105, 48): "('List %s not found' % opt.list)"}, {}), "('List %s not found' % opt.list)", False, 'from lib.log import log, opstatus\n'), ((107, 8, 107, 19), 'sys.exit', 'sys.exit', ({(107, 17, 107, 18): '(1)'}, {}), '(1)', False, 'import sys\n'), ((114, 17, 114, 42), 'os.listdir', 'os.listdir', ({(114, 28, 114, 41): 'opt.directory'}, {}), '(opt.directory)', False, 'import os\n'), ((97, 8, 97, 19), 'sys.exit', 'sys.exit', ({(97, 17, 97, 18): '(1)'}, {}), '(1)', False, 'import sys\n'), ((115, 12, 115, 45), 'lib.log.log.status', 'log.status', ({(115, 23, 115, 44): "('Parsing file %s' % x)"}, {}), "('Parsing file %s' % x)", False, 'from lib.log import log, opstatus\n'), ((142, 21, 142, 51), 'lib.mbox.MailboxBreakupParser', 'MailboxBreakupParser', ({(142, 42, 142, 50): 'opt.mbox'}, {}), '(opt.mbox)', False, 'from lib.mbox import MailboxBreakupParser\n'), ((165, 13, 165, 36), 'lib.storage.ArchivesParserStorage', 'ArchivesParserStorage', ({}, {}), '()', False, 'from lib.storage import ArchivesParserStorage\n'), ((182, 4, 182, 22), 'lib.varnish.VarnishPurger', 'VarnishPurger', ({(182, 18, 182, 21): 'cfg'}, {}), '(cfg)', False, 'from lib.varnish import VarnishPurger\n'), ((80, 50, 80, 78), 'os.path.dirname', 'os.path.dirname', ({(80, 66, 80, 77): 'sys.argv[0]'}, {}), '(sys.argv[0])', False, 'import os\n'), ((117, 21, 117, 44), 'lib.storage.ArchivesParserStorage', 'ArchivesParserStorage', ({}, {}), '()', False, 'from lib.storage import ArchivesParserStorage\n'), ((139, 15, 139, 39), 'os.path.isfile', 'os.path.isfile', ({(139, 30, 139, 38): 'opt.mbox'}, {}), '(opt.mbox)', False, 'import os\n'), ((141, 12, 141, 23), 'sys.exit', 'sys.exit', ({(141, 21, 141, 22): '(1)'}, {}), '(1)', False, 'import sys\n'), ((144, 17, 144, 40), 'lib.storage.ArchivesParserStorage', 'ArchivesParserStorage', ({}, {}), '()', False, 'from lib.storage import ArchivesParserStorage\n'), ((160, 12, 160, 46), 'lib.log.log.error', 'log.error', ({(160, 22, 160, 45): '"""Failed to parse mbox:"""'}, {}), "('Failed to parse mbox:')", False, 'from lib.log import log, opstatus\n'), ((162, 12, 162, 23), 'sys.exit', 'sys.exit', ({(162, 21, 162, 22): '(1)'}, {}), '(1)', False, 'import sys\n'), ((176, 12, 176, 67), 'lib.log.log.log', 'log.log', ({(176, 20, 176, 66): "('Stored message with message-id %s' % ap.msgid)"}, {}), "('Stored message with message-id %s' % ap.msgid)", False, 'from lib.log import log, opstatus\n'), ((116, 22, 116, 52), 'os.path.join', 'os.path.join', ({(116, 35, 116, 48): 'opt.directory', (116, 50, 116, 51): 'x'}, {}), '(opt.directory, x)', False, 'import os\n'), ((172, 12, 172, 23), 'sys.exit', 'sys.exit', ({(172, 21, 172, 22): '(1)'}, {}), '(1)', False, 'import sys\n'), ((124, 60, 124, 90), 'os.path.join', 'os.path.join', ({(124, 73, 124, 86): 'opt.directory', (124, 88, 124, 89): 'x'}, {}), '(opt.directory, x)', False, 'import os\n')]