mailcap.py 9.1 KB

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