Home | History | Annotate | Download | only in Lib
      1 """Mailcap file handling.  See RFC 1524."""
      2 
      3 import os
      4 import warnings
      5 
      6 __all__ = ["getcaps","findmatch"]
      7 
      8 
      9 def lineno_sort_key(entry):
     10     # Sort in ascending order, with unspecified entries at the end
     11     if 'lineno' in entry:
     12         return 0, entry['lineno']
     13     else:
     14         return 1, 0
     15 
     16 
     17 # Part 1: top-level interface.
     18 
     19 def getcaps():
     20     """Return a dictionary containing the mailcap database.
     21 
     22     The dictionary maps a MIME type (in all lowercase, e.g. 'text/plain')
     23     to a list of dictionaries corresponding to mailcap entries.  The list
     24     collects all the entries for that MIME type from all available mailcap
     25     files.  Each dictionary contains key-value pairs for that MIME type,
     26     where the viewing command is stored with the key "view".
     27 
     28     """
     29     caps = {}
     30     lineno = 0
     31     for mailcap in listmailcapfiles():
     32         try:
     33             fp = open(mailcap, 'r')
     34         except OSError:
     35             continue
     36         with fp:
     37             morecaps, lineno = _readmailcapfile(fp, lineno)
     38         for key, value in morecaps.items():
     39             if not key in caps:
     40                 caps[key] = value
     41             else:
     42                 caps[key] = caps[key] + value
     43     return caps
     44 
     45 def listmailcapfiles():
     46     """Return a list of all mailcap files found on the system."""
     47     # This is mostly a Unix thing, but we use the OS path separator anyway
     48     if 'MAILCAPS' in os.environ:
     49         pathstr = os.environ['MAILCAPS']
     50         mailcaps = pathstr.split(os.pathsep)
     51     else:
     52         if 'HOME' in os.environ:
     53             home = os.environ['HOME']
     54         else:
     55             # Don't bother with getpwuid()
     56             home = '.' # Last resort
     57         mailcaps = [home + '/.mailcap', '/etc/mailcap',
     58                 '/usr/etc/mailcap', '/usr/local/etc/mailcap']
     59     return mailcaps
     60 
     61 
     62 # Part 2: the parser.
     63 def readmailcapfile(fp):
     64     """Read a mailcap file and return a dictionary keyed by MIME type."""
     65     warnings.warn('readmailcapfile is deprecated, use getcaps instead',
     66                   DeprecationWarning, 2)
     67     caps, _ = _readmailcapfile(fp, None)
     68     return caps
     69 
     70 
     71 def _readmailcapfile(fp, lineno):
     72     """Read a mailcap file and return a dictionary keyed by MIME type.
     73 
     74     Each MIME type is mapped to an entry consisting of a list of
     75     dictionaries; the list will contain more than one such dictionary
     76     if a given MIME type appears more than once in the mailcap file.
     77     Each dictionary contains key-value pairs for that MIME type, where
     78     the viewing command is stored with the key "view".
     79     """
     80     caps = {}
     81     while 1:
     82         line = fp.readline()
     83         if not line: break
     84         # Ignore comments and blank lines
     85         if line[0] == '#' or line.strip() == '':
     86             continue
     87         nextline = line
     88         # Join continuation lines
     89         while nextline[-2:] == '\\\n':
     90             nextline = fp.readline()
     91             if not nextline: nextline = '\n'
     92             line = line[:-2] + nextline
     93         # Parse the line
     94         key, fields = parseline(line)
     95         if not (key and fields):
     96             continue
     97         if lineno is not None:
     98             fields['lineno'] = lineno
     99             lineno += 1
    100         # Normalize the key
    101         types = key.split('/')
    102         for j in range(len(types)):
    103             types[j] = types[j].strip()
    104         key = '/'.join(types).lower()
    105         # Update the database
    106         if key in caps:
    107             caps[key].append(fields)
    108         else:
    109             caps[key] = [fields]
    110     return caps, lineno
    111 
    112 def parseline(line):
    113     """Parse one entry in a mailcap file and return a dictionary.
    114 
    115     The viewing command is stored as the value with the key "view",
    116     and the rest of the fields produce key-value pairs in the dict.
    117     """
    118     fields = []
    119     i, n = 0, len(line)
    120     while i < n:
    121         field, i = parsefield(line, i, n)
    122         fields.append(field)
    123         i = i+1 # Skip semicolon
    124     if len(fields) < 2:
    125         return None, None
    126     key, view, rest = fields[0], fields[1], fields[2:]
    127     fields = {'view': view}
    128     for field in rest:
    129         i = field.find('=')
    130         if i < 0:
    131             fkey = field
    132             fvalue = ""
    133         else:
    134             fkey = field[:i].strip()
    135             fvalue = field[i+1:].strip()
    136         if fkey in fields:
    137             # Ignore it
    138             pass
    139         else:
    140             fields[fkey] = fvalue
    141     return key, fields
    142 
    143 def parsefield(line, i, n):
    144     """Separate one key-value pair in a mailcap entry."""
    145     start = i
    146     while i < n:
    147         c = line[i]
    148         if c == ';':
    149             break
    150         elif c == '\\':
    151             i = i+2
    152         else:
    153             i = i+1
    154     return line[start:i].strip(), i
    155 
    156 
    157 # Part 3: using the database.
    158 
    159 def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):
    160     """Find a match for a mailcap entry.
    161 
    162     Return a tuple containing the command line, and the mailcap entry
    163     used; (None, None) if no match is found.  This may invoke the
    164     'test' command of several matching entries before deciding which
    165     entry to use.
    166 
    167     """
    168     entries = lookup(caps, MIMEtype, key)
    169     # XXX This code should somehow check for the needsterminal flag.
    170     for e in entries:
    171         if 'test' in e:
    172             test = subst(e['test'], filename, plist)
    173             if test and os.system(test) != 0:
    174                 continue
    175         command = subst(e[key], MIMEtype, filename, plist)
    176         return command, e
    177     return None, None
    178 
    179 def lookup(caps, MIMEtype, key=None):
    180     entries = []
    181     if MIMEtype in caps:
    182         entries = entries + caps[MIMEtype]
    183     MIMEtypes = MIMEtype.split('/')
    184     MIMEtype = MIMEtypes[0] + '/*'
    185     if MIMEtype in caps:
    186         entries = entries + caps[MIMEtype]
    187     if key is not None:
    188         entries = [e for e in entries if key in e]
    189     entries = sorted(entries, key=lineno_sort_key)
    190     return entries
    191 
    192 def subst(field, MIMEtype, filename, plist=[]):
    193     # XXX Actually, this is Unix-specific
    194     res = ''
    195     i, n = 0, len(field)
    196     while i < n:
    197         c = field[i]; i = i+1
    198         if c != '%':
    199             if c == '\\':
    200                 c = field[i:i+1]; i = i+1
    201             res = res + c
    202         else:
    203             c = field[i]; i = i+1
    204             if c == '%':
    205                 res = res + c
    206             elif c == 's':
    207                 res = res + filename
    208             elif c == 't':
    209                 res = res + MIMEtype
    210             elif c == '{':
    211                 start = i
    212                 while i < n and field[i] != '}':
    213                     i = i+1
    214                 name = field[start:i]
    215                 i = i+1
    216                 res = res + findparam(name, plist)
    217             # XXX To do:
    218             # %n == number of parts if type is multipart/*
    219             # %F == list of alternating type and filename for parts
    220             else:
    221                 res = res + '%' + c
    222     return res
    223 
    224 def findparam(name, plist):
    225     name = name.lower() + '='
    226     n = len(name)
    227     for p in plist:
    228         if p[:n].lower() == name:
    229             return p[n:]
    230     return ''
    231 
    232 
    233 # Part 4: test program.
    234 
    235 def test():
    236     import sys
    237     caps = getcaps()
    238     if not sys.argv[1:]:
    239         show(caps)
    240         return
    241     for i in range(1, len(sys.argv), 2):
    242         args = sys.argv[i:i+2]
    243         if len(args) < 2:
    244             print("usage: mailcap [MIMEtype file] ...")
    245             return
    246         MIMEtype = args[0]
    247         file = args[1]
    248         command, e = findmatch(caps, MIMEtype, 'view', file)
    249         if not command:
    250             print("No viewer found for", type)
    251         else:
    252             print("Executing:", command)
    253             sts = os.system(command)
    254             if sts:
    255                 print("Exit status:", sts)
    256 
    257 def show(caps):
    258     print("Mailcap files:")
    259     for fn in listmailcapfiles(): print("\t" + fn)
    260     print()
    261     if not caps: caps = getcaps()
    262     print("Mailcap entries:")
    263     print()
    264     ckeys = sorted(caps)
    265     for type in ckeys:
    266         print(type)
    267         entries = caps[type]
    268         for e in entries:
    269             keys = sorted(e)
    270             for k in keys:
    271                 print("  %-15s" % k, e[k])
    272             print()
    273 
    274 if __name__ == '__main__':
    275     test()
    276