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");