changeset 900:b68213ccb9d2

Add README.footers and footer-related resources.
author Ben Schmidt
date Thu, 26 Feb 2015 07:29:01 +1100
parents 04d916168efb
children 3f4aee02898b
files ChangeLog Makefile.am README.footers README.listtexts contrib/Makefile.am contrib/foot_filter/Makefile contrib/foot_filter/foot_filter.c contrib/pymime/LICENSE contrib/pymime/README.md contrib/pymime/integration/mlmmj/mlmmj-pymime contrib/pymime/src/pymime.py contrib/pymime/src/test.eml
diffstat 12 files changed, 3923 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Wed Feb 25 18:22:37 2015 +1100
+++ b/ChangeLog	Thu Feb 26 07:29:01 2015 +1100
@@ -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
--- a/Makefile.am	Wed Feb 25 18:22:37 2015 +1100
+++ b/Makefile.am	Thu Feb 26 07:29:01 2015 +1100
@@ -4,7 +4,7 @@
 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 \
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.footers	Thu Feb 26 07:29:01 2015 +1100
@@ -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.
+
+
+
--- a/README.listtexts	Wed Feb 25 18:22:37 2015 +1100
+++ b/README.listtexts	Thu Feb 26 07:29:01 2015 +1100
@@ -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
--- a/contrib/Makefile.am	Wed Feb 25 18:22:37 2015 +1100
+++ b/contrib/Makefile.am	Thu Feb 26 07:29:01 2015 +1100
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/foot_filter/Makefile	Thu Feb 26 07:29:01 2015 +1100
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/foot_filter/foot_filter.c	Thu Feb 26 07:29:01 2015 +1100
@@ -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;
+	}
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/pymime/LICENSE	Thu Feb 26 07:29:01 2015 +1100
@@ -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>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/pymime/README.md	Thu Feb 26 07:29:01 2015 +1100
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/pymime/integration/mlmmj/mlmmj-pymime	Thu Feb 26 07:29:01 2015 +1100
@@ -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 "$@"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/pymime/src/pymime.py	Thu Feb 26 07:29:01 2015 +1100
@@ -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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/pymime/src/test.eml	Thu Feb 26 07:29:01 2015 +1100
@@ -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--