mailcap.py 8.0 KB

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