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