Mercurial > addons > slrn-macros > slrn-mime-support-macro
changeset 0:cdc3d19f5ba5 default tip
Initial revision
author | Guido Berhoerster <guido+slrn@berhoerster.name> |
---|---|
date | Sat, 21 May 2016 11:12:14 +0200 |
parents | |
children | |
files | COPYING README mime-support.sl |
diffstat | 3 files changed, 1361 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/COPYING Sat May 21 11:12:14 2016 +0200 @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Sat May 21 11:12:14 2016 +0200 @@ -0,0 +1,97 @@ +slrn MIME Support Macro +======================= + +The slrn MIME support macro adds comprehensive support for displaying and +processing MIME messages to slrn. When opening a MIME message with parts other +than plain text or non-MIME messages with a content type other than plain text +the macro automatically converts these parts to plain text provided that the +conversion has been allowed in the configuration there is an appropriate entry +in the mailcap file. Parts with multipart/alternative content type are handled +intelligently by preferring the text part if available and falling back to +converting another part for which automatic conversion has been enabled. The +macro can process arbitrarily nested parts with message/rfc822, +multipart/alternative, multipart/mixed, multipart/digest, multipart/related, +and multipart/signed content type. It also features full support for mailcap +files as defined by RFC 1524, including substitutions. Furthermore, it +provides methods for selecting any MIME part for saving or viewing with an +external viewer and for toggling between the processed and raw message. + +Usage +----- + +The slrn MIME support macro can be used by including it in the .slrnrc user +initialization file via the `interpret` command, e.g. provided that the file +mime-support.sl is located in one of the directories specified by the +macro_directory configuration variable: + + interpret "mime-support.sl" + +The macro can be configured through the following slang variables: + +MIMESupport->config.auto_view +: Array that specifies which content types may be automatically converted to + plain text when opening a MIME message. + +It provides the following methods: + +MIMESupport->mime_save_part() +: Displays a dialog allowing the user to save a MIME part. + +MIMESupport->mime_view_part() +: Displays a dialog allowing the user to view a MIME part using the command + specified in the mailcap entry corresponding to its content type. + +MIMESupport->mime_toggle_view() +: Toggles between the processed and raw form. + +The following environment variables are observed: + +TMPDIR +: Path for temorary files when invoking the command specified in a mailcap + entry. + +PAGER +: The pager used for handling the output of a command specified in a mailcap + entry which contains a copiousoutput flag. + +Contact +------- + +Please send any feedback, translations or bug reports via email to +<guido+slrn@berhoerster.name>. + +Bug Reports +----------- + +When sending bug reports, please always mention the exact version of the +macro with which the issue occurs as well as the version of slrn, slang and +the operating system you are using and make sure that you provide sufficient +information to reproduce the issue and include any input, output, any error +messages and slang stack traces. + +License +------- + +Except otherwise noted, all files are Copyright (C) 2013 Guido Berhoerster and +distributed under the following license terms: + +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.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mime-support.sl Sat May 21 11:12:14 2016 +0200 @@ -0,0 +1,925 @@ +% 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");