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