mailcap.py 9.1 KB

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