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