mailcap.py 8.9 KB

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