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