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