[PATCH] Split off MIME handling primitives and make them public

Guido Berhoerster <[email protected]> Sat, 25 Jul 2015 15:47:43 +0200
Newsgroups gmane.network.slrn.user
Message-ID <20150725134743.GD21857@hal>
This splits off MIME handling primitives into mimelib.sl and makes them
public so that alternative MIME handling macros can make use of them.
---
 macros/mime.sl    | 303 +++-------------------------------------------------
 macros/mimelib.sl | 311 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 325 insertions(+), 289 deletions(-)
 create mode 100644 macros/mimelib.sl

diff --git a/macros/mime.sl b/macros/mime.sl
index 3d58547..2e01fff 100644
--- a/macros/mime.sl
+++ b/macros/mime.sl
@@ -36,212 +36,9 @@
 
 autoload ("mailcap_lookup_entry", "mailcap");
 
-private variable Mime_Save_Dir = make_home_filename ("");
-private variable Mime_Save_Charset = get_charset ("display");
-
-define mime_set_save_charset (charset)
-{
-   Mime_Save_Charset = charset;
-}
-
-private define set_header_key (hash, name, value)
-{
-   hash[strlow(name)] = struct
-     {
-	name = name,
-	value = strtrim (value),
-     };
-}
-
-private define get_header_key (hash, name, lowercase)
-{
-   try
-     {
-	variable h = hash[strlow (name)];
-	return h.value;
-     }
-   catch AnyError;
-   return "";
-}
-
-private define merge_headers (a, b)
-{
-   variable c = Assoc_Type[Struct_Type];
-   variable value;
-
-   foreach value (a) using ("values")
-     set_header_key (c, value.name, value.value);
-   foreach value (b) using ("values")
-     set_header_key (c, value.name, value.value);
-   return c;
-}
-
-private define 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);
-	set_header_key (hash, fields[0], strjoin (fields[[1:]], ":"));
-     }
-   variable body = substrbytes (art, ofs+2, -1);
-   return hash, body;
-}
-
-
-private define 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 = get_header_key (header, "Content-Type", 0);
-   if (ct == "")
-     return NULL;
-
-   ifnot (is_substr (strlow (ct), "multipart/"))
-     return NULL;
-
-   variable boundary = 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, charset, encoding	       %  non-multipart decoded message
-};
-
-private define 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),
-     boundary_end = boundary + "--", blen_end = blen + 2;
-
-   node.list = {};
-
-   body = strchop (body, '\n', 0);
-   variable i = 0, imax = length(body);
-   while (i < imax)
-     {
-	if (strnbytecmp (body[i], boundary, blen))
-	  {
-	     i++;
-	     continue;
-	  }
-
-	if (0 == strnbytecmp (body[i], boundary_end, blen_end))
-	  break;
-
-	i++;
-	variable i0 = i;
-	if (i0 == imax)
-	  break;
-
-	while (i < imax)
-	  {
-	     if (strnbytecmp (body[i], boundary, blen))
-	       {
-		  i++;
-		  continue;
-	       }
-	     break;
-	  }
-	variable new_node = parse_mime (strjoin (body[[i0:i-1]], "\n"));
-	if (new_node != NULL)
-	  list_append (node.list, new_node);
-     }
-}
-
-private define extract_mimetype (content_type)
-{
-   return strlow (strtrim (strchop (content_type, ';', 0)[0]));
-}
-
-private define parse_mime (art)
-{
-   variable header, body;
-   (header, body) = split_article (art);
-
-   variable node = @Mime_Node_Type;
-   node.content_type = get_header_key (header, "Content-Type", 1);
-   node.disposition = get_header_key (header, "Content-Disposition", 0);
-   node.header = header;
-   node.mimetype = extract_mimetype (node.content_type);
-
-   if (is_substr (node.mimetype, "multipart/"))
-     {
-	parse_multipart (node, body);
-	return node;
-     }
-
-   node.message = body;
-
-   variable encoding = 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 = parse_subkeyword (node.content_type, "charset");
+require ("mimelib")
 
-   return node;
-}
-
-private define flatten_node_tree (node, leaves);   %  recursive
-private define flatten_node_tree (node, leaves)
-{
-   if (node.list == NULL)
-     {
-	list_append (leaves, node);
-	return;
-     }
-
-   foreach node (node.list)
-     flatten_node_tree (node, leaves);
-}
+private variable Mime_Save_Dir = make_home_filename ("");
 
 % Search for the first node whose type matches one in the types list.
 private define find_first_matching_leaf (leaves, types)
@@ -263,19 +60,6 @@ private variable Mime_Object_List = NULL;
 private variable Mime_MessageID = NULL;
 private variable Mime_Article_Headers = NULL;
 
-% Returns NULL if the message is not Mime Encoded, otherwise it
-% returns the value of the Content-Type header.
-private define is_mime_message ()
-{
-   variable h = extract_article_header ("Mime-Version");
-   if ((h == NULL) || (h == ""))
-     return NULL;
-
-   h = extract_article_header ("Content-Type");
-   if (h == "") h = NULL;
-   return h;
-}
-
 % This function returns the top-level headers in the message
 private define process_mime_message ()
 {
@@ -287,10 +71,10 @@ private define process_mime_message ()
    Mime_Object_List = NULL;
 
    variable art = raw_article_as_string ();
-   variable nodes = parse_mime (art);
+   variable nodes = mime_parse_mime (art);
 
    variable leaf, leaves = {};
-   flatten_node_tree (nodes, leaves);
+   mime_flatten_node_tree (nodes, leaves);
 
    Mime_MessageID = msgid;
    Mime_Object_List = leaves;
@@ -300,7 +84,7 @@ private define process_mime_message ()
 private define replace_article_with_mime_obj (obj)
 {
    % Replace some of the headers in the raw article by subpart headers
-   variable header = merge_headers (Mime_Article_Headers, obj.header);
+   variable header = mime_merge_headers (Mime_Article_Headers, obj.header);
 
    variable value, art = "";
    foreach value (header) using ("values")
@@ -311,28 +95,6 @@ private define replace_article_with_mime_obj (obj)
    replace_cooked_article (art, 1);
 }
 
-private define is_attachment (node)
-{
-   return is_substrbytes (strlow (node.disposition), "attachment");
-}
-
-private define is_text (node)
-{
-   return is_substrbytes (node.mimetype, "text/");
-}
-
-private define get_mime_filename (node)
-{
-   variable file = parse_subkeyword (node.disposition, "filename");
-   if (file != NULL)
-     return file;
-   file = parse_subkeyword (node.content_type, "name");
-   if (file != NULL)
-     return file;
-
-   return "";
-}
-
 private define format_type (type, width)
 {
    if (0 == strnbytecmp (type, "application", 11))
@@ -345,43 +107,6 @@ private define format_type (type, width)
    return type;
 }
 
-private define 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 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 = convert_mime_object (obj);
-
-   () = fwrite (str, fp);
-   () = fflush (fp);
-}
-
 private define make_safe_filename (file)
 {
    return strtrans (file, "-+_/.%@{}:A-Za-z0-9", "_");
@@ -402,7 +127,7 @@ private define view_mime_object (obj)
    if (mc == NULL)
      throw NotImplementedError, "No viewer for $type available"$;
 
-   variable str = convert_mime_object (obj);
+   variable str = mime_convert_mime_object (obj);
 
    variable e;
    try (e)
@@ -426,7 +151,7 @@ private define view_mime_object (obj)
 
 define mime_browse ()
 {
-   if (NULL == is_mime_message ())
+   if (NULL == mime_is_mime_message ())
      return;
 
    process_mime_message ();
@@ -436,12 +161,12 @@ define mime_browse ()
    list_append (descriptions, "View full message with all parts");
    foreach node (Mime_Object_List)
      {
-	variable filename = get_mime_filename (node);
+	variable filename = mime_get_mime_filename (node);
 	filename = path_basename (rfc1522_decode_string (filename));
 	list_append (filenames, filename);
 	variable attachment = "";
 	variable charset = node.charset; if (charset == NULL) charset = "";
-	if (is_attachment (node)) attachment = "[attachment]";
+	if (mime_is_attachment (node)) attachment = "[attachment]";
 	list_append (descriptions,
 		     sprintf ("%-16s |%12s| %s%S",
 			      format_type (node.mimetype, 16),
@@ -485,7 +210,7 @@ define mime_browse ()
 		       if (n == -1)
 			 return;
 		    }
-		  save_mime_object (node, filename);
+		  mime_save_mime_object (node, filename);
 		  Mime_Save_Dir = path_dirname (filename);
 		  break;
 	       }
@@ -510,12 +235,12 @@ define mime_browse ()
 
 define mime_process_multipart ()
 {
-   variable h = is_mime_message ();
+   variable h = mime_is_mime_message ();
 
    if (h == NULL)
      return;
 
-   variable mimetype = extract_mimetype (h);
+   variable mimetype = mime_extract_mimetype (h);
 
    if (0 == is_substrbytes (mimetype, "multipart/"))
      {
@@ -537,9 +262,9 @@ define mime_process_multipart ()
      num_text = 0, num_html = 0, num_plain = 0;
    foreach node (Mime_Object_List)
      {
-	if (is_attachment (node))
+	if (mime_is_attachment (node))
 	  num_attchments++;
-	else if (is_text (node))
+	else if (mime_is_text (node))
 	  {
 	     num_text++;
 	     num_html += (0 != is_substrbytes (node.mimetype, "/html"));
diff --git a/macros/mimelib.sl b/macros/mimelib.sl
new file mode 100644
index 0000000..5245473
--- /dev/null
+++ b/macros/mimelib.sl
@@ -0,0 +1,311 @@
+% The routines in this file parse a multipart MIME message.
+% Copyright (C) 2012 John E. Davis <[email protected]>
+%
+% This may be distributed under the terms of the GNU General Public
+% License.  See the file COPYING for more information.
+%
+% Public functions in this file:
+%
+%     mime_set_save_charset (charset)
+%     mime_get_save_charset ()
+%     mime_set_header_key (hash, name, value)
+%     mime_get_header_key (hash, name, lowercase)
+%     mime_merge_headers (a, b)
+%     mime_split_article (art)
+%     mime_parse_subkeyword (key, word)
+%     mime_extract_mimetype (content_type)
+%     mime_parse_mime (art)
+%     mime_flatten_node_tree (node, leaves)
+%     mime_is_mime_message ()
+%     mime_is_attachment (node)
+%     mime_is_text (node)
+%     mime_get_mime_filename (node)
+%     mime_convert_mime_object (obj)
+%     mime_save_mime_object (obj, fp)
+%
+% Thes functions constitute the building blocks for MIME message handling such
+% as the implementation in mime.sl.
+
+private variable Mime_Save_Charset = get_charset ("display");
+
+define mime_set_save_charset (charset)
+{
+   Mime_Save_Charset = charset;
+}
+
+define mime_get_save_charset ()
+{
+   return Mime_Save_Charset;
+}
+
+define mime_set_header_key (hash, name, value)
+{
+   hash[strlow(name)] = struct
+     {
+	name = name,
+	value = strtrim (value),
+     };
+}
+
+define mime_get_header_key (hash, name, lowercase)
+{
+   try
+     {
+	variable h = hash[strlow (name)];
+	return h.value;
+     }
+   catch AnyError;
+   return "";
+}
+
+define mime_merge_headers (a, b)
+{
+   variable c = Assoc_Type[Struct_Type];
+   variable value;
+
+   foreach value (a) using ("values")
+     mime_set_header_key (c, value.name, value.value);
+   foreach value (b) using ("values")
+     mime_set_header_key (c, value.name, value.value);
+   return c;
+}
+
+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;
+}
+
+
+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, charset, encoding	       %  non-multipart decoded message
+};
+
+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),
+     boundary_end = boundary + "--", blen_end = blen + 2;
+
+   node.list = {};
+
+   body = strchop (body, '\n', 0);
+   variable i = 0, imax = length(body);
+   while (i < imax)
+     {
+	if (strnbytecmp (body[i], boundary, blen))
+	  {
+	     i++;
+	     continue;
+	  }
+
+	if (0 == strnbytecmp (body[i], boundary_end, blen_end))
+	  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);
+     }
+}
+
+define mime_extract_mimetype (content_type)
+{
+   return strlow (strtrim (strchop (content_type, ';', 0)[0]));
+}
+
+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;
+}
+
+define mime_flatten_node_tree (node, leaves);   %  recursive
+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.
+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 == "") h = NULL;
+   return h;
+}
+
+define mime_is_attachment (node)
+{
+   return is_substrbytes (strlow (node.disposition), "attachment");
+}
+
+define mime_is_text (node)
+{
+   return is_substrbytes (node.mimetype, "text/");
+}
+
+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 "";
+}
+
+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;
+}
+
+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);
+}
+
-- 
2.1.4


------------------------------------------------------------------------------