]> git.ipfire.org Git - thirdparty/mlmmj.git/commitdiff
Add README.footers and footer-related resources.
authorBen Schmidt <none@none>
Wed, 25 Feb 2015 20:29:01 +0000 (07:29 +1100)
committerBen Schmidt <none@none>
Wed, 25 Feb 2015 20:29:01 +0000 (07:29 +1100)
--HG--
extra : amend_source : 5176f923c05681ad5b64caea4dd8eb21cdb058af
extra : histedit_source : 563dd72b622372a0628e25692e1f3b9dd39e34af

12 files changed:
ChangeLog
Makefile.am
README.footers [new file with mode: 0644]
README.listtexts
contrib/Makefile.am
contrib/foot_filter/Makefile [new file with mode: 0644]
contrib/foot_filter/foot_filter.c [new file with mode: 0644]
contrib/pymime/LICENSE [new file with mode: 0644]
contrib/pymime/README.md [new file with mode: 0644]
contrib/pymime/integration/mlmmj/mlmmj-pymime [new file with mode: 0644]
contrib/pymime/src/pymime.py [new file with mode: 0755]
contrib/pymime/src/test.eml [new file with mode: 0644]

index 4503e08103a949bace3ec3bd44e481239d43111e..2714c8e2f6dadac52194f81d60f7285a5cf4fed1 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,4 @@
+ o Add README.footers and footer-related resources
  o Support ESMTP so OpenSMTPD uses 8 bits (Paul Fariello)
  o Use iconv to convert unknown character sets
  o Handle unfolded header lines better
index e7bb8ee329969358347eb999d2bc08c6f879b7bc..bd1ad59c5a531880753fdb1b03292f6123f01165 100644 (file)
@@ -4,7 +4,7 @@ AUTOMAKE_OPTIONS = foreign dist-bzip2
 EXTRA_DIST = include VERSION LICENSE UPGRADE FAQ \
             TUNABLES README.access README.archives README.listtexts man \
             README.exim4 README.sendmail README.security README.qmail \
-            README.postfix
+            README.postfix README.footers
 CLEANFILES = *~ mlmmj-*.tar.*
 
 man1_MANS = man/mlmmj-bounce.1 man/mlmmj-make-ml.1 man/mlmmj-receive.1 \
diff --git a/README.footers b/README.footers
new file mode 100644 (file)
index 0000000..d84bfff
--- /dev/null
@@ -0,0 +1,47 @@
+README.footers
+
+Footers in Mlmmj
+================
+
+Mlmmj's built-in footer support is very rudimentary. It will work for plain
+text emails, and that's about it. It doesn't understand HTML or MIME or
+anything like that.
+
+There are a few solutions to this. They all involve piping incoming mail
+through a filter before it reaches Mlmmj. A script to do this, called
+mlmmj-amime-receive, is included in amime-receive in the contrib directory.
+
+It can be used with a number of different pre-processors. One day we also hope
+to improve the integration of these external filters with Mlmmj, e.g. so only
+list posts are processed. However, the piping solution has worked for a number
+of people over the years quite satisfactorily, so this is not a high priority.
+
+Here are some pre-processors you can use.
+
+alterMIME
+---------
+
+The mlmmj-amime-receive script is designed to work with a program called
+alterMIME. The script itself (in amime-receive in the contrib directory)
+contains links to that software, and instructions.
+
+Foot Filter
+-----------
+
+alterMIME didn't allow me to reach my particular goals, so I wrote an
+alternative called Foot Filter. It is a single source-file C program; the code
+and a very simple Makefile can be found in foot_filter in the contrib
+directory, along with an altered version of mlmmj-amime-receive, called
+mlmmj-recieve-ff.
+
+Foot Filter will output documentation if you run it without arguments, and
+again, instructions for the script that handles the piping are found within it.
+
+Py-MIME
+-------
+
+A third option is Py-MIME. It was developed for use at The Document Foundation
+(LibreOffice) and is included in pymime in the contrib directory.
+
+
+
index 7256720cbf77b75fbdc95929af262279fd5a3af6..9e8a78d10fb68ecd4c4baa9d655e093a083a8e53 100644 (file)
@@ -1,6 +1,6 @@
 README.listtexts
 
-List texts in mlmmj
+List texts in Mlmmj
 ===================
 
 List texts are stored in listdir/text. They specify the content of various
index f445091e4aff7e0cf641aa51ab1e56e18c26ba74..2608844f090ec91682b72d2038d9b6ac879fcae3 100644 (file)
@@ -1,4 +1,4 @@
 ## Process this file with automake to produce Makefile.in
 
-EXTRA_DIST = web amime-receive
+EXTRA_DIST = web amime-receive foot_filter pymime
 SUBDIRS = receivestrip
diff --git a/contrib/foot_filter/Makefile b/contrib/foot_filter/Makefile
new file mode 100644 (file)
index 0000000..eb22bc9
--- /dev/null
@@ -0,0 +1,14 @@
+all: foot_filter
+dev: tags splint foot_filter
+.PHONY: splint clean clobber
+tags: foot_filter.c
+       ctags --excmd=number '--regex-c=-/\*[[:blank:]]*tag:[[:blank:]]*([[:alnum:]_]+)-\1-' foot_filter.c
+splint:
+       splint +unixlib -exitarg -initallelements foot_filter.c
+foot_filter: foot_filter.c
+       gcc -Wall -g -o foot_filter foot_filter.c -O3
+clean:
+       -rm tags
+clobber: clean
+       -rm foot_filter
+       -rm test
diff --git a/contrib/foot_filter/foot_filter.c b/contrib/foot_filter/foot_filter.c
new file mode 100644 (file)
index 0000000..6bbbff7
--- /dev/null
@@ -0,0 +1,2685 @@
+/*
+
+foot_filter.c
+
+(C) 2010 Ben Schmidt
+
+This Source Code Form is subject to the terms of the Mozilla Public License
+Version 2.0. If a copy of the MPL was not distributed with this file, You can
+obtain one at http://mozilla.org/MPL/2.0/.
+
+*/
+
+// Check out the -V option; it outputs this and more
+#define FOOT_FILTER_VERSION "foot_filter version 1.2, (C) 2010 Ben Schmidt"
+
+static const char * USAGE="\n\
+usage: foot_filter [-p plain_footer_file] [-h html_footer_file]\n\
+                   [{-P|-H} mime_footer_file] [-s]\n\
+       foot_filter -V\n\
+\n\
+plain_footer_file, if present, will be appended to mails with plain text\n\
+sections only. Similarly, html_footer_file. If mime_footer_file (either\n\
+plain, -P, or HTML, -H) is given, it will be used when a mail with\n\
+alternative formats is encountered, or if the footer for the relevant\n\
+type of mail is not present; a new MIME section will be added.\n\
+\n\
+-s turns on smart mode which endeavours to remove included/quoted copies of\n\
+the (or a similar) footer by surrounding the footer with patterns it later\n\
+recognises. It also endeavours to strip 'padding' surrounding the old\n\
+footers to make things as clean as possible. This includes whitespace\n\
+(including '&nbsp;' and '<br>'), '>' quoting characters, various pairs of\n\
+HTML tags (p, blockquote, div, span, font; it's naive, it doesn't check\n\
+tags in between are balanced at all, so in '<p>prefix</p><p>suffix</p>' the\n\
+first and last tags are paired), and even horizontal rules when inside\n\
+paired tags (e.g. use '<div><hr/>footer</div>'). If the smart strings are\n\
+found in the footer, they won't be added by the program, so you have the\n\
+necessary control to do this.\n\
+\n\
+New footers are added prior to trailing whitespace and a few closing html\n\
+tags (body, html) as well. You almost certainly want to begin your footer\n\
+with an empty line because of this.\n\
+\n\
+Since these alterations, by their very nature, break signed mail,\n\
+signatures are removed while processing. To keep some value from signatures,\n\
+have the MTA verify them and add a header (or even supply an alternative\n\
+footer to this program), and resign them to authenticate they came from the\n\
+mailing list directly after the signature verification was done and recorded.\n\
+Or don't use these kinds of transformations at all.\n\
+\n\
+-V shows the version and exits.\n\
+\n\
+Program is running now. Send EOF or interrupt to stop it. To avoid this usage\n\
+message if wanting to run without arguments, use '--' as an argument.\n\
+\n";
+
+/*
+
+This is a fairly simple program not expecting much extension. As such, some
+liberties have been taken and some fun has been had by the author. Correctness
+has been prioritised in design, but speed and efficiency have been taken into
+consideration and prioritised above readability and modularity and other such
+generally recommended programming practices. If making changes, great care
+should be taken to understand how and where (everywhere) globals are used
+before making them. Don't try to modify the program without understanding how
+the whole thing works together or you will get burnt. You have been warned.
+
+Relevant RFCs:
+http://www.ietf.org/rfc/rfc2015.txt
+http://www.ietf.org/rfc/rfc3851.txt
+http://www.ietf.org/rfc/rfc2045.txt
+http://www.ietf.org/rfc/rfc2046.txt
+http://www.ietf.org/rfc/rfc822.txt
+http://www.ietf.org/rfc/rfc2183.txt
+
+For program configuration, see the 'constants' section below.
+
+Also see code comments throughout.
+
+Future possibilities:
+
+- Saving copies of original mail in 'semi-temp' files for debugging.
+
+- Stripping attachments and save them (e.g. in a location that can become a
+  'files uploaded' section on a website). Replace them with links to the
+  website, even.
+
+- Making the prefixes, suffixes, replacements, padding, guts, pairs,
+  configurable at runtime.
+
+- Attaching signed mail, or wrapping in a multipart rather than removing
+  signatures; wouldn't be hard if always using MIME footers.
+
+- Following a script to allow various other header transformations (addition,
+  removal, etc.), or other transformations.
+
+- Prologues as well as or instead of footers.
+
+*/
+
+/* tag: includes */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <time.h>
+#include <syslog.h>
+#include <errno.h>
+#include <string.h>
+#include <sysexits.h>
+
+/* tag: typedefs */
+
+// splint has bools, but C doesn't!
+#ifndef S_SPLINT_S
+typedef int bool;
+#define false (0)
+#define true (1)
+#endif
+
+// This is mostly to be able to include splint annotations
+typedef /*@null@*//*@observer@*/ const char * const_null_string;
+typedef /*@null@*/ char * null_string;
+typedef /*@null@*//*@owned@*/ char * owned_null_string;
+typedef /*@null@*//*@dependent@*/ char * dependent_null_string;
+
+// 'Callbacks'; they communicate primarily using globals, see below
+typedef bool (*callback_t)();
+typedef void (*function_t)();
+
+// For fill()
+typedef enum {
+       echo,
+       encode,
+       shunt,
+       discard,
+       stop,
+       fail
+} when_full_t;
+
+// Various places
+typedef enum {
+       unencoded,
+       quoted_printable,
+       base64
+} encoding_t;
+
+// For returning multiple characters, and a request to delete backlog
+// when decoding
+typedef struct {
+       int r;
+       int c1;
+       int c2;
+       int c3;
+} decode_t;
+
+/* tag: constants */
+
+/* tag: header_constants */
+
+// How many MIME Content- headers we expect, maximum, in a mail. If we have
+// more than that, we won't be able to process MIME so well, but we won't fail
+// catastrophically.
+#define mime_headers_max 16
+
+/* tag: footer_constants */
+
+// Stuff for processing the footer's smart removal and (smart or not)
+// insertion
+
+static const char * plain_prefix = "------~----------";
+static const char * plain_suffix = "------~~---------";
+static const char * plain_replacement = "\r\n\r\n";
+static const_null_string plain_tails[] = {
+       " ","\t","\r","\n",
+       NULL
+};
+static const_null_string plain_padding[] = {
+       ">"," ","\t","\r","\n",
+       NULL
+};
+static const_null_string plain_guts[] = {
+       NULL
+};
+static const_null_string plain_pairs[] = {
+       NULL
+};
+
+static const char * html_prefix = "------~----------";
+static const char * html_suffix = "------~~---------";
+static const char * html_replacement = "\r\n<br/><br/>\r\n";
+static const_null_string html_tails[] = {
+       "</html>","</HTML>","</body>","</BODY>",
+       " ","&nbsp;","&NBSP;","\t","\r","\n",
+       "<br>","<BR>","<br/>","<BR/>","<br />","<BR />",
+       NULL
+};
+static const_null_string html_padding[] = {
+       "&gt;","&GT;",
+       " ","&nbsp;","&NBSP;","\t","\r","\n",
+       "<br>","<BR>","<br/>","<BR/>","<br />","<BR />",
+       NULL
+};
+static const_null_string html_guts[] = {
+       // These are removed in an attempt to make a pair
+       "<hr>","<HR>","<hr/>","<HR/>","<hr />","<HR />",
+       " ","&nbsp;","&NBSP;","\t","\r","\n",
+       "<br>","<BR>","<br/>","<BR/>","<br />","<BR />",
+       NULL
+};
+static const_null_string html_pairs[] = {
+       // Closing part (or NULL to mark no more), end of opening part,
+       // start of opening part, NULL
+       // The search strategy is fairly naive; if it finds the closing part,
+       // it checks for the end of the opening part; if it finds that, it
+       // searches back for the first character of each of the opening part
+       // variants, and if that character is found and is the beginning of the
+       // whole variant, it removes the pair.
+       "</p>",">","<p>","<p ",NULL,
+       "</P>",">","<P>","<P ",NULL,
+       "</blockquote>",">","<blockquote>","<blockquote ",NULL,
+       "</BLOCKQUOTE>",">","<BLOCKQUOTE>","<BLOCKQUOTE ",NULL,
+       "</div>",">","<div>","<div ",NULL,
+       "</DIV>",">","<DIV>","<DIV ",NULL,
+       "</span>",">","<span>","<span ",NULL,
+       "</SPAN>",">","<SPAN>","<SPAN ",NULL,
+       "</font>",">","<font>","<font ",NULL,
+       "</FONT>",">","<FONT>","<FONT ",NULL,
+       NULL
+};
+
+/* tag: buffer_constants */
+
+// Also see comment at buffer_globals about how the buffer works.
+
+// The buffer size limits how far back footers can be deleted; the
+// section of mail from the closing boundary back this far will be
+// searched for footers to remove.
+#define mem_buffer_size 65536
+// mem_buffer_keep is how much we keep in memory when shunting
+// off to disk; as we must be able to shunt off at least something
+// to disk each time we need to, this must be at least 2 bytes
+// less than mem_buffer_size. This is how much we will be able to
+// backtrack in memory, e.g. to strip whitespace. Something a little
+// larger than the SMTP line length limit of 998 should keep it safe.
+#define mem_buffer_keep 1024
+// mem_buffer_margin is how much space we keep in memory in case
+// a callback (decoding!) needs to use it. Must be at least 4 for
+// decoding not to cause nasty corruption.
+#define mem_buffer_margin 8
+// The number of replacements we may wish to make; usually removing
+// the MIME headers is the most complicated thing to be done, plus
+// removing the MIME version header and the newline that ends the
+// headers.
+#define replacements_max (mime_headers_max+4)
+
+/* tag: error_constants */
+
+// Enable one or both of these
+#define USE_SYSLOG
+//#define USE_STDERR
+
+/* tag: helper_constants */
+
+// Disable this to use stdin/stdout with CRLF line endings as the spec
+// and transports do; footer files are always expected to have UNIX line
+// endings
+#define UNIX_EOL
+
+/* tag: globals */
+
+/* tag: header_globals */
+
+// Offsets into the buffer; only valid while it's still in there!
+static int mime_header_starts[mime_headers_max]={0};
+static int mime_header_ends[mime_headers_max]={0};
+// String copies (until freed)
+static owned_null_string saved_mime_headers[mime_headers_max]={NULL};
+// Count applies to all header globals above
+static int mime_headers_count = 0;
+
+// Individual header strings which are processed to have comments
+// removed and have normalised syntax for easy deduction of types,
+// etc.. _header_body pointers point into the _header strings.
+// Indexes are into the header arrays above.
+// We store the version start and end separately as we want to delete it,
+// but not reoutput it.
+static /*@owned@*/ null_string version_header=NULL;
+static /*@dependent@*/ null_string version_header_body=NULL;
+static int version_header_start=0;
+static int version_header_end=0;
+static /*@owned@*/ null_string type_header=NULL;
+static /*@dependent@*/ null_string type_header_body=NULL;
+static int type_header_index=0;
+static /*@owned@*/ null_string transfer_header=NULL;
+static /*@dependent@*/ null_string transfer_header_body=NULL;
+static int transfer_header_index=0;
+static /*@owned@*/ null_string disposition_header=NULL;
+static /*@dependent@*/ null_string disposition_header_body=NULL;
+static int disposition_header_index=0;
+
+// Flag that we had errors reading headers so not to do anything fancy
+static bool mime_bad = false;
+
+/* tag: footer_globals */
+
+// Footer text; we just store \n but it is translated to CRLF
+// The actual buffers
+static /*@owned@*/ null_string plain_footer_buffer = NULL;
+static /*@owned@*/ null_string html_footer_buffer = NULL;
+static /*@owned@*/ null_string mime_footer_buffer = NULL;
+// Pointer to the string we're actually using
+static /*@dependent@*/ null_string plain_footer = NULL;
+static /*@dependent@*/ null_string html_footer = NULL;
+static /*@dependent@*/ null_string mime_footer = NULL;
+static bool html_mime_footer = false;
+// Whether to attempt deletion by surrounding the footer with special strings
+static bool smart_footer = false;
+
+/* tag: buffer_globals */
+
+// We have a buffer that may be partly on disk (disk_buffer), then
+// in memory (mem_buffer). The memory buffer wraps around as necessary.
+// The disk buffer begins at disk_buffer_start in the file. The _filled
+// variables say how much is in each part of the buffer, and the total
+// is in buffer_filled. A portion of the buffer is considered to have
+// been 'read'; before this is the lookbehind, and after this is the
+// lookahead. The buffer may also be marked at a certain location,
+// which is and should be almost always in the lookbehind (if in the
+// lookahead this should only be very temporary).
+
+static char mem_buffer_start[mem_buffer_size];
+static char * mem_buffer_end = mem_buffer_start+mem_buffer_size;
+static char * mem_buffer_next_empty = mem_buffer_start; // logical start
+static char * mem_buffer_next_fill = mem_buffer_start; // logical end + 1
+static int mem_buffer_filled=0;
+
+static /*@null@*/ FILE * disk_buffer = NULL;
+// Cannot pass mkstemp an unwritable string, so careful to declare this
+// as an array, not a pointer which would observe the string constant.
+static char disk_buffer_template[] = "foot_filter.XXXXXX";
+static int disk_buffer_start=0; // offset into temp file for buffer
+static int disk_buffer_filled=0;
+// disk_buffer_sought: location we are at in the temp file
+static int disk_buffer_sought=0;
+
+// at all times, buffer_filled == mem_buffer_filled + disk_buffer_filled
+static int buffer_filled=0;
+
+static int buffer_read=0; // offset into buffer
+static int buffer_mark=0; // offset into buffer; should be in lookbehind
+static bool buffer_marked=false;
+
+// The first and 'after last' characters to replace are stored
+static int replacement_starts[replacements_max] = {0};
+static int replacement_ends[replacements_max] = {0};
+static const_null_string replacement_strings[replacements_max] = {NULL};
+static int replacements_count=0;
+
+/* tag: callback_globals */
+
+// Used to communicate a character from the buffer to callbacks by the
+// functions the callback is directly called by
+static int buffer_char;
+
+// Used to communicate information between callback functions and the
+// functions that set up the callback (not the function that actually
+// calls the callback), and possibly for internal callback state. The
+// callbacks document how these should be used. Take special care to
+// follow the instructions, or things will go bad and be hard to track
+// down!
+static /*@dependent@*/ null_string callback_save;
+static const_null_string callback_compare;
+static int callback_match;
+static int callback_int;
+static bool callback_bool;
+
+/* tag: encoding_globals */
+
+// Current modes
+static encoding_t encoding;
+static encoding_t decoding;
+
+// State for the routines
+static char encoding_buffer[4];
+static int encoding_filled=0;
+static int encoding_echoed=0;
+static char decoding_buffer[4];
+static int decoding_filled=0;
+static int decoding_white=0;
+
+/* tag: prototypes */
+
+// Comments are made at function definitions where warranted.
+
+/* tag: main_prototypes */
+
+int main(int argc,char * argv[]);
+
+static void load_footer(/*@out@*//*@shared@*/ char ** footer,
+               /*@reldef@*/ char ** footer_buffer,
+               char * file,
+               /*@unique@*/ const_null_string prefix,
+               /*@unique@*/ const_null_string suffix);
+
+static void process_section(bool add_footer,
+               bool can_reenvelope, /*@null@*/ bool * parent_needs_footer);
+
+/* tag: header_prototypes */
+
+static inline void read_and_save_mime_headers();
+static char * remove_comments(/*@returned@*/ char * header,bool ext);
+static inline bool delimiting(char c,bool ext);
+static inline void remove_mime_headers();
+static inline void output_saved_mime_headers();
+static void free_saved_mime_headers();
+static bool /*@falsewhennull@*/ is_multipart(/*@null@*//*@out@*/ char ** boundary);
+static inline bool is_signed();
+static inline bool is_alternative();
+static inline bool is_mixed();
+static inline bool is_html();
+static inline bool is_plain();
+static inline bool is_signature();
+static inline bool is_attachment();
+static inline void set_decoding_type();
+static inline void change_to_mixed();
+static inline void generate_boundary(/*@out@*/ char ** boundary);
+static inline void output_mime_mixed_headers(const char * boundary);
+static inline void output_prolog();
+static void output_mime_footer(const char * boundary);
+static inline void output_boundary(const char * boundary);
+static inline void output_final_boundary(const char * boundary);
+static bool at_final_boundary(char * boundary);
+
+/* tag: footer_prototypes */
+
+static inline void process_text_section(bool add_footer,
+               /*@null@*/ const char * footer,
+               const char * prefix, const char * suffix, const char * replacement,
+               const_null_string * tails, const_null_string * padding,
+               const_null_string * guts, const_null_string * pairs,
+               char * boundary);
+static inline void pad(const_null_string * padding,
+               const_null_string * guts, const_null_string * pairs,
+               int * prefix_pos, int * suffix_pos);
+static inline void mark_tail(const_null_string * padding);
+static inline void encode_footer(const char * footer);
+
+/* tag: buffer_prototypes */
+
+static inline void read_buffer();
+static inline void echo_buffer();
+static inline void skip_buffer();
+static inline void echo_lookbehind();
+static inline void encode_lookbehind();
+static inline void encode_replacements();
+static inline void make_replacements(callback_t one_char,
+               callback_t start_marked);
+static inline void encode_to_mark();
+static inline void echo_disk_buffer();
+static inline void skip_disk_buffer();
+static inline void read_boundary(/*@out@*/ char ** boundary);
+static inline void echo_to_boundary(const char * boundary);
+static inline void skip_to_boundary(const char * boundary);
+static inline void decode_and_read_to_boundary_encoding_when_full(
+               const char * boundary);
+static inline bool process_one_line_checking_boundary(callback_t n_chars,
+               /*@null@*/ function_t process, callback_t processing,
+               when_full_t when_full, const char * boundary);
+
+static int pos_of(const char * text,int from,int to);
+
+static bool look(callback_t callback,int from,bool read);
+static bool lookback(callback_t callback,int from,bool mark);
+static bool empty(callback_t callback);
+static bool fill(callback_t callback, when_full_t when_full);
+
+static inline void create_disk_buffer();
+static void remove_disk_buffer();
+static inline void shunt_to_disk(int n);
+
+/* tag: callback_prototypes */
+
+static bool one_char();
+static bool echoing_one_char();
+static bool encoding_one_char();
+static bool n_chars();
+static bool echoing_n_chars();
+static bool encoding_n_chars();
+static bool saving_n_chars();
+static bool n_chars_until_match();
+static bool until_eol();
+// static bool echoing_until_eol();
+static bool counting_until_eol();
+static bool saving_until_eol();
+static bool decoding_until_eol();
+// static bool until_no_lookbehind();
+static bool echoing_until_no_lookbehind();
+static bool encoding_until_no_lookbehind();
+static bool until_no_disk_buffer();
+static bool echoing_until_no_disk_buffer();
+static bool encoding_until_no_disk_buffer();
+static bool until_no_buffer();
+// static bool echoing_until_no_buffer();
+static bool until_start_marked();
+static bool echoing_until_start_marked();
+static bool encoding_until_start_marked();
+static bool until_match();
+static bool comparing_head();
+static bool case_insensitively_comparing_head();
+
+/* tag: encoding_prototypes */
+
+static inline void encode_string(const char * s);
+static void encodechar(int c);
+static inline void finish_encoding();
+static /*@reldef@*/ decode_t decodechar(int c);
+static void decode_lookahead();
+static inline void finish_decoding();
+
+static inline int decode_hex(int c);
+static inline int decode_64(int c);
+static inline void encode_hex_byte(unsigned int h);
+static inline void encode_64(unsigned int b);
+
+/* tag: error_prototypes */
+
+static inline void * alloc_or_exit(size_t s) /*@allocates result@*/;
+static inline void /*@noreturnwhentrue@*/
+               resort_to_exit(bool when,const char * message,int status);
+static inline void /*@noreturnwhentrue@*/
+               resort_to_errno(bool when,const char * message,int status);
+static inline void resort_to_warning(bool when,const char * message);
+static inline void warning(const char * message);
+
+/* tag: helper_prototypes */
+
+static inline int get();
+static inline int put(int c);
+static inline int putstr(const char * s);
+
+static inline bool case_insensitively_heads(const char * head,const char * buffer);
+
+/* tag: functions */
+
+/* tag: main_functions */
+
+int main(int argc, char * argv[]) {
+       int opt;
+       bool show_version=false;
+       null_string plain_footer_file=NULL;
+       null_string html_footer_file=NULL;
+       null_string mime_footer_file=NULL;
+       // Initialise
+       resort_to_errno(atexit(remove_disk_buffer)!=0,
+                       "cannot register exit function",EX_OSERR);
+       srandom((unsigned int)(getpid()*time(NULL)));
+       // Parse args
+       while ((opt=getopt(argc,argv,"p:h:P:H:sV"))!=-1) {
+               switch ((char)opt) {
+               case 'p': plain_footer_file=optarg; break;
+               case 'h': html_footer_file=optarg; break;
+               case 'P': mime_footer_file=optarg; html_mime_footer=false; break;
+               case 'H': mime_footer_file=optarg; html_mime_footer=true; break;
+               case 's': smart_footer=true; break;
+               case 'V': show_version=true; break;
+               default: warning("unrecognised commandline option");
+               }
+       }
+       if (show_version||argc<2) {
+               printf("%s\n",FOOT_FILTER_VERSION);
+#ifdef UNIX_EOL
+               printf("   with UNIX line endings\n");
+#else
+               printf("   with DOS line endings\n");
+#endif
+               printf("   reporting errors to: ");
+#ifdef USE_SYSLOG
+               printf("syslog ");
+#endif
+#ifdef USE_STDERR
+               printf("stderr ");
+#endif
+               printf("\n");
+               if (argc<2) fprintf(stderr,"%s",USAGE);
+               if (show_version) exit(EX_OK);
+       }
+       argc-=optind;
+       argv+=optind;
+       resort_to_warning(argc>0,"unexpected commandline argument");
+       // Load footers
+       if (plain_footer_file!=NULL)
+                       load_footer(&plain_footer,&plain_footer_buffer,
+                       plain_footer_file,
+                       smart_footer?plain_prefix:NULL,smart_footer?plain_suffix:NULL);
+       if (html_footer_file!=NULL)
+                       load_footer(&html_footer,&html_footer_buffer,
+                       html_footer_file,
+                       smart_footer?html_prefix:NULL,smart_footer?html_suffix:NULL);
+       if (mime_footer_file!=NULL)
+                       load_footer(&mime_footer,&mime_footer_buffer,
+                       mime_footer_file,NULL,NULL);
+       // Do the job
+       process_section(true,true,NULL);
+       // Finish
+       if (plain_footer_buffer!=NULL) free(plain_footer_buffer);
+       if (html_footer_buffer!=NULL) free(html_footer_buffer);
+       if (mime_footer_buffer!=NULL) free(mime_footer_buffer);
+       exit(EX_OK);
+}
+
+static void load_footer(/*@out@*//*@shared@*/ char ** footer,
+               /*@reldef@*/ char ** footer_buffer,
+               char * file,
+               /*@unique@*/ const_null_string prefix,
+               /*@unique@*/ const_null_string suffix) {
+       FILE * f;
+       int prefixl=0, footerl=0, suffixl=0;
+       char * ff;
+       if (prefix!=NULL&&suffix!=NULL) {
+               prefixl=(int)strlen(prefix);
+               suffixl=(int)strlen(suffix);
+       }
+       f=fopen(file,"r");
+       resort_to_errno(f==NULL,"error opening footer file",EX_NOINPUT);
+       resort_to_errno(fseek(f,0,SEEK_END)!=0,
+                       "error seeking end of footer file",EX_IOERR);
+       resort_to_errno((footerl=(int)ftell(f))==-1,
+                       "error finding footer length",EX_IOERR);
+       resort_to_errno(fseek(f,0,SEEK_SET)!=0,
+                       "error seeking in footer file",EX_IOERR);
+       // prefix, \n, footer, \n, suffix, \0
+       *footer_buffer=alloc_or_exit(sizeof(char)*(prefixl+footerl+suffixl+3));
+       *footer=*footer_buffer;
+       *footer+=prefixl+1;
+       resort_to_errno(fread(*footer,1,(size_t)footerl,f)<(size_t)footerl,
+                       "error reading footer",EX_IOERR);
+       // We strip off a single trailing newline to keep them from accumulating
+       // but to allow the user the option of adding them if desired
+       if ((*footer)[footerl-1]=='\n') --footerl;
+       (*footer)[footerl]='\0';
+       if (prefix==NULL||suffix==NULL) return;
+       // Put in the prefix and suffix as necessary
+       ff=strstr(*footer,prefix);
+       if (ff!=NULL) {
+               ff=strstr(ff,suffix);
+               if (ff!=NULL) return;
+               (*footer)[footerl]='\n';
+               ++footerl;
+               strcpy(*footer+footerl,suffix);
+               (*footer)[footerl+suffixl]='\0';
+       } else {
+               ff=strstr(*footer,suffix);
+               if (ff==NULL) {
+                       (*footer)[footerl]='\n';
+                       ++footerl;
+                       strcpy(*footer+footerl,suffix);
+                       (*footer)[footerl+suffixl]='\0';
+               }
+               *footer-=prefixl+1;
+               strcpy(*footer,prefix);
+               (*footer)[prefixl]='\n';
+       }
+}
+
+// Should be called with the boundary for the section as lookahead
+// in the buffer, but nothing more, and no lookbehind.
+static void process_section(bool add_footer,
+               bool can_reenvelope, /*@null@*/ bool * parent_needs_footer) {
+       char * external=NULL;
+       char * internal=NULL;
+       char * generated=NULL;
+       bool reenveloping=false;
+       bool child_needed_footer=false;
+       bool needs_footer=false;
+       bool unsigning=false;
+       if (parent_needs_footer!=NULL) *parent_needs_footer=false;
+       // The headers must be read, saved and echoed before making any
+       // recursive calls, as I'm naughty and using globals.
+       read_boundary(&external);
+       read_and_save_mime_headers();
+       if (mime_bad) {
+               // If an error, just resort to echoing
+               echo_buffer(); // Boundary and headers
+               // End headers with the extra line break
+               resort_to_errno(putstr("\r\n")==EOF,
+                               "error echoing string",EX_IOERR);
+               free_saved_mime_headers();
+               // Body
+               echo_to_boundary(external);
+               free(external);
+               return;
+       }
+       // Headers determining we skip this section
+       if (is_signature()) {
+               skip_buffer(); // Boundary and headers
+               skip_to_boundary(external);
+               return;
+       }
+       // Header processing
+       if (is_signed()) unsigning=true;
+       if (unsigning) change_to_mixed();
+       if (add_footer&&mime_footer!=NULL&&(
+                       is_alternative()||(is_multipart(NULL)&&!is_mixed())||
+                       (is_plain()&&plain_footer==NULL)||
+                       (is_html()&&html_footer==NULL)
+                       )) {
+               add_footer=false;
+               if (can_reenvelope) {
+                       reenveloping=true;
+                       remove_mime_headers();
+               } else if (parent_needs_footer!=NULL) *parent_needs_footer=true;
+       }
+       // Headers
+       echo_buffer(); // Boundary and possibly modified headers
+       if (reenveloping) {
+               generate_boundary(&generated);
+               output_mime_mixed_headers(generated);
+               output_prolog();
+               output_boundary(generated);
+               output_saved_mime_headers();
+       }
+       // End the headers with the extra line break
+       resort_to_errno(putstr("\r\n")==EOF,
+                       "error echoing string",EX_IOERR);
+       // Body processing
+       if (is_multipart(&internal)) {
+               // This branch frees the MIME headers before recursing.
+               // Don't include the prolog if it used to be signed;
+               // it usually says something like 'this message is signed'
+               if (unsigning) {
+                       skip_to_boundary(internal);
+                       resort_to_errno(putstr("\r\n")==EOF,
+                                       "error echoing string",EX_IOERR);
+               } else {
+                       echo_to_boundary(internal);
+               }
+               // The recursive call needs these globals
+               free_saved_mime_headers();
+               while (!at_final_boundary(internal)) {
+                       process_section(add_footer,false,&child_needed_footer);
+                       if (child_needed_footer) needs_footer=true;
+               }
+               if (needs_footer) output_mime_footer(internal);
+               free(internal);
+               echo_to_boundary(external);
+       } else {
+               // This branch frees the MIME headers at the end
+               if (!is_attachment()&&(
+                               (is_plain()&&plain_footer!=NULL)||
+                               (is_html()&&html_footer!=NULL))) {
+               // alternatively
+               // if (!is_attachment()&&(
+               //              (is_plain()&&((add_footer&&plain_footer!=NULL)||smart_footer))||
+               //              (is_html()&&((add_footer&&html_footer!=NULL)||smart_footer)))) {
+                       if (is_plain()) {
+                               process_text_section(add_footer,plain_footer,
+                                               plain_prefix,plain_suffix,plain_replacement,
+                                               plain_tails,plain_padding,plain_guts,plain_pairs,external);
+                       } else {
+                               process_text_section(add_footer,html_footer,
+                                               html_prefix,html_suffix,html_replacement,
+                                               html_tails,html_padding,html_guts,html_pairs,external);
+                       }
+               } else {
+                       echo_to_boundary(external);
+               }
+               free_saved_mime_headers();
+       }
+       // MIME stuff is freed now; take care not to use it.
+       /*@-branchstate@*/
+       if (reenveloping) {
+               // We ensure generated is not null in another if(reenveloping)
+               // conditional above
+               /*@-nullpass@*/
+               output_mime_footer(generated);
+               output_final_boundary(generated);
+               free(generated);
+               /*@=nullpass@*/
+       }
+       /*@=branchstate@*/
+       free(external);
+}
+
+/* tag: header_functions */
+
+static inline void read_and_save_mime_headers() {
+       /*@-mustfreeonly@*/
+       mime_bad=false;
+       // Mark current end of buffer
+       buffer_mark=buffer_read;
+       buffer_marked=true;
+       for (;;) {
+               do {
+                       // Extend current header until beginning of next
+                       callback_bool=false;
+                       (void)fill(until_eol,shunt);
+                       if (buffer_filled==buffer_read) {
+                               // We probably hit EOF; just get out, and the whole
+                               // mail will end up echoed out
+                               warning("unexpected end of input");
+                               break;
+                       }
+                       (void)look(one_char,buffer_read,false);
+                       if (callback_int==(int)' '||callback_int==(int)'\t') {
+                               // Continuation of previous header; read it
+                               read_buffer();
+                               continue;
+                       }
+                       // Start of new header; don't read it; process the old one
+                       // (from the mark to the end of the lookbehind)
+                       break;
+               } while (true);
+               // Process the old header, if there is one
+               if (buffer_mark<buffer_read) {
+                       do {
+                               callback_compare="MIME-Version:";
+                               (void)look(case_insensitively_comparing_head,buffer_mark,false);
+                               if (callback_bool) {
+                                       // MIME version header
+                                       version_header_start=buffer_mark;
+                                       version_header_end=buffer_read;
+                                       version_header=alloc_or_exit(sizeof(char)*(buffer_read-buffer_mark+1));
+                                       callback_save=version_header;
+                                       callback_int=buffer_read-buffer_mark;
+                                       callback_save[callback_int]='\0';
+                                       (void)look(saving_n_chars,buffer_mark,false);
+                                       callback_save=NULL;
+                                       version_header_body=remove_comments(version_header,true);
+                                       if (!case_insensitively_heads("1.0",version_header_body)) {
+                                               mime_bad=true;
+                                       }
+                                       break;
+                               }
+                               callback_compare="Content-";
+                               (void)look(case_insensitively_comparing_head,buffer_mark,false);
+                               if (!callback_bool) break;
+                               // Another MIME header
+                               if (mime_headers_count==mime_headers_max) {
+                                       warning("too many MIME headers");
+                                       mime_bad=true;
+                                       break;
+                               }
+                               mime_header_starts[mime_headers_count]=buffer_mark;
+                               mime_header_ends[mime_headers_count]=buffer_read;
+                               saved_mime_headers[mime_headers_count]=
+                                               alloc_or_exit(sizeof(char)*(buffer_read-buffer_mark)+1);
+                               saved_mime_headers[mime_headers_count][0]='\0';
+                               callback_save=saved_mime_headers[mime_headers_count];
+                               callback_int=buffer_read-buffer_mark;
+                               callback_save[callback_int]='\0';
+                               (void)look(saving_n_chars,buffer_mark,false);
+                               callback_compare="Content-Type:";
+                               (void)look(case_insensitively_comparing_head,buffer_mark,false);
+                               if (callback_bool) {
+                                       type_header=alloc_or_exit(sizeof(char)*(buffer_read-buffer_mark+1));
+                                       strcpy(type_header,saved_mime_headers[mime_headers_count]);
+                                       type_header_body=remove_comments(type_header,true);
+                                       type_header_index=mime_headers_count;
+                                       ++mime_headers_count;
+                                       break;
+                               }
+                               callback_compare="Content-Transfer-Encoding:";
+                               (void)look(case_insensitively_comparing_head,buffer_mark,false);
+                               if (callback_bool) {
+                                       transfer_header=alloc_or_exit(sizeof(char)*(buffer_read-buffer_mark+1));
+                                       strcpy(transfer_header,saved_mime_headers[mime_headers_count]);
+                                       transfer_header_body=remove_comments(transfer_header,true);
+                                       transfer_header_index=mime_headers_count;
+                                       ++mime_headers_count;
+                                       break;
+                               }
+                               callback_compare="Content-Disposition:";
+                               (void)look(case_insensitively_comparing_head,buffer_mark,false);
+                               if (callback_bool) {
+                                       disposition_header=alloc_or_exit(sizeof(char)*(buffer_read-buffer_mark+1));
+                                       strcpy(disposition_header,saved_mime_headers[mime_headers_count]);
+                                       disposition_header_body=remove_comments(disposition_header,true);
+                                       disposition_header_index=mime_headers_count;
+                                       ++mime_headers_count;
+                                       break;
+                               }
+                               ++mime_headers_count;
+                       } while (false);
+               }
+               // Mark the new header
+               buffer_mark=buffer_read;
+               if (buffer_read==buffer_filled) {
+                       // EOF; return
+                       return;
+               }
+               // Read the first part of the new header; loop to read rest
+               read_buffer();
+               callback_compare="\r\n";
+               (void)look(comparing_head,buffer_mark,false);
+               if (callback_bool) {
+                       // End of headers; strip the extra line; return
+                       resort_to_exit(replacements_count==replacements_max,
+                                       "internal error: too many replacements",EX_SOFTWARE);
+                       replacement_starts[replacements_count]=buffer_read-2;
+                       replacement_ends[replacements_count]=buffer_read;
+                       replacement_strings[replacements_count]=NULL;
+                       ++replacements_count;
+                       return;
+               }
+       }
+       /*@=mustfreeonly@*/
+}
+// Returns a pointer to the body part of the header field
+static char * remove_comments(/*@returned@*/ char * header,bool ext) {
+       // This removes comments and any superfluous whitespace in the
+       // header (a structured header, that is, RFC822); it fiddles with
+       // the quoted strings in such a way that backslash escaping means
+       // simply take the next character literally, rather than needing
+       // to do funny things with folded strings. The result is not suitable
+       // for output.
+       char * h=header;
+       char * hh;
+       char * hhh;
+       char * body=NULL;
+       char close;
+       int levels=0;
+       while (*h!=':') ++h;
+       ++h;
+       if (*h==' '||*h=='\t') ++h;
+       body=h;
+       hh=h;
+       while (*h!='\0') {
+               if (*h=='\r'&&*(h+1)=='\n') {
+                       h+=2;
+                       continue;
+               } else if ((*h==' '||*h=='\t')&&delimiting(*(hh-1),ext)) {
+                       ++h;
+                       continue;
+               } else if (delimiting(*h,ext)&&(*(hh-1)==' '||*(hh-1)=='\t')) {
+                       if (hh!=body) --hh;
+               }
+               if (*h=='(') {
+                       ++h;
+                       levels=1;
+                       while (levels>0) {
+                               if (*h=='\0') break;
+                               if (*h=='\\') {
+                                       ++h;
+                                       if (*h=='\0') break;
+                               }
+                               else if (*h=='(') ++levels;
+                               else if (*h==')') --levels;
+                               ++h;
+                       }
+                       if (!delimiting(*h,ext)&&!delimiting(*(hh-1),ext)) {
+                               // Put in some whitespace if something delimiting isn't
+                               // coming and hasn't just been
+                               *hh=' ';
+                               ++hh;
+                       }
+                       continue;
+               } else if (*h=='"'||*h=='[') {
+                       if (*h=='[') close=']';
+                       else close='"';
+                       *hh=*h;
+                       ++h; ++hh;
+                       hhh=hh;
+                       while (*h!='\0'&&*h!=close) {
+                               if (*h=='\\') {
+                                       *hh=*h;
+                                       ++hh; ++h;
+                                       if (*h=='\0') break;
+                                       if (*h=='\r'&&*(h+1)=='\n') {
+                                               *hh=*h; ++hh; ++h;
+                                               *hh=*h; ++hh; ++h;
+                                               if (*h=='\0') break;
+                                               ++hh; ++h;
+                                               continue;
+                                       }
+                               } else if (*h==(char)8) {
+                                       --hh; ++h;
+                                       if (hh<hhh) hh=hhh;
+                                       continue;
+                               } else if (*h=='\r'&&*(h+1)=='\n') {
+                                       h+=2;
+                                       continue;
+                                       // alternatively
+                                       // *hh=*h; ++hh; ++h;
+                                       // *hh=*h; ++hh; ++h;
+                                       // if (*h=='\0') break;
+                                       // ++h;
+                                       // continue;
+                                       // or perhaps even (the spec is a bit ambiguous)
+                                       // h+=2;
+                                       // if (*h=='\0') break;
+                                       // ++h;
+                                       // continue;
+                               }
+                               *hh=*h;
+                               ++h; ++hh;
+                       }
+                       if (*h=='\0') break;
+               }
+               *hh=*h;
+               ++h; ++hh;
+       }
+       if (*(hh-1)==' ') *(hh-1)='\0';
+       else *hh='\0';
+       return body;
+}
+static inline bool delimiting(char c,bool ext) {
+       // 'ext' (extended) delimiters include '/', '?' and '=' and assist
+       // by removing whitespace surrounding those, as these are
+       // delimiters in the MIME header fields, even though not RFC822;
+       // note that MIME doesn't use '.' but we still do.
+       return (c==' '||c=='\t'||c=='<'||c=='>'||c=='@'||
+                       c==','||c==';'||c==':'||c=='\\'||c=='"'||
+                       c=='.'||c=='['||c==']'||
+                       (ext&&(c=='/'||c=='='||c=='?')));
+}
+static inline void remove_mime_headers() {
+       int h;
+       for (h=0;h<mime_headers_count;++h) {
+               resort_to_exit(replacements_count==replacements_max,
+                               "internal error: too many replacements",EX_SOFTWARE);
+               replacement_starts[replacements_count]=mime_header_starts[h];
+               replacement_ends[replacements_count]=mime_header_ends[h];
+               replacement_strings[replacements_count]=NULL;
+               ++replacements_count;
+       }
+       if (version_header!=NULL) {
+               resort_to_exit(replacements_count==replacements_max,
+                               "internal error: too many replacements",EX_SOFTWARE);
+               replacement_starts[replacements_count]=version_header_start;
+               replacement_ends[replacements_count]=version_header_end;
+               replacement_strings[replacements_count]=NULL;
+               ++replacements_count;
+       }
+}
+static inline void output_saved_mime_headers() {
+       int h;
+       for (h=0;h<mime_headers_count;++h) {
+               // The header includes its terminating CRLF
+               /*@-nullpass@*/
+               resort_to_errno(putstr(saved_mime_headers[h])==EOF,
+                               "error echoing string",EX_IOERR);
+               /*@=nullpass@*/
+       }
+}
+static void free_saved_mime_headers() {
+       int h;
+       for (h=0;h<mime_headers_count;++h) {
+               /*@-nullpass@*/
+               free(saved_mime_headers[h]);
+               /*@=nullpass@*/
+       }
+       mime_headers_count=0;
+       if (version_header!=NULL) free(version_header);
+       version_header=NULL;
+       version_header_body=NULL;
+       if (type_header!=NULL) free(type_header);
+       type_header=NULL;
+       type_header_body=NULL;
+       if (transfer_header!=NULL) free(transfer_header);
+       transfer_header=NULL;
+       transfer_header_body=NULL;
+       if (disposition_header!=NULL) free(disposition_header);
+       disposition_header=NULL;
+       disposition_header_body=NULL;
+}
+static bool /*@falsewhennull@*/ is_multipart(/*@null@*//*@out@*/ char ** boundary) {
+       char * b;
+       /*@dependent@*/ char * bb;
+       int l=0;
+       if (type_header_body==NULL||
+                       !case_insensitively_heads("multipart/",type_header_body)) {
+               if (boundary!=NULL) *boundary=NULL;
+               return false;
+       }
+       b=type_header_body+10;
+       for (;;) {
+               while (*b!='\0'&&*b!=';') ++b;
+               if (*b=='\0') {
+                       warning("no boundary given");
+                       if (boundary!=NULL) *boundary=NULL;
+                       return false;
+               }
+               ++b;
+               if (case_insensitively_heads("boundary=",b)) break;
+       }
+       b+=9;
+       if (*b=='"') {
+               for (bb=b+1;*bb!='\0'&&*bb!='"';++bb) {
+                       if (*bb=='\\') {
+                               ++bb;
+                               if (*bb=='\0') break;
+                       }
+                       ++l;
+               }
+               if (*bb=='\0') {
+                       warning("error in boundary syntax");
+                       if (boundary!=NULL) *boundary=NULL;
+                       return false;
+               }
+       } else {
+               // MIME tokens can include '.'
+               for (bb=b;*bb!='\0'&&(!delimiting(*bb,true)||*bb=='.');++bb) ++l;
+       }
+       /*@-mustdefine@*/
+       if (boundary==NULL) return true;
+       /*@=mustdefine@*/
+       // Room for leading and trailing '--', and terminator
+       *boundary=alloc_or_exit(sizeof(char)*(l+5));
+       bb=*boundary;
+       *bb++='-';
+       *bb++='-';
+       if (*b=='"') {
+               ++b;
+               while (*b!='\0'&&*b!='"') {
+                       if (*b=='\\') ++b;
+                       *bb++=*b++;
+               }
+       } else {
+               // MIME tokens can include '.'
+               while (*b!='\0'&&(!delimiting(*b,true)||*b=='.')) *bb++=*b++;
+       }
+       *bb='\0';
+       return true;
+}
+static inline bool is_signed() {
+       return type_header_body!=NULL&&
+                       case_insensitively_heads("multipart/signed",type_header_body);
+}
+static inline bool is_alternative() {
+       return type_header_body!=NULL&&
+                       case_insensitively_heads("multipart/alternative",type_header_body);
+}
+static inline bool is_mixed() {
+       return type_header_body!=NULL&&
+                       case_insensitively_heads("multipart/mixed",type_header_body);
+}
+static inline bool is_html() {
+       return type_header_body!=NULL&&
+                       case_insensitively_heads("text/html",type_header_body);
+}
+static inline bool is_plain() {
+       return type_header_body==NULL||
+                       case_insensitively_heads("text/plain",type_header_body);
+}
+static inline bool is_signature() {
+       return type_header_body!=NULL&&(
+                       case_insensitively_heads("application/x-pkcs7-signature",
+                       type_header_body)||
+                       case_insensitively_heads("application/pgp-signature",
+                       type_header_body));
+}
+static inline bool is_attachment() {
+       return disposition_header_body!=NULL&&
+                       case_insensitively_heads("attachment",disposition_header_body);
+}
+static inline void set_decoding_type() {
+       if (transfer_header_body==NULL) {
+               decoding=unencoded;
+               return;
+       }
+       if (case_insensitively_heads("quoted-printable",transfer_header_body)) {
+               decoding=quoted_printable;
+               return;
+       }
+       if (case_insensitively_heads("base64",transfer_header_body)) {
+               decoding=base64;
+               return;
+       }
+       decoding=unencoded;
+       if (case_insensitively_heads("7bit",transfer_header_body)) return;
+       if (case_insensitively_heads("8bit",transfer_header_body)) return;
+       if (case_insensitively_heads("binary",transfer_header_body)) return;
+       warning("unrecognised transfer encoding");
+}
+static inline void change_to_mixed() {
+       char * boundary=NULL;
+       char * header;
+       char * b, * h;
+       int l=0;
+       if (!is_multipart(&boundary)) {
+               warning("internal error: changing non-multipart to mixed");
+               return;
+       }
+       boundary+=2;
+       // The special cases should never happen, as '\', '"', '\r' and '\n'
+       // aren't allowed in boundaries...but...just in case...
+       for (b=boundary;*b!='\0';++b) {
+               if (*b=='\r'||*b=='\n') {
+                       warning("boundary with newline");
+                       return;
+               }
+               if (*b=='"'||*b=='\\') ++l;
+               ++l;
+       }
+       header=alloc_or_exit(sizeof(char)*(50+l)); // Play it safe
+       strcpy(header,"Content-Type: multipart/mixed;\r\n boundary=\"");
+       for (h=header+43,b=boundary;*b!='\0';++h,++b) {
+               if (*b=='"'||*b=='\\') { *h='\\'; ++h; }
+               *h=*b;
+       }
+       *h++='"'; *h++='\r'; *h++='\n'; *h++='\0';
+       free(boundary-2);
+       /*@-nullpass@*/
+       free(saved_mime_headers[type_header_index]);
+       /*@=nullpass@*/
+       saved_mime_headers[type_header_index]=header;
+       resort_to_exit(replacements_count==replacements_max,
+                       "internal error: too many replacements",EX_SOFTWARE);
+       replacement_starts[replacements_count]=
+                       mime_header_starts[type_header_index];
+       replacement_ends[replacements_count]=
+                       mime_header_ends[type_header_index];
+       replacement_strings[replacements_count]=
+                       saved_mime_headers[type_header_index];
+       ++replacements_count;
+}
+static inline void generate_boundary(/*@out@*/ char ** boundary) {
+       int r;
+       *boundary=alloc_or_exit(sizeof(char)*42); // Life, the universe and everything
+       strcpy(*boundary,"--=_foot_filter_boundary_0123456789012_=");
+       for (r=25;r<38;++r) {
+               (*boundary)[r]=(char)((int)'A'+(random()%16));
+       }
+}
+static inline void output_mime_mixed_headers(const char * boundary) {
+       resort_to_errno(
+                       putstr("MIME-Version: 1.0\r\n")==EOF,
+                       "error echoing string",EX_IOERR);
+       resort_to_errno(
+                       putstr("Content-Type: multipart/mixed;\r\n boundary=\"")==EOF,
+                       "error echoing string",EX_IOERR);
+       resort_to_errno(putstr(boundary+2)==EOF,
+                       "error echoing string",EX_IOERR);
+       // I put an extra CRLF just in case some mail reader expects the
+       // initial boundary to include one that is separate from the one that
+       // ends the headers.
+       resort_to_errno(
+                       putstr("\"\r\n\r\n\r\n")==EOF,
+                       "error echoing string",EX_IOERR);
+}
+static inline void output_prolog() {
+       // Deliberately empty; we don't need any prolog
+}
+static void output_mime_footer(const char * boundary) {
+       output_boundary(boundary);
+       if (html_mime_footer) {
+               resort_to_errno(
+                               putstr("Content-Type: text/html; charset=UTF-8\r\n")==EOF,
+                               "error echoing string",EX_IOERR);
+       } else {
+               resort_to_errno(
+                               putstr("Content-Type: text/plain; charset=UTF-8\r\n")==EOF,
+                               "error echoing string",EX_IOERR);
+       }
+       resort_to_errno(
+                       putstr("Content-Transfer-Encoding: quoted-printable\r\n")==EOF,
+                       "error echoing string",EX_IOERR);
+       encoding=quoted_printable;
+       resort_to_errno(putstr("\r\n")==EOF,
+                       "error echoing string",EX_IOERR);
+       /*@-nullpass@*/
+       encode_footer(mime_footer);
+       /*@=nullpass@*/
+       encodechar((int)'\r');
+       encodechar((int)'\n');
+       finish_encoding();
+       // This is logically part of the boundary that is about to come
+       resort_to_errno(putstr("\r\n")==EOF,
+                       "error echoing string",EX_IOERR);
+}
+static inline void output_boundary(const char * boundary) {
+       resort_to_errno(putstr(boundary)==EOF,"error echoing string",EX_IOERR);
+       resort_to_errno(putstr("\r\n")==EOF,"error echoing string",EX_IOERR);
+}
+static inline void output_final_boundary(const char * boundary) {
+       resort_to_errno(putstr(boundary)==EOF,"error echoing string",EX_IOERR);
+       resort_to_errno(putstr("--\r\n")==EOF,"error echoing string",EX_IOERR);
+}
+static bool at_final_boundary(char * boundary) {
+       int l;
+       // If no lookahead, we probably hit EOF, so we primarily just need to get
+       // out of loops and exit
+       if (buffer_filled-buffer_read==0) {
+               warning("probably unexpected end of input");
+               return true;
+       }
+       l=(int)strlen(boundary);
+       boundary[l]='-';
+       boundary[l+1]='-';
+       boundary[l+2]='\0';
+       /*@-temptrans@*/
+       callback_compare=boundary;
+       /*@=temptrans@*/
+       (void)look(comparing_head,buffer_read,false);
+       callback_compare=NULL;
+       boundary[l]='\0';
+       return callback_bool;
+}
+
+/* tag: footer_functions */
+
+static inline void process_text_section(bool add_footer,
+               /*@null@*/ const char * footer,
+               const char * prefix, const char * suffix, const char * replacement,
+               const_null_string * tails, const_null_string * padding,
+               const_null_string * guts, const_null_string * pairs,
+               char * boundary) {
+       int prefix_pos;
+       int later_prefix_pos;
+       int suffix_pos;
+       bool removed_footers=false;
+       bool removed_newline=false;
+       bool boundary_newline=false;
+       int prefixl=(int)strlen(prefix);
+       int suffixl=(int)strlen(suffix);
+       resort_to_exit(buffer_filled>0,
+                       "internal error: unexpected data in buffer",EX_SOFTWARE);
+       set_decoding_type();
+       encoding=decoding;
+       decode_and_read_to_boundary_encoding_when_full(boundary);
+       if (smart_footer&&footer!=NULL) {
+       // alternatively
+       // if (smart_footer) {
+               for (;;) {
+                       prefix_pos=pos_of(prefix,0,buffer_read);
+                       if (prefix_pos==EOF) break;
+                       suffix_pos=pos_of(suffix,prefix_pos,buffer_read);
+                       if (suffix_pos==EOF) break;
+                       for (;;) {
+                               later_prefix_pos=
+                                               pos_of(prefix,prefix_pos+prefixl,suffix_pos-prefixl);
+                               if (later_prefix_pos!=EOF) prefix_pos=later_prefix_pos;
+                               else break;
+                       }
+                       suffix_pos+=suffixl;
+                       pad(padding,guts,pairs,&prefix_pos,&suffix_pos);
+                       replacement_starts[replacements_count]=prefix_pos;
+                       replacement_ends[replacements_count]=suffix_pos;
+                       // We may not want the last replacement so replace
+                       // with nothing first
+                       replacement_strings[replacements_count]=NULL;
+                       ++replacements_count;
+                       // We want the last replacement; encode it now before
+                       // doing any more encoding
+                       if (removed_footers) encode_string(replacement);
+                       encode_replacements();
+                       removed_footers=true;
+               }
+       }
+       if (*boundary!='\0'&&(decoding==quoted_printable||decoding==unencoded)) {
+               // If we're not using base64 encoding, and we're in multipart, there
+               // will be a final CRLF that is part of the input but logically part of
+               // the boundary, not the text. Removing the footer may have already
+               // removed it, so we need to check if it's here or not.
+               if (buffer_read>1) {
+                       callback_compare="\r\n";
+                       (void)look(comparing_head,buffer_read-2,false);
+                       callback_compare=NULL;
+                       if (callback_bool) boundary_newline=true;
+               }
+       }
+       if (add_footer&&footer!=NULL) {
+               // This will skip past the boundary newline
+               mark_tail(tails);
+               if (removed_footers&&buffer_mark==0) {
+                       // The last replacement coincides with where the footer
+                       // is going to go; don't use the replacement text.
+                       removed_footers=false;
+               }
+       }
+       if (removed_footers) encode_string(replacement);
+       if (add_footer&&footer!=NULL) {
+               if (buffer_mark<buffer_read-2||
+                               (buffer_mark==buffer_read-2&&!boundary_newline)) {
+                       // If we tailed back past a newline (that wasn't the boundary
+                       // newline which isn't really there) we don't bother appending
+                       // a new one to the footer
+                       callback_compare="\r\n";
+                       (void)look(comparing_head,buffer_mark,false);
+                       if (callback_bool) removed_newline=true;
+               }
+               encode_to_mark();
+               encodechar((int)'\r');
+               encodechar((int)'\n');
+               encode_footer(footer);
+               if (!removed_newline) {
+                       encodechar((int)'\r');
+                       encodechar((int)'\n');
+               }
+       }
+       if (boundary_newline) {
+               // Actually remove the boundary newline now
+               if (replacements_count==replacements_max) {
+                       warning("internal error: too many replacements");
+               } else {
+                       replacement_starts[replacements_count]=buffer_read-2;
+                       replacement_ends[replacements_count]=buffer_read;
+                       replacement_strings[replacements_count]=NULL;
+                       ++replacements_count;
+               }
+       }
+       encode_lookbehind();
+       finish_encoding();
+       if (*boundary!='\0') {
+               // This is logically part of the boundary
+               resort_to_errno(putstr("\r\n")==EOF,"error echoing string",EX_IOERR);
+       }
+}
+static inline void pad(const_null_string * padding,
+               const_null_string * guts, const_null_string * pairs,
+               int * prefix_pos, int * suffix_pos) {
+       const char ** run;
+       const char ** test;
+       const char ** opening;
+       const char ** closing;
+       int saved_prefix_pos;
+       int definite_prefix_pos;
+       int definite_suffix_pos;
+       bool pair_succeeded;
+       bool pad_succeeded;
+       // Could generate lengths at init time for speed
+       int l;
+       int ll;
+       // If we succeed for one thing, we try it again straight away,
+       // as a number of types of padding are likely to occur in multiples.
+       // Then we keep trying the whole lot until nothing is left to do.
+       do {
+               // Try each piece of padding (or guts)
+               definite_prefix_pos=EOF;
+               definite_suffix_pos=EOF;
+               run=padding;
+               do {
+                       do {
+                               pad_succeeded=false;
+                               test=run;
+                               while (*test!=NULL) {
+                                       l=(int)strlen(*test);
+                                       for (;;) {
+                                               // Check for padding at tail
+                                               if (buffer_filled-*suffix_pos<l) break;
+                                               callback_compare=*test;
+                                               (void)look(comparing_head,*suffix_pos,false);
+                                               if (!callback_bool) break;
+                                               *suffix_pos+=l;
+                                               pad_succeeded=true;
+                                       }
+                                       for (;;) {
+                                               // Check for padding at head
+                                               if (*prefix_pos-l<0) break;
+                                               callback_compare=*test;
+                                               (void)look(comparing_head,*prefix_pos-l,false);
+                                               if (!callback_bool) break;
+                                               *prefix_pos-=l;
+                                               pad_succeeded=true;
+                                       }
+                                       ++test;
+                               }
+                       } while (pad_succeeded);
+                       if (definite_prefix_pos==EOF) {
+                               // Do a second run to deal with guts in a more tentative way
+                               definite_prefix_pos=*prefix_pos;
+                               definite_suffix_pos=*suffix_pos;
+                               run=guts;
+                       } else break;
+               } while (true);
+               // Try each pair
+               pair_succeeded=false;
+               closing=pairs;
+               while (*closing!=NULL) {
+                       l=(int)strlen(*closing);
+                       opening=closing+1;
+                       // This loop is just to be broken out of; we don't try
+                       // pairs twice when they succeed as they aren't too likely
+                       // to nest.
+                       do {
+                               // Check for closing part
+                               if (buffer_filled-*suffix_pos<l) break;
+                               callback_compare=*closing;
+                               (void)look(comparing_head,*suffix_pos,false);
+                               if (!callback_bool) break;
+                               // Check for end of opening part
+                               ll=(int)strlen(*opening);
+                               if (*prefix_pos-ll<0) break;
+                               callback_compare=*opening;
+                               (void)look(comparing_head,*prefix_pos-ll,false);
+                               if (!callback_bool) break;
+                               saved_prefix_pos=*prefix_pos;
+                               // Try each variant
+                               for (++opening;*opening!=NULL;
+                                               *prefix_pos=saved_prefix_pos,++opening) {
+                                       // Search back for first character
+                                       ll=(int)strlen(*opening);
+                                       if (*prefix_pos-ll<0) continue;
+                                       *prefix_pos-=ll;
+                                       callback_match=(int)**opening;
+                                       if (lookback(until_match,*prefix_pos,true)) {
+                                               // Check if this is the variant
+                                               callback_compare=*opening;
+                                               (void)look(comparing_head,*prefix_pos,false);
+                                               if (!callback_bool) continue;
+                                               *suffix_pos+=l;
+                                               pair_succeeded=true;
+                                               break;
+                                       }
+                               }
+                       } while (false);
+                       // Skip all the variants if not skipped already
+                       while (*opening!=NULL) ++opening;
+                       closing=opening+1;
+               }
+               if (!pair_succeeded) {
+                       // If the pair didn't succeed, we don't remove the guts
+                       *prefix_pos=definite_prefix_pos;
+                       *suffix_pos=definite_suffix_pos;
+               }
+       } while (pair_succeeded);
+}
+static inline void mark_tail(const_null_string * padding) {
+       // This is basically pad() with deletions, but adding the two lines
+       // at the start to initially place the mark. Comments at pad() apply.
+       bool something_changed;
+       const char ** test;
+       int l;
+       buffer_mark=buffer_read;
+       buffer_marked=true;
+       do {
+               something_changed=false;
+               // Try each piece of padding
+               test=padding;
+               while (*test!=NULL) {
+                       l=(int)strlen(*test);
+                       for (;;) {
+                               // Check for padding at head
+                               if (buffer_mark-l<0) break;
+                               callback_compare=*test;
+                               (void)look(comparing_head,buffer_mark-l,false);
+                               if (!callback_bool) break;
+                               buffer_mark-=l;
+                               something_changed=true;
+                       }
+                       ++test;
+               }
+       } while (something_changed);
+}
+static inline void encode_footer(const char * footer) {
+       while (*footer!='\0') {
+               if (*footer=='\n') encodechar((int)'\r');
+               encodechar((int)(unsigned int)*footer);
+               footer++;
+       }
+}
+
+/* tag: buffer_functions */
+
+// The first few of these handle replacements, the rest not.
+static inline void read_buffer() {
+       buffer_read=buffer_filled;
+}
+static inline void echo_buffer() {
+       read_buffer();
+       echo_lookbehind();
+}
+static inline void skip_buffer() {
+       if (buffer_filled>0) (void)empty(until_no_buffer);
+}
+static inline void echo_lookbehind() {
+       make_replacements(echoing_one_char,echoing_until_start_marked);
+       if (buffer_read>0) (void)empty(echoing_until_no_lookbehind);
+}
+static inline void encode_lookbehind() {
+       make_replacements(encoding_one_char,encoding_until_start_marked);
+       if (buffer_read>0) (void)empty(encoding_until_no_lookbehind);
+}
+static inline void encode_replacements() {
+       make_replacements(encoding_one_char,encoding_until_start_marked);
+}
+static inline void make_replacements(callback_t one_char,
+               callback_t start_marked) {
+       int r, minr=0;
+       const char * c;
+       if (buffer_read==0) return;
+       buffer_marked=false;
+       while (replacements_count>0) {
+               for (r=0;r<replacements_count;++r) {
+                       if (!buffer_marked||replacement_starts[r]<buffer_mark) {
+                               buffer_marked=true;
+                               minr=r;
+                               buffer_mark=replacement_starts[r];
+                       }
+               }
+               for (r=0;r<replacements_count;++r) {
+                       replacement_starts[r]-=buffer_mark;
+                       replacement_ends[r]-=buffer_mark;
+               }
+               if (buffer_mark>0) (void)empty(start_marked);
+               c = replacement_strings[minr];
+               if (c!=NULL) {
+                       while (*c!='\0') {
+                               buffer_char=(int)(unsigned int)*c;
+                               (void)(*one_char)();
+                               ++c;
+                       }
+               }
+               buffer_marked=true;
+               buffer_mark=replacement_ends[minr];
+               for (r=0;r<replacements_count;++r) {
+                       replacement_starts[r]-=buffer_mark;
+                       replacement_ends[r]-=buffer_mark;
+               }
+               if (buffer_mark>0) (void)empty(until_start_marked);
+               for (r=minr;r<replacements_count-1;++r) {
+                       replacement_starts[r]=replacement_starts[r+1];
+                       replacement_ends[r]=replacement_ends[r+1];
+                       replacement_strings[r]=replacement_strings[r+1];
+               }
+               --replacements_count;
+               buffer_marked=false;
+       }
+}
+static inline void encode_to_mark() {
+       if (buffer_mark>0) (void)empty(encoding_until_start_marked);
+}
+static inline void echo_disk_buffer() {
+       if (disk_buffer_filled>0) (void)empty(echoing_until_no_disk_buffer);
+}
+static inline void encode_disk_buffer() {
+       if (disk_buffer_filled>0) (void)empty(encoding_until_no_disk_buffer);
+}
+static inline void skip_disk_buffer() {
+       if (disk_buffer_filled>0) (void)empty(until_no_disk_buffer);
+}
+static inline void read_boundary(/*@out@*/ char ** boundary) {
+       int l=0;
+       if (buffer_filled>buffer_read) {
+               callback_bool=false;
+               callback_int=0;
+               resort_to_exit(!look(counting_until_eol,buffer_read,false),
+                               "internal error: missing eol at section boundary",EX_SOFTWARE);
+               l=callback_int-2; // remove the CRLF, but keep the leading '--'
+       }
+       // Leave room to append a trailing '--' for testing final boundary;
+       // the CRLF will be written in this space by saving_until_eol too.
+       *boundary = alloc_or_exit(sizeof(char)*(l+3));
+       if (buffer_filled>buffer_read) {
+               callback_bool=false;
+               callback_save=*boundary;
+               (void)look(saving_until_eol,buffer_read,false);
+               callback_save=NULL;
+       }
+       (*boundary)[l]='\0';
+       if (buffer_filled>buffer_read) {
+               callback_bool=false;
+               (void)look(until_eol,buffer_read,true);
+       }
+}
+static inline void echo_to_boundary(const char * boundary) {
+       do {
+               echo_buffer();
+       } while (!process_one_line_checking_boundary(
+                       echoing_n_chars,NULL,until_eol,echo,boundary));
+}
+static inline void skip_to_boundary(const char * boundary) {
+       do {
+               skip_buffer();
+       } while (!process_one_line_checking_boundary(
+                       n_chars,NULL,until_eol,discard,boundary));
+}
+static inline void decode_and_read_to_boundary_encoding_when_full(
+               const char * boundary) {
+       do {
+               read_buffer();
+       } while (!process_one_line_checking_boundary(
+                       encoding_n_chars,decode_lookahead,
+                       decoding_until_eol,encode,boundary));
+       finish_decoding(); // This just sets state, doesn't change data
+}
+static inline bool process_one_line_checking_boundary(callback_t n_chars,
+               /*@null@*/ function_t process, callback_t processing,
+               when_full_t when_full, const char * boundary) {
+       bool stopped_by_design;
+       if (feof(stdin)!=0) {
+               // We're done! Call it a boundary (even if it isn't--we need to
+               // get out of loops cleanly and tidy up as best we can).
+               return true;
+       }
+       // Empty until enough space for boundary
+       if (mem_buffer_size-mem_buffer_filled<80) {
+               callback_int=80-(mem_buffer_size-mem_buffer_filled);
+               (void)empty(n_chars);
+       }
+       callback_bool=false;
+       stopped_by_design=fill(until_eol,stop);
+       if (stopped_by_design||feof(stdin)!=0) {
+               if (buffer_filled-buffer_read==0) {
+                       return *boundary=='\0';
+               }
+               callback_bool=false;
+               if (*boundary!='\0') {
+                       // Can only be at a boundary without being at EOF if there
+                       // really is a boundary
+                       /*@-temptrans@*/
+                       callback_compare=boundary;
+                       /*@=temptrans@*/
+                       (void)look(comparing_head,buffer_read,false);
+                       callback_compare=NULL;
+               }
+               if (!callback_bool&&process!=NULL) (*process)();
+               return callback_bool;
+       } else {
+               // Line is too long to be a boundary, so must be decoded
+               if (process!=NULL) (*process)();
+               callback_bool=false;
+               (void)fill(processing,when_full);
+               return false;
+       }
+}
+
+// Return the position of text whose start may occur in the buffer
+// anywhere between from and (just before) to. Use EOF for from to
+// go from current location; use EOF for to to read indefinitely;
+// EOF is returned if text is not found.
+static int pos_of(const char * text,int from,int to) {
+       int saved_buffer_read;
+       int pos=EOF;
+       if (*text=='\0') return from;
+       saved_buffer_read=buffer_read;
+       if (from!=EOF) buffer_read=from;
+       callback_match=(int)(unsigned int)*text;
+       for (;;) {
+               if (to!=EOF) {
+                       callback_int=to-buffer_read;
+                       if (!look(n_chars_until_match,buffer_read,true)) break;
+               } else {
+                       if (!look(until_match,buffer_read,true)) break;
+               }
+               if (!callback_bool) break;
+               /*@-temptrans@*/
+               callback_compare=text+1;
+               /*@=temptrans@*/
+               (void)look(comparing_head,buffer_read,false);
+               callback_compare=NULL;
+               if (callback_bool) {
+                       // Include the first character
+                       pos=buffer_read-1;
+                       break;
+               }
+       }
+       buffer_read=saved_buffer_read;
+       return pos;
+}
+
+// Look at characters in the buffer, starting at offset from,
+// 'reading' if so indicated (and looking at that location).
+// The callback is called after updating the reading pointer
+// and placing the character in the buffer. The character is
+// also passed by means of the buffer_char global.
+// EOF is sent to the callback when we run out of data.
+// There is no automatic attempt to fill the buffer.
+// The callback should return a boolean indicating whether
+// to continue. This function will return true if the callback
+// indicated to stop (including if it so indicated on EOF), or
+// false if it stopped for EOF.
+// We always call the callback at least once, so don't call
+// this function at all unless you definitely want to look
+// at something.
+static bool look(callback_t callback,int from,bool read) {
+       int pos=from;
+       int disk_buffer_pos;
+       char * mem_buffer_pos;
+       if (pos<disk_buffer_filled) {
+               disk_buffer_pos=disk_buffer_start+pos;
+               if (disk_buffer_sought!=disk_buffer_pos) {
+                       /*@-nullpass@*/
+                       resort_to_errno(fseek(disk_buffer,disk_buffer_pos,SEEK_SET)!=0,
+                                       "error seeking in temporary file",EX_IOERR);
+                       /*@=nullpass@*/
+                       disk_buffer_sought=disk_buffer_pos;
+               }
+               while (pos<disk_buffer_filled) {
+                       /*@-nullpass@*/
+                       buffer_char=getc(disk_buffer);
+                       /*@=nullpass@*/
+                       resort_to_errno(buffer_char==EOF,
+                                       "error reading temporary file",EX_IOERR);
+                       // ++disk_buffer_pos; logically this happens, but it is unnecessary
+                       ++disk_buffer_sought;
+                       if (read&&pos==buffer_read) ++buffer_read;
+                       ++pos;
+                       if (!(*callback)()) return true;
+               }
+       }
+       if (pos<buffer_filled) {
+               mem_buffer_pos=mem_buffer_next_empty+(pos-disk_buffer_filled);
+               if (mem_buffer_pos>=mem_buffer_end) mem_buffer_pos-=mem_buffer_size;
+               while (pos<buffer_filled) {
+                       buffer_char=(int)(unsigned int)*mem_buffer_pos;
+                       ++mem_buffer_pos;
+                       if (mem_buffer_pos==mem_buffer_end) mem_buffer_pos=mem_buffer_start;
+                       if (read&&pos==buffer_read) ++buffer_read;
+                       ++pos;
+                       if (!(*callback)()) return true;
+               }
+       }
+       buffer_char=EOF;
+       if (!(*callback)()) return true;
+       return false;
+}
+// Does the same backwards, moving the mark if so indicated (and
+// looking at that location). The callback is called before the
+// mark is moved, though, and if it returns false, the mark is
+// not moved, but the function returns true immediately.
+// There is no call to the callback with EOF when we get to the
+// start of the buffer, so the function always returns false in
+// that case, and unmarks the buffer. Again, the callback is
+// always called at least once.
+static bool lookback(callback_t callback,int from,bool mark) {
+       int pos=from;
+       int disk_buffer_pos;
+       char * mem_buffer_pos;
+       if (pos>=disk_buffer_filled) {
+               mem_buffer_pos=mem_buffer_next_empty+(pos-disk_buffer_filled);
+               if (mem_buffer_pos>=mem_buffer_end) mem_buffer_pos-=mem_buffer_size;
+               while (pos>=disk_buffer_filled) {
+                       buffer_char=(int)(unsigned int)*mem_buffer_pos;
+                       if (!(*callback)()) return true;
+                       --mem_buffer_pos;
+                       if (mem_buffer_pos==mem_buffer_start-1)
+                                       mem_buffer_pos=mem_buffer_end-1;
+                       if (mark&&pos==buffer_mark) --buffer_mark;
+                       --pos;
+               }
+       }
+       if (pos>=0&&disk_buffer_filled>0) {
+               disk_buffer_pos=disk_buffer_start+pos;
+               // Reading backwards in the disk buffer is potentially very nasty;
+               // hopefully it never actually happens
+               while (pos>=0) {
+                       /*@-nullpass@*/
+                       resort_to_errno(fseek(disk_buffer,disk_buffer_pos,SEEK_SET)!=0,
+                                       "error seeking in temporary file",EX_IOERR);
+                       disk_buffer_sought=disk_buffer_pos;
+                       buffer_char=getc(disk_buffer);
+                       /*@=nullpass@*/
+                       resort_to_errno(buffer_char==EOF,
+                                       "error reading temporary file",EX_IOERR);
+                       ++disk_buffer_sought;
+                       if (!(*callback)()) return true;
+                       --disk_buffer_pos;
+                       if (mark&&pos==buffer_mark) --buffer_mark;
+                       --pos;
+               }
+       }
+       if (mark&&buffer_mark==-1) {
+               buffer_mark=0;
+               buffer_marked=false;
+       }
+       // We don't call the callback on EOF when going backwards
+       // buffer_char=EOF;
+       // (void)(*callback)();
+       return false;
+}
+// Remove characters from the (beginning of the) buffer. The same
+// general principles as for look() apply. The callback is called
+// after the character is removed and all accounting has been done, so
+// perhaps the only place you can reliably find the character is in
+// the buffer_char global. Again the callback gets an EOF call if
+// there's nothing more to empty, and no automatic filling is done.
+// The callback and function return values are as for look() and
+// again, the callback is always called at least once; this means at
+// least one character is always removed from the buffer, so only call
+// the function if something definitely should be removed.
+static bool empty(callback_t callback) {
+       if (disk_buffer_filled>0) {
+               if (disk_buffer_sought!=disk_buffer_start) {
+                       /*@-nullpass@*/
+                       resort_to_errno(fseek(disk_buffer,disk_buffer_start,SEEK_SET)!=0,
+                                       "error seeking in temporary file",EX_IOERR);
+                       /*@=nullpass@*/
+                       disk_buffer_sought=disk_buffer_start;
+               }
+               while (disk_buffer_filled>0) {
+                       /*@-nullpass@*/
+                       buffer_char=getc(disk_buffer);
+                       /*@=nullpass@*/
+                       resort_to_errno(buffer_char==EOF,
+                                       "error reading temporary file",EX_IOERR);
+                       ++disk_buffer_sought;
+                       ++disk_buffer_start;
+                       --disk_buffer_filled;
+                       --buffer_filled;
+                       if (buffer_read>0) --buffer_read;
+                       if (buffer_marked) {
+                               if (buffer_mark>0) --buffer_mark;
+                               else buffer_marked=false;
+                       }
+                       if (!(*callback)()) return true;
+               }
+       }
+       while (mem_buffer_filled>0) {
+               buffer_char=(int)(unsigned int)*mem_buffer_next_empty;
+               ++mem_buffer_next_empty;
+               if (mem_buffer_next_empty==mem_buffer_end) mem_buffer_next_empty=mem_buffer_start;
+               --mem_buffer_filled;
+               --buffer_filled;
+               if (buffer_read>0) --buffer_read;
+               if (buffer_marked) {
+                       if (buffer_mark>0) --buffer_mark;
+                       else buffer_marked=false;
+               }
+               if (!(*callback)()) return true;
+       }
+       buffer_char=EOF;
+       if (!(*callback)()) return true;
+       return false;
+}
+// Get more characters into the (end of the) buffer. The same
+// general principles as for look() apply. The callback is called
+// after the character is added and all accounting has been done,
+// gets the character via buffer_char, including an EOF when no more
+// input is available (EOF on stdin). It should return whether to get
+// more characters, and this function will return whether its exit was
+// requested by the callback or not (the callback may signal EOF is
+// an appropriate place to stop and we still return true).
+// When the buffer is full there are a number of automatic options
+// echo old the data to stdout or call encodechar for it one character
+// at a time; shunt a block off to disk, keeping mem_buffer_keep in
+// memory, discard it a character at a time, stop (and return false;
+// no EOF call is made), or fail (exit). Here 'full' is defined as
+// less than mem_buffer_margin of space after adding the most recent
+// character, so there is always a bit of space for callbacks to do
+// input transformations. Again, at least one character is always
+// added (if possible), and thus consumed from stdin, so only call this
+// if you really want to do that.
+static bool fill(callback_t callback, when_full_t when_full) {
+       if (feof(stdin)!=0) {
+               buffer_char=EOF;
+               if (!(*callback)()) return true;
+               return false;
+       }
+       for (;;) {
+               /*@-infloops@*/
+               while (mem_buffer_filled>=mem_buffer_size-mem_buffer_margin) {
+                       switch (when_full) {
+                       case echo:
+                               if (disk_buffer_filled>0) echo_disk_buffer();
+                               (void)empty(echoing_one_char);
+                               break;
+                       case encode:
+                               if (disk_buffer_filled>0) encode_disk_buffer();
+                               (void)empty(encoding_one_char);
+                               break;
+                       case discard:
+                               if (disk_buffer_filled>0) skip_disk_buffer();
+                               (void)empty(one_char);
+                               break;
+                       case shunt:
+                               shunt_to_disk(mem_buffer_filled-mem_buffer_keep);
+                               break;
+                       case stop:
+                               return false;
+                       case fail: default:
+                               resort_to_exit(true,"buffer full",EX_SOFTWARE);
+                       }
+               }
+               /*@=infloops@*/
+               buffer_char=get();
+               if (buffer_char==EOF) {
+                       resort_to_errno(ferror(stdin)!=0,"error reading input",EX_IOERR);
+                       if (!(*callback)()) return true;
+                       return false;
+               }
+               *mem_buffer_next_fill=(char)buffer_char;
+               ++mem_buffer_next_fill;
+               if (mem_buffer_next_fill==mem_buffer_end) mem_buffer_next_fill=mem_buffer_start;
+               ++mem_buffer_filled;
+               ++buffer_filled;
+               if (!(*callback)()) return true;
+       }
+}
+
+static inline void create_disk_buffer() {
+       int fildes;
+       fildes=mkstemp(disk_buffer_template);
+       resort_to_errno(fildes==-1,
+                       "cannot create temporary file",EX_CANTCREAT);
+       disk_buffer=fdopen(fildes,"rw");
+       resort_to_errno(disk_buffer==NULL,
+                       "cannot create temporary stream",EX_CANTCREAT);
+}
+static void remove_disk_buffer() {
+       if (disk_buffer!=NULL) {
+               resort_to_warning(fclose(disk_buffer)!=0,
+                               "error closing temporary file");
+               disk_buffer=NULL;
+               resort_to_warning(unlink(disk_buffer_template)!=0,
+                               "error removing temporary file");
+       }
+}
+static inline void shunt_to_disk(int n) {
+       if (disk_buffer==NULL) create_disk_buffer();
+       if (disk_buffer_sought!=disk_buffer_start+disk_buffer_filled) {
+               disk_buffer_sought=disk_buffer_start+disk_buffer_filled;
+               /*@-nullpass@*/
+               resort_to_errno(fseek(disk_buffer,
+                               disk_buffer_start+disk_buffer_filled,SEEK_SET)!=0,
+                               "cannot seek to end of temporary file",EX_IOERR);
+               /*@=nullpass@*/
+       }
+       while (n>0) {
+               resort_to_exit(mem_buffer_filled==0,
+                               "internal error: shunting too much to disk",EX_SOFTWARE);
+               /*@-nullpass@*/
+               resort_to_errno(putc(*mem_buffer_next_empty,disk_buffer)==EOF,
+                               "error writing to temporary file",EX_IOERR);
+               /*@=nullpass@*/
+               ++disk_buffer_sought;
+               ++disk_buffer_filled;
+               ++mem_buffer_next_empty;
+               if (mem_buffer_next_empty==mem_buffer_end) mem_buffer_next_empty=mem_buffer_start;
+               --mem_buffer_filled;
+               --n;
+       }
+}
+
+/* tag: callback_functions */
+
+static bool one_char() {
+       callback_int=buffer_char;
+       return false;
+}
+static bool echoing_one_char() {
+       if (buffer_char!=EOF) {
+               resort_to_errno(put(buffer_char)==EOF,"error echoing",EX_IOERR);
+       }
+       callback_int=buffer_char;
+       return false;
+}
+static bool encoding_one_char() {
+       if (buffer_char!=EOF) encodechar(buffer_char);
+       callback_int=buffer_char;
+       return false;
+}
+// Set up callback_int before using this.
+static bool n_chars() {
+       return --callback_int>0;
+}
+// Set up callback_int before using this.
+static bool echoing_n_chars() {
+       if (buffer_char!=EOF) {
+               resort_to_errno(put(buffer_char)==EOF,"error echoing",EX_IOERR);
+       }
+       return --callback_int>0;
+}
+// Set up callback_int before using this.
+static bool encoding_n_chars() {
+       if (buffer_char!=EOF) encodechar(buffer_char);
+       return --callback_int>0;
+}
+// Set up callback_int and callback_save before using this.
+static bool saving_n_chars() {
+       if (buffer_char!=EOF) *callback_save++=(char)buffer_char;
+       // We don't actually need this, though it's a good idea, really!
+       // *callback_save='\0';
+       return --callback_int>0;
+}
+// Set up callback_int and callback_match before using this.
+static bool n_chars_until_match() {
+       callback_bool=buffer_char==callback_match;
+       return --callback_int>0&&buffer_char!=callback_match;
+}
+// Do callback_bool=false before using this.
+static bool until_eol() {
+       if (buffer_char==(int)'\n') return !callback_bool;
+       callback_bool=buffer_char==(int)'\r';
+       return true;
+}
+// Do callback_bool=false before using this.
+/*static bool echoing_until_eol() {
+       if (buffer_char!=EOF) {
+               resort_to_errno(put(buffer_char)==EOF,"error echoing",EX_IOERR);
+       }
+       if (buffer_char==(int)'\n') return !callback_bool;
+       callback_bool=buffer_char==(int)'\r';
+       return true;
+}*/
+// Do callback_bool=false, callback_int=0 before using this.
+static bool counting_until_eol() {
+       if (buffer_char!=EOF) ++callback_int;
+       if (buffer_char==(int)'\n') return !callback_bool;
+       callback_bool=buffer_char==(int)'\r';
+       return true;
+}
+// Do callback_bool=false and set up callback_save before using this.
+static bool saving_until_eol() {
+       if (buffer_char!=EOF) *callback_save++=(char)buffer_char;
+       // We don't actually need this, though it's a good idea, really!
+       // *callback_save='\0';
+       if (buffer_char==(int)'\n') return !callback_bool;
+       callback_bool=buffer_char==(int)'\r';
+       return true;
+}
+// Do callback_bool=false before using this.
+static bool decoding_until_eol() {
+       // We decode as we fill and work directly in the buffer to make
+       // the transformation. We are guaranteed enough space to do this by
+       // mem_buffer_margin.
+       decode_t decoded;
+       decoded=decodechar(buffer_char);
+       // We always remove the latest undecoded character from the
+       // buffer.
+       ++decoded.r;
+       if (decoded.r>mem_buffer_filled) {
+               // This will only happen for quoted-printable decoding
+               // whitespace stripping, and we can just live with it
+               // if we can't get rid of it all; with sensible constants
+               // something really is disobeying MIME and probably SMTP
+               // about line length anyway if this happens.
+               warning("unable to strip all whitespace; not enough in memory");
+               decoded.r=mem_buffer_filled;
+       }
+       if (buffer_filled-decoded.r<buffer_read) {
+               // We should always be working in lookahead when this happens,
+               // but better safe than sorry!
+               warning("unable to strip all whitespace; not enough unread");
+               decoded.r=buffer_filled-buffer_read;
+       }
+       if (buffer_marked&&buffer_filled-decoded.r<buffer_mark) {
+               // Marks should be in lookbehind too, but again,
+               // better safe than sorry! We unmark. Filling often
+               // does that anyway.
+               buffer_marked=false;
+               buffer_mark=0;
+       }
+       mem_buffer_next_fill-=decoded.r;
+       if (mem_buffer_next_fill<mem_buffer_start)
+                       mem_buffer_next_fill+=mem_buffer_size;
+       mem_buffer_filled-=decoded.r;
+       buffer_filled-=decoded.r;
+       if (decoded.c1!=EOF) {
+               *mem_buffer_next_fill=(char)decoded.c1;
+               ++mem_buffer_next_fill;
+               if (mem_buffer_next_fill==mem_buffer_end) mem_buffer_next_fill=mem_buffer_start;
+               ++mem_buffer_filled;
+               ++buffer_filled;
+               if (decoded.c2!=EOF) {
+                       *mem_buffer_next_fill=(char)decoded.c2;
+                       ++mem_buffer_next_fill;
+                       if (mem_buffer_next_fill==mem_buffer_end) mem_buffer_next_fill=mem_buffer_start;
+                       ++mem_buffer_filled;
+                       ++buffer_filled;
+                       if (decoded.c3!=EOF) {
+                               *mem_buffer_next_fill=(char)decoded.c3;
+                               ++mem_buffer_next_fill;
+                               if (mem_buffer_next_fill==mem_buffer_end) mem_buffer_next_fill=mem_buffer_start;
+                               ++mem_buffer_filled;
+                               ++buffer_filled;
+                       }
+               }
+       }
+       // We check for eol using the input stream, not the decoded
+       // stream, as it's all about the upcoming boundary
+       if (buffer_char==(int)'\n') return !callback_bool;
+       callback_bool=buffer_char==(int)'\r';
+       return true;
+}
+/*static bool until_no_lookbehind() {
+       return buffer_read!=0;
+}*/
+static bool echoing_until_no_lookbehind() {
+       if (buffer_char!=EOF) {
+               resort_to_errno(put(buffer_char)==EOF,"error echoing",EX_IOERR);
+       }
+       return buffer_read!=0;
+}
+static bool encoding_until_no_lookbehind() {
+       if (buffer_char!=EOF) encodechar(buffer_char);
+       return buffer_read!=0;
+}
+static bool until_no_disk_buffer() {
+       return disk_buffer_filled!=0;
+}
+static bool echoing_until_no_disk_buffer() {
+       if (buffer_char!=EOF) {
+               resort_to_errno(put(buffer_char)==EOF,"error echoing",EX_IOERR);
+       }
+       return disk_buffer_filled!=0;
+}
+static bool encoding_until_no_disk_buffer() {
+       if (buffer_char!=EOF) encodechar(buffer_char);
+       return disk_buffer_filled!=0;
+}
+static bool until_no_buffer() {
+       return buffer_filled!=0;
+}
+/*static bool echoing_until_no_buffer() {
+       if (buffer_char!=EOF) {
+               resort_to_errno(put(buffer_char)==EOF,"error echoing",EX_IOERR);
+       }
+       return buffer_filled!=0;
+}*/
+static bool until_start_marked() {
+       return !(buffer_marked?buffer_mark==0:buffer_read==0);
+}
+static bool echoing_until_start_marked() {
+       if (buffer_char!=EOF) {
+               resort_to_errno(put(buffer_char)==EOF,"error echoing",EX_IOERR);
+       }
+       return !(buffer_marked?buffer_mark==0:buffer_read==0);
+}
+static bool encoding_until_start_marked() {
+       if (buffer_char!=EOF) encodechar(buffer_char);
+       return !(buffer_marked?buffer_mark==0:buffer_read==0);
+}
+// Set up callback_match before using this.
+static bool until_match() {
+       return buffer_char!=callback_match;
+}
+// Set up callback_compare before using this.
+static bool comparing_head() {
+       /*@-nullderef@*/
+       if (buffer_char!=(int)(unsigned int)*callback_compare) {
+               callback_bool=false;
+               return false;
+       }
+       /*@-modobserver@*/
+       ++callback_compare;
+       /*@=modobserver@*/
+       if (*callback_compare=='\0') {
+               callback_bool=true;
+               return false;
+       }
+       return true;
+       /*@=nullderef@*/
+}
+// Set up callback_compare before using this.
+static bool case_insensitively_comparing_head() {
+       /*@-nullderef@*/
+       int c1=(int)(unsigned int)*callback_compare;
+       int c2=buffer_char;
+       if (c1!=c2&&
+                       (c1<(int)'A'||c1>(int)'Z'||c2!=c1-(int)'A'+(int)'a')&&
+                       (c2<(int)'A'||c2>(int)'Z'||c1!=c2-(int)'A'+(int)'a')) {
+               callback_bool=false;
+               return false;
+       }
+       /*@-modobserver@*/
+       ++callback_compare;
+       /*@=modobserver@*/
+       if (*callback_compare=='\0') {
+               callback_bool=true;
+               return false;
+       }
+       return true;
+       /*@=nullderef@*/
+}
+
+/* tag: encoding_functions */
+
+static inline void encode_string(const char * s) {
+       while (*s!='\0') {
+               encodechar((int)(unsigned int)*s);
+               s++;
+       }
+}
+static void encodechar(int c) {
+       if (encoding==unencoded) {
+               if (c!=EOF) resort_to_errno(put(c)==EOF,"error encoding",EX_IOERR);
+               return;
+       } else if (encoding==quoted_printable) {
+               if (encoding_echoed>=68) {
+                       // We need a soft line break, or are close enough to needing
+                       // one (76 chars max; unclear whether that counts the CRLF; and
+                       // we may output two 3 character sequences which we don't want
+                       // to follow with an unescaped CRLF). This scheme will probably
+                       // make mail look a bit awful, but that's fairly standard anyway,
+                       // and it shouldn't degrade.
+                       resort_to_errno(putstr("=\r\n")==EOF,
+                                       "error encoding string",EX_IOERR);
+                       encoding_echoed=0;
+               }
+               if (encoding_filled==1) {
+                       // Whatever happens, we'll deal with this now
+                       encoding_filled=0;
+                       if (encoding_buffer[0]=='\r') {
+                               if (c==(int)'\n') {
+                                       // Output them as is and we're done for now
+                                       resort_to_errno(putstr("\r\n")==EOF,
+                                                       "error encoding string",EX_IOERR);
+                                       encoding_echoed=0;
+                                       return;
+                               } else {
+                                       // Must encode the bare CR and continue as normal
+                                       resort_to_errno(put((int)'=')==EOF,"error encoding",EX_IOERR);
+                                       encode_hex_byte((unsigned int)'\r');
+                                       encoding_echoed+=3;
+                               }
+                       } else {
+                               // encoding_buffer[0] must be whitespace
+                               if (c==EOF||c==(int)'\r') {
+                                       // Must encode it
+                                       resort_to_errno(put((int)'=')==EOF,"error encoding",EX_IOERR);
+                                       encode_hex_byte((unsigned int)encoding_buffer[0]);
+                                       encoding_echoed+=3;
+                               } else {
+                                       // It is fine to output it now as something else is coming
+                                       resort_to_errno(put(
+                                                       (int)(unsigned int)encoding_buffer[0])==EOF,
+                                                       "error encoding",EX_IOERR);
+                                       encoding_echoed+=1;
+                               }
+                       }
+               }
+               if ((c>=33&&c<=60)||(c>=62&&c<=126)) {
+                       resort_to_errno(put(c)==EOF,"error encoding",EX_IOERR);
+                       ++encoding_echoed;
+               } else if (c==(int)' '||c==(int)'\t') {
+                       if (encoding_echoed>=55) {
+                               // My concession to readability; since it's likely to be
+                               // a big mess with a 68 character width, we might as well
+                               // break a bit earlier on a nice word boundary. And it'll
+                               // in fact look better if we break with roughly equal size
+                               // lines, assuming they come in at close to 76 characters
+                               // wide, so we might as well make a nice skinny column.
+                               // rather than a ragged one that uses the same amount of
+                               // space. Compromising between the two, then, as some
+                               // formats, like HTML, don't have many hard line breaks
+                               // anyway, is what we get.
+                               resort_to_errno(put(c)==EOF,"error encoding",EX_IOERR);
+                               resort_to_errno(putstr("=\r\n")==EOF,
+                                               "error encoding string",EX_IOERR);
+                               encoding_echoed=0;
+                       } else {
+                               // Store it; we may need to encode it if it's at end of line
+                               encoding_filled=1;
+                               encoding_buffer[0]=(char)c;
+                       }
+               } else if (c==(int)'\r') {
+                       // Store it; '\n' may be coming up
+                       encoding_filled=1;
+                       encoding_buffer[0]='\r';
+               } else if (c==EOF) {
+                       // No buffer, and we're done! Reset for another run.
+                       encoding_echoed=0;
+               } else {
+                       // Anything else must be encoded as a sequence.
+                       resort_to_errno(put((int)'=')==EOF,"error encoding",EX_IOERR);
+                       encode_hex_byte((unsigned int)c);
+                       encoding_echoed+=3;
+               }
+       } else if (encoding==base64) {
+               if (c==EOF) {
+                       // Reset for next run; we won't need it here
+                       encoding_echoed=0;
+                       if (encoding_filled==0) return;
+                       encoding_buffer[encoding_filled]='\0';
+               } else {
+                       encoding_buffer[encoding_filled++]=(char)c;
+               }
+               if (encoding_filled==3||c==EOF) {
+                       encode_64((((unsigned int)encoding_buffer[0]>>2)&0x3f));
+                       encode_64((((unsigned int)encoding_buffer[0]&0x03)<<4)|
+                                       (((unsigned int)encoding_buffer[1]>>4)&0x0f));
+                       if (encoding_filled==1) {
+                               resort_to_errno(put((int)'=')==EOF,"error encoding",EX_IOERR);
+                               resort_to_errno(put((int)'=')==EOF,"error encoding",EX_IOERR);
+                               // Reset for next run
+                               encoding_filled=0;
+                               return;
+                       }
+                       encode_64((((unsigned int)encoding_buffer[1]&0x0f)<<2)|
+                                       (((unsigned int)encoding_buffer[2]>>6)&0x03));
+                       if (encoding_filled==2) {
+                               resort_to_errno(put((int)'=')==EOF,"error encoding",EX_IOERR);
+                               // Reset for next run
+                               encoding_filled=0;
+                               return;
+                       }
+                       encode_64((((unsigned int)encoding_buffer[2]&0x3f)));
+                       encoding_echoed+=4;
+                       if (encoding_echoed>=72) {
+                               resort_to_errno(putstr("\r\n")==EOF,
+                                               "error encoding string",EX_IOERR);
+                               encoding_echoed=0;
+                       }
+                       encoding_filled=0;
+               }
+       } else {
+               resort_to_exit(true,"internal error: unknown encoding",EX_SOFTWARE);
+       }
+}
+static inline void finish_encoding() {
+       encodechar(EOF);
+}
+// The function takes an input character c and returns up to four output
+// characters (a character will be EOF to indicate no further characters
+// to store; note that this doesn't mean there will be no more ever; only
+// if EOF is returned when EOF was input does it meant this), and a number
+// of characters to remove before adding the aforementioned characters.
+static decode_t decodechar(int c) {
+       int h;
+       unsigned int b1, b2, b3, b4;
+       decode_t o;
+       o.r=0; o.c1=EOF; o.c2=EOF; o.c3=EOF;
+       if (decoding==unencoded) {
+               o.c1=c;
+               return o;
+       } else if (decoding==quoted_printable) {
+               // decoding_buffer may hold '=' and maybe a hex digit or a CR.
+               if (decoding_filled==2) {
+                       // Whatever happens, it's all settled now.
+                       decoding_filled=0;
+                       if (decoding_buffer[1]=='\r') {
+                               if (c==(int)'\n') { return o; }
+                               // Invalid; leave as is--will be encoded later.
+                               o.c1=(int)'='; o.c2=(int)'\r'; o.c3=c;
+                               return o;
+                       }
+                       h=decode_hex(c);
+                       if (h==EOF) {
+                               // Invalid; leave as is--will be encoded later.
+                               o.c1=(int)'='; o.c2=(int)(unsigned int)decoding_buffer[1]; o.c3=c;
+                               return o;
+                       }
+                       // We have a full sequence representing a single character.
+                       o.c1=decode_hex((int)(unsigned int)decoding_buffer[1])*16+h;
+                       return o;
+               } else if (decoding_filled==1) {
+                       if (c==(int)'\r'||decode_hex(c)!=EOF) {
+                               // Valid character after =
+                               decoding_filled=2;
+                               decoding_buffer[1]=(char)c;
+                               return o;
+                       }
+                       // Invalid; leave as is--will be encoded later.
+                       decoding_filled=0;
+                       o.c1=(int)'='; o.c2=c;
+                       return o;
+               } else if (decoding_filled==0) {
+                       if (c==(int)'=') {
+                               // The first character can only ever be '=' so we
+                               // don't actually bother to store it; just say it's there.
+                               decoding_white=0;
+                               decoding_filled=1;
+                               return o;
+                       }
+                       // Keep track of whitespace.
+                       if (c==(int)' '||c==(int)'\t') ++decoding_white;
+                       else decoding_white=0;
+                       // Remove trailing whitespace.
+                       if (c==EOF||c==(int)'\r') { o.r=decoding_white; decoding_white=0; }
+                       // Otherwise we just keep it. If it's EOF, we're done.
+                       o.c1=c;
+                       return o;
+               } else {
+                       warning("internal error: decoding buffer too full");
+                       return o;
+               }
+       } else if (decoding==base64) {
+               if (c==EOF) {
+                       // Just in case it was corrupted, make sure we're reset
+                       decoding_filled=0;
+                       return o;
+               }
+               if (c==(int)'='||decode_64(c)!=EOF)
+                       decoding_buffer[decoding_filled++]=(char)c;
+               if (decoding_filled==4) {
+                       // We empty it whatever happens here
+                       decoding_filled=0;
+                       b1=(unsigned int)decode_64((int)decoding_buffer[0]);
+                       b2=(unsigned int)decode_64((int)decoding_buffer[1]);
+                       o.c1=(int)(((b1&0x3f)<<2)|((b2>>4)&0x03));
+                       if (decoding_buffer[2]=='=') return o;
+                       b3=(unsigned int)decode_64((int)decoding_buffer[2]);
+                       o.c2=(int)(((b2&0x0f)<<4)|((b3>>2)&0x0f));
+                       if (decoding_buffer[3]=='=') return o;
+                       b4=(unsigned int)decode_64((int)decoding_buffer[3]);
+                       o.c3=(int)(((b3&0x03)<<6)|(b4&0x3f));
+               }
+               return o;
+       } else {
+               resort_to_exit(true,"internal error: unknown encoding",EX_SOFTWARE);
+               // Never reached
+               return o;
+       }
+}
+static void decode_lookahead() {
+       // Decoding will always shrink, so this is quite easy
+       char * c;
+       char * cc;
+       decode_t decoded;
+       int pos=buffer_read;
+       int decpos=buffer_read;
+       resort_to_exit(buffer_read<disk_buffer_filled,
+                       "internal error: decoding from disk",EX_SOFTWARE);
+       c=mem_buffer_next_empty+pos-disk_buffer_filled;
+       if (c>=mem_buffer_end) c-=mem_buffer_size;
+       cc=c;
+       while (pos<buffer_filled) {
+               decoded=decodechar((int)(unsigned int)*c);
+               if (decoded.r>0) {
+                       resort_to_exit(decpos-decoded.r<buffer_read,
+                                       "internal error: removing more than was decoded",EX_SOFTWARE);
+                       decpos-=decoded.r;
+                       cc-=decoded.r;
+                       if (cc<mem_buffer_start) cc+=mem_buffer_size;
+               }
+               if (decoded.c1!=EOF) {
+                       *cc=(char)decoded.c1;
+                       ++decpos; ++cc;
+                       if (cc==mem_buffer_end) cc=mem_buffer_start;
+                       if (decoded.c2!=EOF) {
+                               *cc=(char)decoded.c2;
+                               ++decpos; ++cc;
+                               if (cc==mem_buffer_end) cc=mem_buffer_start;
+                               if (decoded.c3!=EOF) {
+                                       *cc=(char)decoded.c3;
+                                       ++decpos; ++cc;
+                                       if (cc==mem_buffer_end) cc=mem_buffer_start;
+                               }
+                       }
+               }
+               ++pos; ++c;
+               if (c==mem_buffer_end) c=mem_buffer_start;
+       }
+       buffer_filled+=decpos-pos;
+       mem_buffer_filled+=decpos-pos;
+       mem_buffer_next_fill+=decpos-pos;
+       if (mem_buffer_next_fill<mem_buffer_start)
+                       mem_buffer_next_fill+=mem_buffer_size;
+}
+static inline void finish_decoding() {
+       // As it will have just experienced a CRLF or an EOF, the only thing
+       // this can do is reset the state if base64 was truncated.
+       // We won't gain any more characters or need to remove anything.
+       // It is important that this is always the case as other routines
+       // rely on it.
+       (void)decodechar(EOF);
+}
+
+// These are slow but easy to write! Lookup tables would be quicker.
+// Still, I think it'll probably be fast enough.
+static inline int decode_hex(int c) {
+       if (c>=(int)'0'&&c<=(int)'9') return c-(int)'0';
+       if (c>=(int)'A'&&c<=(int)'F') return c-(int)'A'+10;
+       return EOF;
+}
+static inline int decode_64(int c) {
+       if (c>=(int)'A'&&c<=(int)'Z') return c-(int)'A';
+       if (c>=(int)'a'&&c<=(int)'z') return c-(int)'a'+26;
+       if (c>=(int)'0'&&c<=(int)'9') return c-(int)'0'+52;
+       if (c==(int)'+') return 62;
+       if (c==(int)'/') return 63;
+       // if (c==(int)'=') return EOF;
+       return EOF;
+}
+static inline void encode_hex_byte(unsigned int h) {
+       int h1=(int)((h>>4)&0x0f);
+       int h2=(int)(h&0x0f);
+       if (h1<10) resort_to_errno(put((int)'0'+h1)==EOF,"error encoding",EX_IOERR);
+       else if (h1<16)
+                       resort_to_errno(put((int)'A'+h1-10)==EOF,"error encoding",EX_IOERR);
+       else resort_to_exit(true,"internal error: byte too large",EX_SOFTWARE);
+       if (h2<10) resort_to_errno(put((int)'0'+h2)==EOF,"error encoding",EX_IOERR);
+       else if (h2<16)
+                       resort_to_errno(put((int)'A'+h2-10)==EOF,"error encoding",EX_IOERR);
+       else resort_to_exit(true,"internal error: byte too large",EX_SOFTWARE);
+}
+static inline void encode_64(unsigned int b) {
+       if (b<26)
+                       resort_to_errno(put((int)'A'+b)==EOF,"error encoding",EX_IOERR);
+       else if (b<52)
+                       resort_to_errno(put((int)'a'+b-26)==EOF,"error encoding",EX_IOERR);
+       else if (b<62)
+                       resort_to_errno(put((int)'0'+b-52)==EOF,"error encoding",EX_IOERR);
+       else if (b==62)
+                       resort_to_errno(put((int)'+')==EOF,"error encoding",EX_IOERR);
+       else if (b==63)
+                       resort_to_errno(put((int)'/')==EOF,"error encoding",EX_IOERR);
+       else resort_to_exit(true,
+                       "internal error: base64 value too large",EX_SOFTWARE);
+}
+
+/* tag: error_functions */
+
+// Syslog constants:
+// level: LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG
+// facility: LOG_MAIL, LOG_DAEMON, LOG_USER, LOG_LOCALn(0-7)
+
+static inline void * alloc_or_exit(size_t s) /*@allocates result@*/ {
+       void * m;
+       m=malloc(s);
+       if (m==NULL) {
+#ifdef USE_STDERR
+               fprintf(stderr,"foot_filter: %s\n","out of memory");
+#endif
+#ifdef USE_SYSLOG
+               syslog(LOG_ERR|LOG_MAIL,"%s\n","out of memory");
+#endif
+               exit(EX_OSERR);
+       }
+       return m;
+}
+static inline void /*noreturnwhentrue*/
+               resort_to_exit(bool when,const char * message,int status) {
+       if (when) {
+#ifdef USE_STDERR
+               fprintf(stderr,"foot_filter: %s\n",message);
+#endif
+#ifdef USE_SYSLOG
+               syslog(LOG_ERR|LOG_MAIL,"%s\n",message);
+#endif
+               exit(status);
+       }
+}
+static inline void /*noreturnwhentrue*/
+               resort_to_errno(bool when,const char * message,int status) {
+       if (when) {
+#ifdef USE_STDERR
+               fprintf(stderr,"foot_filter: %s (%s)\n",message,strerror(errno));
+#endif
+#ifdef USE_SYSLOG
+               syslog(LOG_ERR|LOG_MAIL,"%s (%m)\n",message);
+#endif
+               exit(status);
+       }
+}
+static inline void resort_to_warning(bool when,const char * message) {
+       if (when) warning(message);
+}
+static inline void warning(const char * message) {
+#ifdef USE_STDERR
+       fprintf(stderr,"foot_filter: %s\n",message);
+#endif
+#ifdef USE_SYSLOG
+       syslog(LOG_WARNING|LOG_MAIL,"%s\n",message);
+#endif
+}
+
+/* tag: helper_functions */
+
+// The program was written following all the specs using CRLF for newlines,
+// but we get them from Postfix with LF only, so these wrapper functions
+// do the translation in such a way that it can easily be disabled if desired.
+static inline int get() {
+       int c;
+#ifdef UNIX_EOL
+       static bool got_nl=false;
+       if (got_nl) {
+               got_nl=false;
+               return 10;
+       }
+#endif
+       c=getchar();
+#ifdef UNIX_EOL
+       if (c==10) {
+               got_nl=true;
+               return 13;
+       }
+#endif
+       return c;
+}
+static inline int put(int c) {
+#ifdef UNIX_EOL
+       if (c==13) return c;
+#endif
+       return putchar(c);
+}
+static inline int putstr(const char * s) {
+       while (*s!='\0') if (put((int)(unsigned int)*s++)==EOF) return EOF;
+       return 0;
+}
+
+static inline bool case_insensitively_heads(const char * head,const char * buffer) {
+       const char * s1=head;
+       const char * s2=buffer;
+       for (;;) {
+               if (*s1=='\0') return true; /* for equality return *s2=='\0'; */
+               else if (*s2=='\0') return false;
+               if (*s1!=*s2&&
+                               (*s1<'A'||*s1>'Z'||*s2!=*s1-'A'+'a')&&
+                               (*s2<'A'||*s2>'Z'||*s1!=*s2-'A'+'a')) return false;
+               ++s1; ++s2;
+       }
+}
+
diff --git a/contrib/pymime/LICENSE b/contrib/pymime/LICENSE
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  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
+them 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 prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  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.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey 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;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  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.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+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.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     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
+state 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 3 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, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program 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, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU 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.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/contrib/pymime/README.md b/contrib/pymime/README.md
new file mode 100644 (file)
index 0000000..a0e425c
--- /dev/null
@@ -0,0 +1,55 @@
+pymime - A MIME formatter in python
+===================================
+
+This program is designed to take a MIME-formatted email and do the following actions:
+- convert HTML-parts of singlepart-mails to plaintext
+- strip HTML-parts of multipart-mails
+- strip attachments
+- optionally append a footer to the mail
+- optionally add an X-Archived-At header for mail-archive.com
+
+Requirements
+------------
+
+Tested with python 2.6 and 2.7.
+Currently not compatible with python 3.X, as this would require dropping 2.6 support.
+
+Usage
+-----
+
+       Usage: pymime.py [options]
+       Options:
+         -h, --help
+                   show this help message and exit
+         -i INPUT, --input=INPUT
+                   Where to read the mail from. Defaults to STDIN
+         -o OUTPUT, --output=OUTPUT
+                   Where to write the transformed mail. Defaults to STDOUT
+         -f FOOTER, --footer=FOOTER
+                   UTF-8 encoded footer to append to every mail.
+         -k, --keep-going      
+                   Ignore failures (ATM only missing footer file) as much
+                   as possible before failing.
+         -a, --archive-header      
+                   Add Archived-At header for mail-archive.com
+       
+
+License
+-------
+
+GPLv3
+
+Source
+------
+
+https://github.com/tdf/pymime
+
+Bugs
+----
+
+https://github.com/tdf/pymime/issues
+
+Contact
+-------
+
+alex@documentfoundation.org
\ No newline at end of file
diff --git a/contrib/pymime/integration/mlmmj/mlmmj-pymime b/contrib/pymime/integration/mlmmj/mlmmj-pymime
new file mode 100644 (file)
index 0000000..6fac072
--- /dev/null
@@ -0,0 +1,49 @@
+#!/bin/bash
+MLMMJRECIEVE=/usr/bin/mlmmj-recieve
+PYMIME=/var/spool/mlmmj/pymime.py
+
+# check executables
+if ! [ -x $MLMMJRECIEVE ]; then
+    echo "can't find $MLMMJRECIEVE executable, aborting"
+    exit 1
+fi
+
+if ! [ -x $PYMIME ]; then
+    echo "can't find $PYMIME executable, aborting"
+    exit 1
+fi
+
+# read parameters
+I=1
+PARAM_L=0
+while [ $I -le $# ] && [ $PARAM_L == 0 ]; do
+    if [ "${!I}" == "-L" ]; then
+        PARAM_L=1
+    fi
+    I=$[$I+1]
+done
+
+if [ $PARAM_L == 1 ] && [ $I -le $# ]; then
+    MLPATH="${!I}"
+else
+    echo "parameter -L /path/to/listdir missing, aborting"
+    exit 1
+fi
+
+if ! [ -d "${MLPATH}" ]; then
+    echo "${MLPATH} is not existing or no directory, aborting"
+    exit 1
+fi
+
+CONTROLD="${MLPATH}/control"
+
+if ! [ -d "${CONTROLD}" ]; then
+    echo "${CONTROLD} is not existing or no directory, aborting"
+    exit 1
+fi
+
+# go to the mailinglist directory
+cd $MLPATH
+
+# pipe the calls
+$PYMIME | $MLMMJRECIEVE "$@"
diff --git a/contrib/pymime/src/pymime.py b/contrib/pymime/src/pymime.py
new file mode 100755 (executable)
index 0000000..5cb0bbd
--- /dev/null
@@ -0,0 +1,288 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+#    Copyright 2011 Alexander Werner
+#    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 3 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, see <http://www.gnu.org/licenses/>.
+
+"""
+This script takes a MIME-formatted email and does various transformations with it,
+e.g. converts HTML-mails to plain text mails and strips attachements.
+"""
+__version__="1.0.0"
+import HTMLParser, email, email.utils, sys
+from optparse import OptionParser
+# for archive-header
+import hashlib
+import base64
+#-------------------------------------------------------------------------------
+# Configuration Options start here
+
+IGNORETAGS = ( "script", "head", "title", "link" )
+MAP_STARTTAGS = {"li": "\n* ",
+                 "p": "\n"}
+MAP_ENDTAGS = { "p": "\n",
+               "div": "\n",
+               "h1": "\n==============================================================\n\n",
+               "h2": "\n--------------------------------------------------------------\n\n",
+               "h3": "\n",
+               "h4": "\n",
+               "h5": "\n",
+               "h6": "\n"}
+MAXNUMNEWLINES = 2
+
+# No Configuration beneath this line
+# -------------------------------------------------------------------------------
+
+# Parse command line options
+parser = OptionParser()
+parser.add_option( "-i", "--input", dest = "input", default = "-",
+                   help = "Where to read the mail from. Defaults to STDIN" )
+parser.add_option( "-o", "--output", dest = "output", default = "-",
+                   help = "Where to write the transformed mail. Defaults to STDOUT" )
+parser.add_option( "-f", "--footer", dest = "footer", default = None,
+                   help = "UTF-8 encoded footer to append to every mail." )
+parser.add_option( "-k", "--keep-going", dest = "keep_going", action = "store_true", default = False,
+                   help = "Ignore failures (ATM only missing footer file) as much as possible before failing." )
+parser.add_option( "-a", "--archive-header", dest = "archive_header", action = "store_true", default = False,
+                   help = "Add Archived-At header for mail-archive.com" )
+parser.add_option( "-V", "--version", dest = "print_version", action = "store_true", default = False, help = "Show version and exit" )
+options, args = parser.parse_args()
+
+
+class StripHTML( HTMLParser.HTMLParser ):
+    """
+    This class provides the necessary logic to convert HTML to plain text.
+    """
+    def __init__( self ):
+        self.reset()
+        self.plain = []
+        self.last_starttag = None
+    def handle_starttag( self, tag, attributes ):
+        self.last_starttag = tag
+        if tag in MAP_STARTTAGS:
+            self.plain.append( MAP_STARTTAGS[tag] )
+    def handle_endtag( self, tag ):
+        if tag in MAP_ENDTAGS.keys():
+            self.plain.append( MAP_ENDTAGS[tag] )
+    def handle_data( self, data ):
+        if self.last_starttag not in IGNORETAGS:
+            self.plain.append( data.strip() )
+    def remove_whitespace( self ):
+        # Split at newlines instead of tags
+        self.plain = "".join( self.plain ).split( "\n" )
+        numspace = 0
+        # Copy the whole text
+        oldplain = self.plain[:]
+        self.plain = []
+        for line in oldplain:
+            if line.isspace() or line is "":
+                numspace = numspace + 1
+                if numspace <= MAXNUMNEWLINES:
+                    # number of blank newlines is lower than limit, append line
+                    self.plain.append( "\n" )
+            else:
+                numspace = 0
+                # line is no blank newline, append line
+                self.plain.append( line )
+    def get_plain_data( self ):
+        self.remove_whitespace()
+        return "\n".join( self.plain )
+
+class EMail( object ):
+    """
+    This class represents an email or a single payload of a multipart email.
+    """
+    def __init__( self, keep_going = False ):
+        self.to_include = False
+        self.keep_going = keep_going
+    def feed( self, fp = None, string = None, message = None ):
+        """
+        Feeds the EMail object with data. If fp is supplied, a new message will be created using
+        email.message_from_file, if string is supplied, a new message will be created using
+        email.message_from_string, if message is supplied, the supplied message will be used.
+        """
+        if fp is not None:
+            self.message = email.message_from_file( fp )
+        elif string is not None:
+            self.message = email.message_from_string( string )
+        elif message is not None:
+            self.message = message
+        else:
+            raise AttributeError
+    def parse( self ):
+        """
+        Parses the supplied message object. EMail.feed must have been called before.
+        """
+        if self.message.is_multipart():
+            self.parse_multipart()
+        else:
+            self.parse_singlepart()
+    def parse_multipart( self ):
+        """
+        Parses a multipart message object.
+        """
+        mails = []
+        #---------------------------------------------------------------------------
+        # Test if the message has a text/plain and a text/html part
+        has_plaintext = False
+        has_html = False
+        for part in self.message.walk():
+            content_type = part.get_content_type()
+            if content_type == "text/plain":
+                has_plaintext = True
+            if content_type == "text/html":
+                has_html = True
+        #---------------------------------------------------------------------------
+        # If the message has both text/plain and text/html parts, use the existing text/plain part
+        # and dismiss all other parts
+        if has_plaintext and has_html:
+            for part in self.message.walk():
+                content_type = part.get_content_type()
+                if content_type == "text/plain":
+                    mails.append( part )
+        #---------------------------------------------------------------------------
+        # Parse every part of the message for inclusion
+        else:
+            for part in self.message.walk():
+                # Avoid infinite recursion, message.walk also yields the parent message.
+                if part.is_multipart():
+                    continue
+                # Parse the submessage
+                mail = EMail()
+                mail.feed( message = part )
+                mail.parse()
+                # If the submessage contains useful data, append it to the final list of submessages
+                if mail.to_include:
+                    mails.append( mail.message )
+        #---------------------------------------------------------------------------
+        # Test if at least one submessage survived the parsing
+        if mails:
+            # Delete the body from the message
+            self.message.set_payload( None )
+            # Only one submessage to be included in the body, a restructuring of the parent
+            # message is necessary.
+            if len( mails ) == 1:
+                mail = mails[0]
+                # Copy the Headers from the submessage to the parent message
+                for key in mail.keys():
+                    del self.message[key]
+                    self.message[ key] = mail[key]
+                # If the parent message is still multipart, the submessage didn't have a Content-Type
+                # Header. Replace the old parent Content-Type with a default.
+                if "multipart" in self.message.get_content_maintype():
+                    del self.message["Content-Type"]
+                    self.message["Content-Type"] = "text/plain"
+                # Copy the body from the submessage to teh parent message
+                self.message.set_payload( mails[0].get_payload() )
+            # Multiple submessages are to be included, simply attach them.
+            else:
+                for mail in mails:
+                    self.message.attach( mail )
+        # No Message is to be included
+        else:
+            raise Exception
+    def addArchiveHeader( self ):
+        """
+        Add mail-archive.com direct-link to archive http://www.mail-archive.com/faq.html#listserver
+        """
+        message_id = self.message['message-id']
+        list_post = email.utils.parseaddr(self.message['to'])
+        if ( message_id is not None ) and ( list_post[1] is not '' ):
+            # remove < and > from msg-id
+            sha = hashlib.sha1( message_id[1:-1] )
+            sha.update( list_post[1] )
+            hash = base64.urlsafe_b64encode( sha.digest() )
+            url = "<http://go.mail-archive.com/%s>" % hash
+            self.message['Archived-At'] = url
+            # in case debugging is needed
+            #self.message['X-Archived-At-msgid'] = message_id[1:-1]
+            #self.message['X-Archived-At-list-post'] = list_post[1]
+
+    def parse_singlepart( self ):
+        """
+        Parses a singlepart message object.
+        """
+        content_type = self.message.get_content_type()
+        if content_type == "text/plain":
+            # text/plain is fine, if I'm a submessage I want to be included
+            self.to_include = True
+        elif content_type == "text/html":
+            # text/html must be stripped down to text/plain
+            s = StripHTML()
+            # Feed the HTML-Stripper with the body of the message
+            s.feed( self.message.get_payload() )
+            # And set the body of the message to the output of the HTML-Stripper 
+            self.message.set_payload( s.get_plain_data() )
+            # if I'm a submessage I also want to be included
+            self.to_include = True
+            # rewrite the Content-Type header, im no longer an evil HTML mail :)
+            self.message.set_type( "text/plain" )
+    def get_string( self ):
+        """
+        Returns the string representation of the supplied message.
+        """
+        return self.message.as_string()
+
+    def append_footer_from_file( self, filename ):
+        rawfooter = None
+        footer = None
+        orig_cs = self.message.get_content_charset()
+        if orig_cs == None:
+            cs = "iso-8859-15"
+            orig_cs = "ascii"
+        else:
+            cs = orig_cs
+        try:
+            with open( filename ) as f:
+                rawfooter = f.read()
+        except:
+            if not self.keep_going:
+                raise
+        if rawfooter:
+            try:
+                footer = rawfooter.decode( "utf-8" ).encode( cs )
+            except:
+                cs = "utf-8"
+                footer = rawfooter.decode( "utf-8" ).encode( cs )
+        if footer:
+            sep = "\n"
+            if footer.startswith( "\n" ):
+                sep = ""
+            payload = self.message.get_payload( decode = True ).decode( orig_cs ).encode( cs ) + sep + footer
+            del self.message["MIME-Version"]
+            del self.message["Content-Transfer-Encoding"]
+            self.message.set_payload( payload, cs )
+
+
+if __name__ == "__main__":
+    if options.print_version:
+        print("pymime "+__version__)
+        exit()
+    if options.input == "-":
+        input = sys.stdin
+    else:
+        input = file( options.input )
+    if options.output == "-":
+        output = sys.stdout
+    else:
+        output = file( options.output, "w" )
+    e = EMail( keep_going = options.keep_going )
+    e.feed( fp = input )
+    e.parse()
+    if options.archive_header:
+        e.addArchiveHeader()
+    if options.footer:
+        e.append_footer_from_file( options.footer )
+    output.write( e.get_string() )
+    output.close()
diff --git a/contrib/pymime/src/test.eml b/contrib/pymime/src/test.eml
new file mode 100644 (file)
index 0000000..99f86ea
--- /dev/null
@@ -0,0 +1,107 @@
+Message-ID: <mhtml-17@dsv.su.se>
+Date: Wed, 04 Apr 2000 04:17:00 +0200
+From: MHTML <mhtml@dsv.su.se>
+MIME-Version: 1.0
+To: mhtml@dsv.su.se
+Subject: Test message no. 17 (modification of no. 13)
+Content-Type: multipart/alternative; boundary="==boundary-2"
+
+--==boundary-2
+
+This is test message no. 17
+===========================
+
+Here comes the red test image:
+-----------------------------
+
+<red test image>
+
+Here comes the yellow test image:
+--------------------------------
+
+<yellow test image>
+
+This is the last line of this test message.
+--==boundary-2
+Content-Type: multipart/related; boundary="==boundary-1";type="text/html"
+
+Text displayed only to non-MIME-compliant mailers
+--==boundary-1
+Content-Type: text/html; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
+<HTML>
+<HEAD><TITLE>Test message no. 17</TITLE>
+</HEAD>
+<BODY>
+<H1>This is test message no. 17</H1>
+
+<H2>Here comes the red test image:</H2>
+<IMG SRC="cid:image1.mhtml-17@dsv.su.se" BORDER=0 HEIGHT=32 WIDTH=117
+ALT="red test image">
+
+<H2>Here comes the yellow test image:</H2>
+<IMG SRC="cid:image2.mhtml-17@dsv.su.se" BORDER=0 HEIGHT=32 WIDTH=152
+ALT="yellow test image">
+
+<P>This is the last line of this test message.
+</BODY></HTML>
+
+--==boundary-1
+Content-Type: image/gif
+Content-ID: <image1.mhtml-17@dsv.su.se>
+Content-Transfer-Encoding: base64
+Content-Disposition: inline; filename="red-test-image.gif"
+
+R0lGODlhdQAgAPcAAP//////zP//mf//Zv//M///AP/M///MzP/Mmf/MZv/MM//MAP+Z//+Z
+zP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9mZv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8A
+zP8Amf8AZv8AM/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zMAMyZ/8yZ
+zMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz/8wzzMwzmcwzZswzM8wzAMwA/8wA
+zMwAmcwAZswAM8wAAJn//5n/zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZ
+zJmZmZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkzZpkzM5kzAJkA/5kA
+zJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZ
+zGaZmWaZZmaZM2aZAGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA/2YA
+zGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPMzDPMmTPMZjPMMzPMADOZ/zOZ
+zDOZmTOZZjOZMzOZADNm/zNmzDNmmTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMA
+zDMAmTMAZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDMMwDMAACZ/wCZ
+zACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBmAAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAA
+zAAAmQAAZgAAM+4AAN0AALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI
+AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAARAAAIgAAEe7u7t3d3bu7
+u6qqqoiIiHd3d1VVVURERCIiIhEREQAAACH5BAAAAAAALAAAAAB1ACAAQAj/AP8JHEiwoMGD
+CBMqXMiwocOGIyJKnEixosWLGDNq3MixY0aBFKuJrBZypMiIJ0ekrLhSpUmSKEdObMkSpsuY
+OG/qTMnzJUWQHoMKHUq0KEagRpMqXaoUaU6dG2lKlOqRKtOkTq9q3VrV5sd/XMOKZZp1rNmz
+GsuiXct2hNq2cMVmXdkzZ12LLe/ehYrXpsy/MPUGHvw04lzCdhFbzasYMd+aUxsnnrzTq1uw
+cTN3tVrxrebPWDGDHr3UM+nTHE2jXn1RNevXEl3Dfi179urDJrte5BzVcknNhyNHZiyzJnGv
+uWMuppu7uHLkyV1Kxe1ccOGZ0Cn/xshcu8/K2g2LQ8bJGPJj4eh3+/WNHb118PAtBn8aXTrn
+6s7tl2QP9b399fhNN55tbe31FYEITlRbgqAtyCBwAz5I20MUVmjhhRgyFBAAOw==
+--==boundary-1
+Content-Type: image/gif
+Content-ID: <image2.mhtml-17@dsv.su.se>
+Content-Transfer-Encoding: base64
+Content-Disposition: inline; filename="yellow-test-image.gif"
+
+R0lGODlhmAAgAPcAAP//////zP//mf//Zv//M///AP/M///MzP/Mmf/MZv/MM//MAP+Z//+Z
+zP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9mZv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8A
+zP8Amf8AZv8AM/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zMAMyZ/8yZ
+zMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz/8wzzMwzmcwzZswzM8wzAMwA/8wA
+zMwAmcwAZswAM8wAAJn//5n/zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZ
+zJmZmZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkzZpkzM5kzAJkA/5kA
+zJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZ
+zGaZmWaZZmaZM2aZAGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA/2YA
+zGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPMzDPMmTPMZjPMMzPMADOZ/zOZ
+zDOZmTOZZjOZMzOZADNm/zNmzDNmmTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMA
+zDMAmTMAZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDMMwDMAACZ/wCZ
+zACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBmAAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAA
+zAAAmQAAZgAAM+4AAN0AALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI
+AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAARAAAIgAAEe7u7t3d3bu7
+u6qqqoiIiHd3d1VVVURERCIiIhEREQAAACH5BAAAAAAALAAAAACYACAAQAj/AP8JHEiwoMGD
+CBMqXMiwocOHECMWmEixosWLGDNq3Mixo8ePIEN+FPixWrWKJlOenGgS5coCLWG+VKlSY0yW
+NCnW1PkS482YQFcGxUlU5s6MJEUqXcq0qdOnGpNCnUq1qlWlUm0K7emz502qX0WGvTo1K9mz
+aNOO7Wg2rdu3cDG2jUu3Ltm5dvPqZYp3r9+/G/u6/LmVp0vDMosO7Xo4sWPCGwmfTPm4cGXF
+XC0KbowZcdHLiNd6Br3YcUbIpC0vLi33H+DXbo+G3Ay7tl7atnPHxa27N1revoNXBS68uFPi
+xpOLRK68OVvXHFlfpIwz81PRHrGb9ku7tOyv4Kmnbu4avvxM8RbF50T//ajs1tnVW96OWnLO
+015VW9def/791fPBFx9nBPbXGH/5RXaegqHp1xloz5XEVXsFogchfueZl55oBpp2n1H2ccSc
+c4BJ59GIJKZYEYoqqshiiyS+CGNzEdVo44045qhjQwEBADs=
+--==boundary-1--
+--==boundary-2--