#!/usr/bin/env python2.3

"""
MMS2LJ script polls a POP3 mailbox for MMS messages and posts them to
LiveJournal. Looks for configuration in ~/.mms2ljrc. Creates a sample
configuration file if none is found. Since the config file contains
passwords, please ensure that it is not world-readable. Use this script
with a scheduler like cron.

Copyright (C) 2004-06, Kiran Jonnalagadda. All rights reserved.
Source code may be used under the terms of the BSD license.

Version 0.6. ChangeLog:

0.6 (2006/10/29): Added support for a static image, for use as a widget.

0.5 (2004/07/25): Added support for multiple From addresses. The
    compiled configuration file .mms2ljrcc is removed if found.

0.4 (2004/07/20): Added support for audio attachments.

0.3 (2004/06/11): Added an encodings mapping option. AirTel's MMS to
    email gateway uses iso-10646-ucs-2 which Python doesn't recognise,
    but which seems to be the same as utf-16.

0.2 (2004/06/08): Added support for video attachments.

0.1 (2004/05/15): Initial release.
"""

config_template="""
# This configuration file contains settings for the MMS to LJ gateway.
# Since it contains passwords, please ensure that it is not
# world-readable. Your only security against abuse of the gateway is
# that the POP3 account is unknown (to abusers), and that the From
# address in each message is your mobile phone (in case a regular email
# spammer accidentally hits your account). There is no per-post security
# mechanism.

# The POP3 server that will be polled for new messages. This account
# should receive only posts for LJ.
pop3user = 'test'
pop3pass = 'testpass'
pop3host = 'your-mail-server-here'
mailfrom = 'your-mobile-number@your-service-provider'
rejects_to = 'your@email' # Rejected mail is redirected to this address.
# The rejects option is currently not implemented.
ignore_subject = False

# LiveJournal account settings.
ljserver = 'http://www.livejournal.com/'
ljuser = 'test'
ljpass = 'test'
ljjournal = 'test'
picture_keyword = ''
allow_comments = True
email_comments = True
html_quote = True

# Image handling options.
scale_images = True
scale_image_constraints = (320, 320)
static_image = True
static_image_constraints = (160, 160)
link_to_full_size_image = True
img_alt_text = 'MMS Image'
img_class = ''
img_style = ''
img_border = 0

# Server paths. Images will be placed under file_root/year/month/day.
file_root = '~/public_html/moblog/'
server_root = 'http://your-web-server/~your-user-name/moblog/'
"""

import imp, sys, os, os.path, time, poplib, email, cgi, xmlrpclib, md5
from types import StringTypes
from binascii import hexlify
from PIL import Image

encoding_mapping = {
    'iso-10646-ucs-2': 'utf-16'
    }

config_filename = os.path.expanduser('~/.mms2ljrc')

# Check if configuration file exists. If not, create it and exit.
try:
    config_file = file(config_filename, 'r')
except IOError:
    config_file = file(config_filename, 'w')
    config_file.write(config_template)
    config_file.close()
    print "A sample configuration file was written to ~/.mms2ljrc"
    print "Please edit your settings and rerun this script."
    sys.exit(1)

# Try to read configuration. Note: this is not secure as the
# configuration file is an unrestricted python script. We assume the
# user does not want to cause damage to himself/herself.
try:
    config = imp.load_module('mms2ljrc', config_file, '.mms2ljrc',
        ('rc', 'r', imp.PY_SOURCE))
finally:
    config_file.close()

# Remove the compiled configuration file if it was created. This may
# fail on Windows (not tested).
try:
    os.unlink('.mms2ljrcc')
except OSError:
    pass

# Determine current day, for where in path to place images.
post_year, post_month, post_day = time.localtime()[:3]
# Stuff these into configuration
config.post_year = '%04d' % post_year
config.post_month = '%02d' % post_month
config.post_day = '%02d' % post_day

# Counter for all images processed in this run of the script.
imagecounter = 0

def md5sum(text):
    m = md5.new()
    m.update(text)
    return hexlify(m.digest())

def stuff_defaults(config, **kw):
    """
    Add default values to configuration.
    """
    for key, value in kw.items():
        if not hasattr(config, key):
            setattr(config, key, value)

def post_to_lj(config, subject, lj_text):
    """
    Post the given text to LiveJournal.
    """
    if config.ignore_subject:
        subject = ''
    post_time = time.localtime()

    lj = xmlrpclib.Server(os.path.join(config.ljserver,
        'interface/xmlrpc')).LJ.XMLRPC
    lj.postevent({
        'username': config.ljuser,
        'hpassword': md5sum(config.ljpass), # FIXME: Not secure!!!
        'event': lj_text,
        'lineendings': 'pc',
        'subject': subject,
        'year': str(post_time[0]),
        'mon': str(post_time[1]),
        'day': str(post_time[2]),
        'hour': str(post_time[3]),
        'min': str(post_time[4]),
        'usejournal': config.ljjournal,
        'props': {
            'opt_nocomments': bool(not config.allow_comments),
            'opt_noemail': bool(not config.email_comments),
            'picture_keyword': config.picture_keyword,
            },
        })

def process_attachment(config, type, subtype, raw_data):
    """
    Process an image and return an image tag.
    """
    global imagecounter
    filename = '%d%d' % (time.time(), imagecounter)
    smallfilename = filename + '-scaled'
    staticfilename = "static"
    imagecounter += 1
    subtype = subtype.lower()
    if subtype in ['jpeg', 'jpg']:
        filename += '.jpg'
        smallfilename += '.jpg'
        staticfilename += '.jpg'
    elif subtype == '3gpp':
        filename += '.3gp'
        smallfilename += '.3gp'
        staticfilename += '.3gp'
    elif subtype == 'amr':
        filename += '.amr'
        smallfilename += '.amr'
        staticfilename += '.amr'
    else:
        filename += '.' + subtype
        smallfilename += '.' + subtype
        staticfilename += '.' + subtype

    # Make folder hierarchy.
    filepath = os.path.join(os.path.expanduser(config.file_root),
        config.post_year, config.post_month, config.post_day)
    serverpath = os.path.join(config.server_root,
        config.post_year, config.post_month, config.post_day)
    try:
        os.makedirs(filepath)
        # The odd thing here is that makedirs raises os.error both when
        # the folder already exists and when it can't be created.
    except os.error:
        pass

    # Write raw data to file.
    file(os.path.join(filepath, filename), 'w').write(raw_data)

    if type == 'image':
        # Read it back again via PIL to scale image and get size.
        image = Image.open(os.path.join(filepath, filename))
        # Generate static image
        if config.static_image:
            staticimage = image.copy()
            if (image.size[0] > config.static_image_constraints[0] or
                image.size[1] > config.static_image_constraints[1]):
                staticimage.thumbnail(config.static_image_constraints,
                                getattr(Image, 'ANTIALIAS', Image.NEAREST))
            staticimage.save(os.path.join(os.path.expanduser(config.file_root),
                                          staticfilename), quality=90)
                
        # Don't scale if not big enough.
        noscale = False
        if (image.size[0] < config.scale_image_constraints[0] and
            image.size[1] < config.scale_image_constraints[1]):
            noscale = True
        if config.scale_images and not noscale:
            image.thumbnail(config.scale_image_constraints,
                getattr(Image, 'ANTIALIAS', Image.NEAREST))
            image.save(os.path.join(filepath, smallfilename),
                quality=90)
            imgsrc = os.path.join(serverpath, smallfilename)
            linkhref = os.path.join(serverpath, filename)
        else:
            imgsrc = os.path.join(serverpath, filename)
        imgwidth = image.size[0]
        imgheight = image.size[1]
        imgtag = '<img src="%s" width="%d" height="%d" alt="%s"' \
            ' border="%d"' % (imgsrc, imgwidth, imgheight,
            config.img_alt_text, config.img_border)
        if config.img_class:
            imgtag += ' class="%s"' % config.img_class
        if config.img_style:
            imgtag += ' style="%s"' % config.img_style
        imgtag += '>'

        if config.scale_images and config.link_to_full_size_image and \
            not noscale:
            imgtag = '<a href="%s">%s</a>' % (linkhref, imgtag)

        return imgtag
    elif type == 'video':
        linktag = '<a href="%s">See video (%s kB) &raquo;</a>' % (
            os.path.join(serverpath, filename), len(raw_data) / 1024)
        return linktag
    elif type == 'audio':
        linktag = '<a href="%s">Play audio (%s kB) &raquo;</a>' % (
            os.path.join(serverpath, filename), len(raw_data) / 1024)
        return linktag
    else:
        return ''

def process_message(config, msg_body):
    """
    Processes a given message body: parses body and posts to LJ.
    """
    lj_parts = []
    message = email.message_from_string(msg_body)
    from_right = False
    mfrom = message.get('From', None)
    if mfrom is not None:
        for cfrom in config.mailfrom:
            if mfrom.find(cfrom) != -1:
                from_right = True
    if from_right:
        if message.is_multipart():
            parts = message.get_payload()
        else:
            parts = [message]
        for part in parts:
            if not part.is_multipart(): # Lazy programmer syndrome.
                # Decode from transfer encoding to raw data.
                pvalue = part.get_payload()
                ce = part.get('Content-Transfer-Encoding', '')
                if ce == 'base64':
                    pvalue = email.base64MIME.decodestring(pvalue)
                elif ce == 'quoted-printable':
                    pvalue = email.quopriMIME.decodestring(pvalue)

                # Check content type.
                ct = part.get_content_type()
                if ct.startswith('text/'): # Treat all text as text.
                    # Check charset. Convert to UTF-8 if not already.
                    charset = part.get_content_charset()
                    if charset != 'utf-8':
                        # Convert to UTF-8.
                        pvalue = unicode(pvalue, encoding_mapping.get(
                            charset, charset)).encode('utf-8')
                    # HTML-quote text if type is text/plain.
                    if ct == 'text/plain' and config.html_quote:
                        pvalue = cgi.escape(pvalue)
                    lj_parts.append(pvalue)

                elif ct.startswith('image/'):
                    lj_parts.append(process_attachment(config, 'image',
                        part.get_subtype(), pvalue))
                elif ct.startswith('video/'):
                    lj_parts.append(process_attachment(config, 'video',
                        part.get_subtype(), pvalue))
                elif ct.startswith('audio/'):
                    lj_parts.append(process_attachment(config, 'audio',
                        part.get_subtype(), pvalue))
                # All other MIME types are ignored.
        # All parts processed. Post to LJ now.
        post_to_lj(config, message.get('Subject', ''),
            '\r\n'.join(lj_parts))
    else:
        # TODO: Reject code comes here.
        pass

def process_pop3(config):
    """
    Process messages on the POP3 server.
    """
    # Access POP server.
    pop3 = poplib.POP3(config.pop3host)
    pop3.user(config.pop3user)
    pop3.pass_(config.pop3pass)
    message_count = len(pop3.list()[1])
    for i in range(message_count):
        msg_body = '\r\n'.join(pop3.retr(i+1)[1])
        # Parse message body into components and make LJ post.
        process_message(config, msg_body)
        pop3.dele(i+1)
    pop3.quit()


# Main loop.
stuff_defaults(config,
    pop3user = '',
    pop3pass = '',
    pop3host = '',
    mailfrom = '',
    rejects_to = '',
    ignore_subject = False,
    ljserver = 'http://www.livejournal.com/',
    ljuser = '',
    ljpass = '',
    picture_keyword = '',
    allow_comments = True,
    email_comments = True,
    html_quote = True,
    scale_images = True,
    scale_image_constraints = (320, 320),
    static_image = True,
    static_image_constraints = (160, 160),
    link_to_full_size_image = True,
    img_alt_text = 'MMS Image',
    img_class = '',
    img_style = '',
    img_border = 0,
    file_root = '~/public_html/moblog/',
    server_root = ''
    )

# Set journal to default.
stuff_defaults(config, ljjournal = config.ljuser)
if isinstance(config.mailfrom, StringTypes):
    config.mailfrom = [config.mailfrom]

process_pop3(config)
# Temporary for testing:
#process_message(config, msg_body = file('test.eml', 'r').read())
