]> git.phdru.name Git - mimedecode.git/blob - mimedecode/mailcap_312.py
Version 3.2.0: Copy `mailcap.py` from Python 3.12
[mimedecode.git] / mimedecode / mailcap_312.py
1 """Mailcap file handling.  See RFC 1524."""
2
3 import os
4 import warnings
5 import 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.')
13 # warnings._deprecated(__name__, _DEPRECATION_MSG, remove=(3, 13))
14
15
16 def 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
25 class UnsafeMailcapInput(Warning):
26     """Warning raised when refusing unsafe input"""
27
28
29 # Part 1: top-level interface.
30
31 def 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
57 def 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.
75 def 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
83 def _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 line := fp.readline():
94         # Ignore comments and blank lines
95         if line[0] == '#' or line.strip() == '':
96             continue
97         nextline = line
98         # Join continuation lines
99         while nextline[-2:] == '\\\n':
100             nextline = fp.readline()
101             if not nextline: nextline = '\n'
102             line = line[:-2] + nextline
103         # Parse the line
104         key, fields = parseline(line)
105         if not (key and fields):
106             continue
107         if lineno is not None:
108             fields['lineno'] = lineno
109             lineno += 1
110         # Normalize the key
111         types = key.split('/')
112         for j in range(len(types)):
113             types[j] = types[j].strip()
114         key = '/'.join(types).lower()
115         # Update the database
116         if key in caps:
117             caps[key].append(fields)
118         else:
119             caps[key] = [fields]
120     return caps, lineno
121
122 def parseline(line):
123     """Parse one entry in a mailcap file and return a dictionary.
124
125     The viewing command is stored as the value with the key "view",
126     and the rest of the fields produce key-value pairs in the dict.
127     """
128     fields = []
129     i, n = 0, len(line)
130     while i < n:
131         field, i = parsefield(line, i, n)
132         fields.append(field)
133         i = i+1 # Skip semicolon
134     if len(fields) < 2:
135         return None, None
136     key, view, rest = fields[0], fields[1], fields[2:]
137     fields = {'view': view}
138     for field in rest:
139         i = field.find('=')
140         if i < 0:
141             fkey = field
142             fvalue = ""
143         else:
144             fkey = field[:i].strip()
145             fvalue = field[i+1:].strip()
146         if fkey in fields:
147             # Ignore it
148             pass
149         else:
150             fields[fkey] = fvalue
151     return key, fields
152
153 def parsefield(line, i, n):
154     """Separate one key-value pair in a mailcap entry."""
155     start = i
156     while i < n:
157         c = line[i]
158         if c == ';':
159             break
160         elif c == '\\':
161             i = i+2
162         else:
163             i = i+1
164     return line[start:i].strip(), i
165
166
167 # Part 3: using the database.
168
169 def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):
170     """Find a match for a mailcap entry.
171
172     Return a tuple containing the command line, and the mailcap entry
173     used; (None, None) if no match is found.  This may invoke the
174     'test' command of several matching entries before deciding which
175     entry to use.
176
177     """
178     if _find_unsafe(filename):
179         msg = "Refusing to use mailcap with filename %r. Use a safe temporary filename." % (filename,)
180         warnings.warn(msg, UnsafeMailcapInput)
181         return None, None
182     entries = lookup(caps, MIMEtype, key)
183     # XXX This code should somehow check for the needsterminal flag.
184     for e in entries:
185         if 'test' in e:
186             test = subst(e['test'], filename, plist)
187             if test is None:
188                 continue
189             if test and os.system(test) != 0:
190                 continue
191         command = subst(e[key], MIMEtype, filename, plist)
192         if command is not None:
193             return command, e
194     return None, None
195
196 def lookup(caps, MIMEtype, key=None):
197     entries = []
198     if MIMEtype in caps:
199         entries = entries + caps[MIMEtype]
200     MIMEtypes = MIMEtype.split('/')
201     MIMEtype = MIMEtypes[0] + '/*'
202     if MIMEtype in caps:
203         entries = entries + caps[MIMEtype]
204     if key is not None:
205         entries = [e for e in entries if key in e]
206     entries = sorted(entries, key=lineno_sort_key)
207     return entries
208
209 def subst(field, MIMEtype, filename, plist=[]):
210     # XXX Actually, this is Unix-specific
211     res = ''
212     i, n = 0, len(field)
213     while i < n:
214         c = field[i]; i = i+1
215         if c != '%':
216             if c == '\\':
217                 c = field[i:i+1]; i = i+1
218             res = res + c
219         else:
220             c = field[i]; i = i+1
221             if c == '%':
222                 res = res + c
223             elif c == 's':
224                 res = res + filename
225             elif c == 't':
226                 if _find_unsafe(MIMEtype):
227                     msg = "Refusing to substitute MIME type %r into a shell command." % (MIMEtype,)
228                     warnings.warn(msg, UnsafeMailcapInput)
229                     return None
230                 res = res + MIMEtype
231             elif c == '{':
232                 start = i
233                 while i < n and field[i] != '}':
234                     i = i+1
235                 name = field[start:i]
236                 i = i+1
237                 param = findparam(name, plist)
238                 if _find_unsafe(param):
239                     msg = "Refusing to substitute parameter %r (%s) into a shell command" % (param, name)
240                     warnings.warn(msg, UnsafeMailcapInput)
241                     return None
242                 res = res + param
243             # XXX To do:
244             # %n == number of parts if type is multipart/*
245             # %F == list of alternating type and filename for parts
246             else:
247                 res = res + '%' + c
248     return res
249
250 def findparam(name, plist):
251     name = name.lower() + '='
252     n = len(name)
253     for p in plist:
254         if p[:n].lower() == name:
255             return p[n:]
256     return ''
257
258
259 # Part 4: test program.
260
261 def test():
262     import sys
263     caps = getcaps()
264     if not sys.argv[1:]:
265         show(caps)
266         return
267     for i in range(1, len(sys.argv), 2):
268         args = sys.argv[i:i+2]
269         if len(args) < 2:
270             print("usage: mailcap [MIMEtype file] ...")
271             return
272         MIMEtype = args[0]
273         file = args[1]
274         command, e = findmatch(caps, MIMEtype, 'view', file)
275         if not command:
276             print("No viewer found for", type)
277         else:
278             print("Executing:", command)
279             sts = os.system(command)
280             sts = os.waitstatus_to_exitcode(sts)
281             if sts:
282                 print("Exit status:", sts)
283
284 def show(caps):
285     print("Mailcap files:")
286     for fn in listmailcapfiles(): print("\t" + fn)
287     print()
288     if not caps: caps = getcaps()
289     print("Mailcap entries:")
290     print()
291     ckeys = sorted(caps)
292     for type in ckeys:
293         print(type)
294         entries = caps[type]
295         for e in entries:
296             keys = sorted(e)
297             for k in keys:
298                 print("  %-15s" % k, e[k])
299             print()
300
301 if __name__ == '__main__':
302     test()