"""Mailcap file handling.  See RFC 1524."""



import os



__all__ = ["getcaps","findmatch"]



# Part 1: top-level interface.



def getcaps():

    """Return a dictionary containing the mailcap database.



    The dictionary maps a MIME type (in all lowercase, e.g. 'text/plain')

    to a list of dictionaries corresponding to mailcap entries.  The list

    collects all the entries for that MIME type from all available mailcap

    files.  Each dictionary contains key-value pairs for that MIME type,

    where the viewing command is stored with the key "view".



    """

    caps = {}

    for mailcap in listmailcapfiles():

        try:

            fp = open(mailcap, 'r')

        except IOError:

            continue

        morecaps = readmailcapfile(fp)

        fp.close()

        for key, value in morecaps.iteritems():

            if not key in caps:

                caps[key] = value

            else:

                caps[key] = caps[key] + value

    return caps



def listmailcapfiles():

    """Return a list of all mailcap files found on the system."""

    # XXX Actually, this is Unix-specific

    if 'MAILCAPS' in os.environ:

        str = os.environ['MAILCAPS']

        mailcaps = str.split(':')

    else:

        if 'HOME' in os.environ:

            home = os.environ['HOME']

        else:

            # Don't bother with getpwuid()

            home = '.' # Last resort

        mailcaps = [home + '/.mailcap', '/etc/mailcap',

                '/usr/etc/mailcap', '/usr/local/etc/mailcap']

    return mailcaps





# Part 2: the parser.



def readmailcapfile(fp):

    """Read a mailcap file and return a dictionary keyed by MIME type.



    Each MIME type is mapped to an entry consisting of a list of

    dictionaries; the list will contain more than one such dictionary

    if a given MIME type appears more than once in the mailcap file.

    Each dictionary contains key-value pairs for that MIME type, where

    the viewing command is stored with the key "view".

    """

    caps = {}

    while 1:

        line = fp.readline()

        if not line: break

        # Ignore comments and blank lines

        if line[0] == '#' or line.strip() == '':

            continue

        nextline = line

        # Join continuation lines

        while nextline[-2:] == '\\\n':

            nextline = fp.readline()

            if not nextline: nextline = '\n'

            line = line[:-2] + nextline

        # Parse the line

        key, fields = parseline(line)

        if not (key and fields):

            continue

        # Normalize the key

        types = key.split('/')

        for j in range(len(types)):

            types[j] = types[j].strip()

        key = '/'.join(types).lower()

        # Update the database

        if key in caps:

            caps[key].append(fields)

        else:

            caps[key] = [fields]

    return caps



def parseline(line):

    """Parse one entry in a mailcap file and return a dictionary.



    The viewing command is stored as the value with the key "view",

    and the rest of the fields produce key-value pairs in the dict.

    """

    fields = []

    i, n = 0, len(line)

    while i < n:

        field, i = parsefield(line, i, n)

        fields.append(field)

        i = i+1 # Skip semicolon

    if len(fields) < 2:

        return None, None

    key, view, rest = fields[0], fields[1], fields[2:]

    fields = {'view': view}

    for field in rest:

        i = field.find('=')

        if i < 0:

            fkey = field

            fvalue = ""

        else:

            fkey = field[:i].strip()

            fvalue = field[i+1:].strip()

        if fkey in fields:

            # Ignore it

            pass

        else:

            fields[fkey] = fvalue

    return key, fields



def parsefield(line, i, n):

    """Separate one key-value pair in a mailcap entry."""

    start = i

    while i < n:

        c = line[i]

        if c == ';':

            break

        elif c == '\\':

            i = i+2

        else:

            i = i+1

    return line[start:i].strip(), i





# Part 3: using the database.



def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):

    """Find a match for a mailcap entry.



    Return a tuple containing the command line, and the mailcap entry

    used; (None, None) if no match is found.  This may invoke the

    'test' command of several matching entries before deciding which

    entry to use.



    """

    entries = lookup(caps, MIMEtype, key)

    # XXX This code should somehow check for the needsterminal flag.

    for e in entries:

        if 'test' in e:

            test = subst(e['test'], filename, plist)

            if test and os.system(test) != 0:

                continue

        command = subst(e[key], MIMEtype, filename, plist)

        return command, e

    return None, None



def lookup(caps, MIMEtype, key=None):

    entries = []

    if MIMEtype in caps:

        entries = entries + caps[MIMEtype]

    MIMEtypes = MIMEtype.split('/')

    MIMEtype = MIMEtypes[0] + '/*'

    if MIMEtype in caps:

        entries = entries + caps[MIMEtype]

    if key is not None:

        entries = filter(lambda e, key=key: key in e, entries)

    return entries



def subst(field, MIMEtype, filename, plist=[]):

    # XXX Actually, this is Unix-specific

    res = ''

    i, n = 0, len(field)

    while i < n:

        c = field[i]; i = i+1

        if c != '%':

            if c == '\\':

                c = field[i:i+1]; i = i+1

            res = res + c

        else:

            c = field[i]; i = i+1

            if c == '%':

                res = res + c

            elif c == 's':

                res = res + filename

            elif c == 't':

                res = res + MIMEtype

            elif c == '{':

                start = i

                while i < n and field[i] != '}':

                    i = i+1

                name = field[start:i]

                i = i+1

                res = res + findparam(name, plist)

            # XXX To do:

            # %n == number of parts if type is multipart/*

            # %F == list of alternating type and filename for parts

            else:

                res = res + '%' + c

    return res



def findparam(name, plist):

    name = name.lower() + '='

    n = len(name)

    for p in plist:

        if p[:n].lower() == name:

            return p[n:]

    return ''





# Part 4: test program.



def test():

    import sys

    caps = getcaps()

    if not sys.argv[1:]:

        show(caps)

        return

    for i in range(1, len(sys.argv), 2):

        args = sys.argv[i:i+2]

        if len(args) < 2:

            print "usage: mailcap [MIMEtype file] ..."

            return

        MIMEtype = args[0]

        file = args[1]

        command, e = findmatch(caps, MIMEtype, 'view', file)

        if not command:

            print "No viewer found for", type

        else:

            print "Executing:", command

            sts = os.system(command)

            if sts:

                print "Exit status:", sts



def show(caps):

    print "Mailcap files:"

    for fn in listmailcapfiles(): print "\t" + fn

    print

    if not caps: caps = getcaps()

    print "Mailcap entries:"

    print

    ckeys = caps.keys()

    ckeys.sort()

    for type in ckeys:

        print type

        entries = caps[type]

        for e in entries:

            keys = e.keys()

            keys.sort()

            for k in keys:

                print "  %-15s" % k, e[k]

            print



if __name__ == '__main__':

    test()

