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