#!/usr/bin/python
# xmpp_mail.py
#       Detect new & unseen emails, XMPP them to us
import sys, imaplib2, os, xmpp, time
from email.header import decode_header
from email.utils import parseaddr

# Our imap account
IMAP_SERVER = "localhost"
IMAP_USER = "imap_uname"
IMAP_PASSWD = "imap_passwd"

# Our XMPP account
XMPP_SERVER = "xmpp.someserver.domain"
XMPP_USER = "myagent@xmpp.someserver.domain"
XMPP_PASSWD = "xmpp-password"
XMPP_DEST = "who-to-tell@xmpp.someserver.domain"

# File with list of Message-ID's already announced
SEEN = os.getenv("HOME") + "/var/xmpp_seen.txt"
seen = None

# Send an XMPP message announcing the arrival of this email
def _xmpp_ping(fields):
    global XMPP_SERVER, XMPP_USER, XMPP_PASSWD

    # Build short summary of message
    if "from" not in fields:
	return
    if "subject" not in fields:
	return
    tup = parseaddr(fields["from"])
    sender = tup[0] or tup[1] or "---"
    subj = fields["subject"]
    msg = "%s: %s" % (sender, subj)

    # Connect to the XMPP server
    jid = xmpp.JID(XMPP_USER)
    conn = xmpp.Client(XMPP_SERVER, debug=[])
    conn.connect()
    result = conn.auth(jid.getNode(),
	XMPP_PASSWD, "email%d" % (os.getpid(),) )
    conn.sendInitPresence()

    # Send our message
    conn.send( xmpp.Message(XMPP_DEST, msg) )

    # Done
    conn.disconnect()

# Parse mail file style fields, where continuations
#  are done with leading tabs of following lines
# Return a list of lines, one entire field per entry
def _collect_fields(fields):
    res = []

    for f in fields.replace('\r', '').split('\n'):
	# Empty, ignore
	if not f:
	    continue

	# New field
	if f[0] != '\t':
	    res.append(f)
	    continue

	# Continuation
	assert res
	res[-1] += f[1:]

    return res

# Turn this selection of message fields into a dict
# Flatten all keys to lower case
def _field_dict(fields):
    res = {}
    fields = _collect_fields(fields)
    for f in fields:
	if not f:
	    continue
	if ": " not in f:
	    continue
	idx = f.index(": ")
	v = f[idx+2:]

	# For encoded (typically "q code") headers
	dv = decode_header(v)
	if not dv:
	    dv = v
	else:
	    dv = dv[0][0]
	res[f[:idx].lower()] = dv
    return res

# All the state needed to watch for new mail and
#  message it.
class MailWatch(object):
    def __init__(self):
	global IMAP_SERVER, IMAP_USER, IMAP_PASSWD

	# List of what we've already announced
	self.load_seen()

	# Get our imap server ready
	self.srv = srv = imaplib2.IMAP4_SSL(IMAP_SERVER)
	srv.login(IMAP_USER, IMAP_PASSWD)

	# We're checking our inbox, without modifying it
	tup = srv.select("inbox", readonly=True)
	if tup[0] != "OK":
	    raise Exception, "Can not select inbox"

    # Load/save list of seen messages
    def load_seen(self):
	global SEEN

	self.seen = seen = set()
	try:
	    f = open(SEEN, "r")
	except:
	    return
	for l in f:
	    l = l.strip()
	    seen.add(l)
	f.close()
    def save_seen(self):
	global SEEN

	f = open(SEEN, "w")
	for i in self.seen:
	    f.write(i + "\n")
	f.close()

    # Do a round of new mail checking
    def check_mail(self):
	# Get unread messages
	srv = self.srv
	tup = srv.search(None, '(UNSEEN)')
	sys.stderr.write("check result %r\n" % (tup,))
	if tup[0] != "OK":
	    raise Exception, "Can't list messages"

	# New message numbers
	msgs = [int(s) for s in tup[1][0].split()]

	# Walk messages, tabulate what's in there
	seen = self.seen
	curseen = set()
	changed = False
	for m in msgs:
	    # Get dope on this message number
	    tup = srv.fetch(m,
	     '(BODY[HEADER.FIELDS (FROM SUBJECT MESSAGE-ID)])')
	    if tup[0] != 'OK':
		sys.stderr.write("Can't fetch message %d\n" % (m,))
		sys.exit(0)
	    sys.stderr.write("fetch %d tup[1] = %r\n" % (m, tup[1]))

	    # imaplib2 appears to have a bug where it gives us the
	    #  flags sometimes, even though we didn't ask.
	    for targ in tup[1]:
		if "FLAGS" not in targ:
		    break
		sys.stderr.write("FLAGS present, skipping them\n")
	    else:
		sys.stderr.write("Only FLAGS present, skipping message\n")
		continue

	    # Decode fields, get Message-ID
	    fields = _field_dict(targ[1])
	    mid = fields.get("message-id")
	    if mid is None:
		print fields
		sys.stderr.write(" no msgid %r\n" % (m,))
		continue

	    # Tally all of them
	    curseen.add(mid)

	    # Already announced this one
	    if mid in seen:
		sys.stderr.write(" already notified %r\n" % (m,))
		continue
	    seen.add(mid)

	    # Announce this one
	    sys.stderr.write("New mail %s\n" % (fields,))
	    _xmpp_ping(fields)
	    changed = True

	if changed:
	    # Save back the current set of message-id's
	    self.seen = curseen
	    self.save_seen()

    # Endless service loop, using IDLE
    def run(self):
	srv = self.srv
	sys.stderr.write("xmpp_mail started, first check\n")
	while True:
	    self.check_mail()
	    sys.stderr.write("xmpp_mail IDLE\n")
	    tup = srv.idle()
	    tm = time.ctime()
	    sys.stderr.write(" IDLE back %r %s\n" % (tup, tm))
	    if tup[0] != 'OK':
		raise Exception, tup[1]

if __name__ == "__main__":
    m = MailWatch()
    m.run()
