changeset 22123:328d0bbd19c7

lib-mail: Add qp encoder
author Aki Tuomi <aki.tuomi@dovecot.fi>
date Mon, 29 May 2017 13:39:18 +0300
parents 53bd79a3814a
children 1321733273ba
files src/lib-mail/Makefile.am src/lib-mail/istream-qp-encoder.c src/lib-mail/istream-qp.h src/lib-mail/qp-encoder.c src/lib-mail/qp-encoder.h src/lib-mail/test-istream-qp-encoder.c src/lib-mail/test-qp-encoder.c
diffstat 7 files changed, 596 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/src/lib-mail/Makefile.am	Thu May 25 19:03:32 2017 +0300
+++ b/src/lib-mail/Makefile.am	Mon May 29 13:39:18 2017 +0300
@@ -13,6 +13,7 @@
 	istream-header-filter.c \
 	istream-nonuls.c \
 	istream-qp-decoder.c \
+	istream-qp-encoder.c \
 	mail-html2text.c \
 	mail-user-hash.c \
 	mbox-from.c \
@@ -34,6 +35,7 @@
 	message-snippet.c \
 	ostream-dot.c \
 	qp-decoder.c \
+	qp-encoder.c \
 	quoted-printable.c \
 	rfc2231-parser.c \
 	rfc822-parser.c
@@ -71,6 +73,7 @@
 	message-snippet.h \
 	ostream-dot.h \
 	qp-decoder.h \
+	qp-encoder.h \
 	quoted-printable.h \
 	rfc2231-parser.h \
 	rfc822-parser.h
@@ -84,6 +87,7 @@
 	test-istream-binary-converter \
 	test-istream-header-filter \
 	test-istream-qp-decoder \
+	test-istream-qp-encoder \
 	test-mail-html2text \
 	test-mbox-from \
 	test-message-address \
@@ -100,6 +104,7 @@
 	test-message-snippet \
 	test-ostream-dot \
 	test-qp-decoder \
+	test-qp-encoder \
 	test-quoted-printable \
 	test-rfc2231-parser \
 	test-rfc822-parser
@@ -121,6 +126,10 @@
 test_istream_qp_decoder_LDADD = $(test_libs)
 test_istream_qp_decoder_DEPENDENCIES = $(test_deps)
 
+test_istream_qp_encoder_SOURCES = test-istream-qp-encoder.c
+test_istream_qp_encoder_LDADD = $(test_libs)
+test_istream_qp_encoder_DEPENDENCIES = $(test_deps)
+
 test_istream_binary_converter_SOURCES = test-istream-binary-converter.c
 test_istream_binary_converter_LDADD = $(test_libs)
 test_istream_binary_converter_DEPENDENCIES = $(test_deps)
@@ -197,6 +206,10 @@
 test_qp_decoder_LDADD = $(test_libs)
 test_qp_decoder_DEPENDENCIES = $(test_deps)
 
+test_qp_encoder_SOURCES = test-qp-encoder.c
+test_qp_encoder_LDADD = $(test_libs)
+test_qp_encoder_DEPENDENCIES = $(test_deps)
+
 test_quoted_printable_SOURCES = test-quoted-printable.c
 test_quoted_printable_LDADD = $(test_libs)
 test_quoted_printable_DEPENDENCIES = $(test_deps)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-mail/istream-qp-encoder.c	Mon May 29 13:39:18 2017 +0300
@@ -0,0 +1,128 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "qp-encoder.h"
+#include "istream-private.h"
+#include "istream-qp.h"
+
+struct qp_encoder_istream {
+	struct istream_private istream;
+	buffer_t *buf;
+	struct qp_encoder *qp;
+};
+
+static void i_stream_qp_encoder_close(struct iostream_private *stream,
+				      bool close_parent)
+{
+	struct qp_encoder_istream *bstream =
+		(struct qp_encoder_istream *)stream;
+
+	if (bstream->qp != NULL)
+		qp_encoder_deinit(&bstream->qp);
+	if (bstream->buf != NULL)
+		buffer_free(&bstream->buf);
+	if (close_parent)
+		i_stream_close(bstream->istream.parent);
+}
+
+static ssize_t i_stream_qp_encoder_read(struct istream_private *stream)
+{
+	struct qp_encoder_istream *bstream =
+		(struct qp_encoder_istream *)stream;
+	const unsigned char *data;
+	size_t size;
+	int ret;
+
+	for(;;) {
+		if (stream->skip > 0) {
+			i_assert(stream->skip <= bstream->buf->used);
+			buffer_delete(bstream->buf, 0, stream->skip);
+			stream->pos -= stream->skip;
+			stream->skip = 0;
+		}
+
+		stream->buffer = bstream->buf->data;
+		i_assert(stream->pos <= bstream->buf->used);
+
+		if (stream->pos >= bstream->istream.max_buffer_size) {
+			/* stream buffer still at maximum */
+			return -2;
+		}
+
+		/* if something is already interpolated, return as much of it as
+		   we can */
+		if (bstream->buf->used > 0) {
+			size_t new_pos, bytes;
+
+			/* only return up to max_buffer_size bytes, even when buffer
+			   actually has more, as not to confuse the caller */
+			if (bstream->buf->used <= bstream->istream.max_buffer_size) {
+				new_pos = bstream->buf->used;
+				if (stream->parent->eof)
+					stream->istream.eof = TRUE;
+			} else {
+				new_pos = bstream->istream.max_buffer_size;
+			}
+
+			bytes = new_pos - stream->pos;
+			stream->pos = new_pos;
+			return (ssize_t)bytes;
+		}
+
+		/* need to read more input */
+		ret = i_stream_read_more(stream->parent, &data, &size);
+		if (ret == 0)
+			return ret;
+		if (size == 0 && ret == -1) {
+			stream->istream.stream_errno =
+				stream->parent->stream_errno;
+			stream->istream.eof = stream->parent->eof;
+			return ret;
+		}
+		qp_encoder_more(bstream->qp, data, size);
+		i_stream_skip(stream->parent, size);
+	}
+}
+
+static void
+i_stream_qp_encoder_seek(struct istream_private *stream,
+			     uoff_t v_offset, bool mark)
+{
+	struct qp_encoder_istream *bstream =
+		(struct qp_encoder_istream *)stream;
+
+	if (v_offset < stream->istream.v_offset) {
+		/* seeking backwards - go back to beginning and seek
+		   forward from there. */
+		stream->parent_expected_offset = stream->parent_start_offset;
+		stream->skip = stream->pos = 0;
+		stream->istream.v_offset = 0;
+		i_stream_seek(stream->parent, 0);
+		qp_encoder_finish(bstream->qp);
+		buffer_set_used_size(bstream->buf, 0);
+	}
+	i_stream_default_seek_nonseekable(stream, v_offset, mark);
+}
+
+struct istream *i_stream_create_qp_encoder(struct istream *input,
+					   enum qp_encoder_flag flags)
+{
+	struct qp_encoder_istream *bstream;
+
+	bstream = i_new(struct qp_encoder_istream, 1);
+	bstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+	bstream->buf = buffer_create_dynamic(default_pool, 128);
+	bstream->qp = qp_encoder_init(bstream->buf, ISTREAM_QP_ENCODER_MAX_LINE_LENGTH, flags);
+
+	bstream->istream.iostream.close = i_stream_qp_encoder_close;
+	bstream->istream.read = i_stream_qp_encoder_read;
+	bstream->istream.seek = i_stream_qp_encoder_seek;
+
+	bstream->istream.istream.readable_fd = FALSE;
+	bstream->istream.istream.blocking = input->blocking;
+	bstream->istream.istream.seekable = input->seekable;
+	return i_stream_create(&bstream->istream, input,
+			       i_stream_get_fd(input));
+}
--- a/src/lib-mail/istream-qp.h	Thu May 25 19:03:32 2017 +0300
+++ b/src/lib-mail/istream-qp.h	Mon May 29 13:39:18 2017 +0300
@@ -1,6 +1,12 @@
 #ifndef ISTREAM_QP_H
 #define ISTREAM_QP_H
 
+#include "qp-encoder.h"
+
+#define ISTREAM_QP_ENCODER_MAX_LINE_LENGTH 75
+
 struct istream *i_stream_create_qp_decoder(struct istream *input);
+struct istream *i_stream_create_qp_encoder(struct istream *input,
+					   enum qp_encoder_flag flags);
 
 #endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-mail/qp-encoder.c	Mon May 29 13:39:18 2017 +0300
@@ -0,0 +1,140 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-private.h"
+#include "qp-encoder.h"
+#include <ctype.h>
+
+struct qp_encoder {
+	const char *linebreak;
+	string_t *dest;
+	size_t line_len;
+	size_t max_len;
+	enum qp_encoder_flag flags;
+	bool add_header_preamble:1;
+	bool cr_last:1;
+};
+
+struct qp_encoder *
+qp_encoder_init(string_t *dest, unsigned int max_len, enum qp_encoder_flag flags)
+{
+	i_assert(max_len > 0);
+
+	if ((flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0 &&
+	    (flags & QP_ENCODER_FLAG_BINARY_DATA) != 0)
+		i_panic("qp encoder cannot do header format with binary data");
+
+	struct qp_encoder *qp = i_new(struct qp_encoder, 1);
+	qp->flags = flags;
+	qp->dest = dest;
+	qp->max_len = max_len;
+
+	if ((flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0) {
+		qp->linebreak = "?=\r\n =?utf-8?Q?";
+		qp->add_header_preamble = TRUE;
+	} else {
+		qp->linebreak = "=\r\n";
+	}
+	return qp;
+}
+
+void qp_encoder_deinit(struct qp_encoder **qp)
+{
+	i_free(*qp);
+}
+
+static inline void
+qp_encode_or_break(struct qp_encoder *qp, unsigned char c)
+{
+	bool encode = FALSE;
+
+	if ((qp->flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0) {
+		if (c == ' ')
+			c = '_';
+		else if (c != '\t' &&
+			 (c == '?' || c == '_' || c == '=' || c < 33 || c > 126))
+			encode = TRUE;
+	} else if (c != ' ' && c != '\t' &&
+		   (c == '=' || c < 33 || c > 126)) {
+		encode = TRUE;
+	}
+
+	/* Include terminating = as well */
+	if ((c == ' ' || c == '\t') && qp->line_len + 4 >= qp->max_len) {
+		const char *ptr = strchr(qp->linebreak, '\n');
+		str_printfa(qp->dest, "=%02X%s", c, qp->linebreak);
+		if (ptr != NULL)
+			qp->line_len = strlen(ptr+1);
+		else
+			qp->line_len = 0;
+		return;
+	}
+
+	/* Include terminating = as well */
+	if (qp->line_len + (encode?4:2) >= qp->max_len) {
+		str_append(qp->dest, qp->linebreak);
+		qp->line_len = 0;
+	}
+
+	if (encode) {
+		str_printfa(qp->dest, "=%02X", c);
+		qp->line_len += 3;
+	} else {
+		str_append_c(qp->dest, c);
+		qp->line_len += 1;
+	}
+}
+
+void qp_encoder_more(struct qp_encoder *qp, const void *_src, size_t src_size)
+{
+	const unsigned char *src = _src;
+	i_assert(_src != NULL || src_size == 0);
+	if (src_size == 0)
+		return;
+	if (qp->add_header_preamble) {
+		size_t used = qp->dest->used;
+		qp->add_header_preamble = FALSE;
+		str_append(qp->dest, "=?utf-8?Q?");
+		qp->line_len = qp->dest->used - used;
+	}
+	for(unsigned int i = 0; i < src_size; i++) {
+		unsigned char c = src[i];
+		/* if input is not binary data and we encounter newline
+		   convert it as crlf, or if the last byte was CR, preserve
+		   CRLF */
+		if (c == '\n' &&
+		    ((qp->flags & (QP_ENCODER_FLAG_BINARY_DATA|QP_ENCODER_FLAG_HEADER_FORMAT)) == 0 ||
+		      qp->cr_last)) {
+			str_append_c(qp->dest, '\r');
+			str_append_c(qp->dest, '\n');
+			/* reset line length here */
+			qp->line_len = 0;
+			qp->cr_last = FALSE;
+			continue;
+		} else if (qp->cr_last) {
+			qp_encode_or_break(qp, '\r');
+			qp->cr_last = FALSE;
+		}
+		if (c == '\r') {
+			qp->cr_last = TRUE;
+		} else {
+			qp_encode_or_break(qp, c);
+		}
+	}
+}
+
+void qp_encoder_finish(struct qp_encoder *qp)
+{
+	if (qp->cr_last)
+		qp_encode_or_break(qp, '\r');
+
+	if ((qp->flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0 &&
+	    !qp->add_header_preamble)
+		str_append(qp->dest, "?=");
+	if ((qp->flags & QP_ENCODER_FLAG_HEADER_FORMAT) != 0)
+		qp->add_header_preamble = TRUE;
+	qp->line_len = 0;
+	qp->cr_last = FALSE;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-mail/qp-encoder.h	Mon May 29 13:39:18 2017 +0300
@@ -0,0 +1,25 @@
+#ifndef QP_ENCODER_H
+#define QP_ENCODER_H 1
+
+enum qp_encoder_flag {
+	/* encode spaces as underscores, encode crlfs, adds =?utf-8?q?..?= encapsulation */
+	QP_ENCODER_FLAG_HEADER_FORMAT = 0x1,
+	/* treat input as true binary, no lf => crlf conversion, only CRLF is preserved */
+	QP_ENCODER_FLAG_BINARY_DATA   = 0x2,
+};
+
+/* Initialize quoted-printable encoder. Write all the encoded output to dest. */
+struct qp_encoder *qp_encoder_init(string_t *dest, unsigned int max_length,
+				   enum qp_encoder_flag flags);
+void qp_encoder_deinit(struct qp_encoder **qp);
+
+/* Translate more (binary) data into quoted printable.
+   If QP_ENCODER_FLAG_BINARY_DATA is not set, text is assumed to be in
+   UTF-8 (but not enforced). No other character sets are supported. */
+void qp_encoder_more(struct qp_encoder *qp, const void *src, size_t src_size);
+/* Finish encoding any pending input.
+   This function also resets the entire encoder state, so the same encoder can
+   be used to encode more data if wanted. */
+void qp_encoder_finish(struct qp_encoder *qp);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-mail/test-istream-qp-encoder.c	Mon May 29 13:39:18 2017 +0300
@@ -0,0 +1,131 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "istream-private.h"
+#include "istream-qp.h"
+
+static const struct {
+	const void *input;
+	const char *output;
+	int stream_errno;
+} tests[] = {
+	{ "", "", 0 },
+	{ "short test", "short test", 0 },
+	{ "C'est une cha\xc3\xaene de test simple", "C'est une cha=C3=AEne de test simple", 0 },
+	{
+	  "wrap after 76 characters wrap after 76 characters wrap after 76 characters wrap after 76 characters",
+	  "wrap after 76 characters wrap after 76 characters wrap after 76 character=\r\ns wrap after 76 characters",
+	  0
+	},
+	{
+	  /* the string is split up to avoid C compilers thinking \x99ed as escape */
+	  "P\xc5\x99" "edstavitel\xc3\xa9 francouzsk\xc3\xa9ho lidu, ustanoveni v "
+	  "N\xc3\xa1rodn\xc3\xadm shrom\xc3\xa1\xc5\xbe" "d\xc4\x9bn\xc3\xad, domn"
+	  "\xc3\xadvaj\xc3\xad" "ce se, \xc5\xbe" "e nev\xc4\x9b" "domost, zapomenut\xc3"
+	  "\xad nebo pohrd\xc3\xa1n\xc3\xad lidsk\xc3\xbdmi pr\xc3\xa1vy jsou j"
+	  "edin\xc3\xbdmi p\xc5\x99\xc3\xad\xc4\x8dinami ve\xc5\x99" "ejn\xc3\xb"
+	  "dch ne\xc5\xa1t\xc4\x9bst\xc3\xad a zkorumpov\xc3\xa1n\xc3\xad vl"
+	  "\xc3\xa1" "d, rozhodli se vylo\xc5\xbeit v slavnostn\xc3\xad Deklara"
+	  "ci p\xc5\x99irozen\xc3\xa1, nezciziteln\xc3\xa1 a posv\xc3\xa1tn\xc3"
+	  "\xa1 pr\xc3\xa1va \xc4\x8dlov\xc4\x9bka za t\xc3\xadm \xc3\xba\xc4"
+	  "\x8d" "elem, aby tato Deklarace, neust\xc3\xa1le jsouc p\xc5\x99" "ed o"
+	  "\xc4\x8" "dima v\xc5\xa1" "em \xc4\x8dlen\xc5\xafm lidsk\xc3\xa9 spo"
+	  "le\xc4\x8dnosti, uv\xc3\xa1" "d\xc4\x9bla jim st\xc3\xa1le na pam\xc4"
+	  "\x9b\xc5\xa5 jejich pr\xc3\xa1va a jejich povinnosti; aby \xc4\x8din"
+	  "y z\xc3\xa1konod\xc3\xa1rn\xc3\xa9 moci a \xc4\x8diny v\xc3\xbdkonn"
+	  "\xc3\xa9 moci mohly b\xc3\xbdt v ka\xc5\xbe" "d\xc3\xa9 chv\xc3\xadli p"
+	  "orovn\xc3\xa1v\xc3\xa1ny s \xc3\xba\xc4\x8d" "elem ka\xc5\xbe" "d\xc3"
+	  "\xa9 politick\xc3\xa9 instituce a byly v d\xc5\xafsledku toho chov"
+	  "\xc3\xa1ny je\xc5\xa1t\xc4\x9b v\xc3\xad" "ce v \xc3\xba" "ct\xc4"
+	  "\x9b; aby po\xc5\xbe" "adavky ob\xc4\x8d" "an\xc5\xaf, kdy\xc5\xbe s"
+	  "e budou nap\xc5\x99\xc3\xad\xc5\xa1t\xc4\x9b zakl\xc3\xa1" "dat na j"
+	  "ednoduch\xc3\xb" "dch a nepop\xc3\xadrateln\xc3\xbd" "ch z\xc3\xa1sa"
+	  "d\xc3\xa1" "ch, sm\xc4\x9b\xc5\x99ovaly v\xc5\xb" "edy k zachov\xc3"
+	  "\xa1n\xc3\xad \xc3\xbastavy a ku blahu v\xc5\xa1" "ech.",
+	  "P=C5=99edstavitel=C3=A9 francouzsk=C3=A9ho lidu, ustanoveni v N=C3=A1rodn=\r\n"
+	  "=C3=ADm shrom=C3=A1=C5=BEd=C4=9Bn=C3=AD, domn=C3=ADvaj=C3=ADce se, =C5=BE=\r\n"
+	  "e nev=C4=9Bdomost, zapomenut=C3=AD nebo pohrd=C3=A1n=C3=AD lidsk=C3=BDmi=20=\r\n"
+	  "pr=C3=A1vy jsou jedin=C3=BDmi p=C5=99=C3=AD=C4=8Dinami ve=C5=99ejn=C3=0Bd=\r\n"
+	  "ch ne=C5=A1t=C4=9Bst=C3=AD a zkorumpov=C3=A1n=C3=AD vl=C3=A1d, rozhodli=20=\r\n"
+	  "se vylo=C5=BEit v slavnostn=C3=AD Deklaraci p=C5=99irozen=C3=A1, nezcizit=\r\n"
+	  "eln=C3=A1 a posv=C3=A1tn=C3=A1 pr=C3=A1va =C4=8Dlov=C4=9Bka za t=C3=ADm=20=\r\n"
+	  "=C3=BA=C4=8Delem, aby tato Deklarace, neust=C3=A1le jsouc p=C5=99ed o=C4=\r\n"
+	  "=08dima v=C5=A1em =C4=8Dlen=C5=AFm lidsk=C3=A9 spole=C4=8Dnosti, uv=C3=A1=\r\n"
+	  "d=C4=9Bla jim st=C3=A1le na pam=C4=9B=C5=A5 jejich pr=C3=A1va a jejich po=\r\n"
+	  "vinnosti; aby =C4=8Diny z=C3=A1konod=C3=A1rn=C3=A9 moci a =C4=8Diny v=C3=\r\n"
+	  "=BDkonn=C3=A9 moci mohly b=C3=BDt v ka=C5=BEd=C3=A9 chv=C3=ADli porovn=C3=\r\n"
+	  "=A1v=C3=A1ny s =C3=BA=C4=8Delem ka=C5=BEd=C3=A9 politick=C3=A9 instituce=20=\r\n"
+	  "a byly v d=C5=AFsledku toho chov=C3=A1ny je=C5=A1t=C4=9B v=C3=ADce v =C3=\r\n"
+	  "=BAct=C4=9B; aby po=C5=BEadavky ob=C4=8Dan=C5=AF, kdy=C5=BE se budou nap=\r\n"
+	  "=C5=99=C3=AD=C5=A1t=C4=9B zakl=C3=A1dat na jednoduch=C3=0Bdch a nepop=C3=\r\n"
+	  "=ADrateln=C3=BDch z=C3=A1sad=C3=A1ch, sm=C4=9B=C5=99ovaly v=C5=0Bedy k za=\r\n"
+	  "chov=C3=A1n=C3=AD =C3=BAstavy a ku blahu v=C5=A1ech.",
+	  0
+	},
+};
+
+static void
+encode_test(const char *qp_input, const char *output, int stream_errno,
+	    unsigned int buffer_size)
+{
+	size_t qp_input_len = strlen(qp_input);
+	struct istream *input_data, *input;
+	const unsigned char *data;
+	size_t i, size;
+	string_t *str = t_str_new(32);
+	int ret = 0;
+
+	input_data = test_istream_create_data(qp_input, qp_input_len);
+	test_istream_set_max_buffer_size(input_data, buffer_size);
+	test_istream_set_allow_eof(input_data, FALSE);
+	input = i_stream_create_qp_encoder(input_data, 0);
+
+	for (i = 1; i <= qp_input_len; i++) {
+		test_istream_set_size(input_data, i);
+		while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+			str_append_n(str, data, size);
+			i_stream_skip(input, size);
+		}
+		if (ret == -1 && stream_errno != 0)
+			break;
+		test_assert(ret == 0);
+	}
+	if (ret == 0) {
+		test_istream_set_allow_eof(input_data, TRUE);
+		while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+			str_append_n(str, data, size);
+			i_stream_skip(input, size);
+		}
+	}
+
+	test_assert(ret == -1);
+	test_assert(input->stream_errno == stream_errno);
+	test_assert(strcmp(str_c(str), output) == 0);
+
+	i_stream_unref(&input);
+	i_stream_unref(&input_data);
+}
+
+static void test_istream_qp_encoder(void)
+{
+	unsigned int i, j;
+
+	for (i = 0; i < N_ELEMENTS(tests); i++) {
+		test_begin(t_strdup_printf("istream qp encoder %u", i+1));
+		for (j = 1; j < 10; j++) T_BEGIN {
+			encode_test(tests[i].input, tests[i].output,
+				    tests[i].stream_errno, j);
+		} T_END;
+		test_end();
+	}
+}
+
+int main(void)
+{
+	static void (*const test_functions[])(void) = {
+		test_istream_qp_encoder,
+		NULL
+	};
+	return test_run(test_functions);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/lib-mail/test-qp-encoder.c	Mon May 29 13:39:18 2017 +0300
@@ -0,0 +1,153 @@
+/* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "qp-encoder.h"
+#include "test-common.h"
+
+struct test_quoted_printable_encode_data {
+	const void *input;
+	size_t input_len;
+	const char *output;
+};
+
+static void test_qp_encoder(void)
+{
+	static struct test_quoted_printable_encode_data tests[] = {
+		{ "", 0, "" },
+		{ "a", 1, "a" },
+		{ "a b \r c d", 9, "a b =0D c d" },
+		{ "a b \n c d", 9, "a b \r\n c d" },
+		{
+		  "test wrap at max 20 characters tab\ttoo", 38,
+		  "test wrap at max=20=\r\n20 characters tab=09=\r\ntoo"
+		},
+		{ "Invalid UTF-8 sequence in \x99", 27, "Invalid UTF-8 sequ=\r\nence in =99" },
+		{ "keep CRLF\r\non two lines", 23, "keep CRLF\r\non two lines" },
+	};
+	string_t *str;
+	unsigned int i, j;
+
+	test_begin("qp-encoder");
+	str = t_str_new(128);
+	for (i = 0; i < N_ELEMENTS(tests); i++) {
+		const unsigned char *input = tests[i].input;
+		struct qp_encoder *qp = qp_encoder_init(str, 20, 0);
+
+		/* try all at once */
+		qp_encoder_more(qp, input, tests[i].input_len);
+		qp_encoder_finish(qp);
+
+		test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+		/* try in small pieces */
+		str_truncate(str, 0);
+		for (j = 0; j < tests[i].input_len; j++) {
+			unsigned char c = input[j];
+			qp_encoder_more(qp, &c, 1);
+		}
+		qp_encoder_finish(qp);
+		test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+		qp_encoder_deinit(&qp);
+		str_truncate(str, 0);
+	}
+	test_end();
+}
+
+static void test_qp_encoder_binary(void)
+{
+	static struct test_quoted_printable_encode_data tests[] = {
+		{ "\0nil\0delimited\0string\0", 22, "=00nil=00delimited=\r\n=00string=00" },
+		{
+		  "\xef\x4e\xc5\xe0\x31\x66\xd7\xef\xae\x12\x7d\x45\x1e\x05\xc7\x2a",
+		  16,
+		  "=EFN=C5=E01f=D7=EF=\r\n=AE=12}E=1E=05=C7*"
+		},
+	};
+
+	string_t *str;
+	unsigned int i, j;
+
+	test_begin("qp-encoder (binary safe)");
+	str = t_str_new(128);
+	for (i = 0; i < N_ELEMENTS(tests); i++) {
+		const unsigned char *input = tests[i].input;
+		struct qp_encoder *qp = qp_encoder_init(str, 20, QP_ENCODER_FLAG_BINARY_DATA);
+
+		/* try all at once */
+		qp_encoder_more(qp, input, tests[i].input_len);
+		qp_encoder_finish(qp);
+
+		test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+		/* try in small pieces */
+		str_truncate(str, 0);
+		for (j = 0; j < tests[i].input_len; j++) {
+			unsigned char c = input[j];
+			qp_encoder_more(qp, &c, 1);
+		}
+		qp_encoder_finish(qp);
+		test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+		qp_encoder_deinit(&qp);
+		str_truncate(str, 0);
+	}
+	test_end();
+}
+
+static void test_qp_encoder_header(void)
+{
+        static struct test_quoted_printable_encode_data tests[] = {
+		{ "simple", 6, "=?utf-8?Q?simple?=" },
+		{ "J'esuis de paris caf\xc3\xa9", 22, "=?utf-8?Q?J'esuis_de_paris_caf=C3=A9?=" },
+		{ "hello_world", 11, "=?utf-8?Q?hello=5Fworld?=" },
+		{
+		  "make sure this wraps and that the actual lines are not longer than maximum length including preamble",
+		  100,
+		  "=?utf-8?Q?make_sure_this_wraps_and_that_the_actual_lines_are_not_longer_t?=\r\n"
+		  " =?utf-8?Q?han_maximum_length_including_preamble?="
+		},
+        };
+
+        string_t *str;
+        unsigned int i, j;
+
+        test_begin("qp-encoder (header format)");
+        str = t_str_new(128);
+        for (i = 0; i < N_ELEMENTS(tests); i++) {
+                const unsigned char *input = tests[i].input;
+                struct qp_encoder *qp = qp_encoder_init(str, 75, QP_ENCODER_FLAG_HEADER_FORMAT);
+
+                /* try all at once */
+                qp_encoder_more(qp, input, tests[i].input_len);
+                qp_encoder_finish(qp);
+
+                test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+                /* try in small pieces */
+                str_truncate(str, 0);
+                for (j = 0; j < tests[i].input_len; j++) {
+                        unsigned char c = input[j];
+                        qp_encoder_more(qp, &c, 1);
+                }
+                qp_encoder_finish(qp);
+                test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+
+                qp_encoder_deinit(&qp);
+                str_truncate(str, 0);
+        }
+        test_end();
+}
+
+
+int main(void)
+{
+	static void (*const test_functions[])(void) = {
+		test_qp_encoder,
+		test_qp_encoder_binary,
+		test_qp_encoder_header,
+		NULL
+	};
+	return test_run(test_functions);
+}