1"""Mailcap file handling.  See RFC 1524."""
2
3import os
4import warnings
5
6__all__ = ["getcaps","findmatch"]
7
8
9def 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
19def 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
45def 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.
63def 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
71def _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
112def 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
143def 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
159def 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
179def 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
192def 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
224def 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
235def 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
257def 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
274if __name__ == '__main__':
275    test()
276