Mercurial > addons > slrn-macros > slrn-mime-support-macro
comparison mime-support.sl @ 0:cdc3d19f5ba5 default tip
Initial revision
author | Guido Berhoerster <guido+slrn@berhoerster.name> |
---|---|
date | Sat, 21 May 2016 11:12:14 +0200 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:cdc3d19f5ba5 |
---|---|
1 % Copyright (C) 2013 Guido Berhoerster <guido+slrn@berhoerster.name> | |
2 % | |
3 % This file incorporates work from the file mime.sl distributed with slrn under | |
4 % the terms of the GNU General Public Licens version 2 or later. | |
5 % | |
6 % Copyright (C) 2012 John E. Davis <jed@jedsoft.org> | |
7 % | |
8 % This program is free software; you can redistribute it and/or modify | |
9 % it under the terms of the GNU General Public License as published by | |
10 % the Free Software Foundation; either version 2 of the License, or | |
11 % (at your option) any later version. | |
12 % | |
13 % This program is distributed in the hope that it will be useful, | |
14 % but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 % GNU General Public License for more details. | |
17 % | |
18 % You should have received a copy of the GNU General Public License along | |
19 % with this program; if not, write to the Free Software Foundation, Inc., | |
20 % 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
21 | |
22 %open_log_file(make_home_filename("slrn-debug.log")); | |
23 %_traceback = 1; | |
24 | |
25 require("rand"); | |
26 require("mailcap"); | |
27 | |
28 implements("MIMESupport"); | |
29 | |
30 private variable FILENAME_CHARS = | |
31 "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |
32 private variable FILENAME_CHARS_LEN = strlen(FILENAME_CHARS); | |
33 private variable mime_save_dir = make_home_filename(""); | |
34 | |
35 private define quote_shell_arg(arg) | |
36 { | |
37 variable c; | |
38 variable result = "'"; | |
39 | |
40 foreach c (arg) using ("bytes") { | |
41 if (c == '\'') | |
42 result += "'\"'\"'"; | |
43 else | |
44 result += char(c); | |
45 } | |
46 result += "'"; | |
47 | |
48 return result; | |
49 } | |
50 | |
51 private define mkstemps(template, suffix_len) | |
52 { | |
53 variable fd; | |
54 variable temp_filename; | |
55 variable suffix; | |
56 variable template_len = strlen(@template); | |
57 variable c; | |
58 | |
59 if (template_len < 6) | |
60 return NULL; | |
61 c = template_len - suffix_len - 6; | |
62 suffix = substr(@template, template_len - suffix_len + 1, suffix_len); | |
63 if (substr(@template, c + 1, 6) != "XXXXXX") | |
64 return NULL; | |
65 | |
66 loop (10000) { | |
67 temp_filename = substr(@template, 1, c); | |
68 loop(6) { | |
69 temp_filename += FILENAME_CHARS[[rand() mod FILENAME_CHARS_LEN]]; | |
70 } | |
71 temp_filename += suffix; | |
72 fd = open(temp_filename, O_CREAT|O_EXCL|O_RDWR, 0600); | |
73 if (fd != NULL) { | |
74 @template = temp_filename; | |
75 break; | |
76 } | |
77 } | |
78 | |
79 return fd; | |
80 } | |
81 | |
82 private define mkstemp(template) | |
83 { | |
84 return mkstemps(template, 0); | |
85 } | |
86 | |
87 private variable mime_save_charset = get_charset("display"); | |
88 private variable raw_article; | |
89 private variable rendered_article; | |
90 private variable article; | |
91 private variable mime_object_list; | |
92 private variable tmpdir = getenv("TMPDIR"); | |
93 if (tmpdir == NULL) | |
94 tmpdir = "/tmp"; | |
95 private variable pager_command = getenv("PAGER"); | |
96 if (pager_command == NULL) | |
97 pager_command = "more"; | |
98 private variable auto_view_mailcap_entries = NULL; | |
99 static variable config = struct { | |
100 auto_view = ["text/html"] | |
101 }; | |
102 | |
103 private define mime_set_save_charset(charset) | |
104 { | |
105 mime_save_charset = charset; | |
106 } | |
107 | |
108 private define mime_get_save_charset() | |
109 { | |
110 return mime_save_charset; | |
111 } | |
112 | |
113 private define mime_set_header_key(hash, name, value) | |
114 { | |
115 hash[strlow(name)] = struct { | |
116 name = name, | |
117 value = strtrim(value), | |
118 }; | |
119 } | |
120 | |
121 private define mime_get_header_key(hash, name, lowercase) | |
122 { | |
123 try { | |
124 variable h = hash[strlow(name)]; | |
125 return h.value; | |
126 } catch AnyError; | |
127 | |
128 return ""; | |
129 } | |
130 | |
131 private define mime_split_article(art) | |
132 { | |
133 variable ofs = is_substrbytes(art, "\n\n"); | |
134 | |
135 if (ofs == 0) { | |
136 throw DataError, "Unable to find the header separator"; | |
137 } | |
138 | |
139 variable header = substrbytes(art, 1, ofs - 1); | |
140 (header,) = strreplace(header, "\n ", " ", strbytelen(header)); | |
141 (header,) = strreplace(header, "\n\t", " ", strbytelen(header)); | |
142 header = strchop(header, '\n', 0); | |
143 | |
144 variable hash = Assoc_Type[Struct_Type]; | |
145 _for (0, length(header) - 1, 1) { | |
146 variable i = (); | |
147 variable fields = strchop(header[i], ':', 0); | |
148 mime_set_header_key(hash, fields[0], strjoin(fields[[1:]], ":")); | |
149 } | |
150 variable body = substrbytes(art, ofs + 2, -1); | |
151 | |
152 return hash, body; | |
153 } | |
154 | |
155 private define mime_parse_subkeyword(key, word) | |
156 { | |
157 variable val = string_matches(key, `\C` + word + ` *= *"\([^"]+\)"`); | |
158 if (val == NULL) { | |
159 val = string_matches(key, `\C` + word + ` *= *\([^; ]+\)`); | |
160 } | |
161 if (val == NULL) { | |
162 return val; | |
163 } | |
164 | |
165 return val[1]; | |
166 } | |
167 | |
168 private define get_multipart_boundary(header) | |
169 { | |
170 variable ct = mime_get_header_key(header, "Content-Type", 0); | |
171 if (ct == "") { | |
172 return NULL; | |
173 } | |
174 | |
175 ifnot (is_substr(strlow(ct), "multipart/")) { | |
176 return NULL; | |
177 } | |
178 | |
179 variable boundary = mime_parse_subkeyword(ct, "boundary"); | |
180 if (boundary == NULL) { | |
181 return NULL; | |
182 } | |
183 | |
184 return boundary; | |
185 } | |
186 | |
187 % The idea here is to represent an article as a list of mime objects | |
188 % in the form of a tree. For a non-multipart article, there is only | |
189 % one node. For a multipart message, there will be a linked list of | |
190 % nodes, one for each subpart. If the subpart is a multipart, a new | |
191 % subtree will begin. For example, here is an article with a | |
192 % two-multiparts, with the second contained in the first. | |
193 % | |
194 % article | |
195 % / \ | |
196 % /\ | |
197 % | |
198 private variable Mime_Node_Type = struct { | |
199 mimetype, % lowercase type/subtype, from content-type | |
200 disposition, % content-disposition header | |
201 content_type, % full content-type header | |
202 header, % assoc array of header keywords | |
203 list, % non-null list of nodes if multipart | |
204 message, % non-multipart decoded message | |
205 charset, | |
206 encoding | |
207 }; | |
208 | |
209 private define mime_parse_mime(); | |
210 private define parse_multipart(node, body) | |
211 { | |
212 variable boundary = get_multipart_boundary(node.header); | |
213 if (boundary == NULL) { | |
214 return; | |
215 } | |
216 | |
217 boundary = "--" + boundary; | |
218 variable blen = strbytelen(boundary); | |
219 variable boundary_end = boundary + "--"; | |
220 variable blen_end = blen + 2; | |
221 | |
222 node.list = {}; | |
223 | |
224 body = strchop(body, '\n', 0); | |
225 variable i = 0; | |
226 variable imax = length(body); | |
227 while (i < imax) { | |
228 if (strnbytecmp(body[i], boundary, blen)) { | |
229 i++; | |
230 continue; | |
231 } | |
232 | |
233 if (strnbytecmp(body[i], boundary_end, blen_end) == 0) { | |
234 break; | |
235 } | |
236 | |
237 i++; | |
238 variable i0 = i; | |
239 if (i0 == imax) { | |
240 break; | |
241 } | |
242 | |
243 while (i < imax) { | |
244 if (strnbytecmp(body[i], boundary, blen)) { | |
245 i++; | |
246 continue; | |
247 } | |
248 break; | |
249 } | |
250 variable new_node = mime_parse_mime(strjoin(body[[i0:i-1]], "\n")); | |
251 if (new_node != NULL) { | |
252 list_append(node.list, new_node); | |
253 } | |
254 } | |
255 } | |
256 | |
257 private define mime_extract_mimetype(content_type) | |
258 { | |
259 return strlow(strtrim(strchop(content_type, ';', 0)[0])); | |
260 } | |
261 | |
262 private define mime_parse_mime(art) | |
263 { | |
264 variable header, body; | |
265 (header, body) = mime_split_article(art); | |
266 | |
267 variable node = @Mime_Node_Type; | |
268 node.content_type = mime_get_header_key(header, "Content-Type", 1); | |
269 node.disposition = mime_get_header_key(header, "Content-Disposition", 0); | |
270 node.header = header; | |
271 node.mimetype = mime_extract_mimetype(node.content_type); | |
272 | |
273 if (is_substr(node.mimetype, "multipart/")) { | |
274 parse_multipart(node, body); | |
275 return node; | |
276 } | |
277 | |
278 node.message = body; | |
279 | |
280 variable encoding = mime_get_header_key(header, | |
281 "Content-Transfer-Encoding", 1); | |
282 encoding = strlow(encoding); | |
283 if (is_substr(encoding, "base64")) { | |
284 node.encoding = "base64"; | |
285 } else if (is_substr(encoding, "quoted-printable")) { | |
286 node.encoding = "quoted-printable"; | |
287 } | |
288 | |
289 node.charset = mime_parse_subkeyword(node.content_type, "charset"); | |
290 | |
291 return node; | |
292 } | |
293 | |
294 private define mime_flatten_node_tree(node, leaves); % recursive | |
295 private define mime_flatten_node_tree(node, leaves) | |
296 { | |
297 if (node.list == NULL) { | |
298 list_append(leaves, node); | |
299 return; | |
300 } | |
301 | |
302 foreach node (node.list) { | |
303 mime_flatten_node_tree(node, leaves); | |
304 } | |
305 } | |
306 | |
307 % Returns NULL if the message is not Mime Encoded, otherwise it | |
308 % returns the value of the Content-Type header. | |
309 private define mime_is_mime_message() | |
310 { | |
311 variable h = extract_article_header("Mime-Version"); | |
312 if ((h == NULL) || (h == "")) { | |
313 return NULL; | |
314 } | |
315 | |
316 h = extract_article_header("Content-Type"); | |
317 if (h == "") { | |
318 return NULL; | |
319 } | |
320 return h; | |
321 } | |
322 | |
323 private define mime_is_attachment(node) | |
324 { | |
325 return is_substrbytes(strlow(node.disposition), "attachment"); | |
326 } | |
327 | |
328 private define mime_is_text(node) | |
329 { | |
330 return is_substrbytes(node.mimetype, "text/"); | |
331 } | |
332 | |
333 private define mime_get_mime_filename(node) | |
334 { | |
335 variable file = mime_parse_subkeyword(node.disposition, "filename"); | |
336 if (file != NULL) { | |
337 return file; | |
338 } | |
339 file = mime_parse_subkeyword(node.content_type, "name"); | |
340 if (file != NULL) { | |
341 return file; | |
342 } | |
343 | |
344 return ""; | |
345 } | |
346 | |
347 private define mime_convert_mime_object(obj) | |
348 { | |
349 variable str = obj.message; | |
350 if (str == "") { | |
351 return str; | |
352 } | |
353 | |
354 if (obj.encoding == "base64") { | |
355 str = decode_base64_string(str); | |
356 } else if (obj.encoding == "quoted-printable") { | |
357 str = decode_qp_string(str); | |
358 } | |
359 | |
360 variable charset = obj.charset; | |
361 if ((charset != NULL) && (charset != "") && (mime_save_charset != NULL) && | |
362 (strlow(charset) != strlow(mime_save_charset))) { | |
363 str = charset_convert_string(str, charset, mime_save_charset, 0); | |
364 } | |
365 return str; | |
366 } | |
367 | |
368 private define mime_save_mime_object(obj, fp) | |
369 { | |
370 if (typeof(fp) == String_Type) { | |
371 variable file = fp; | |
372 fp = fopen(file, "w"); | |
373 if (fp == NULL) { | |
374 throw OpenError, "Could not open $file for writing"$; | |
375 } | |
376 } | |
377 | |
378 variable str = mime_convert_mime_object(obj); | |
379 | |
380 () = fwrite(str, fp); | |
381 () = fflush(fp); | |
382 } | |
383 | |
384 private define find_filename_placeholder(template) | |
385 { | |
386 variable i = 0; | |
387 variable s; | |
388 variable len = strbytelen(template); | |
389 | |
390 while (i + 1 < len) { | |
391 s = template[[i:i + 1]]; | |
392 if (s == "\\%") { | |
393 i += 2; | |
394 } else if (s == "%s") { | |
395 return i; | |
396 } else { | |
397 i++; | |
398 } | |
399 } | |
400 | |
401 return NULL; | |
402 } | |
403 | |
404 private define mailcap_substitute(template, filename, content_type) | |
405 { | |
406 variable mimetype = mime_extract_mimetype(content_type); | |
407 variable i = 0; | |
408 variable j; | |
409 variable s; | |
410 variable len = strbytelen(template); | |
411 variable key; | |
412 variable value; | |
413 variable result = ""; | |
414 | |
415 while (i < len) { | |
416 if (i + 1 < len) { | |
417 s = template[[i:i + 1]]; | |
418 switch(s) | |
419 { | |
420 case "\\%": | |
421 result += "%"; | |
422 i += 2; | |
423 } | |
424 { | |
425 case "%s": | |
426 result += filename; | |
427 i += 2; | |
428 } | |
429 { | |
430 case "%t": | |
431 result += mimetype; | |
432 i += 2; | |
433 } | |
434 { | |
435 case "%{": | |
436 key = NULL; | |
437 for (j = i + 2; j < len; j++) { | |
438 if (template[j] == '}') { | |
439 key = template[[i + 2:j -1]]; | |
440 break; | |
441 } else ifnot (isalnum(template[j])) { | |
442 break; | |
443 } | |
444 } | |
445 if (key != NULL) { | |
446 if (key == "charset") | |
447 value = mime_get_save_charset(); | |
448 else | |
449 value = mime_parse_subkeyword(content_type, key); | |
450 if (value == NULL) | |
451 value = ""; | |
452 result += quote_shell_arg(value); | |
453 i = j + 1; | |
454 } else { | |
455 result += template[[i]]; | |
456 i++; | |
457 } | |
458 } | |
459 { | |
460 result += template[[i]]; | |
461 i++; | |
462 } | |
463 } else { | |
464 result += template[[i]]; | |
465 i++; | |
466 } | |
467 } | |
468 | |
469 return result; | |
470 } | |
471 | |
472 private define mailcap_view_part(mc_entry, data) | |
473 { | |
474 variable filter = qualifier_exists("filter"); | |
475 variable lines; | |
476 variable command; | |
477 variable use_input_tmpfile; | |
478 variable mask; | |
479 variable fd = NULL; | |
480 variable fp_in = NULL; | |
481 variable fp_out = NULL; | |
482 variable fp_pager = NULL; | |
483 variable i; | |
484 variable tmpfilename; | |
485 variable suffixlen; | |
486 variable command_status = 0; | |
487 variable pager_status = 0; | |
488 variable text; | |
489 | |
490 if (filter && (mc_entry._copiousoutput == 0)) | |
491 return NULL; | |
492 | |
493 try { | |
494 command = mc_entry._command; | |
495 use_input_tmpfile = (find_filename_placeholder(command) != NULL); | |
496 if (use_input_tmpfile) { | |
497 % the command reads the input from a temporary file | |
498 tmpfilename = mc_entry._nametemplate; | |
499 if ((tmpfilename == NULL) || | |
500 (find_filename_placeholder(tmpfilename) == NULL)) | |
501 tmpfilename = "slrn%s"; | |
502 tmpfilename = path_concat(tmpdir, path_basename(tmpfilename)); | |
503 i = find_filename_placeholder(tmpfilename); | |
504 suffixlen = strbytelen(tmpfilename) - (i + 2); | |
505 tmpfilename = tmpfilename[[0:i - 1]] + "XXXXXX" + | |
506 tmpfilename[[i + 2:]]; | |
507 mask = umask(077); | |
508 fd = mkstemps(&tmpfilename, suffixlen); | |
509 if (fd == NULL) | |
510 throw OpenError, "Could not create temporary file"; | |
511 fp_in = fdopen(fd, "w+"); | |
512 if (fp_in == NULL) | |
513 throw OpenError, "Could not open temporary file"; | |
514 if (fwrite(data, fp_in) == -1) { | |
515 throw WriteError, | |
516 "Failed to write to file \"$tmpfilename\": "$ + | |
517 errno_string(errno); | |
518 } | |
519 () = fflush(fp_in); | |
520 | |
521 command = mailcap_substitute(command, tmpfilename, mc_entry._type); | |
522 if (mc_entry._copiousoutput) { | |
523 % output is read back from the command's stdout | |
524 fp_out = popen(command, "r"); | |
525 if (fp_out == NULL) | |
526 throw OSError, "Failed to execute $command"$; | |
527 } else { | |
528 system(command); | |
529 } | |
530 } else { | |
531 % the command reads the input from its stdin | |
532 command = mailcap_substitute(command, "", mc_entry._type); | |
533 if (mc_entry._copiousoutput) { | |
534 % create temporary file for the output if the command is | |
535 % non-interactive | |
536 tmpfilename = path_concat(tmpdir, "slrnXXXXXX"); | |
537 mask = umask(077); | |
538 fd = mkstemp(&tmpfilename); | |
539 if (fd == NULL) | |
540 throw OpenError, "Could not create temporary file"; | |
541 fp_out = fdopen(fd, "r+"); | |
542 if (fp_out == NULL) | |
543 throw OpenError, "Could not open temporary file"; | |
544 | |
545 command += " > " + tmpfilename; | |
546 } | |
547 | |
548 fp_in = popen(command, "w"); | |
549 if (fp_in == NULL) | |
550 throw OSError, "Failed to execute $command"$; | |
551 if (fputs(data, fp_in) == -1) | |
552 throw WriteError, | |
553 "Failed to write to command \"$command\": "$ + | |
554 errno_string(errno); | |
555 () = fflush(fp_in); | |
556 command_status = pclose(fp_in); | |
557 fp_in = NULL; | |
558 ifnot (command_status == 0) { | |
559 throw OSError, | |
560 "Command \"$command\" returned a non-zero exit "$ + | |
561 "status: " + string(command_status); | |
562 } | |
563 } | |
564 | |
565 % read back the output if the command is non-interactive | |
566 if (mc_entry._copiousoutput) { | |
567 lines = fgetslines(fp_out); | |
568 if (lines == NULL) | |
569 throw ReadError, "Failed to read output: " + | |
570 errno_string(errno); | |
571 text = strjoin(lines, ""); | |
572 | |
573 if (filter) { | |
574 return text; | |
575 } else { | |
576 fp_pager = popen(pager_command, "w"); | |
577 if (fp_pager == NULL) | |
578 throw OSError, "Failed to execute $pager_command"$; | |
579 if (fputs(text, fp_pager) == -1) | |
580 throw WriteError, | |
581 "Failed to write to command \"$command\": "$ + | |
582 errno_string(errno); | |
583 () = fflush(fp_pager); | |
584 } | |
585 } | |
586 | |
587 return NULL; | |
588 } finally { | |
589 % remove temporary input or output file | |
590 if (fd != NULL) | |
591 () = remove(tmpfilename); | |
592 | |
593 if (use_input_tmpfile) { | |
594 if (fp_in != NULL) | |
595 () = fclose(fp_in); | |
596 else if (fd != NULL) | |
597 () = close(fd); | |
598 | |
599 if (fp_out != NULL) | |
600 command_status = pclose(fp_out); | |
601 } else { | |
602 if (mc_entry._copiousoutput) { | |
603 if (fp_out != NULL) | |
604 () = fclose(fp_out); | |
605 else if (fd != NULL) | |
606 () = close(fd); | |
607 } | |
608 | |
609 if (fp_in != NULL) | |
610 command_status = pclose(fp_in); | |
611 } | |
612 | |
613 if (fp_pager != NULL) { | |
614 pager_status = pclose(fp_pager); | |
615 } | |
616 | |
617 if (command_status != 0) | |
618 throw OSError, | |
619 "Command \"$command\" returned a non-zero exit "$ + | |
620 "status: " + string(command_status); | |
621 | |
622 if (pager_status != 0) | |
623 throw OSError, | |
624 "Command \"$pager_command\" returned a"$ + | |
625 "non-zero exit status: " + string(pager_status); | |
626 } | |
627 } | |
628 | |
629 private define render_part(node, rendered_message); | |
630 private define render_part(node, rendered_message) | |
631 { | |
632 variable mc_entry; | |
633 variable i; | |
634 variable j; | |
635 variable best_match_node = NULL; | |
636 variable text_node = NULL; | |
637 variable subnode; | |
638 variable text; | |
639 variable raw_message; | |
640 variable header; | |
641 variable value; | |
642 | |
643 if (node.mimetype == "multipart/alternative") { | |
644 % select best match based on the order of the entries in | |
645 % config.auto_view, text/plain is always preferred and the first text | |
646 % part is used as a fallback in case there is no match | |
647 j = length(auto_view_mailcap_entries); | |
648 foreach subnode (node.list) { | |
649 if (subnode.mimetype == "text/plain") { | |
650 best_match_node = subnode; | |
651 break; | |
652 } | |
653 for (i = 0; i < j; i++) { | |
654 if (subnode.mimetype == auto_view_mailcap_entries[i]._type) { | |
655 best_match_node = subnode; | |
656 j = i; | |
657 break; | |
658 } else if ((text_node == NULL) && mime_is_text(subnode)) { | |
659 text_node = subnode; | |
660 } | |
661 } | |
662 } | |
663 if (best_match_node != NULL) { | |
664 render_part(best_match_node, rendered_message); | |
665 } else if (text_node != NULL) { | |
666 render_part(text_node, rendered_message); | |
667 } else { | |
668 @rendered_message += "[-- Unhandled MIME Alternative Parts --]\n\n"; | |
669 } | |
670 } else if ((node.mimetype == "multipart/mixed") || | |
671 (node.mimetype == "multipart/digest") || | |
672 (node.mimetype == "multipart/related") || | |
673 (node.mimetype == "multipart/signed")) { | |
674 foreach subnode (node.list) { | |
675 render_part(subnode, rendered_message); | |
676 } | |
677 } else if (node.mimetype == "message/rfc822") { | |
678 % inline message | |
679 subnode = mime_parse_mime(node.message); | |
680 | |
681 @rendered_message += "[-- MIME Message (" + node.mimetype + ") --]\n\n"; | |
682 foreach header (strchop(get_visible_headers(), ',', 0)) { | |
683 value = mime_get_header_key(subnode.header, strtrim_end(header, ":"), | |
684 1); | |
685 ifnot (value == "") { | |
686 @rendered_message += sprintf("%s %s\n", header, value); | |
687 } | |
688 } | |
689 @rendered_message += "\n"; | |
690 | |
691 if (subnode.mimetype != "") { | |
692 render_part(subnode, rendered_message); | |
693 } else { | |
694 @rendered_message += subnode.message + "\n"; | |
695 } | |
696 } else if (node.mimetype == "text/plain") { | |
697 @rendered_message += mime_convert_mime_object(node) + "\n"; | |
698 } else { | |
699 foreach mc_entry (auto_view_mailcap_entries) { | |
700 % check if the MIME type is in config.auto_view and if a | |
701 % corresponding mailcap entry exists | |
702 if (node.mimetype == mc_entry._type) { | |
703 @rendered_message += "[-- MIME Part (" + node.mimetype + | |
704 ") --]\n\n"; | |
705 text = mailcap_view_part(mc_entry, | |
706 mime_convert_mime_object(node); filter); | |
707 if (text == NULL) | |
708 text = "[-- Failed to convert MIME Part to text/plain " + | |
709 "--]\n\n"; | |
710 @rendered_message += text + "\n"; | |
711 break; | |
712 } | |
713 } then { | |
714 if (mime_is_text(node)) { | |
715 % otherwise check if the part has a text MIME type and display | |
716 % that as-is | |
717 @rendered_message += "[-- MIME Part (" + node.mimetype + | |
718 ") --]\n\n" + mime_convert_mime_object(node) + "\n"; | |
719 } else { | |
720 @rendered_message += "[-- Unhandled MIME Part (" + | |
721 node.mimetype + ") --]\n\n"; | |
722 } | |
723 } | |
724 } | |
725 } | |
726 | |
727 static define mime_process_article() | |
728 { | |
729 variable content_type = extract_article_header("Content-Type"); | |
730 variable mimetype; | |
731 variable mc_entry; | |
732 variable header; | |
733 variable body = NULL; | |
734 variable text; | |
735 variable node; | |
736 variable value; | |
737 | |
738 % initialize list of existing config.auto_view mailcap entries | |
739 if (auto_view_mailcap_entries == NULL) { | |
740 auto_view_mailcap_entries = {}; | |
741 foreach mimetype (config.auto_view) { | |
742 mc_entry = mailcap_lookup_entry(mimetype); | |
743 if ((mc_entry != NULL) && mc_entry._copiousoutput) | |
744 list_append(auto_view_mailcap_entries, mc_entry); | |
745 } | |
746 } | |
747 | |
748 raw_article = raw_article_as_string(); | |
749 rendered_article = NULL; | |
750 article = &raw_article; | |
751 | |
752 if (mime_is_mime_message() == NULL) { | |
753 mime_object_list = NULL; | |
754 mimetype = mime_extract_mimetype(content_type); | |
755 | |
756 % handle non-MIME-encoded articles with a Content-Type | |
757 foreach mc_entry (auto_view_mailcap_entries) { | |
758 % check if the MIME type is in config.auto_view and if a | |
759 % corresponding mailcap entry exists | |
760 if (mimetype == mc_entry._type) { | |
761 (header, body) = mime_split_article(raw_article); | |
762 | |
763 rendered_article = ""; | |
764 foreach value (header) using ("values") { | |
765 rendered_article += sprintf("%s: %s\n", value.name, | |
766 value.value); | |
767 } | |
768 rendered_article += "\n"; | |
769 text = mailcap_view_part(mc_entry, body; filter); | |
770 if (text != NULL) { | |
771 rendered_article += "[-- Content (" + mimetype + | |
772 ") --]\n\n" + text + "\n"; | |
773 } else { | |
774 rendered_article += "[-- Failed to convert content to " + | |
775 "text/plain --]\n\n"; | |
776 } | |
777 break; | |
778 } | |
779 } | |
780 | |
781 return; | |
782 } | |
783 mime_object_list = {}; | |
784 node = mime_parse_mime(raw_article); | |
785 | |
786 mime_flatten_node_tree(node, mime_object_list); | |
787 | |
788 rendered_article = ""; | |
789 foreach value (node.header) using ("values") { | |
790 rendered_article += sprintf("%s: %s\n", value.name, value.value); | |
791 } | |
792 rendered_article += "\n"; | |
793 | |
794 render_part(node, &rendered_article); | |
795 | |
796 return; | |
797 } | |
798 | |
799 static define mime_show_raw_article() | |
800 { | |
801 if (article != &raw_article) { | |
802 article = &raw_article; | |
803 replace_article(raw_article); | |
804 update(); | |
805 } | |
806 } | |
807 | |
808 static define mime_show_rendered_article() | |
809 { | |
810 if ((article != &rendered_article) && (rendered_article != NULL)) { | |
811 article = &rendered_article; | |
812 replace_article(rendered_article); | |
813 update(); | |
814 } | |
815 } | |
816 | |
817 static define mime_toggle_view() | |
818 { | |
819 if (article == &raw_article) | |
820 mime_show_rendered_article(); | |
821 else | |
822 mime_show_raw_article(); | |
823 } | |
824 | |
825 static define mime_select_part(title) | |
826 { | |
827 variable selection_list = {}; | |
828 variable i; | |
829 variable len; | |
830 variable selection; | |
831 | |
832 if (mime_object_list == NULL) | |
833 return NULL; | |
834 | |
835 len = length(mime_object_list); | |
836 for (i = 0; i < len; i++) { | |
837 list_append(selection_list, sprintf("%d. %s", i + 1, | |
838 mime_object_list[i].mimetype)); | |
839 } | |
840 if (i < 1) | |
841 return NULL; | |
842 | |
843 title; | |
844 __push_list(selection_list); | |
845 i; | |
846 0; | |
847 selection = select_list_box(); | |
848 if (selection == "") | |
849 return NULL; | |
850 | |
851 return mime_object_list[integer(substr(selection, 1, 1)) - 1]; | |
852 } | |
853 | |
854 static define mime_save_part() | |
855 { | |
856 variable node = mime_select_part("Select MIME part to save"); | |
857 variable filename; | |
858 variable st; | |
859 variable n; | |
860 | |
861 if (node == NULL) | |
862 return; | |
863 | |
864 filename = path_basename(rfc1522_decode_string(mime_get_mime_filename(node))); | |
865 filename = path_concat(mime_save_dir, filename); | |
866 filename = strtrim(filename); | |
867 forever { | |
868 filename = read_mini_filename("Save to:", "", filename); | |
869 filename = strtrim(filename); | |
870 if ((filename == "") || (filename == mime_save_dir)) { | |
871 message_now("Cancelled"); | |
872 return; | |
873 } | |
874 st = stat_file(filename); | |
875 if (st != NULL) { | |
876 if (stat_is("lnk", st.st_mode) || stat_is("reg", st.st_mode)) { | |
877 n = get_yes_no_cancel("File '$filename' exists, Overwrite?"$, | |
878 0); | |
879 if (n == 0) | |
880 continue; | |
881 else if (n == -1) | |
882 message_now("Cancelled"); | |
883 return; | |
884 } else { | |
885 throw OpenError, "Could not open '$filename' for writing"$; | |
886 } | |
887 } | |
888 mime_save_mime_object(node, filename); | |
889 message_now("Saved to '$filename'"$); | |
890 mime_save_dir = path_dirname(filename); | |
891 break; | |
892 } | |
893 } | |
894 | |
895 static define mime_view_part() | |
896 { | |
897 variable mc; | |
898 variable e; | |
899 variable line; | |
900 variable node = mime_select_part("Select MIME part to view"); | |
901 if (node == NULL) | |
902 return; | |
903 | |
904 mc = mailcap_lookup_entry(node.content_type); | |
905 if (mc == NULL) | |
906 throw NotImplementedError, "No viewer for '" + node.mimetype + | |
907 "' available"; | |
908 mc.view = &mailcap_view_part; | |
909 | |
910 try (e) { | |
911 set_display_state(0); | |
912 mc.view(mime_convert_mime_object(node)); | |
913 } catch OSError: { | |
914 () = fprintf(stdout, "\n*** ERROR: %S\n\nPress enter to continue.", | |
915 e.message); | |
916 () = fgets(&line, stdin); | |
917 throw; | |
918 } finally { | |
919 set_display_state(1); | |
920 } | |
921 } | |
922 | |
923 () = register_hook("read_article_hook", | |
924 "MIMESupport->mime_show_rendered_article"); | |
925 () = register_hook("read_article_hook", "MIMESupport->mime_process_article"); |