#! /usr/bin/env python
#
# Copyright (C) 1998,1999,2000 by the Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed 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 General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software 
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

"""Poll the NNTP servers for messages to be gatewayed to mailing lists.

Usage: gate_news [options]

Where options are

    --stderrs
    -s
        Print errors to stderr in addition to being logged to logs/fromusenet

    --quiet
    -q
        Run quietly.  Nothing is output unless there is an exception.

    --help
    -h
        Print this text and exit.

"""

import sys
import os
import string
import time
import traceback
import socket
import getopt
from errno import EEXIST

import paths
from Mailman import mm_cfg
from Mailman import MailList
from Mailman import Utils
from Mailman import Message
from Mailman.Logging.Utils import LogStdErr

# The version we have is from Python 1.5.2+ and fixes the "mode reader"
# problem.
from Mailman.pythonlib import nntplib

# Work around known problems with some RedHat cron daemons
import signal
signal.signal(signal.SIGCHLD, signal.SIG_DFL)


VERBOSE = 1
BLOCKFILE = 'gate_news.lck'



def open_newsgroup(mlist):
    # Open up a "mode reader" connection to the gated newsgroup.  Let
    # exceptions percolate up.
    conn = nntplib.NNTP(mlist.nntp_host, readermode=1)
    # Get the GROUP information for the list, but we're only really interested
    # in the first article number and the last article number
    r,c,f,l,n = conn.group(mlist.linked_newsgroup)
    return conn, int(f), int(l)



# XXX: Bogus, but might as we do it `legally'
QuickEscape = 'QuickEscape'

def poll_newsgroup(mlist, conn, first, last):
    # NEWNEWS is not portable and has synchronization issues.
    for num in range(first, last):
        try:
            headers = conn.head(`num`)[3]
            found_to = 0
            for header in headers:
                i = string.find(header, ':')
                value = string.lower(header[:i])
                if i > 0 and value == 'to':
                    found_to = 1
                if value <> 'x-beenthere':
                    continue
                if header[i:] == ': %s' % mlist.GetListEmail():
                    raise QuickEscape
            body = conn.body(`num`)[3]
            # Usenet originated messages will not have a Unix envelope
            # (i.e. "From " header).  This breaks Pipermail archiving, so we
            # will synthesize one.  Be sure to use the format searched for by
            # mailbox.UnixMailbox._isrealfromline()
            timehdr = time.asctime(time.localtime(time.time()))
            lines = ['From ' + mlist.GetAdminEmail() + '  ' + timehdr]
            lines.extend(headers)
            lines.append('')
            lines.extend(body)
            lines.append('')
            msg = Message.OutgoingMessage(string.join(lines, '\n'))
            msg.fromusenet = 1
            if found_to:
                msg['X-Originally-To'] = msg['To']
            msg['To'] = mlist.GetListEmail()
            # Post the message to the locked list
            if VERBOSE:
                sys.stderr.write('posting msgid %d to list %s\n' %
                                 (num, mlist.internal_name()))
            mlist.Post(msg)
            # record the last gated article number
            mlist.usenet_watermark = num
            if VERBOSE:
                sys.stderr.write('posted msgid %d to list %s\n' %
                                 (num, mlist.internal_name()))
        except nntplib.error_temp, msg:
            sys.stderr.write('encountered NNTP error for list %s\n' %
                             mlist.internal_name())
            sys.stderr.write(str(msg) + '\n')
        except QuickEscape:
            pass # We gated this TO news, don't repost it!



def gate_list(mlist):
    # Get the list's watermark, i.e. the last article number that this gated
    # from news to mail.  None means that this list has never polled its
    # newsgroup.
    watermark = getattr(mlist, 'usenet_watermark', None)
    # Open the newsgroup, but let exceptions percolate up
    conn, first, last = open_newsgroup(mlist)
    try:
        if watermark is None:
            # This is the first time we've tried to gate this newsgroup.  We
            # essentially do a mass catch-up, otherwise we'd flood the mailing
            # list.
            mlist.usenet_watermark = last
        else:
            # The list has been polled previously, so now we simply grab all
            # the messages on the newsgroup that have not been seen by the
            # mailing list.  The first such article is the maximum of the
            # lowest article available on the list and the watermark.  It's
            # possible that some articles have been expired since the last
            # time gate_news has run.  Not much we can do about that.
            poll_newsgroup(mlist, conn, max(watermark+1, first), last+1)
    finally:
        conn.quit()



def reap(children):
    if not children:
        return
    # See if any children have exited yet
    pid, status = os.waitpid(-1, os.WNOHANG)
    if pid == 0:
        # Nope, none are ready
        return
    try:
        del children[pid]
    except KeyError:
        # Huh?  how could this happen?
        pass



def process_lists():
    # for waitpids
    children = {}
    for listname in Utils.list_names():
        # Open the list unlocked just to check to see if it is gating news to
        # mail.  If not, we're done with the list.  Otherwise, create a fork
        # for gating, and immediately lock the group.
        mlist = MailList.MailList(listname, lock=0)
        if not mlist.gateway_to_mail:
            continue
        pid = os.fork()
        if pid:
            # In the parent.  record the pid of the child, the child's list
            # name, and last message number.  when the child successfully
            # exits, we'll update it's watermark
            children[pid] = pid
        else:
            # In the child.
            status = 0
            try:
                try:
                    mlist.Lock()
                    gate_list(mlist)
                    if VERBOSE:
                        sys.stderr.write('%s watermark: %d\n' %
                                         (mlist.internal_name(),
                                          mlist.usenet_watermark))
                except:
                    # if anything else bad happens, log the exception to
                    # stderr.  TBD: we should probably generalize
                    # scripts/driver to handle this situation
                    status = 1
                    traceback.print_exc()
            finally:
                mlist.Save()
                mlist.Unlock()
                os._exit(status)
    # we're done forking off all the gating children, now just wait for them
    # all to exit, and then we're done
    while children:
        reap(children)



def main():
    # block any other gate_news process from running
    blockfile = os.path.join(mm_cfg.DATA_DIR, BLOCKFILE)
    try:
        fd = os.open(blockfile, os.O_CREAT | os.O_EXCL)
        os.close(fd)
    except OSError, e:
        if e.errno <> EEXIST:
            raise
        # some other gate_news process is already running
        if VERBOSE:
            sys.stderr.write('some other gate_news is already running\n')
        return
    try:
        process_lists()
    finally:
        os.unlink(blockfile)



def usage(status, msg=''):
    print __doc__ % globals()
    if msg:
        print msg
    sys.exit(status)



if __name__ == '__main__':
    global VERBOSE

    try:
        opts, args = getopt.getopt(sys.argv[1:], 'shq',
                                   ['stderrs', 'quiet', 'help'])
    except getopt.error, msg:
        usage(1, msg)

    if args:
        usage(1, 'No args are expected')

    tee_to_stdout = 0
    VERBOSE = 1
    for opt, arg in opts:
        if opt in ('-h', '--help'):
            usage(0)
        elif opt in ('-s', '--stderrs'):
            tee_to_stdout = 1
        elif opt in ('-q', '--quiet'):
            VERBOSE = 0

    # Set up stderr
    LogStdErr('fromusenet', 'gate_news', tee_to_stdout=tee_to_stdout)
    if VERBOSE:
        sys.stderr.write('begin gating\n')
    main()
    if VERBOSE:
        sys.stderr.write('end gating\n')
