Mercurial > addons > slrn-macros > slrn-mime-support-macro
view 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 |
line wrap: on
line source
% Copyright (C) 2013 Guido Berhoerster <guido+slrn@berhoerster.name> % % This file incorporates work from the file mime.sl distributed with slrn under % the terms of the GNU General Public Licens version 2 or later. % % Copyright (C) 2012 John E. Davis <jed@jedsoft.org> % % This program is free software; you can redistribute it and/or modify % it under the terms of the GNU General Public License as published by % the Free Software Foundation; either version 2 of the License, or % (at your option) any later version. % % This program is distributed in the hope that it will be useful, % but WITHOUT ANY WARRANTY; without even the implied warranty of % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the % GNU General Public License for more details. % % You should have received a copy of the GNU General Public License along % with this program; if not, write to the Free Software Foundation, Inc., % 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %open_log_file(make_home_filename("slrn-debug.log")); %_traceback = 1; require("rand"); require("mailcap"); implements("MIMESupport"); private variable FILENAME_CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; private variable FILENAME_CHARS_LEN = strlen(FILENAME_CHARS); private variable mime_save_dir = make_home_filename(""); private define quote_shell_arg(arg) { variable c; variable result = "'"; foreach c (arg) using ("bytes") { if (c == '\'') result += "'\"'\"'"; else result += char(c); } result += "'"; return result; } private define mkstemps(template, suffix_len) { variable fd; variable temp_filename; variable suffix; variable template_len = strlen(@template); variable c; if (template_len < 6) return NULL; c = template_len - suffix_len - 6; suffix = substr(@template, template_len - suffix_len + 1, suffix_len); if (substr(@template, c + 1, 6) != "XXXXXX") return NULL; loop (10000) { temp_filename = substr(@template, 1, c); loop(6) { temp_filename += FILENAME_CHARS[[rand() mod FILENAME_CHARS_LEN]]; } temp_filename += suffix; fd = open(temp_filename, O_CREAT|O_EXCL|O_RDWR, 0600); if (fd != NULL) { @template = temp_filename; break; } } return fd; } private define mkstemp(template) { return mkstemps(template, 0); } private variable mime_save_charset = get_charset("display"); private variable raw_article; private variable rendered_article; private variable article; private variable mime_object_list; private variable tmpdir = getenv("TMPDIR"); if (tmpdir == NULL) tmpdir = "/tmp"; private variable pager_command = getenv("PAGER"); if (pager_command == NULL) pager_command = "more"; private variable auto_view_mailcap_entries = NULL; static variable config = struct { auto_view = ["text/html"] }; private define mime_set_save_charset(charset) { mime_save_charset = charset; } private define mime_get_save_charset() { return mime_save_charset; } private define mime_set_header_key(hash, name, value) { hash[strlow(name)] = struct { name = name, value = strtrim(value), }; } private define mime_get_header_key(hash, name, lowercase) { try { variable h = hash[strlow(name)]; return h.value; } catch AnyError; return ""; } private define mime_split_article(art) { variable ofs = is_substrbytes(art, "\n\n"); if (ofs == 0) { throw DataError, "Unable to find the header separator"; } variable header = substrbytes(art, 1, ofs - 1); (header,) = strreplace(header, "\n ", " ", strbytelen(header)); (header,) = strreplace(header, "\n\t", " ", strbytelen(header)); header = strchop(header, '\n', 0); variable hash = Assoc_Type[Struct_Type]; _for (0, length(header) - 1, 1) { variable i = (); variable fields = strchop(header[i], ':', 0); mime_set_header_key(hash, fields[0], strjoin(fields[[1:]], ":")); } variable body = substrbytes(art, ofs + 2, -1); return hash, body; } private define mime_parse_subkeyword(key, word) { variable val = string_matches(key, `\C` + word + ` *= *"\([^"]+\)"`); if (val == NULL) { val = string_matches(key, `\C` + word + ` *= *\([^; ]+\)`); } if (val == NULL) { return val; } return val[1]; } private define get_multipart_boundary(header) { variable ct = mime_get_header_key(header, "Content-Type", 0); if (ct == "") { return NULL; } ifnot (is_substr(strlow(ct), "multipart/")) { return NULL; } variable boundary = mime_parse_subkeyword(ct, "boundary"); if (boundary == NULL) { return NULL; } return boundary; } % The idea here is to represent an article as a list of mime objects % in the form of a tree. For a non-multipart article, there is only % one node. For a multipart message, there will be a linked list of % nodes, one for each subpart. If the subpart is a multipart, a new % subtree will begin. For example, here is an article with a % two-multiparts, with the second contained in the first. % % article % / \ % /\ % private variable Mime_Node_Type = struct { mimetype, % lowercase type/subtype, from content-type disposition, % content-disposition header content_type, % full content-type header header, % assoc array of header keywords list, % non-null list of nodes if multipart message, % non-multipart decoded message charset, encoding }; private define mime_parse_mime(); private define parse_multipart(node, body) { variable boundary = get_multipart_boundary(node.header); if (boundary == NULL) { return; } boundary = "--" + boundary; variable blen = strbytelen(boundary); variable boundary_end = boundary + "--"; variable blen_end = blen + 2; node.list = {}; body = strchop(body, '\n', 0); variable i = 0; variable imax = length(body); while (i < imax) { if (strnbytecmp(body[i], boundary, blen)) { i++; continue; } if (strnbytecmp(body[i], boundary_end, blen_end) == 0) { break; } i++; variable i0 = i; if (i0 == imax) { break; } while (i < imax) { if (strnbytecmp(body[i], boundary, blen)) { i++; continue; } break; } variable new_node = mime_parse_mime(strjoin(body[[i0:i-1]], "\n")); if (new_node != NULL) { list_append(node.list, new_node); } } } private define mime_extract_mimetype(content_type) { return strlow(strtrim(strchop(content_type, ';', 0)[0])); } private define mime_parse_mime(art) { variable header, body; (header, body) = mime_split_article(art); variable node = @Mime_Node_Type; node.content_type = mime_get_header_key(header, "Content-Type", 1); node.disposition = mime_get_header_key(header, "Content-Disposition", 0); node.header = header; node.mimetype = mime_extract_mimetype(node.content_type); if (is_substr(node.mimetype, "multipart/")) { parse_multipart(node, body); return node; } node.message = body; variable encoding = mime_get_header_key(header, "Content-Transfer-Encoding", 1); encoding = strlow(encoding); if (is_substr(encoding, "base64")) { node.encoding = "base64"; } else if (is_substr(encoding, "quoted-printable")) { node.encoding = "quoted-printable"; } node.charset = mime_parse_subkeyword(node.content_type, "charset"); return node; } private define mime_flatten_node_tree(node, leaves); % recursive private define mime_flatten_node_tree(node, leaves) { if (node.list == NULL) { list_append(leaves, node); return; } foreach node (node.list) { mime_flatten_node_tree(node, leaves); } } % Returns NULL if the message is not Mime Encoded, otherwise it % returns the value of the Content-Type header. private define mime_is_mime_message() { variable h = extract_article_header("Mime-Version"); if ((h == NULL) || (h == "")) { return NULL; } h = extract_article_header("Content-Type"); if (h == "") { return NULL; } return h; } private define mime_is_attachment(node) { return is_substrbytes(strlow(node.disposition), "attachment"); } private define mime_is_text(node) { return is_substrbytes(node.mimetype, "text/"); } private define mime_get_mime_filename(node) { variable file = mime_parse_subkeyword(node.disposition, "filename"); if (file != NULL) { return file; } file = mime_parse_subkeyword(node.content_type, "name"); if (file != NULL) { return file; } return ""; } private define mime_convert_mime_object(obj) { variable str = obj.message; if (str == "") { return str; } if (obj.encoding == "base64") { str = decode_base64_string(str); } else if (obj.encoding == "quoted-printable") { str = decode_qp_string(str); } variable charset = obj.charset; if ((charset != NULL) && (charset != "") && (mime_save_charset != NULL) && (strlow(charset) != strlow(mime_save_charset))) { str = charset_convert_string(str, charset, mime_save_charset, 0); } return str; } private define mime_save_mime_object(obj, fp) { if (typeof(fp) == String_Type) { variable file = fp; fp = fopen(file, "w"); if (fp == NULL) { throw OpenError, "Could not open $file for writing"$; } } variable str = mime_convert_mime_object(obj); () = fwrite(str, fp); () = fflush(fp); } private define find_filename_placeholder(template) { variable i = 0; variable s; variable len = strbytelen(template); while (i + 1 < len) { s = template[[i:i + 1]]; if (s == "\\%") { i += 2; } else if (s == "%s") { return i; } else { i++; } } return NULL; } private define mailcap_substitute(template, filename, content_type) { variable mimetype = mime_extract_mimetype(content_type); variable i = 0; variable j; variable s; variable len = strbytelen(template); variable key; variable value; variable result = ""; while (i < len) { if (i + 1 < len) { s = template[[i:i + 1]]; switch(s) { case "\\%": result += "%"; i += 2; } { case "%s": result += filename; i += 2; } { case "%t": result += mimetype; i += 2; } { case "%{": key = NULL; for (j = i + 2; j < len; j++) { if (template[j] == '}') { key = template[[i + 2:j -1]]; break; } else ifnot (isalnum(template[j])) { break; } } if (key != NULL) { if (key == "charset") value = mime_get_save_charset(); else value = mime_parse_subkeyword(content_type, key); if (value == NULL) value = ""; result += quote_shell_arg(value); i = j + 1; } else { result += template[[i]]; i++; } } { result += template[[i]]; i++; } } else { result += template[[i]]; i++; } } return result; } private define mailcap_view_part(mc_entry, data) { variable filter = qualifier_exists("filter"); variable lines; variable command; variable use_input_tmpfile; variable mask; variable fd = NULL; variable fp_in = NULL; variable fp_out = NULL; variable fp_pager = NULL; variable i; variable tmpfilename; variable suffixlen; variable command_status = 0; variable pager_status = 0; variable text; if (filter && (mc_entry._copiousoutput == 0)) return NULL; try { command = mc_entry._command; use_input_tmpfile = (find_filename_placeholder(command) != NULL); if (use_input_tmpfile) { % the command reads the input from a temporary file tmpfilename = mc_entry._nametemplate; if ((tmpfilename == NULL) || (find_filename_placeholder(tmpfilename) == NULL)) tmpfilename = "slrn%s"; tmpfilename = path_concat(tmpdir, path_basename(tmpfilename)); i = find_filename_placeholder(tmpfilename); suffixlen = strbytelen(tmpfilename) - (i + 2); tmpfilename = tmpfilename[[0:i - 1]] + "XXXXXX" + tmpfilename[[i + 2:]]; mask = umask(077); fd = mkstemps(&tmpfilename, suffixlen); if (fd == NULL) throw OpenError, "Could not create temporary file"; fp_in = fdopen(fd, "w+"); if (fp_in == NULL) throw OpenError, "Could not open temporary file"; if (fwrite(data, fp_in) == -1) { throw WriteError, "Failed to write to file \"$tmpfilename\": "$ + errno_string(errno); } () = fflush(fp_in); command = mailcap_substitute(command, tmpfilename, mc_entry._type); if (mc_entry._copiousoutput) { % output is read back from the command's stdout fp_out = popen(command, "r"); if (fp_out == NULL) throw OSError, "Failed to execute $command"$; } else { system(command); } } else { % the command reads the input from its stdin command = mailcap_substitute(command, "", mc_entry._type); if (mc_entry._copiousoutput) { % create temporary file for the output if the command is % non-interactive tmpfilename = path_concat(tmpdir, "slrnXXXXXX"); mask = umask(077); fd = mkstemp(&tmpfilename); if (fd == NULL) throw OpenError, "Could not create temporary file"; fp_out = fdopen(fd, "r+"); if (fp_out == NULL) throw OpenError, "Could not open temporary file"; command += " > " + tmpfilename; } fp_in = popen(command, "w"); if (fp_in == NULL) throw OSError, "Failed to execute $command"$; if (fputs(data, fp_in) == -1) throw WriteError, "Failed to write to command \"$command\": "$ + errno_string(errno); () = fflush(fp_in); command_status = pclose(fp_in); fp_in = NULL; ifnot (command_status == 0) { throw OSError, "Command \"$command\" returned a non-zero exit "$ + "status: " + string(command_status); } } % read back the output if the command is non-interactive if (mc_entry._copiousoutput) { lines = fgetslines(fp_out); if (lines == NULL) throw ReadError, "Failed to read output: " + errno_string(errno); text = strjoin(lines, ""); if (filter) { return text; } else { fp_pager = popen(pager_command, "w"); if (fp_pager == NULL) throw OSError, "Failed to execute $pager_command"$; if (fputs(text, fp_pager) == -1) throw WriteError, "Failed to write to command \"$command\": "$ + errno_string(errno); () = fflush(fp_pager); } } return NULL; } finally { % remove temporary input or output file if (fd != NULL) () = remove(tmpfilename); if (use_input_tmpfile) { if (fp_in != NULL) () = fclose(fp_in); else if (fd != NULL) () = close(fd); if (fp_out != NULL) command_status = pclose(fp_out); } else { if (mc_entry._copiousoutput) { if (fp_out != NULL) () = fclose(fp_out); else if (fd != NULL) () = close(fd); } if (fp_in != NULL) command_status = pclose(fp_in); } if (fp_pager != NULL) { pager_status = pclose(fp_pager); } if (command_status != 0) throw OSError, "Command \"$command\" returned a non-zero exit "$ + "status: " + string(command_status); if (pager_status != 0) throw OSError, "Command \"$pager_command\" returned a"$ + "non-zero exit status: " + string(pager_status); } } private define render_part(node, rendered_message); private define render_part(node, rendered_message) { variable mc_entry; variable i; variable j; variable best_match_node = NULL; variable text_node = NULL; variable subnode; variable text; variable raw_message; variable header; variable value; if (node.mimetype == "multipart/alternative") { % select best match based on the order of the entries in % config.auto_view, text/plain is always preferred and the first text % part is used as a fallback in case there is no match j = length(auto_view_mailcap_entries); foreach subnode (node.list) { if (subnode.mimetype == "text/plain") { best_match_node = subnode; break; } for (i = 0; i < j; i++) { if (subnode.mimetype == auto_view_mailcap_entries[i]._type) { best_match_node = subnode; j = i; break; } else if ((text_node == NULL) && mime_is_text(subnode)) { text_node = subnode; } } } if (best_match_node != NULL) { render_part(best_match_node, rendered_message); } else if (text_node != NULL) { render_part(text_node, rendered_message); } else { @rendered_message += "[-- Unhandled MIME Alternative Parts --]\n\n"; } } else if ((node.mimetype == "multipart/mixed") || (node.mimetype == "multipart/digest") || (node.mimetype == "multipart/related") || (node.mimetype == "multipart/signed")) { foreach subnode (node.list) { render_part(subnode, rendered_message); } } else if (node.mimetype == "message/rfc822") { % inline message subnode = mime_parse_mime(node.message); @rendered_message += "[-- MIME Message (" + node.mimetype + ") --]\n\n"; foreach header (strchop(get_visible_headers(), ',', 0)) { value = mime_get_header_key(subnode.header, strtrim_end(header, ":"), 1); ifnot (value == "") { @rendered_message += sprintf("%s %s\n", header, value); } } @rendered_message += "\n"; if (subnode.mimetype != "") { render_part(subnode, rendered_message); } else { @rendered_message += subnode.message + "\n"; } } else if (node.mimetype == "text/plain") { @rendered_message += mime_convert_mime_object(node) + "\n"; } else { foreach mc_entry (auto_view_mailcap_entries) { % check if the MIME type is in config.auto_view and if a % corresponding mailcap entry exists if (node.mimetype == mc_entry._type) { @rendered_message += "[-- MIME Part (" + node.mimetype + ") --]\n\n"; text = mailcap_view_part(mc_entry, mime_convert_mime_object(node); filter); if (text == NULL) text = "[-- Failed to convert MIME Part to text/plain " + "--]\n\n"; @rendered_message += text + "\n"; break; } } then { if (mime_is_text(node)) { % otherwise check if the part has a text MIME type and display % that as-is @rendered_message += "[-- MIME Part (" + node.mimetype + ") --]\n\n" + mime_convert_mime_object(node) + "\n"; } else { @rendered_message += "[-- Unhandled MIME Part (" + node.mimetype + ") --]\n\n"; } } } } static define mime_process_article() { variable content_type = extract_article_header("Content-Type"); variable mimetype; variable mc_entry; variable header; variable body = NULL; variable text; variable node; variable value; % initialize list of existing config.auto_view mailcap entries if (auto_view_mailcap_entries == NULL) { auto_view_mailcap_entries = {}; foreach mimetype (config.auto_view) { mc_entry = mailcap_lookup_entry(mimetype); if ((mc_entry != NULL) && mc_entry._copiousoutput) list_append(auto_view_mailcap_entries, mc_entry); } } raw_article = raw_article_as_string(); rendered_article = NULL; article = &raw_article; if (mime_is_mime_message() == NULL) { mime_object_list = NULL; mimetype = mime_extract_mimetype(content_type); % handle non-MIME-encoded articles with a Content-Type foreach mc_entry (auto_view_mailcap_entries) { % check if the MIME type is in config.auto_view and if a % corresponding mailcap entry exists if (mimetype == mc_entry._type) { (header, body) = mime_split_article(raw_article); rendered_article = ""; foreach value (header) using ("values") { rendered_article += sprintf("%s: %s\n", value.name, value.value); } rendered_article += "\n"; text = mailcap_view_part(mc_entry, body; filter); if (text != NULL) { rendered_article += "[-- Content (" + mimetype + ") --]\n\n" + text + "\n"; } else { rendered_article += "[-- Failed to convert content to " + "text/plain --]\n\n"; } break; } } return; } mime_object_list = {}; node = mime_parse_mime(raw_article); mime_flatten_node_tree(node, mime_object_list); rendered_article = ""; foreach value (node.header) using ("values") { rendered_article += sprintf("%s: %s\n", value.name, value.value); } rendered_article += "\n"; render_part(node, &rendered_article); return; } static define mime_show_raw_article() { if (article != &raw_article) { article = &raw_article; replace_article(raw_article); update(); } } static define mime_show_rendered_article() { if ((article != &rendered_article) && (rendered_article != NULL)) { article = &rendered_article; replace_article(rendered_article); update(); } } static define mime_toggle_view() { if (article == &raw_article) mime_show_rendered_article(); else mime_show_raw_article(); } static define mime_select_part(title) { variable selection_list = {}; variable i; variable len; variable selection; if (mime_object_list == NULL) return NULL; len = length(mime_object_list); for (i = 0; i < len; i++) { list_append(selection_list, sprintf("%d. %s", i + 1, mime_object_list[i].mimetype)); } if (i < 1) return NULL; title; __push_list(selection_list); i; 0; selection = select_list_box(); if (selection == "") return NULL; return mime_object_list[integer(substr(selection, 1, 1)) - 1]; } static define mime_save_part() { variable node = mime_select_part("Select MIME part to save"); variable filename; variable st; variable n; if (node == NULL) return; filename = path_basename(rfc1522_decode_string(mime_get_mime_filename(node))); filename = path_concat(mime_save_dir, filename); filename = strtrim(filename); forever { filename = read_mini_filename("Save to:", "", filename); filename = strtrim(filename); if ((filename == "") || (filename == mime_save_dir)) { message_now("Cancelled"); return; } st = stat_file(filename); if (st != NULL) { if (stat_is("lnk", st.st_mode) || stat_is("reg", st.st_mode)) { n = get_yes_no_cancel("File '$filename' exists, Overwrite?"$, 0); if (n == 0) continue; else if (n == -1) message_now("Cancelled"); return; } else { throw OpenError, "Could not open '$filename' for writing"$; } } mime_save_mime_object(node, filename); message_now("Saved to '$filename'"$); mime_save_dir = path_dirname(filename); break; } } static define mime_view_part() { variable mc; variable e; variable line; variable node = mime_select_part("Select MIME part to view"); if (node == NULL) return; mc = mailcap_lookup_entry(node.content_type); if (mc == NULL) throw NotImplementedError, "No viewer for '" + node.mimetype + "' available"; mc.view = &mailcap_view_part; try (e) { set_display_state(0); mc.view(mime_convert_mime_object(node)); } catch OSError: { () = fprintf(stdout, "\n*** ERROR: %S\n\nPress enter to continue.", e.message); () = fgets(&line, stdin); throw; } finally { set_display_state(1); } } () = register_hook("read_article_hook", "MIMESupport->mime_show_rendered_article"); () = register_hook("read_article_hook", "MIMESupport->mime_process_article");