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