summaryrefslogtreecommitdiffstats
path: root/src/lib-mail
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:36:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 17:36:47 +0000
commit0441d265f2bb9da249c7abf333f0f771fadb4ab5 (patch)
tree3f3789daa2f6db22da6e55e92bee0062a7d613fe /src/lib-mail
parentInitial commit. (diff)
downloaddovecot-0441d265f2bb9da249c7abf333f0f771fadb4ab5.tar.xz
dovecot-0441d265f2bb9da249c7abf333f0f771fadb4ab5.zip
Adding upstream version 1:2.3.21+dfsg1.upstream/1%2.3.21+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/lib-mail/Makefile.am262
-rw-r--r--src/lib-mail/Makefile.in1629
-rw-r--r--src/lib-mail/fuzz-message-parser.c28
-rw-r--r--src/lib-mail/html-entities.h253
-rw-r--r--src/lib-mail/istream-attachment-connector.c149
-rw-r--r--src/lib-mail/istream-attachment-connector.h28
-rw-r--r--src/lib-mail/istream-attachment-extractor.c740
-rw-r--r--src/lib-mail/istream-attachment-extractor.h62
-rw-r--r--src/lib-mail/istream-binary-converter.c309
-rw-r--r--src/lib-mail/istream-binary-converter.h6
-rw-r--r--src/lib-mail/istream-dot.c236
-rw-r--r--src/lib-mail/istream-dot.h9
-rw-r--r--src/lib-mail/istream-header-filter.c762
-rw-r--r--src/lib-mail/istream-header-filter.h52
-rw-r--r--src/lib-mail/istream-nonuls.c79
-rw-r--r--src/lib-mail/istream-nonuls.h7
-rw-r--r--src/lib-mail/istream-qp-decoder.c140
-rw-r--r--src/lib-mail/istream-qp-encoder.c127
-rw-r--r--src/lib-mail/istream-qp.h12
-rw-r--r--src/lib-mail/mail-html2text.c354
-rw-r--r--src/lib-mail/mail-html2text.h22
-rw-r--r--src/lib-mail/mail-types.h25
-rw-r--r--src/lib-mail/mail-user-hash.c48
-rw-r--r--src/lib-mail/mail-user-hash.h10
-rw-r--r--src/lib-mail/mbox-from.c301
-rw-r--r--src/lib-mail/mbox-from.h12
-rw-r--r--src/lib-mail/message-address.c672
-rw-r--r--src/lib-mail/message-address.h61
-rw-r--r--src/lib-mail/message-binary-part.c44
-rw-r--r--src/lib-mail/message-binary-part.h29
-rw-r--r--src/lib-mail/message-date.c283
-rw-r--r--src/lib-mail/message-date.h12
-rw-r--r--src/lib-mail/message-decoder.c390
-rw-r--r--src/lib-mail/message-decoder.h54
-rw-r--r--src/lib-mail/message-header-decode.c188
-rw-r--r--src/lib-mail/message-header-decode.h22
-rw-r--r--src/lib-mail/message-header-encode.c406
-rw-r--r--src/lib-mail/message-header-encode.h27
-rw-r--r--src/lib-mail/message-header-hash.c72
-rw-r--r--src/lib-mail/message-header-hash.h18
-rw-r--r--src/lib-mail/message-header-parser.c474
-rw-r--r--src/lib-mail/message-header-parser.h85
-rw-r--r--src/lib-mail/message-id.c129
-rw-r--r--src/lib-mail/message-id.h8
-rw-r--r--src/lib-mail/message-parser-from-parts.c365
-rw-r--r--src/lib-mail/message-parser-private.h62
-rw-r--r--src/lib-mail/message-parser.c907
-rw-r--r--src/lib-mail/message-parser.h112
-rw-r--r--src/lib-mail/message-part-data.c594
-rw-r--r--src/lib-mail/message-part-data.h101
-rw-r--r--src/lib-mail/message-part-serialize.c269
-rw-r--r--src/lib-mail/message-part-serialize.h16
-rw-r--r--src/lib-mail/message-part.c103
-rw-r--r--src/lib-mail/message-part.h67
-rw-r--r--src/lib-mail/message-search.c246
-rw-r--r--src/lib-mail/message-search.h40
-rw-r--r--src/lib-mail/message-size.c174
-rw-r--r--src/lib-mail/message-size.h28
-rw-r--r--src/lib-mail/message-snippet.c207
-rw-r--r--src/lib-mail/message-snippet.h14
-rw-r--r--src/lib-mail/ostream-dot.c235
-rw-r--r--src/lib-mail/ostream-dot.h15
-rw-r--r--src/lib-mail/qp-decoder.c285
-rw-r--r--src/lib-mail/qp-decoder.h19
-rw-r--r--src/lib-mail/qp-encoder.c162
-rw-r--r--src/lib-mail/qp-encoder.h25
-rw-r--r--src/lib-mail/quoted-printable.c49
-rw-r--r--src/lib-mail/quoted-printable.h8
-rw-r--r--src/lib-mail/rfc2231-parser.c179
-rw-r--r--src/lib-mail/rfc2231-parser.h13
-rw-r--r--src/lib-mail/rfc822-parser.c522
-rw-r--r--src/lib-mail/rfc822-parser.h71
-rw-r--r--src/lib-mail/test-istream-attachment.c486
-rw-r--r--src/lib-mail/test-istream-binary-converter.c215
-rw-r--r--src/lib-mail/test-istream-dot.c230
-rw-r--r--src/lib-mail/test-istream-header-filter.c701
-rw-r--r--src/lib-mail/test-istream-qp-decoder.c197
-rw-r--r--src/lib-mail/test-istream-qp-encoder.c160
-rw-r--r--src/lib-mail/test-mail-html2text.c120
-rw-r--r--src/lib-mail/test-mail-user-hash.c185
-rw-r--r--src/lib-mail/test-mbox-from.c104
-rw-r--r--src/lib-mail/test-message-address.c532
-rw-r--r--src/lib-mail/test-message-date.c64
-rw-r--r--src/lib-mail/test-message-decoder.c513
-rw-r--r--src/lib-mail/test-message-header-decode.c216
-rw-r--r--src/lib-mail/test-message-header-encode.c295
-rw-r--r--src/lib-mail/test-message-header-hash.c111
-rw-r--r--src/lib-mail/test-message-header-parser.c479
-rw-r--r--src/lib-mail/test-message-id.c46
-rw-r--r--src/lib-mail/test-message-parser.c1398
-rw-r--r--src/lib-mail/test-message-part-serialize.c266
-rw-r--r--src/lib-mail/test-message-part.c117
-rw-r--r--src/lib-mail/test-message-search.c521
-rw-r--r--src/lib-mail/test-message-size.c159
-rw-r--r--src/lib-mail/test-message-snippet.c329
-rw-r--r--src/lib-mail/test-ostream-dot.c103
-rw-r--r--src/lib-mail/test-qp-decoder.c188
-rw-r--r--src/lib-mail/test-qp-encoder.c206
-rw-r--r--src/lib-mail/test-quoted-printable.c52
-rw-r--r--src/lib-mail/test-rfc2231-parser.c51
-rw-r--r--src/lib-mail/test-rfc822-parser.c445
101 files changed, 22013 insertions, 0 deletions
diff --git a/src/lib-mail/Makefile.am b/src/lib-mail/Makefile.am
new file mode 100644
index 0000000..67bb1cc
--- /dev/null
+++ b/src/lib-mail/Makefile.am
@@ -0,0 +1,262 @@
+noinst_LTLIBRARIES = libmail.la
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-smtp
+
+libmail_la_SOURCES = \
+ istream-attachment-connector.c \
+ istream-attachment-extractor.c \
+ istream-binary-converter.c \
+ istream-dot.c \
+ 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 \
+ message-address.c \
+ message-binary-part.c \
+ message-date.c \
+ message-decoder.c \
+ message-header-decode.c \
+ message-header-encode.c \
+ message-header-hash.c \
+ message-header-parser.c \
+ message-id.c \
+ message-parser.c \
+ message-parser-from-parts.c \
+ message-part.c \
+ message-part-data.c \
+ message-part-serialize.c \
+ message-search.c \
+ message-size.c \
+ message-snippet.c \
+ ostream-dot.c \
+ qp-decoder.c \
+ qp-encoder.c \
+ quoted-printable.c \
+ rfc2231-parser.c \
+ rfc822-parser.c
+
+noinst_HEADERS = \
+ html-entities.h \
+ message-parser-private.h
+
+headers = \
+ istream-attachment-connector.h \
+ istream-attachment-extractor.h \
+ istream-binary-converter.h \
+ istream-dot.h \
+ istream-header-filter.h \
+ istream-nonuls.h \
+ istream-qp.h \
+ mail-user-hash.h \
+ mbox-from.h \
+ mail-html2text.h \
+ mail-types.h \
+ message-address.h \
+ message-binary-part.h \
+ message-date.h \
+ message-decoder.h \
+ message-header-decode.h \
+ message-header-encode.h \
+ message-header-hash.h \
+ message-header-parser.h \
+ message-id.h \
+ message-parser.h \
+ message-part.h \
+ message-part-data.h \
+ message-part-serialize.h \
+ message-search.h \
+ message-size.h \
+ message-snippet.h \
+ ostream-dot.h \
+ qp-decoder.h \
+ qp-encoder.h \
+ quoted-printable.h \
+ rfc2231-parser.h \
+ rfc822-parser.h
+
+pkginc_libdir=$(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+
+test_programs = \
+ test-istream-dot \
+ test-istream-attachment \
+ test-istream-binary-converter \
+ test-istream-header-filter \
+ test-istream-qp-decoder \
+ test-istream-qp-encoder \
+ test-mail-html2text \
+ test-mail-user-hash \
+ test-mbox-from \
+ test-message-address \
+ test-message-date \
+ test-message-decoder \
+ test-message-header-decode \
+ test-message-header-encode \
+ test-message-header-hash \
+ test-message-header-parser \
+ test-message-id \
+ test-message-parser \
+ test-message-part \
+ test-message-part-serialize \
+ test-message-search \
+ test-message-size \
+ test-message-snippet \
+ test-ostream-dot \
+ test-qp-decoder \
+ test-qp-encoder \
+ test-quoted-printable \
+ test-rfc2231-parser \
+ test-rfc822-parser
+
+fuzz_programs =
+
+if USE_FUZZER
+fuzz_programs += fuzz-message-parser
+
+nodist_EXTRA_fuzz_message_parser_SOURCES = force-cxx-linking.cxx
+
+fuzz_message_parser_CPPFLAGS = $(FUZZER_CPPFLAGS)
+fuzz_message_parser_LDFLAGS = $(FUZZER_LDFLAGS)
+fuzz_message_parser_SOURCES = fuzz-message-parser.c
+fuzz_message_parser_LDADD = $(test_libs)
+fuzz_message_parser_DEPENDENCIES = $(test_deps)
+
+endif
+
+noinst_PROGRAMS = $(fuzz_programs) $(test_programs)
+
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-charset/libcharset.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+
+test_istream_dot_SOURCES = test-istream-dot.c
+test_istream_dot_LDADD = $(test_libs)
+test_istream_dot_DEPENDENCIES = $(test_deps)
+
+test_istream_qp_decoder_SOURCES = test-istream-qp-decoder.c
+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)
+
+test_istream_attachment_SOURCES = test-istream-attachment.c
+test_istream_attachment_LDADD = $(test_libs)
+test_istream_attachment_DEPENDENCIES = $(test_deps)
+
+test_istream_header_filter_SOURCES = test-istream-header-filter.c
+test_istream_header_filter_LDADD = $(test_libs)
+test_istream_header_filter_DEPENDENCIES = $(test_deps)
+
+test_mbox_from_SOURCES = test-mbox-from.c
+test_mbox_from_LDADD = $(test_libs)
+test_mbox_from_DEPENDENCIES = $(test_deps)
+
+test_message_address_SOURCES = test-message-address.c
+test_message_address_LDADD = $(test_libs)
+test_message_address_DEPENDENCIES = $(test_deps)
+
+test_message_date_SOURCES = test-message-date.c
+test_message_date_LDADD = $(test_libs)
+test_message_date_DEPENDENCIES = $(test_deps)
+
+test_message_decoder_SOURCES = test-message-decoder.c
+test_message_decoder_LDADD = $(test_libs) ../lib-charset/libcharset.la
+test_message_decoder_DEPENDENCIES = $(test_deps) ../lib-charset/libcharset.la
+
+test_message_header_decode_SOURCES = test-message-header-decode.c
+test_message_header_decode_LDADD = $(test_libs)
+test_message_header_decode_DEPENDENCIES = $(test_deps)
+
+test_message_header_encode_SOURCES = test-message-header-encode.c
+test_message_header_encode_LDADD = $(test_libs)
+test_message_header_encode_DEPENDENCIES = $(test_deps)
+
+test_message_header_hash_SOURCES = test-message-header-hash.c
+test_message_header_hash_LDADD = $(test_libs)
+test_message_header_hash_DEPENDENCIES = $(test_deps)
+
+test_message_header_parser_SOURCES = test-message-header-parser.c
+test_message_header_parser_LDADD = $(test_libs)
+test_message_header_parser_DEPENDENCIES = $(test_deps)
+
+test_message_id_SOURCES = test-message-id.c
+test_message_id_LDADD = $(test_libs)
+test_message_id_DEPENDENCIES = $(test_deps)
+
+test_message_parser_SOURCES = test-message-parser.c
+test_message_parser_LDADD = $(test_libs)
+test_message_parser_DEPENDENCIES = $(test_deps)
+
+test_message_part_SOURCES = test-message-part.c
+test_message_part_LDADD = $(test_libs)
+test_message_part_DEPENDENCIES = $(test_deps)
+
+test_message_search_SOURCES = test-message-search.c
+test_message_search_LDADD = $(test_libs) ../lib-charset/libcharset.la
+test_message_search_DEPENDENCIES = $(test_deps) ../lib-charset/libcharset.la
+
+test_message_size_SOURCES = test-message-size.c
+test_message_size_LDADD = $(test_libs)
+test_message_size_DEPENDENCIES = $(test_deps)
+
+test_message_snippet_SOURCES = test-message-snippet.c
+test_message_snippet_LDADD = $(test_message_decoder_LDADD)
+test_message_snippet_DEPENDENCIES = $(test_deps)
+
+test_mail_html2text_SOURCES = test-mail-html2text.c
+test_mail_html2text_LDADD = $(test_libs)
+test_mail_html2text_DEPENDENCIES = $(test_deps)
+
+test_ostream_dot_SOURCES = test-ostream-dot.c
+test_ostream_dot_LDADD = $(test_libs)
+test_ostream_dot_DEPENDENCIES = $(test_deps)
+
+test_qp_decoder_SOURCES = test-qp-decoder.c
+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)
+
+test_rfc2231_parser_SOURCES = test-rfc2231-parser.c
+test_rfc2231_parser_LDADD = $(test_libs)
+test_rfc2231_parser_DEPENDENCIES = $(test_deps)
+
+test_rfc822_parser_SOURCES = test-rfc822-parser.c
+test_rfc822_parser_LDADD = $(test_libs)
+test_rfc822_parser_DEPENDENCIES = $(test_deps)
+
+test_mail_user_hash_SOURCES = test-mail-user-hash.c
+test_mail_user_hash_LDADD = $(test_libs)
+test_mail_user_hash_DEPENDENCIES = $(test_deps)
+
+test_message_part_serialize_SOURCES = test-message-part-serialize.c
+test_message_part_serialize_LDADD = $(test_libs)
+test_message_part_serialize_DEPENDENCIES = $(test_deps)
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
diff --git a/src/lib-mail/Makefile.in b/src/lib-mail/Makefile.in
new file mode 100644
index 0000000..d656695
--- /dev/null
+++ b/src/lib-mail/Makefile.in
@@ -0,0 +1,1629 @@
+# Makefile.in generated by automake 1.16.3 from Makefile.am.
+# @configure_input@
+
+# Copyright (C) 1994-2020 Free Software Foundation, Inc.
+
+# This Makefile.in is free software; the Free Software Foundation
+# gives unlimited permission to copy and/or distribute it,
+# with or without modifications, as long as this notice is preserved.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY, to the extent permitted by law; without
+# even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE.
+
+@SET_MAKE@
+
+
+
+VPATH = @srcdir@
+am__is_gnu_make = { \
+ if test -z '$(MAKELEVEL)'; then \
+ false; \
+ elif test -n '$(MAKE_HOST)'; then \
+ true; \
+ elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \
+ true; \
+ else \
+ false; \
+ fi; \
+}
+am__make_running_with_option = \
+ case $${target_option-} in \
+ ?) ;; \
+ *) echo "am__make_running_with_option: internal error: invalid" \
+ "target option '$${target_option-}' specified" >&2; \
+ exit 1;; \
+ esac; \
+ has_opt=no; \
+ sane_makeflags=$$MAKEFLAGS; \
+ if $(am__is_gnu_make); then \
+ sane_makeflags=$$MFLAGS; \
+ else \
+ case $$MAKEFLAGS in \
+ *\\[\ \ ]*) \
+ bs=\\; \
+ sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \
+ | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \
+ esac; \
+ fi; \
+ skip_next=no; \
+ strip_trailopt () \
+ { \
+ flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \
+ }; \
+ for flg in $$sane_makeflags; do \
+ test $$skip_next = yes && { skip_next=no; continue; }; \
+ case $$flg in \
+ *=*|--*) continue;; \
+ -*I) strip_trailopt 'I'; skip_next=yes;; \
+ -*I?*) strip_trailopt 'I';; \
+ -*O) strip_trailopt 'O'; skip_next=yes;; \
+ -*O?*) strip_trailopt 'O';; \
+ -*l) strip_trailopt 'l'; skip_next=yes;; \
+ -*l?*) strip_trailopt 'l';; \
+ -[dEDm]) skip_next=yes;; \
+ -[JT]) skip_next=yes;; \
+ esac; \
+ case $$flg in \
+ *$$target_option*) has_opt=yes; break;; \
+ esac; \
+ done; \
+ test $$has_opt = yes
+am__make_dryrun = (target_option=n; $(am__make_running_with_option))
+am__make_keepgoing = (target_option=k; $(am__make_running_with_option))
+pkgdatadir = $(datadir)/@PACKAGE@
+pkgincludedir = $(includedir)/@PACKAGE@
+pkglibdir = $(libdir)/@PACKAGE@
+pkglibexecdir = $(libexecdir)/@PACKAGE@
+am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd
+install_sh_DATA = $(install_sh) -c -m 644
+install_sh_PROGRAM = $(install_sh) -c
+install_sh_SCRIPT = $(install_sh) -c
+INSTALL_HEADER = $(INSTALL_DATA)
+transform = $(program_transform_name)
+NORMAL_INSTALL = :
+PRE_INSTALL = :
+POST_INSTALL = :
+NORMAL_UNINSTALL = :
+PRE_UNINSTALL = :
+POST_UNINSTALL = :
+build_triplet = @build@
+host_triplet = @host@
+@USE_FUZZER_TRUE@am__append_1 = fuzz-message-parser
+noinst_PROGRAMS = $(am__EXEEXT_2) $(am__EXEEXT_3)
+subdir = src/lib-mail
+ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
+am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \
+ $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \
+ $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \
+ $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \
+ $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \
+ $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \
+ $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \
+ $(top_srcdir)/m4/flexible_array_member.m4 \
+ $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \
+ $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \
+ $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \
+ $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \
+ $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \
+ $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \
+ $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \
+ $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \
+ $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \
+ $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \
+ $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \
+ $(top_srcdir)/m4/pr_set_dumpable.m4 \
+ $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \
+ $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \
+ $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \
+ $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \
+ $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \
+ $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \
+ $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \
+ $(top_srcdir)/m4/typeof_dev_t.m4 \
+ $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \
+ $(top_srcdir)/m4/want_apparmor.m4 \
+ $(top_srcdir)/m4/want_bsdauth.m4 \
+ $(top_srcdir)/m4/want_bzlib.m4 \
+ $(top_srcdir)/m4/want_cassandra.m4 \
+ $(top_srcdir)/m4/want_cdb.m4 \
+ $(top_srcdir)/m4/want_checkpassword.m4 \
+ $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \
+ $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \
+ $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \
+ $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \
+ $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \
+ $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \
+ $(top_srcdir)/m4/want_prefetch.m4 \
+ $(top_srcdir)/m4/want_shadow.m4 \
+ $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \
+ $(top_srcdir)/m4/want_sqlite.m4 \
+ $(top_srcdir)/m4/want_stemmer.m4 \
+ $(top_srcdir)/m4/want_systemd.m4 \
+ $(top_srcdir)/m4/want_textcat.m4 \
+ $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \
+ $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac
+am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
+ $(ACLOCAL_M4)
+DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \
+ $(pkginc_lib_HEADERS) $(am__DIST_COMMON)
+mkinstalldirs = $(install_sh) -d
+CONFIG_HEADER = $(top_builddir)/config.h
+CONFIG_CLEAN_FILES =
+CONFIG_CLEAN_VPATH_FILES =
+@USE_FUZZER_TRUE@am__EXEEXT_1 = fuzz-message-parser$(EXEEXT)
+am__EXEEXT_2 = $(am__EXEEXT_1)
+am__EXEEXT_3 = test-istream-dot$(EXEEXT) \
+ test-istream-attachment$(EXEEXT) \
+ test-istream-binary-converter$(EXEEXT) \
+ test-istream-header-filter$(EXEEXT) \
+ test-istream-qp-decoder$(EXEEXT) \
+ test-istream-qp-encoder$(EXEEXT) test-mail-html2text$(EXEEXT) \
+ test-mail-user-hash$(EXEEXT) test-mbox-from$(EXEEXT) \
+ test-message-address$(EXEEXT) test-message-date$(EXEEXT) \
+ test-message-decoder$(EXEEXT) \
+ test-message-header-decode$(EXEEXT) \
+ test-message-header-encode$(EXEEXT) \
+ test-message-header-hash$(EXEEXT) \
+ test-message-header-parser$(EXEEXT) test-message-id$(EXEEXT) \
+ test-message-parser$(EXEEXT) test-message-part$(EXEEXT) \
+ test-message-part-serialize$(EXEEXT) \
+ test-message-search$(EXEEXT) test-message-size$(EXEEXT) \
+ test-message-snippet$(EXEEXT) test-ostream-dot$(EXEEXT) \
+ test-qp-decoder$(EXEEXT) test-qp-encoder$(EXEEXT) \
+ test-quoted-printable$(EXEEXT) test-rfc2231-parser$(EXEEXT) \
+ test-rfc822-parser$(EXEEXT)
+PROGRAMS = $(noinst_PROGRAMS)
+LTLIBRARIES = $(noinst_LTLIBRARIES)
+libmail_la_LIBADD =
+am_libmail_la_OBJECTS = istream-attachment-connector.lo \
+ istream-attachment-extractor.lo istream-binary-converter.lo \
+ istream-dot.lo istream-header-filter.lo istream-nonuls.lo \
+ istream-qp-decoder.lo istream-qp-encoder.lo mail-html2text.lo \
+ mail-user-hash.lo mbox-from.lo message-address.lo \
+ message-binary-part.lo message-date.lo message-decoder.lo \
+ message-header-decode.lo message-header-encode.lo \
+ message-header-hash.lo message-header-parser.lo message-id.lo \
+ message-parser.lo message-parser-from-parts.lo message-part.lo \
+ message-part-data.lo message-part-serialize.lo \
+ message-search.lo message-size.lo message-snippet.lo \
+ ostream-dot.lo qp-decoder.lo qp-encoder.lo quoted-printable.lo \
+ rfc2231-parser.lo rfc822-parser.lo
+libmail_la_OBJECTS = $(am_libmail_la_OBJECTS)
+AM_V_lt = $(am__v_lt_@AM_V@)
+am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@)
+am__v_lt_0 = --silent
+am__v_lt_1 =
+am__fuzz_message_parser_SOURCES_DIST = fuzz-message-parser.c
+@USE_FUZZER_TRUE@am_fuzz_message_parser_OBJECTS = fuzz_message_parser-fuzz-message-parser.$(OBJEXT)
+fuzz_message_parser_OBJECTS = $(am_fuzz_message_parser_OBJECTS)
+fuzz_message_parser_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \
+ $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CXXLD) \
+ $(AM_CXXFLAGS) $(CXXFLAGS) $(fuzz_message_parser_LDFLAGS) \
+ $(LDFLAGS) -o $@
+am_test_istream_attachment_OBJECTS = \
+ test-istream-attachment.$(OBJEXT)
+test_istream_attachment_OBJECTS = \
+ $(am_test_istream_attachment_OBJECTS)
+am_test_istream_binary_converter_OBJECTS = \
+ test-istream-binary-converter.$(OBJEXT)
+test_istream_binary_converter_OBJECTS = \
+ $(am_test_istream_binary_converter_OBJECTS)
+am_test_istream_dot_OBJECTS = test-istream-dot.$(OBJEXT)
+test_istream_dot_OBJECTS = $(am_test_istream_dot_OBJECTS)
+am_test_istream_header_filter_OBJECTS = \
+ test-istream-header-filter.$(OBJEXT)
+test_istream_header_filter_OBJECTS = \
+ $(am_test_istream_header_filter_OBJECTS)
+am_test_istream_qp_decoder_OBJECTS = \
+ test-istream-qp-decoder.$(OBJEXT)
+test_istream_qp_decoder_OBJECTS = \
+ $(am_test_istream_qp_decoder_OBJECTS)
+am_test_istream_qp_encoder_OBJECTS = \
+ test-istream-qp-encoder.$(OBJEXT)
+test_istream_qp_encoder_OBJECTS = \
+ $(am_test_istream_qp_encoder_OBJECTS)
+am_test_mail_html2text_OBJECTS = test-mail-html2text.$(OBJEXT)
+test_mail_html2text_OBJECTS = $(am_test_mail_html2text_OBJECTS)
+am_test_mail_user_hash_OBJECTS = test-mail-user-hash.$(OBJEXT)
+test_mail_user_hash_OBJECTS = $(am_test_mail_user_hash_OBJECTS)
+am_test_mbox_from_OBJECTS = test-mbox-from.$(OBJEXT)
+test_mbox_from_OBJECTS = $(am_test_mbox_from_OBJECTS)
+am_test_message_address_OBJECTS = test-message-address.$(OBJEXT)
+test_message_address_OBJECTS = $(am_test_message_address_OBJECTS)
+am_test_message_date_OBJECTS = test-message-date.$(OBJEXT)
+test_message_date_OBJECTS = $(am_test_message_date_OBJECTS)
+am_test_message_decoder_OBJECTS = test-message-decoder.$(OBJEXT)
+test_message_decoder_OBJECTS = $(am_test_message_decoder_OBJECTS)
+am_test_message_header_decode_OBJECTS = \
+ test-message-header-decode.$(OBJEXT)
+test_message_header_decode_OBJECTS = \
+ $(am_test_message_header_decode_OBJECTS)
+am_test_message_header_encode_OBJECTS = \
+ test-message-header-encode.$(OBJEXT)
+test_message_header_encode_OBJECTS = \
+ $(am_test_message_header_encode_OBJECTS)
+am_test_message_header_hash_OBJECTS = \
+ test-message-header-hash.$(OBJEXT)
+test_message_header_hash_OBJECTS = \
+ $(am_test_message_header_hash_OBJECTS)
+am_test_message_header_parser_OBJECTS = \
+ test-message-header-parser.$(OBJEXT)
+test_message_header_parser_OBJECTS = \
+ $(am_test_message_header_parser_OBJECTS)
+am_test_message_id_OBJECTS = test-message-id.$(OBJEXT)
+test_message_id_OBJECTS = $(am_test_message_id_OBJECTS)
+am_test_message_parser_OBJECTS = test-message-parser.$(OBJEXT)
+test_message_parser_OBJECTS = $(am_test_message_parser_OBJECTS)
+am_test_message_part_OBJECTS = test-message-part.$(OBJEXT)
+test_message_part_OBJECTS = $(am_test_message_part_OBJECTS)
+am_test_message_part_serialize_OBJECTS = \
+ test-message-part-serialize.$(OBJEXT)
+test_message_part_serialize_OBJECTS = \
+ $(am_test_message_part_serialize_OBJECTS)
+am_test_message_search_OBJECTS = test-message-search.$(OBJEXT)
+test_message_search_OBJECTS = $(am_test_message_search_OBJECTS)
+am_test_message_size_OBJECTS = test-message-size.$(OBJEXT)
+test_message_size_OBJECTS = $(am_test_message_size_OBJECTS)
+am_test_message_snippet_OBJECTS = test-message-snippet.$(OBJEXT)
+test_message_snippet_OBJECTS = $(am_test_message_snippet_OBJECTS)
+am_test_ostream_dot_OBJECTS = test-ostream-dot.$(OBJEXT)
+test_ostream_dot_OBJECTS = $(am_test_ostream_dot_OBJECTS)
+am_test_qp_decoder_OBJECTS = test-qp-decoder.$(OBJEXT)
+test_qp_decoder_OBJECTS = $(am_test_qp_decoder_OBJECTS)
+am_test_qp_encoder_OBJECTS = test-qp-encoder.$(OBJEXT)
+test_qp_encoder_OBJECTS = $(am_test_qp_encoder_OBJECTS)
+am_test_quoted_printable_OBJECTS = test-quoted-printable.$(OBJEXT)
+test_quoted_printable_OBJECTS = $(am_test_quoted_printable_OBJECTS)
+am_test_rfc2231_parser_OBJECTS = test-rfc2231-parser.$(OBJEXT)
+test_rfc2231_parser_OBJECTS = $(am_test_rfc2231_parser_OBJECTS)
+am_test_rfc822_parser_OBJECTS = test-rfc822-parser.$(OBJEXT)
+test_rfc822_parser_OBJECTS = $(am_test_rfc822_parser_OBJECTS)
+AM_V_P = $(am__v_P_@AM_V@)
+am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
+am__v_P_0 = false
+am__v_P_1 = :
+AM_V_GEN = $(am__v_GEN_@AM_V@)
+am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@)
+am__v_GEN_0 = @echo " GEN " $@;
+am__v_GEN_1 =
+AM_V_at = $(am__v_at_@AM_V@)
+am__v_at_ = $(am__v_at_@AM_DEFAULT_V@)
+am__v_at_0 = @
+am__v_at_1 =
+DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir)
+depcomp = $(SHELL) $(top_srcdir)/depcomp
+am__maybe_remake_depfiles = depfiles
+am__depfiles_remade = \
+ ./$(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po \
+ ./$(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po \
+ ./$(DEPDIR)/istream-attachment-connector.Plo \
+ ./$(DEPDIR)/istream-attachment-extractor.Plo \
+ ./$(DEPDIR)/istream-binary-converter.Plo \
+ ./$(DEPDIR)/istream-dot.Plo \
+ ./$(DEPDIR)/istream-header-filter.Plo \
+ ./$(DEPDIR)/istream-nonuls.Plo \
+ ./$(DEPDIR)/istream-qp-decoder.Plo \
+ ./$(DEPDIR)/istream-qp-encoder.Plo \
+ ./$(DEPDIR)/mail-html2text.Plo ./$(DEPDIR)/mail-user-hash.Plo \
+ ./$(DEPDIR)/mbox-from.Plo ./$(DEPDIR)/message-address.Plo \
+ ./$(DEPDIR)/message-binary-part.Plo \
+ ./$(DEPDIR)/message-date.Plo ./$(DEPDIR)/message-decoder.Plo \
+ ./$(DEPDIR)/message-header-decode.Plo \
+ ./$(DEPDIR)/message-header-encode.Plo \
+ ./$(DEPDIR)/message-header-hash.Plo \
+ ./$(DEPDIR)/message-header-parser.Plo \
+ ./$(DEPDIR)/message-id.Plo \
+ ./$(DEPDIR)/message-parser-from-parts.Plo \
+ ./$(DEPDIR)/message-parser.Plo \
+ ./$(DEPDIR)/message-part-data.Plo \
+ ./$(DEPDIR)/message-part-serialize.Plo \
+ ./$(DEPDIR)/message-part.Plo ./$(DEPDIR)/message-search.Plo \
+ ./$(DEPDIR)/message-size.Plo ./$(DEPDIR)/message-snippet.Plo \
+ ./$(DEPDIR)/ostream-dot.Plo ./$(DEPDIR)/qp-decoder.Plo \
+ ./$(DEPDIR)/qp-encoder.Plo ./$(DEPDIR)/quoted-printable.Plo \
+ ./$(DEPDIR)/rfc2231-parser.Plo ./$(DEPDIR)/rfc822-parser.Plo \
+ ./$(DEPDIR)/test-istream-attachment.Po \
+ ./$(DEPDIR)/test-istream-binary-converter.Po \
+ ./$(DEPDIR)/test-istream-dot.Po \
+ ./$(DEPDIR)/test-istream-header-filter.Po \
+ ./$(DEPDIR)/test-istream-qp-decoder.Po \
+ ./$(DEPDIR)/test-istream-qp-encoder.Po \
+ ./$(DEPDIR)/test-mail-html2text.Po \
+ ./$(DEPDIR)/test-mail-user-hash.Po \
+ ./$(DEPDIR)/test-mbox-from.Po \
+ ./$(DEPDIR)/test-message-address.Po \
+ ./$(DEPDIR)/test-message-date.Po \
+ ./$(DEPDIR)/test-message-decoder.Po \
+ ./$(DEPDIR)/test-message-header-decode.Po \
+ ./$(DEPDIR)/test-message-header-encode.Po \
+ ./$(DEPDIR)/test-message-header-hash.Po \
+ ./$(DEPDIR)/test-message-header-parser.Po \
+ ./$(DEPDIR)/test-message-id.Po \
+ ./$(DEPDIR)/test-message-parser.Po \
+ ./$(DEPDIR)/test-message-part-serialize.Po \
+ ./$(DEPDIR)/test-message-part.Po \
+ ./$(DEPDIR)/test-message-search.Po \
+ ./$(DEPDIR)/test-message-size.Po \
+ ./$(DEPDIR)/test-message-snippet.Po \
+ ./$(DEPDIR)/test-ostream-dot.Po ./$(DEPDIR)/test-qp-decoder.Po \
+ ./$(DEPDIR)/test-qp-encoder.Po \
+ ./$(DEPDIR)/test-quoted-printable.Po \
+ ./$(DEPDIR)/test-rfc2231-parser.Po \
+ ./$(DEPDIR)/test-rfc822-parser.Po
+am__mv = mv -f
+COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \
+ $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS)
+LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CFLAGS) $(CFLAGS)
+AM_V_CC = $(am__v_CC_@AM_V@)
+am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@)
+am__v_CC_0 = @echo " CC " $@;
+am__v_CC_1 =
+CCLD = $(CC)
+LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
+ $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CCLD = $(am__v_CCLD_@AM_V@)
+am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@)
+am__v_CCLD_0 = @echo " CCLD " $@;
+am__v_CCLD_1 =
+CXXCOMPILE = $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) \
+ $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS)
+LTCXXCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) \
+ $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \
+ $(AM_CXXFLAGS) $(CXXFLAGS)
+AM_V_CXX = $(am__v_CXX_@AM_V@)
+am__v_CXX_ = $(am__v_CXX_@AM_DEFAULT_V@)
+am__v_CXX_0 = @echo " CXX " $@;
+am__v_CXX_1 =
+CXXLD = $(CXX)
+CXXLINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \
+ $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(AM_CXXFLAGS) \
+ $(CXXFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@
+AM_V_CXXLD = $(am__v_CXXLD_@AM_V@)
+am__v_CXXLD_ = $(am__v_CXXLD_@AM_DEFAULT_V@)
+am__v_CXXLD_0 = @echo " CXXLD " $@;
+am__v_CXXLD_1 =
+SOURCES = $(libmail_la_SOURCES) $(fuzz_message_parser_SOURCES) \
+ $(nodist_EXTRA_fuzz_message_parser_SOURCES) \
+ $(test_istream_attachment_SOURCES) \
+ $(test_istream_binary_converter_SOURCES) \
+ $(test_istream_dot_SOURCES) \
+ $(test_istream_header_filter_SOURCES) \
+ $(test_istream_qp_decoder_SOURCES) \
+ $(test_istream_qp_encoder_SOURCES) \
+ $(test_mail_html2text_SOURCES) $(test_mail_user_hash_SOURCES) \
+ $(test_mbox_from_SOURCES) $(test_message_address_SOURCES) \
+ $(test_message_date_SOURCES) $(test_message_decoder_SOURCES) \
+ $(test_message_header_decode_SOURCES) \
+ $(test_message_header_encode_SOURCES) \
+ $(test_message_header_hash_SOURCES) \
+ $(test_message_header_parser_SOURCES) \
+ $(test_message_id_SOURCES) $(test_message_parser_SOURCES) \
+ $(test_message_part_SOURCES) \
+ $(test_message_part_serialize_SOURCES) \
+ $(test_message_search_SOURCES) $(test_message_size_SOURCES) \
+ $(test_message_snippet_SOURCES) $(test_ostream_dot_SOURCES) \
+ $(test_qp_decoder_SOURCES) $(test_qp_encoder_SOURCES) \
+ $(test_quoted_printable_SOURCES) \
+ $(test_rfc2231_parser_SOURCES) $(test_rfc822_parser_SOURCES)
+DIST_SOURCES = $(libmail_la_SOURCES) \
+ $(am__fuzz_message_parser_SOURCES_DIST) \
+ $(test_istream_attachment_SOURCES) \
+ $(test_istream_binary_converter_SOURCES) \
+ $(test_istream_dot_SOURCES) \
+ $(test_istream_header_filter_SOURCES) \
+ $(test_istream_qp_decoder_SOURCES) \
+ $(test_istream_qp_encoder_SOURCES) \
+ $(test_mail_html2text_SOURCES) $(test_mail_user_hash_SOURCES) \
+ $(test_mbox_from_SOURCES) $(test_message_address_SOURCES) \
+ $(test_message_date_SOURCES) $(test_message_decoder_SOURCES) \
+ $(test_message_header_decode_SOURCES) \
+ $(test_message_header_encode_SOURCES) \
+ $(test_message_header_hash_SOURCES) \
+ $(test_message_header_parser_SOURCES) \
+ $(test_message_id_SOURCES) $(test_message_parser_SOURCES) \
+ $(test_message_part_SOURCES) \
+ $(test_message_part_serialize_SOURCES) \
+ $(test_message_search_SOURCES) $(test_message_size_SOURCES) \
+ $(test_message_snippet_SOURCES) $(test_ostream_dot_SOURCES) \
+ $(test_qp_decoder_SOURCES) $(test_qp_encoder_SOURCES) \
+ $(test_quoted_printable_SOURCES) \
+ $(test_rfc2231_parser_SOURCES) $(test_rfc822_parser_SOURCES)
+am__can_run_installinfo = \
+ case $$AM_UPDATE_INFO_DIR in \
+ n|no|NO) false;; \
+ *) (install-info --version) >/dev/null 2>&1;; \
+ esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(pkginc_libdir)"
+HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS)
+am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP)
+# Read a list of newline-separated strings from the standard input,
+# and print each of them once, without duplicates. Input order is
+# *not* preserved.
+am__uniquify_input = $(AWK) '\
+ BEGIN { nonempty = 0; } \
+ { items[$$0] = 1; nonempty = 1; } \
+ END { if (nonempty) { for (i in items) print i; }; } \
+'
+# Make sure the list of sources is unique. This is necessary because,
+# e.g., the same source file might be shared among _SOURCES variables
+# for different programs/libraries.
+am__define_uniq_tagged_files = \
+ list='$(am__tagged_files)'; \
+ unique=`for i in $$list; do \
+ if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \
+ done | $(am__uniquify_input)`
+ETAGS = etags
+CTAGS = ctags
+am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp
+DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST)
+ACLOCAL = @ACLOCAL@
+ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@
+AMTAR = @AMTAR@
+AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@
+APPARMOR_LIBS = @APPARMOR_LIBS@
+AR = @AR@
+AUTH_CFLAGS = @AUTH_CFLAGS@
+AUTH_LIBS = @AUTH_LIBS@
+AUTOCONF = @AUTOCONF@
+AUTOHEADER = @AUTOHEADER@
+AUTOMAKE = @AUTOMAKE@
+AWK = @AWK@
+BINARY_CFLAGS = @BINARY_CFLAGS@
+BINARY_LDFLAGS = @BINARY_LDFLAGS@
+BISON = @BISON@
+CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@
+CASSANDRA_LIBS = @CASSANDRA_LIBS@
+CC = @CC@
+CCDEPMODE = @CCDEPMODE@
+CDB_LIBS = @CDB_LIBS@
+CFLAGS = @CFLAGS@
+CLUCENE_CFLAGS = @CLUCENE_CFLAGS@
+CLUCENE_LIBS = @CLUCENE_LIBS@
+COMPRESS_LIBS = @COMPRESS_LIBS@
+CPP = @CPP@
+CPPFLAGS = @CPPFLAGS@
+CRYPT_LIBS = @CRYPT_LIBS@
+CXX = @CXX@
+CXXCPP = @CXXCPP@
+CXXDEPMODE = @CXXDEPMODE@
+CXXFLAGS = @CXXFLAGS@
+CYGPATH_W = @CYGPATH_W@
+DEFS = @DEFS@
+DEPDIR = @DEPDIR@
+DICT_LIBS = @DICT_LIBS@
+DLLIB = @DLLIB@
+DLLTOOL = @DLLTOOL@
+DSYMUTIL = @DSYMUTIL@
+DUMPBIN = @DUMPBIN@
+ECHO_C = @ECHO_C@
+ECHO_N = @ECHO_N@
+ECHO_T = @ECHO_T@
+EGREP = @EGREP@
+EXEEXT = @EXEEXT@
+FGREP = @FGREP@
+FLEX = @FLEX@
+FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@
+FUZZER_LDFLAGS = @FUZZER_LDFLAGS@
+GREP = @GREP@
+INSTALL = @INSTALL@
+INSTALL_DATA = @INSTALL_DATA@
+INSTALL_PROGRAM = @INSTALL_PROGRAM@
+INSTALL_SCRIPT = @INSTALL_SCRIPT@
+INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@
+KRB5CONFIG = @KRB5CONFIG@
+KRB5_CFLAGS = @KRB5_CFLAGS@
+KRB5_LIBS = @KRB5_LIBS@
+LD = @LD@
+LDAP_LIBS = @LDAP_LIBS@
+LDFLAGS = @LDFLAGS@
+LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@
+LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@
+LIBCAP = @LIBCAP@
+LIBDOVECOT = @LIBDOVECOT@
+LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@
+LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@
+LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@
+LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@
+LIBDOVECOT_LDA = @LIBDOVECOT_LDA@
+LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@
+LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@
+LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@
+LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@
+LIBDOVECOT_LUA = @LIBDOVECOT_LUA@
+LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@
+LIBDOVECOT_SQL = @LIBDOVECOT_SQL@
+LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@
+LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@
+LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@
+LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@
+LIBICONV = @LIBICONV@
+LIBICU_CFLAGS = @LIBICU_CFLAGS@
+LIBICU_LIBS = @LIBICU_LIBS@
+LIBOBJS = @LIBOBJS@
+LIBS = @LIBS@
+LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@
+LIBSODIUM_LIBS = @LIBSODIUM_LIBS@
+LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@
+LIBTIRPC_LIBS = @LIBTIRPC_LIBS@
+LIBTOOL = @LIBTOOL@
+LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@
+LIBUNWIND_LIBS = @LIBUNWIND_LIBS@
+LIBWRAP_LIBS = @LIBWRAP_LIBS@
+LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@
+LIPO = @LIPO@
+LN_S = @LN_S@
+LTLIBICONV = @LTLIBICONV@
+LTLIBOBJS = @LTLIBOBJS@
+LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@
+LUA_CFLAGS = @LUA_CFLAGS@
+LUA_LIBS = @LUA_LIBS@
+MAINT = @MAINT@
+MAKEINFO = @MAKEINFO@
+MANIFEST_TOOL = @MANIFEST_TOOL@
+MKDIR_P = @MKDIR_P@
+MODULE_LIBS = @MODULE_LIBS@
+MODULE_SUFFIX = @MODULE_SUFFIX@
+MYSQL_CFLAGS = @MYSQL_CFLAGS@
+MYSQL_CONFIG = @MYSQL_CONFIG@
+MYSQL_LIBS = @MYSQL_LIBS@
+NM = @NM@
+NMEDIT = @NMEDIT@
+NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@
+OBJDUMP = @OBJDUMP@
+OBJEXT = @OBJEXT@
+OTOOL = @OTOOL@
+OTOOL64 = @OTOOL64@
+PACKAGE = @PACKAGE@
+PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@
+PACKAGE_NAME = @PACKAGE_NAME@
+PACKAGE_STRING = @PACKAGE_STRING@
+PACKAGE_TARNAME = @PACKAGE_TARNAME@
+PACKAGE_URL = @PACKAGE_URL@
+PACKAGE_VERSION = @PACKAGE_VERSION@
+PANDOC = @PANDOC@
+PATH_SEPARATOR = @PATH_SEPARATOR@
+PGSQL_CFLAGS = @PGSQL_CFLAGS@
+PGSQL_LIBS = @PGSQL_LIBS@
+PG_CONFIG = @PG_CONFIG@
+PIE_CFLAGS = @PIE_CFLAGS@
+PIE_LDFLAGS = @PIE_LDFLAGS@
+PKG_CONFIG = @PKG_CONFIG@
+PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@
+PKG_CONFIG_PATH = @PKG_CONFIG_PATH@
+QUOTA_LIBS = @QUOTA_LIBS@
+RANLIB = @RANLIB@
+RELRO_LDFLAGS = @RELRO_LDFLAGS@
+RPCGEN = @RPCGEN@
+RUN_TEST = @RUN_TEST@
+SED = @SED@
+SETTING_FILES = @SETTING_FILES@
+SET_MAKE = @SET_MAKE@
+SHELL = @SHELL@
+SQLITE_CFLAGS = @SQLITE_CFLAGS@
+SQLITE_LIBS = @SQLITE_LIBS@
+SQL_CFLAGS = @SQL_CFLAGS@
+SQL_LIBS = @SQL_LIBS@
+SSL_CFLAGS = @SSL_CFLAGS@
+SSL_LIBS = @SSL_LIBS@
+STRIP = @STRIP@
+SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@
+SYSTEMD_LIBS = @SYSTEMD_LIBS@
+VALGRIND = @VALGRIND@
+VERSION = @VERSION@
+ZSTD_CFLAGS = @ZSTD_CFLAGS@
+ZSTD_LIBS = @ZSTD_LIBS@
+abs_builddir = @abs_builddir@
+abs_srcdir = @abs_srcdir@
+abs_top_builddir = @abs_top_builddir@
+abs_top_srcdir = @abs_top_srcdir@
+ac_ct_AR = @ac_ct_AR@
+ac_ct_CC = @ac_ct_CC@
+ac_ct_CXX = @ac_ct_CXX@
+ac_ct_DUMPBIN = @ac_ct_DUMPBIN@
+am__include = @am__include@
+am__leading_dot = @am__leading_dot@
+am__quote = @am__quote@
+am__tar = @am__tar@
+am__untar = @am__untar@
+bindir = @bindir@
+build = @build@
+build_alias = @build_alias@
+build_cpu = @build_cpu@
+build_os = @build_os@
+build_vendor = @build_vendor@
+builddir = @builddir@
+datadir = @datadir@
+datarootdir = @datarootdir@
+dict_drivers = @dict_drivers@
+docdir = @docdir@
+dvidir = @dvidir@
+exec_prefix = @exec_prefix@
+host = @host@
+host_alias = @host_alias@
+host_cpu = @host_cpu@
+host_os = @host_os@
+host_vendor = @host_vendor@
+htmldir = @htmldir@
+includedir = @includedir@
+infodir = @infodir@
+install_sh = @install_sh@
+libdir = @libdir@
+libexecdir = @libexecdir@
+localedir = @localedir@
+localstatedir = @localstatedir@
+mandir = @mandir@
+mkdir_p = @mkdir_p@
+moduledir = @moduledir@
+oldincludedir = @oldincludedir@
+pdfdir = @pdfdir@
+prefix = @prefix@
+program_transform_name = @program_transform_name@
+psdir = @psdir@
+rundir = @rundir@
+runstatedir = @runstatedir@
+sbindir = @sbindir@
+sharedstatedir = @sharedstatedir@
+sql_drivers = @sql_drivers@
+srcdir = @srcdir@
+ssldir = @ssldir@
+statedir = @statedir@
+sysconfdir = @sysconfdir@
+systemdservicetype = @systemdservicetype@
+systemdsystemunitdir = @systemdsystemunitdir@
+target_alias = @target_alias@
+top_build_prefix = @top_build_prefix@
+top_builddir = @top_builddir@
+top_srcdir = @top_srcdir@
+noinst_LTLIBRARIES = libmail.la
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/lib \
+ -I$(top_srcdir)/src/lib-test \
+ -I$(top_srcdir)/src/lib-charset \
+ -I$(top_srcdir)/src/lib-smtp
+
+libmail_la_SOURCES = \
+ istream-attachment-connector.c \
+ istream-attachment-extractor.c \
+ istream-binary-converter.c \
+ istream-dot.c \
+ 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 \
+ message-address.c \
+ message-binary-part.c \
+ message-date.c \
+ message-decoder.c \
+ message-header-decode.c \
+ message-header-encode.c \
+ message-header-hash.c \
+ message-header-parser.c \
+ message-id.c \
+ message-parser.c \
+ message-parser-from-parts.c \
+ message-part.c \
+ message-part-data.c \
+ message-part-serialize.c \
+ message-search.c \
+ message-size.c \
+ message-snippet.c \
+ ostream-dot.c \
+ qp-decoder.c \
+ qp-encoder.c \
+ quoted-printable.c \
+ rfc2231-parser.c \
+ rfc822-parser.c
+
+noinst_HEADERS = \
+ html-entities.h \
+ message-parser-private.h
+
+headers = \
+ istream-attachment-connector.h \
+ istream-attachment-extractor.h \
+ istream-binary-converter.h \
+ istream-dot.h \
+ istream-header-filter.h \
+ istream-nonuls.h \
+ istream-qp.h \
+ mail-user-hash.h \
+ mbox-from.h \
+ mail-html2text.h \
+ mail-types.h \
+ message-address.h \
+ message-binary-part.h \
+ message-date.h \
+ message-decoder.h \
+ message-header-decode.h \
+ message-header-encode.h \
+ message-header-hash.h \
+ message-header-parser.h \
+ message-id.h \
+ message-parser.h \
+ message-part.h \
+ message-part-data.h \
+ message-part-serialize.h \
+ message-search.h \
+ message-size.h \
+ message-snippet.h \
+ ostream-dot.h \
+ qp-decoder.h \
+ qp-encoder.h \
+ quoted-printable.h \
+ rfc2231-parser.h \
+ rfc822-parser.h
+
+pkginc_libdir = $(pkgincludedir)
+pkginc_lib_HEADERS = $(headers)
+test_programs = \
+ test-istream-dot \
+ test-istream-attachment \
+ test-istream-binary-converter \
+ test-istream-header-filter \
+ test-istream-qp-decoder \
+ test-istream-qp-encoder \
+ test-mail-html2text \
+ test-mail-user-hash \
+ test-mbox-from \
+ test-message-address \
+ test-message-date \
+ test-message-decoder \
+ test-message-header-decode \
+ test-message-header-encode \
+ test-message-header-hash \
+ test-message-header-parser \
+ test-message-id \
+ test-message-parser \
+ test-message-part \
+ test-message-part-serialize \
+ test-message-search \
+ test-message-size \
+ test-message-snippet \
+ test-ostream-dot \
+ test-qp-decoder \
+ test-qp-encoder \
+ test-quoted-printable \
+ test-rfc2231-parser \
+ test-rfc822-parser
+
+fuzz_programs = $(am__append_1)
+@USE_FUZZER_TRUE@nodist_EXTRA_fuzz_message_parser_SOURCES = force-cxx-linking.cxx
+@USE_FUZZER_TRUE@fuzz_message_parser_CPPFLAGS = $(FUZZER_CPPFLAGS)
+@USE_FUZZER_TRUE@fuzz_message_parser_LDFLAGS = $(FUZZER_LDFLAGS)
+@USE_FUZZER_TRUE@fuzz_message_parser_SOURCES = fuzz-message-parser.c
+@USE_FUZZER_TRUE@fuzz_message_parser_LDADD = $(test_libs)
+@USE_FUZZER_TRUE@fuzz_message_parser_DEPENDENCIES = $(test_deps)
+test_libs = \
+ $(noinst_LTLIBRARIES) \
+ ../lib-charset/libcharset.la \
+ ../lib-test/libtest.la \
+ ../lib/liblib.la
+
+test_deps = $(noinst_LTLIBRARIES) $(test_libs)
+test_istream_dot_SOURCES = test-istream-dot.c
+test_istream_dot_LDADD = $(test_libs)
+test_istream_dot_DEPENDENCIES = $(test_deps)
+test_istream_qp_decoder_SOURCES = test-istream-qp-decoder.c
+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)
+test_istream_attachment_SOURCES = test-istream-attachment.c
+test_istream_attachment_LDADD = $(test_libs)
+test_istream_attachment_DEPENDENCIES = $(test_deps)
+test_istream_header_filter_SOURCES = test-istream-header-filter.c
+test_istream_header_filter_LDADD = $(test_libs)
+test_istream_header_filter_DEPENDENCIES = $(test_deps)
+test_mbox_from_SOURCES = test-mbox-from.c
+test_mbox_from_LDADD = $(test_libs)
+test_mbox_from_DEPENDENCIES = $(test_deps)
+test_message_address_SOURCES = test-message-address.c
+test_message_address_LDADD = $(test_libs)
+test_message_address_DEPENDENCIES = $(test_deps)
+test_message_date_SOURCES = test-message-date.c
+test_message_date_LDADD = $(test_libs)
+test_message_date_DEPENDENCIES = $(test_deps)
+test_message_decoder_SOURCES = test-message-decoder.c
+test_message_decoder_LDADD = $(test_libs) ../lib-charset/libcharset.la
+test_message_decoder_DEPENDENCIES = $(test_deps) ../lib-charset/libcharset.la
+test_message_header_decode_SOURCES = test-message-header-decode.c
+test_message_header_decode_LDADD = $(test_libs)
+test_message_header_decode_DEPENDENCIES = $(test_deps)
+test_message_header_encode_SOURCES = test-message-header-encode.c
+test_message_header_encode_LDADD = $(test_libs)
+test_message_header_encode_DEPENDENCIES = $(test_deps)
+test_message_header_hash_SOURCES = test-message-header-hash.c
+test_message_header_hash_LDADD = $(test_libs)
+test_message_header_hash_DEPENDENCIES = $(test_deps)
+test_message_header_parser_SOURCES = test-message-header-parser.c
+test_message_header_parser_LDADD = $(test_libs)
+test_message_header_parser_DEPENDENCIES = $(test_deps)
+test_message_id_SOURCES = test-message-id.c
+test_message_id_LDADD = $(test_libs)
+test_message_id_DEPENDENCIES = $(test_deps)
+test_message_parser_SOURCES = test-message-parser.c
+test_message_parser_LDADD = $(test_libs)
+test_message_parser_DEPENDENCIES = $(test_deps)
+test_message_part_SOURCES = test-message-part.c
+test_message_part_LDADD = $(test_libs)
+test_message_part_DEPENDENCIES = $(test_deps)
+test_message_search_SOURCES = test-message-search.c
+test_message_search_LDADD = $(test_libs) ../lib-charset/libcharset.la
+test_message_search_DEPENDENCIES = $(test_deps) ../lib-charset/libcharset.la
+test_message_size_SOURCES = test-message-size.c
+test_message_size_LDADD = $(test_libs)
+test_message_size_DEPENDENCIES = $(test_deps)
+test_message_snippet_SOURCES = test-message-snippet.c
+test_message_snippet_LDADD = $(test_message_decoder_LDADD)
+test_message_snippet_DEPENDENCIES = $(test_deps)
+test_mail_html2text_SOURCES = test-mail-html2text.c
+test_mail_html2text_LDADD = $(test_libs)
+test_mail_html2text_DEPENDENCIES = $(test_deps)
+test_ostream_dot_SOURCES = test-ostream-dot.c
+test_ostream_dot_LDADD = $(test_libs)
+test_ostream_dot_DEPENDENCIES = $(test_deps)
+test_qp_decoder_SOURCES = test-qp-decoder.c
+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)
+test_rfc2231_parser_SOURCES = test-rfc2231-parser.c
+test_rfc2231_parser_LDADD = $(test_libs)
+test_rfc2231_parser_DEPENDENCIES = $(test_deps)
+test_rfc822_parser_SOURCES = test-rfc822-parser.c
+test_rfc822_parser_LDADD = $(test_libs)
+test_rfc822_parser_DEPENDENCIES = $(test_deps)
+test_mail_user_hash_SOURCES = test-mail-user-hash.c
+test_mail_user_hash_LDADD = $(test_libs)
+test_mail_user_hash_DEPENDENCIES = $(test_deps)
+test_message_part_serialize_SOURCES = test-message-part-serialize.c
+test_message_part_serialize_LDADD = $(test_libs)
+test_message_part_serialize_DEPENDENCIES = $(test_deps)
+all: all-am
+
+.SUFFIXES:
+.SUFFIXES: .c .cxx .lo .o .obj
+$(srcdir)/Makefile.in: @MAINTAINER_MODE_TRUE@ $(srcdir)/Makefile.am $(am__configure_deps)
+ @for dep in $?; do \
+ case '$(am__configure_deps)' in \
+ *$$dep*) \
+ ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \
+ && { if test -f $@; then exit 0; else break; fi; }; \
+ exit 1;; \
+ esac; \
+ done; \
+ echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib-mail/Makefile'; \
+ $(am__cd) $(top_srcdir) && \
+ $(AUTOMAKE) --foreign src/lib-mail/Makefile
+Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status
+ @case '$?' in \
+ *config.status*) \
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \
+ *) \
+ echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \
+ cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \
+ esac;
+
+$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+
+$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps)
+ cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh
+$(am__aclocal_m4_deps):
+
+clean-noinstPROGRAMS:
+ @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \
+ echo " rm -f" $$list; \
+ rm -f $$list || exit $$?; \
+ test -n "$(EXEEXT)" || exit 0; \
+ list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \
+ echo " rm -f" $$list; \
+ rm -f $$list
+
+clean-noinstLTLIBRARIES:
+ -test -z "$(noinst_LTLIBRARIES)" || rm -f $(noinst_LTLIBRARIES)
+ @list='$(noinst_LTLIBRARIES)'; \
+ locs=`for p in $$list; do echo $$p; done | \
+ sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \
+ sort -u`; \
+ test -z "$$locs" || { \
+ echo rm -f $${locs}; \
+ rm -f $${locs}; \
+ }
+
+libmail.la: $(libmail_la_OBJECTS) $(libmail_la_DEPENDENCIES) $(EXTRA_libmail_la_DEPENDENCIES)
+ $(AM_V_CCLD)$(LINK) $(libmail_la_OBJECTS) $(libmail_la_LIBADD) $(LIBS)
+
+fuzz-message-parser$(EXEEXT): $(fuzz_message_parser_OBJECTS) $(fuzz_message_parser_DEPENDENCIES) $(EXTRA_fuzz_message_parser_DEPENDENCIES)
+ @rm -f fuzz-message-parser$(EXEEXT)
+ $(AM_V_CXXLD)$(fuzz_message_parser_LINK) $(fuzz_message_parser_OBJECTS) $(fuzz_message_parser_LDADD) $(LIBS)
+
+test-istream-attachment$(EXEEXT): $(test_istream_attachment_OBJECTS) $(test_istream_attachment_DEPENDENCIES) $(EXTRA_test_istream_attachment_DEPENDENCIES)
+ @rm -f test-istream-attachment$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_attachment_OBJECTS) $(test_istream_attachment_LDADD) $(LIBS)
+
+test-istream-binary-converter$(EXEEXT): $(test_istream_binary_converter_OBJECTS) $(test_istream_binary_converter_DEPENDENCIES) $(EXTRA_test_istream_binary_converter_DEPENDENCIES)
+ @rm -f test-istream-binary-converter$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_binary_converter_OBJECTS) $(test_istream_binary_converter_LDADD) $(LIBS)
+
+test-istream-dot$(EXEEXT): $(test_istream_dot_OBJECTS) $(test_istream_dot_DEPENDENCIES) $(EXTRA_test_istream_dot_DEPENDENCIES)
+ @rm -f test-istream-dot$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_dot_OBJECTS) $(test_istream_dot_LDADD) $(LIBS)
+
+test-istream-header-filter$(EXEEXT): $(test_istream_header_filter_OBJECTS) $(test_istream_header_filter_DEPENDENCIES) $(EXTRA_test_istream_header_filter_DEPENDENCIES)
+ @rm -f test-istream-header-filter$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_header_filter_OBJECTS) $(test_istream_header_filter_LDADD) $(LIBS)
+
+test-istream-qp-decoder$(EXEEXT): $(test_istream_qp_decoder_OBJECTS) $(test_istream_qp_decoder_DEPENDENCIES) $(EXTRA_test_istream_qp_decoder_DEPENDENCIES)
+ @rm -f test-istream-qp-decoder$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_qp_decoder_OBJECTS) $(test_istream_qp_decoder_LDADD) $(LIBS)
+
+test-istream-qp-encoder$(EXEEXT): $(test_istream_qp_encoder_OBJECTS) $(test_istream_qp_encoder_DEPENDENCIES) $(EXTRA_test_istream_qp_encoder_DEPENDENCIES)
+ @rm -f test-istream-qp-encoder$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_istream_qp_encoder_OBJECTS) $(test_istream_qp_encoder_LDADD) $(LIBS)
+
+test-mail-html2text$(EXEEXT): $(test_mail_html2text_OBJECTS) $(test_mail_html2text_DEPENDENCIES) $(EXTRA_test_mail_html2text_DEPENDENCIES)
+ @rm -f test-mail-html2text$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_html2text_OBJECTS) $(test_mail_html2text_LDADD) $(LIBS)
+
+test-mail-user-hash$(EXEEXT): $(test_mail_user_hash_OBJECTS) $(test_mail_user_hash_DEPENDENCIES) $(EXTRA_test_mail_user_hash_DEPENDENCIES)
+ @rm -f test-mail-user-hash$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mail_user_hash_OBJECTS) $(test_mail_user_hash_LDADD) $(LIBS)
+
+test-mbox-from$(EXEEXT): $(test_mbox_from_OBJECTS) $(test_mbox_from_DEPENDENCIES) $(EXTRA_test_mbox_from_DEPENDENCIES)
+ @rm -f test-mbox-from$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_mbox_from_OBJECTS) $(test_mbox_from_LDADD) $(LIBS)
+
+test-message-address$(EXEEXT): $(test_message_address_OBJECTS) $(test_message_address_DEPENDENCIES) $(EXTRA_test_message_address_DEPENDENCIES)
+ @rm -f test-message-address$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_address_OBJECTS) $(test_message_address_LDADD) $(LIBS)
+
+test-message-date$(EXEEXT): $(test_message_date_OBJECTS) $(test_message_date_DEPENDENCIES) $(EXTRA_test_message_date_DEPENDENCIES)
+ @rm -f test-message-date$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_date_OBJECTS) $(test_message_date_LDADD) $(LIBS)
+
+test-message-decoder$(EXEEXT): $(test_message_decoder_OBJECTS) $(test_message_decoder_DEPENDENCIES) $(EXTRA_test_message_decoder_DEPENDENCIES)
+ @rm -f test-message-decoder$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_decoder_OBJECTS) $(test_message_decoder_LDADD) $(LIBS)
+
+test-message-header-decode$(EXEEXT): $(test_message_header_decode_OBJECTS) $(test_message_header_decode_DEPENDENCIES) $(EXTRA_test_message_header_decode_DEPENDENCIES)
+ @rm -f test-message-header-decode$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_header_decode_OBJECTS) $(test_message_header_decode_LDADD) $(LIBS)
+
+test-message-header-encode$(EXEEXT): $(test_message_header_encode_OBJECTS) $(test_message_header_encode_DEPENDENCIES) $(EXTRA_test_message_header_encode_DEPENDENCIES)
+ @rm -f test-message-header-encode$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_header_encode_OBJECTS) $(test_message_header_encode_LDADD) $(LIBS)
+
+test-message-header-hash$(EXEEXT): $(test_message_header_hash_OBJECTS) $(test_message_header_hash_DEPENDENCIES) $(EXTRA_test_message_header_hash_DEPENDENCIES)
+ @rm -f test-message-header-hash$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_header_hash_OBJECTS) $(test_message_header_hash_LDADD) $(LIBS)
+
+test-message-header-parser$(EXEEXT): $(test_message_header_parser_OBJECTS) $(test_message_header_parser_DEPENDENCIES) $(EXTRA_test_message_header_parser_DEPENDENCIES)
+ @rm -f test-message-header-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_header_parser_OBJECTS) $(test_message_header_parser_LDADD) $(LIBS)
+
+test-message-id$(EXEEXT): $(test_message_id_OBJECTS) $(test_message_id_DEPENDENCIES) $(EXTRA_test_message_id_DEPENDENCIES)
+ @rm -f test-message-id$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_id_OBJECTS) $(test_message_id_LDADD) $(LIBS)
+
+test-message-parser$(EXEEXT): $(test_message_parser_OBJECTS) $(test_message_parser_DEPENDENCIES) $(EXTRA_test_message_parser_DEPENDENCIES)
+ @rm -f test-message-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_parser_OBJECTS) $(test_message_parser_LDADD) $(LIBS)
+
+test-message-part$(EXEEXT): $(test_message_part_OBJECTS) $(test_message_part_DEPENDENCIES) $(EXTRA_test_message_part_DEPENDENCIES)
+ @rm -f test-message-part$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_part_OBJECTS) $(test_message_part_LDADD) $(LIBS)
+
+test-message-part-serialize$(EXEEXT): $(test_message_part_serialize_OBJECTS) $(test_message_part_serialize_DEPENDENCIES) $(EXTRA_test_message_part_serialize_DEPENDENCIES)
+ @rm -f test-message-part-serialize$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_part_serialize_OBJECTS) $(test_message_part_serialize_LDADD) $(LIBS)
+
+test-message-search$(EXEEXT): $(test_message_search_OBJECTS) $(test_message_search_DEPENDENCIES) $(EXTRA_test_message_search_DEPENDENCIES)
+ @rm -f test-message-search$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_search_OBJECTS) $(test_message_search_LDADD) $(LIBS)
+
+test-message-size$(EXEEXT): $(test_message_size_OBJECTS) $(test_message_size_DEPENDENCIES) $(EXTRA_test_message_size_DEPENDENCIES)
+ @rm -f test-message-size$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_size_OBJECTS) $(test_message_size_LDADD) $(LIBS)
+
+test-message-snippet$(EXEEXT): $(test_message_snippet_OBJECTS) $(test_message_snippet_DEPENDENCIES) $(EXTRA_test_message_snippet_DEPENDENCIES)
+ @rm -f test-message-snippet$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_message_snippet_OBJECTS) $(test_message_snippet_LDADD) $(LIBS)
+
+test-ostream-dot$(EXEEXT): $(test_ostream_dot_OBJECTS) $(test_ostream_dot_DEPENDENCIES) $(EXTRA_test_ostream_dot_DEPENDENCIES)
+ @rm -f test-ostream-dot$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_ostream_dot_OBJECTS) $(test_ostream_dot_LDADD) $(LIBS)
+
+test-qp-decoder$(EXEEXT): $(test_qp_decoder_OBJECTS) $(test_qp_decoder_DEPENDENCIES) $(EXTRA_test_qp_decoder_DEPENDENCIES)
+ @rm -f test-qp-decoder$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_qp_decoder_OBJECTS) $(test_qp_decoder_LDADD) $(LIBS)
+
+test-qp-encoder$(EXEEXT): $(test_qp_encoder_OBJECTS) $(test_qp_encoder_DEPENDENCIES) $(EXTRA_test_qp_encoder_DEPENDENCIES)
+ @rm -f test-qp-encoder$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_qp_encoder_OBJECTS) $(test_qp_encoder_LDADD) $(LIBS)
+
+test-quoted-printable$(EXEEXT): $(test_quoted_printable_OBJECTS) $(test_quoted_printable_DEPENDENCIES) $(EXTRA_test_quoted_printable_DEPENDENCIES)
+ @rm -f test-quoted-printable$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_quoted_printable_OBJECTS) $(test_quoted_printable_LDADD) $(LIBS)
+
+test-rfc2231-parser$(EXEEXT): $(test_rfc2231_parser_OBJECTS) $(test_rfc2231_parser_DEPENDENCIES) $(EXTRA_test_rfc2231_parser_DEPENDENCIES)
+ @rm -f test-rfc2231-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_rfc2231_parser_OBJECTS) $(test_rfc2231_parser_LDADD) $(LIBS)
+
+test-rfc822-parser$(EXEEXT): $(test_rfc822_parser_OBJECTS) $(test_rfc822_parser_DEPENDENCIES) $(EXTRA_test_rfc822_parser_DEPENDENCIES)
+ @rm -f test-rfc822-parser$(EXEEXT)
+ $(AM_V_CCLD)$(LINK) $(test_rfc822_parser_OBJECTS) $(test_rfc822_parser_LDADD) $(LIBS)
+
+mostlyclean-compile:
+ -rm -f *.$(OBJEXT)
+
+distclean-compile:
+ -rm -f *.tab.c
+
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-attachment-connector.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-attachment-extractor.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-binary-converter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-dot.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-header-filter.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-nonuls.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-qp-decoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/istream-qp-encoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-html2text.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-user-hash.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mbox-from.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-address.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-binary-part.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-date.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-decoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-header-decode.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-header-encode.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-header-hash.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-header-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-id.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-parser-from-parts.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-part-data.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-part-serialize.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-part.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-search.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-size.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/message-snippet.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ostream-dot.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/qp-decoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/qp-encoder.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quoted-printable.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rfc2231-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rfc822-parser.Plo@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-attachment.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-binary-converter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-dot.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-header-filter.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-qp-decoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-istream-qp-encoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-html2text.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mail-user-hash.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-mbox-from.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-address.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-date.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-decoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-header-decode.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-header-encode.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-header-hash.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-header-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-id.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-part-serialize.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-part.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-search.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-size.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-message-snippet.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-ostream-dot.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-qp-decoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-qp-encoder.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-quoted-printable.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-rfc2231-parser.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-rfc822-parser.Po@am__quote@ # am--include-marker
+
+$(am__depfiles_remade):
+ @$(MKDIR_P) $(@D)
+ @echo '# dummy' >$@-t && $(am__mv) $@-t $@
+
+am--depfiles: $(am__depfiles_remade)
+
+.c.o:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $<
+
+.c.obj:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.c.lo:
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $<
+
+fuzz_message_parser-fuzz-message-parser.o: fuzz-message-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT fuzz_message_parser-fuzz-message-parser.o -MD -MP -MF $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Tpo -c -o fuzz_message_parser-fuzz-message-parser.o `test -f 'fuzz-message-parser.c' || echo '$(srcdir)/'`fuzz-message-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Tpo $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fuzz-message-parser.c' object='fuzz_message_parser-fuzz-message-parser.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o fuzz_message_parser-fuzz-message-parser.o `test -f 'fuzz-message-parser.c' || echo '$(srcdir)/'`fuzz-message-parser.c
+
+fuzz_message_parser-fuzz-message-parser.obj: fuzz-message-parser.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT fuzz_message_parser-fuzz-message-parser.obj -MD -MP -MF $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Tpo -c -o fuzz_message_parser-fuzz-message-parser.obj `if test -f 'fuzz-message-parser.c'; then $(CYGPATH_W) 'fuzz-message-parser.c'; else $(CYGPATH_W) '$(srcdir)/fuzz-message-parser.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Tpo $(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='fuzz-message-parser.c' object='fuzz_message_parser-fuzz-message-parser.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o fuzz_message_parser-fuzz-message-parser.obj `if test -f 'fuzz-message-parser.c'; then $(CYGPATH_W) 'fuzz-message-parser.c'; else $(CYGPATH_W) '$(srcdir)/fuzz-message-parser.c'; fi`
+
+.cxx.o:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ $<
+
+.cxx.obj:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ `$(CYGPATH_W) '$<'`
+
+.cxx.lo:
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LTCXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LTCXXCOMPILE) -c -o $@ $<
+
+fuzz_message_parser-force-cxx-linking.o: force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT fuzz_message_parser-force-cxx-linking.o -MD -MP -MF $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Tpo -c -o fuzz_message_parser-force-cxx-linking.o `test -f 'force-cxx-linking.cxx' || echo '$(srcdir)/'`force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Tpo $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='force-cxx-linking.cxx' object='fuzz_message_parser-force-cxx-linking.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o fuzz_message_parser-force-cxx-linking.o `test -f 'force-cxx-linking.cxx' || echo '$(srcdir)/'`force-cxx-linking.cxx
+
+fuzz_message_parser-force-cxx-linking.obj: force-cxx-linking.cxx
+@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -MT fuzz_message_parser-force-cxx-linking.obj -MD -MP -MF $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Tpo -c -o fuzz_message_parser-force-cxx-linking.obj `if test -f 'force-cxx-linking.cxx'; then $(CYGPATH_W) 'force-cxx-linking.cxx'; else $(CYGPATH_W) '$(srcdir)/force-cxx-linking.cxx'; fi`
+@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Tpo $(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='force-cxx-linking.cxx' object='fuzz_message_parser-force-cxx-linking.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(fuzz_message_parser_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) -c -o fuzz_message_parser-force-cxx-linking.obj `if test -f 'force-cxx-linking.cxx'; then $(CYGPATH_W) 'force-cxx-linking.cxx'; else $(CYGPATH_W) '$(srcdir)/force-cxx-linking.cxx'; fi`
+
+mostlyclean-libtool:
+ -rm -f *.lo
+
+clean-libtool:
+ -rm -rf .libs _libs
+install-pkginc_libHEADERS: $(pkginc_lib_HEADERS)
+ @$(NORMAL_INSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ if test -n "$$list"; then \
+ echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \
+ fi; \
+ for p in $$list; do \
+ if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
+ echo "$$d$$p"; \
+ done | $(am__base_list) | \
+ while read files; do \
+ echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \
+ $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \
+ done
+
+uninstall-pkginc_libHEADERS:
+ @$(NORMAL_UNINSTALL)
+ @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \
+ files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \
+ dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir)
+
+ID: $(am__tagged_files)
+ $(am__define_uniq_tagged_files); mkid -fID $$unique
+tags: tags-am
+TAGS: tags
+
+tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ set x; \
+ here=`pwd`; \
+ $(am__define_uniq_tagged_files); \
+ shift; \
+ if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \
+ test -n "$$unique" || unique=$$empty_fix; \
+ if test $$# -gt 0; then \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ "$$@" $$unique; \
+ else \
+ $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \
+ $$unique; \
+ fi; \
+ fi
+ctags: ctags-am
+
+CTAGS: ctags
+ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files)
+ $(am__define_uniq_tagged_files); \
+ test -z "$(CTAGS_ARGS)$$unique" \
+ || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \
+ $$unique
+
+GTAGS:
+ here=`$(am__cd) $(top_builddir) && pwd` \
+ && $(am__cd) $(top_srcdir) \
+ && gtags -i $(GTAGS_ARGS) "$$here"
+cscopelist: cscopelist-am
+
+cscopelist-am: $(am__tagged_files)
+ list='$(am__tagged_files)'; \
+ case "$(srcdir)" in \
+ [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \
+ *) sdir=$(subdir)/$(srcdir) ;; \
+ esac; \
+ for i in $$list; do \
+ if test -f "$$i"; then \
+ echo "$(subdir)/$$i"; \
+ else \
+ echo "$$sdir/$$i"; \
+ fi; \
+ done >> $(top_builddir)/cscope.files
+
+distclean-tags:
+ -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags
+
+distdir: $(BUILT_SOURCES)
+ $(MAKE) $(AM_MAKEFLAGS) distdir-am
+
+distdir-am: $(DISTFILES)
+ @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \
+ list='$(DISTFILES)'; \
+ dist_files=`for file in $$list; do echo $$file; done | \
+ sed -e "s|^$$srcdirstrip/||;t" \
+ -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \
+ case $$dist_files in \
+ */*) $(MKDIR_P) `echo "$$dist_files" | \
+ sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \
+ sort -u` ;; \
+ esac; \
+ for file in $$dist_files; do \
+ if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \
+ if test -d $$d/$$file; then \
+ dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \
+ if test -d "$(distdir)/$$file"; then \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \
+ cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \
+ find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \
+ fi; \
+ cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \
+ else \
+ test -f "$(distdir)/$$file" \
+ || cp -p $$d/$$file "$(distdir)/$$file" \
+ || exit 1; \
+ fi; \
+ done
+check-am: all-am
+ $(MAKE) $(AM_MAKEFLAGS) check-local
+check: check-am
+all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS)
+installdirs:
+ for dir in "$(DESTDIR)$(pkginc_libdir)"; do \
+ test -z "$$dir" || $(MKDIR_P) "$$dir"; \
+ done
+install: install-am
+install-exec: install-exec-am
+install-data: install-data-am
+uninstall: uninstall-am
+
+install-am: all-am
+ @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am
+
+installcheck: installcheck-am
+install-strip:
+ if test -z '$(STRIP)'; then \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ install; \
+ else \
+ $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \
+ install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \
+ "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \
+ fi
+mostlyclean-generic:
+
+clean-generic:
+
+distclean-generic:
+ -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES)
+ -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES)
+
+maintainer-clean-generic:
+ @echo "This command is intended for maintainers to use"
+ @echo "it deletes files that may require special tools to rebuild."
+clean: clean-am
+
+clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \
+ clean-noinstPROGRAMS mostlyclean-am
+
+distclean: distclean-am
+ -rm -f ./$(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po
+ -rm -f ./$(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po
+ -rm -f ./$(DEPDIR)/istream-attachment-connector.Plo
+ -rm -f ./$(DEPDIR)/istream-attachment-extractor.Plo
+ -rm -f ./$(DEPDIR)/istream-binary-converter.Plo
+ -rm -f ./$(DEPDIR)/istream-dot.Plo
+ -rm -f ./$(DEPDIR)/istream-header-filter.Plo
+ -rm -f ./$(DEPDIR)/istream-nonuls.Plo
+ -rm -f ./$(DEPDIR)/istream-qp-decoder.Plo
+ -rm -f ./$(DEPDIR)/istream-qp-encoder.Plo
+ -rm -f ./$(DEPDIR)/mail-html2text.Plo
+ -rm -f ./$(DEPDIR)/mail-user-hash.Plo
+ -rm -f ./$(DEPDIR)/mbox-from.Plo
+ -rm -f ./$(DEPDIR)/message-address.Plo
+ -rm -f ./$(DEPDIR)/message-binary-part.Plo
+ -rm -f ./$(DEPDIR)/message-date.Plo
+ -rm -f ./$(DEPDIR)/message-decoder.Plo
+ -rm -f ./$(DEPDIR)/message-header-decode.Plo
+ -rm -f ./$(DEPDIR)/message-header-encode.Plo
+ -rm -f ./$(DEPDIR)/message-header-hash.Plo
+ -rm -f ./$(DEPDIR)/message-header-parser.Plo
+ -rm -f ./$(DEPDIR)/message-id.Plo
+ -rm -f ./$(DEPDIR)/message-parser-from-parts.Plo
+ -rm -f ./$(DEPDIR)/message-parser.Plo
+ -rm -f ./$(DEPDIR)/message-part-data.Plo
+ -rm -f ./$(DEPDIR)/message-part-serialize.Plo
+ -rm -f ./$(DEPDIR)/message-part.Plo
+ -rm -f ./$(DEPDIR)/message-search.Plo
+ -rm -f ./$(DEPDIR)/message-size.Plo
+ -rm -f ./$(DEPDIR)/message-snippet.Plo
+ -rm -f ./$(DEPDIR)/ostream-dot.Plo
+ -rm -f ./$(DEPDIR)/qp-decoder.Plo
+ -rm -f ./$(DEPDIR)/qp-encoder.Plo
+ -rm -f ./$(DEPDIR)/quoted-printable.Plo
+ -rm -f ./$(DEPDIR)/rfc2231-parser.Plo
+ -rm -f ./$(DEPDIR)/rfc822-parser.Plo
+ -rm -f ./$(DEPDIR)/test-istream-attachment.Po
+ -rm -f ./$(DEPDIR)/test-istream-binary-converter.Po
+ -rm -f ./$(DEPDIR)/test-istream-dot.Po
+ -rm -f ./$(DEPDIR)/test-istream-header-filter.Po
+ -rm -f ./$(DEPDIR)/test-istream-qp-decoder.Po
+ -rm -f ./$(DEPDIR)/test-istream-qp-encoder.Po
+ -rm -f ./$(DEPDIR)/test-mail-html2text.Po
+ -rm -f ./$(DEPDIR)/test-mail-user-hash.Po
+ -rm -f ./$(DEPDIR)/test-mbox-from.Po
+ -rm -f ./$(DEPDIR)/test-message-address.Po
+ -rm -f ./$(DEPDIR)/test-message-date.Po
+ -rm -f ./$(DEPDIR)/test-message-decoder.Po
+ -rm -f ./$(DEPDIR)/test-message-header-decode.Po
+ -rm -f ./$(DEPDIR)/test-message-header-encode.Po
+ -rm -f ./$(DEPDIR)/test-message-header-hash.Po
+ -rm -f ./$(DEPDIR)/test-message-header-parser.Po
+ -rm -f ./$(DEPDIR)/test-message-id.Po
+ -rm -f ./$(DEPDIR)/test-message-parser.Po
+ -rm -f ./$(DEPDIR)/test-message-part-serialize.Po
+ -rm -f ./$(DEPDIR)/test-message-part.Po
+ -rm -f ./$(DEPDIR)/test-message-search.Po
+ -rm -f ./$(DEPDIR)/test-message-size.Po
+ -rm -f ./$(DEPDIR)/test-message-snippet.Po
+ -rm -f ./$(DEPDIR)/test-ostream-dot.Po
+ -rm -f ./$(DEPDIR)/test-qp-decoder.Po
+ -rm -f ./$(DEPDIR)/test-qp-encoder.Po
+ -rm -f ./$(DEPDIR)/test-quoted-printable.Po
+ -rm -f ./$(DEPDIR)/test-rfc2231-parser.Po
+ -rm -f ./$(DEPDIR)/test-rfc822-parser.Po
+ -rm -f Makefile
+distclean-am: clean-am distclean-compile distclean-generic \
+ distclean-tags
+
+dvi: dvi-am
+
+dvi-am:
+
+html: html-am
+
+html-am:
+
+info: info-am
+
+info-am:
+
+install-data-am: install-pkginc_libHEADERS
+
+install-dvi: install-dvi-am
+
+install-dvi-am:
+
+install-exec-am:
+
+install-html: install-html-am
+
+install-html-am:
+
+install-info: install-info-am
+
+install-info-am:
+
+install-man:
+
+install-pdf: install-pdf-am
+
+install-pdf-am:
+
+install-ps: install-ps-am
+
+install-ps-am:
+
+installcheck-am:
+
+maintainer-clean: maintainer-clean-am
+ -rm -f ./$(DEPDIR)/fuzz_message_parser-force-cxx-linking.Po
+ -rm -f ./$(DEPDIR)/fuzz_message_parser-fuzz-message-parser.Po
+ -rm -f ./$(DEPDIR)/istream-attachment-connector.Plo
+ -rm -f ./$(DEPDIR)/istream-attachment-extractor.Plo
+ -rm -f ./$(DEPDIR)/istream-binary-converter.Plo
+ -rm -f ./$(DEPDIR)/istream-dot.Plo
+ -rm -f ./$(DEPDIR)/istream-header-filter.Plo
+ -rm -f ./$(DEPDIR)/istream-nonuls.Plo
+ -rm -f ./$(DEPDIR)/istream-qp-decoder.Plo
+ -rm -f ./$(DEPDIR)/istream-qp-encoder.Plo
+ -rm -f ./$(DEPDIR)/mail-html2text.Plo
+ -rm -f ./$(DEPDIR)/mail-user-hash.Plo
+ -rm -f ./$(DEPDIR)/mbox-from.Plo
+ -rm -f ./$(DEPDIR)/message-address.Plo
+ -rm -f ./$(DEPDIR)/message-binary-part.Plo
+ -rm -f ./$(DEPDIR)/message-date.Plo
+ -rm -f ./$(DEPDIR)/message-decoder.Plo
+ -rm -f ./$(DEPDIR)/message-header-decode.Plo
+ -rm -f ./$(DEPDIR)/message-header-encode.Plo
+ -rm -f ./$(DEPDIR)/message-header-hash.Plo
+ -rm -f ./$(DEPDIR)/message-header-parser.Plo
+ -rm -f ./$(DEPDIR)/message-id.Plo
+ -rm -f ./$(DEPDIR)/message-parser-from-parts.Plo
+ -rm -f ./$(DEPDIR)/message-parser.Plo
+ -rm -f ./$(DEPDIR)/message-part-data.Plo
+ -rm -f ./$(DEPDIR)/message-part-serialize.Plo
+ -rm -f ./$(DEPDIR)/message-part.Plo
+ -rm -f ./$(DEPDIR)/message-search.Plo
+ -rm -f ./$(DEPDIR)/message-size.Plo
+ -rm -f ./$(DEPDIR)/message-snippet.Plo
+ -rm -f ./$(DEPDIR)/ostream-dot.Plo
+ -rm -f ./$(DEPDIR)/qp-decoder.Plo
+ -rm -f ./$(DEPDIR)/qp-encoder.Plo
+ -rm -f ./$(DEPDIR)/quoted-printable.Plo
+ -rm -f ./$(DEPDIR)/rfc2231-parser.Plo
+ -rm -f ./$(DEPDIR)/rfc822-parser.Plo
+ -rm -f ./$(DEPDIR)/test-istream-attachment.Po
+ -rm -f ./$(DEPDIR)/test-istream-binary-converter.Po
+ -rm -f ./$(DEPDIR)/test-istream-dot.Po
+ -rm -f ./$(DEPDIR)/test-istream-header-filter.Po
+ -rm -f ./$(DEPDIR)/test-istream-qp-decoder.Po
+ -rm -f ./$(DEPDIR)/test-istream-qp-encoder.Po
+ -rm -f ./$(DEPDIR)/test-mail-html2text.Po
+ -rm -f ./$(DEPDIR)/test-mail-user-hash.Po
+ -rm -f ./$(DEPDIR)/test-mbox-from.Po
+ -rm -f ./$(DEPDIR)/test-message-address.Po
+ -rm -f ./$(DEPDIR)/test-message-date.Po
+ -rm -f ./$(DEPDIR)/test-message-decoder.Po
+ -rm -f ./$(DEPDIR)/test-message-header-decode.Po
+ -rm -f ./$(DEPDIR)/test-message-header-encode.Po
+ -rm -f ./$(DEPDIR)/test-message-header-hash.Po
+ -rm -f ./$(DEPDIR)/test-message-header-parser.Po
+ -rm -f ./$(DEPDIR)/test-message-id.Po
+ -rm -f ./$(DEPDIR)/test-message-parser.Po
+ -rm -f ./$(DEPDIR)/test-message-part-serialize.Po
+ -rm -f ./$(DEPDIR)/test-message-part.Po
+ -rm -f ./$(DEPDIR)/test-message-search.Po
+ -rm -f ./$(DEPDIR)/test-message-size.Po
+ -rm -f ./$(DEPDIR)/test-message-snippet.Po
+ -rm -f ./$(DEPDIR)/test-ostream-dot.Po
+ -rm -f ./$(DEPDIR)/test-qp-decoder.Po
+ -rm -f ./$(DEPDIR)/test-qp-encoder.Po
+ -rm -f ./$(DEPDIR)/test-quoted-printable.Po
+ -rm -f ./$(DEPDIR)/test-rfc2231-parser.Po
+ -rm -f ./$(DEPDIR)/test-rfc822-parser.Po
+ -rm -f Makefile
+maintainer-clean-am: distclean-am maintainer-clean-generic
+
+mostlyclean: mostlyclean-am
+
+mostlyclean-am: mostlyclean-compile mostlyclean-generic \
+ mostlyclean-libtool
+
+pdf: pdf-am
+
+pdf-am:
+
+ps: ps-am
+
+ps-am:
+
+uninstall-am: uninstall-pkginc_libHEADERS
+
+.MAKE: check-am install-am install-strip
+
+.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \
+ check-local clean clean-generic clean-libtool \
+ clean-noinstLTLIBRARIES clean-noinstPROGRAMS cscopelist-am \
+ ctags ctags-am distclean distclean-compile distclean-generic \
+ distclean-libtool distclean-tags distdir dvi dvi-am html \
+ html-am info info-am install install-am install-data \
+ install-data-am install-dvi install-dvi-am install-exec \
+ install-exec-am install-html install-html-am install-info \
+ install-info-am install-man install-pdf install-pdf-am \
+ install-pkginc_libHEADERS install-ps install-ps-am \
+ install-strip installcheck installcheck-am installdirs \
+ maintainer-clean maintainer-clean-generic mostlyclean \
+ mostlyclean-compile mostlyclean-generic mostlyclean-libtool \
+ pdf pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-pkginc_libHEADERS
+
+.PRECIOUS: Makefile
+
+
+check-local:
+ for bin in $(test_programs); do \
+ if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+ done
+
+# Tell versions [3.59,3.63) of GNU make to not export all variables.
+# Otherwise a system limit (for SysV at least) may be exceeded.
+.NOEXPORT:
diff --git a/src/lib-mail/fuzz-message-parser.c b/src/lib-mail/fuzz-message-parser.c
new file mode 100644
index 0000000..1cb4bdc
--- /dev/null
+++ b/src/lib-mail/fuzz-message-parser.c
@@ -0,0 +1,28 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "test-common.h"
+#include "test-common.h"
+#include "fuzzer.h"
+#include "message-parser.h"
+
+FUZZ_BEGIN_DATA(const unsigned char *data, size_t size)
+{
+ struct istream *input = test_istream_create_data(data, size);
+ const struct message_parser_settings set = {
+ .hdr_flags = 0,
+ .flags = MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS,
+ .max_nested_mime_parts = 0,
+ .max_total_mime_parts = 0,
+ };
+ struct message_parser_ctx *ctx =
+ message_parser_init(pool_datastack_create(), input, &set);
+ struct message_block block ATTR_UNUSED;
+ i_zero(&block);
+ while(message_parser_parse_next_block(ctx, &block) > -1);
+ struct message_part *part ATTR_UNUSED;
+ message_parser_deinit(&ctx, &part);
+ i_stream_unref(&input);
+}
+FUZZ_END
diff --git a/src/lib-mail/html-entities.h b/src/lib-mail/html-entities.h
new file mode 100644
index 0000000..a3a9f96
--- /dev/null
+++ b/src/lib-mail/html-entities.h
@@ -0,0 +1,253 @@
+{ "quot", 0x0022 },
+{ "amp", 0x0026 },
+{ "apos", 0x0027 },
+{ "lt", 0x003C },
+{ "gt", 0x003E },
+{ "nbsp", 0x00A0 },
+{ "iexcl", 0x00A1 },
+{ "cent", 0x00A2 },
+{ "pound", 0x00A3 },
+{ "curren", 0x00A4 },
+{ "yen", 0x00A5 },
+{ "brvbar", 0x00A6 },
+{ "sect", 0x00A7 },
+{ "uml", 0x00A8 },
+{ "copy", 0x00A9 },
+{ "ordf", 0x00AA },
+{ "laquo", 0x00AB },
+{ "not", 0x00AC },
+{ "shy", 0x00AD },
+{ "reg", 0x00AE },
+{ "macr", 0x00AF },
+{ "deg", 0x00B0 },
+{ "plusmn", 0x00B1 },
+{ "sup2", 0x00B2 },
+{ "sup3", 0x00B3 },
+{ "acute", 0x00B4 },
+{ "micro", 0x00B5 },
+{ "para", 0x00B6 },
+{ "middot", 0x00B7 },
+{ "cedil", 0x00B8 },
+{ "sup1", 0x00B9 },
+{ "ordm", 0x00BA },
+{ "raquo", 0x00BB },
+{ "frac14", 0x00BC },
+{ "frac12", 0x00BD },
+{ "frac34", 0x00BE },
+{ "iquest", 0x00BF },
+{ "Agrave", 0x00C0 },
+{ "Aacute", 0x00C1 },
+{ "Acirc", 0x00C2 },
+{ "Atilde", 0x00C3 },
+{ "Auml", 0x00C4 },
+{ "Aring", 0x00C5 },
+{ "AElig", 0x00C6 },
+{ "Ccedil", 0x00C7 },
+{ "Egrave", 0x00C8 },
+{ "Eacute", 0x00C9 },
+{ "Ecirc", 0x00CA },
+{ "Euml", 0x00CB },
+{ "Igrave", 0x00CC },
+{ "Iacute", 0x00CD },
+{ "Icirc", 0x00CE },
+{ "Iuml", 0x00CF },
+{ "ETH", 0x00D0 },
+{ "Ntilde", 0x00D1 },
+{ "Ograve", 0x00D2 },
+{ "Oacute", 0x00D3 },
+{ "Ocirc", 0x00D4 },
+{ "Otilde", 0x00D5 },
+{ "Ouml", 0x00D6 },
+{ "times", 0x00D7 },
+{ "Oslash", 0x00D8 },
+{ "Ugrave", 0x00D9 },
+{ "Uacute", 0x00DA },
+{ "Ucirc", 0x00DB },
+{ "Uuml", 0x00DC },
+{ "Yacute", 0x00DD },
+{ "THORN", 0x00DE },
+{ "szlig", 0x00DF },
+{ "agrave", 0x00E0 },
+{ "aacute", 0x00E1 },
+{ "acirc", 0x00E2 },
+{ "atilde", 0x00E3 },
+{ "auml", 0x00E4 },
+{ "aring", 0x00E5 },
+{ "aelig", 0x00E6 },
+{ "ccedil", 0x00E7 },
+{ "egrave", 0x00E8 },
+{ "eacute", 0x00E9 },
+{ "ecirc", 0x00EA },
+{ "euml", 0x00EB },
+{ "igrave", 0x00EC },
+{ "iacute", 0x00ED },
+{ "icirc", 0x00EE },
+{ "iuml", 0x00EF },
+{ "eth", 0x00F0 },
+{ "ntilde", 0x00F1 },
+{ "ograve", 0x00F2 },
+{ "oacute", 0x00F3 },
+{ "ocirc", 0x00F4 },
+{ "otilde", 0x00F5 },
+{ "ouml", 0x00F6 },
+{ "divide", 0x00F7 },
+{ "oslash", 0x00F8 },
+{ "ugrave", 0x00F9 },
+{ "uacute", 0x00FA },
+{ "ucirc", 0x00FB },
+{ "uuml", 0x00FC },
+{ "yacute", 0x00FD },
+{ "thorn", 0x00FE },
+{ "yuml", 0x00FF },
+{ "OElig", 0x0152 },
+{ "oelig", 0x0153 },
+{ "Scaron", 0x0160 },
+{ "scaron", 0x0161 },
+{ "Yuml", 0x0178 },
+{ "fnof", 0x0192 },
+{ "circ", 0x02C6 },
+{ "tilde", 0x02DC },
+{ "Alpha", 0x0391 },
+{ "Beta", 0x0392 },
+{ "Gamma", 0x0393 },
+{ "Delta", 0x0394 },
+{ "Epsilon", 0x0395 },
+{ "Zeta", 0x0396 },
+{ "Eta", 0x0397 },
+{ "Theta", 0x0398 },
+{ "Iota", 0x0399 },
+{ "Kappa", 0x039A },
+{ "Lambda", 0x039B },
+{ "Mu", 0x039C },
+{ "Nu", 0x039D },
+{ "Xi", 0x039E },
+{ "Omicron", 0x039F },
+{ "Pi", 0x03A0 },
+{ "Rho", 0x03A1 },
+{ "Sigma", 0x03A3 },
+{ "Tau", 0x03A4 },
+{ "Upsilon", 0x03A5 },
+{ "Phi", 0x03A6 },
+{ "Chi", 0x03A7 },
+{ "Psi", 0x03A8 },
+{ "Omega", 0x03A9 },
+{ "alpha", 0x03B1 },
+{ "beta", 0x03B2 },
+{ "gamma", 0x03B3 },
+{ "delta", 0x03B4 },
+{ "epsilon", 0x03B5 },
+{ "zeta", 0x03B6 },
+{ "eta", 0x03B7 },
+{ "theta", 0x03B8 },
+{ "iota", 0x03B9 },
+{ "kappa", 0x03BA },
+{ "lambda", 0x03BB },
+{ "mu", 0x03BC },
+{ "nu", 0x03BD },
+{ "xi", 0x03BE },
+{ "omicron", 0x03BF },
+{ "pi", 0x03C0 },
+{ "rho", 0x03C1 },
+{ "sigmaf", 0x03C2 },
+{ "sigma", 0x03C3 },
+{ "tau", 0x03C4 },
+{ "upsilon", 0x03C5 },
+{ "phi", 0x03C6 },
+{ "chi", 0x03C7 },
+{ "psi", 0x03C8 },
+{ "omega", 0x03C9 },
+{ "thetasym", 0x03D1 },
+{ "upsih", 0x03D2 },
+{ "piv", 0x03D6 },
+{ "ensp", 0x2002 },
+{ "emsp", 0x2003 },
+{ "thinsp", 0x2009 },
+{ "zwnj", 0x200C },
+{ "zwj", 0x200D },
+{ "lrm", 0x200E },
+{ "rlm", 0x200F },
+{ "ndash", 0x2013 },
+{ "mdash", 0x2014 },
+{ "lsquo", 0x2018 },
+{ "rsquo", 0x2019 },
+{ "sbquo", 0x201A },
+{ "ldquo", 0x201C },
+{ "rdquo", 0x201D },
+{ "bdquo", 0x201E },
+{ "dagger", 0x2020 },
+{ "Dagger", 0x2021 },
+{ "bull", 0x2022 },
+{ "hellip", 0x2026 },
+{ "permil", 0x2030 },
+{ "prime", 0x2032 },
+{ "Prime", 0x2033 },
+{ "lsaquo", 0x2039 },
+{ "rsaquo", 0x203A },
+{ "oline", 0x203E },
+{ "frasl", 0x2044 },
+{ "euro", 0x20AC },
+{ "image", 0x2111 },
+{ "weierp", 0x2118 },
+{ "real", 0x211C },
+{ "trade", 0x2122 },
+{ "alefsym", 0x2135 },
+{ "larr", 0x2190 },
+{ "uarr", 0x2191 },
+{ "rarr", 0x2192 },
+{ "darr", 0x2193 },
+{ "harr", 0x2194 },
+{ "crarr", 0x21B5 },
+{ "lArr", 0x21D0 },
+{ "uArr", 0x21D1 },
+{ "rArr", 0x21D2 },
+{ "dArr", 0x21D3 },
+{ "hArr", 0x21D4 },
+{ "forall", 0x2200 },
+{ "part", 0x2202 },
+{ "exist", 0x2203 },
+{ "empty", 0x2205 },
+{ "nabla", 0x2207 },
+{ "isin", 0x2208 },
+{ "notin", 0x2209 },
+{ "ni", 0x220B },
+{ "prod", 0x220F },
+{ "sum", 0x2211 },
+{ "minus", 0x2212 },
+{ "lowast", 0x2217 },
+{ "radic", 0x221A },
+{ "prop", 0x221D },
+{ "infin", 0x221E },
+{ "ang", 0x2220 },
+{ "and", 0x2227 },
+{ "or", 0x2228 },
+{ "cap", 0x2229 },
+{ "cup", 0x222A },
+{ "int", 0x222B },
+{ "there4", 0x2234 },
+{ "sim", 0x223C },
+{ "cong", 0x2245 },
+{ "asymp", 0x2248 },
+{ "ne", 0x2260 },
+{ "equiv", 0x2261 },
+{ "le", 0x2264 },
+{ "ge", 0x2265 },
+{ "sub", 0x2282 },
+{ "sup", 0x2283 },
+{ "nsub", 0x2284 },
+{ "sube", 0x2286 },
+{ "supe", 0x2287 },
+{ "oplus", 0x2295 },
+{ "otimes", 0x2297 },
+{ "perp", 0x22A5 },
+{ "sdot", 0x22C5 },
+{ "lceil", 0x2308 },
+{ "rceil", 0x2309 },
+{ "lfloor", 0x230A },
+{ "rfloor", 0x230B },
+{ "lang", 0x27E8 },
+{ "rang", 0x27E9 },
+{ "loz", 0x25CA },
+{ "spades", 0x2660 },
+{ "clubs", 0x2663 },
+{ "hearts", 0x2665 },
+{ "diams", 0x2666 }
diff --git a/src/lib-mail/istream-attachment-connector.c b/src/lib-mail/istream-attachment-connector.c
new file mode 100644
index 0000000..cb66563
--- /dev/null
+++ b/src/lib-mail/istream-attachment-connector.c
@@ -0,0 +1,149 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "istream.h"
+#include "istream-concat.h"
+#include "istream-sized.h"
+#include "istream-base64.h"
+#include "istream-attachment-connector.h"
+
+struct istream_attachment_connector {
+ pool_t pool;
+ struct istream *base_input;
+ uoff_t base_input_offset, msg_size;
+
+ uoff_t encoded_offset;
+ ARRAY(struct istream *) streams;
+};
+
+struct istream_attachment_connector *
+istream_attachment_connector_begin(struct istream *base_input, uoff_t msg_size)
+{
+ struct istream_attachment_connector *conn;
+ pool_t pool;
+
+ pool = pool_alloconly_create("istream-attachment-connector", 1024);
+ conn = p_new(pool, struct istream_attachment_connector, 1);
+ conn->pool = pool;
+ conn->base_input = base_input;
+ conn->base_input_offset = base_input->v_offset;
+ conn->msg_size = msg_size;
+ p_array_init(&conn->streams, pool, 8);
+ i_stream_ref(conn->base_input);
+ return conn;
+}
+
+int istream_attachment_connector_add(struct istream_attachment_connector *conn,
+ struct istream *decoded_input,
+ uoff_t start_offset, uoff_t encoded_size,
+ unsigned int base64_blocks_per_line,
+ bool base64_have_crlf,
+ const char **error_r)
+{
+ struct istream *input, *input2;
+ uoff_t base_prefix_size;
+
+ if (start_offset < conn->encoded_offset) {
+ *error_r = t_strdup_printf(
+ "Attachment %s points before the previous attachment "
+ "(%"PRIuUOFF_T" < %"PRIuUOFF_T")",
+ i_stream_get_name(decoded_input),
+ start_offset, conn->encoded_offset);
+ return -1;
+ }
+ base_prefix_size = start_offset - conn->encoded_offset;
+ if (start_offset + encoded_size > conn->msg_size) {
+ *error_r = t_strdup_printf(
+ "Attachment %s points outside message "
+ "(%"PRIuUOFF_T" + %"PRIuUOFF_T" > %"PRIuUOFF_T")",
+ i_stream_get_name(decoded_input),
+ start_offset, encoded_size,
+ conn->msg_size);
+ return -1;
+ }
+
+ if (base_prefix_size > 0) {
+ /* add a part of the base message before the attachment */
+ input = i_stream_create_min_sized_range(conn->base_input,
+ conn->base_input_offset, base_prefix_size);
+ i_stream_set_name(input, t_strdup_printf("%s middle",
+ i_stream_get_name(conn->base_input)));
+ array_push_back(&conn->streams, &input);
+ conn->base_input_offset += base_prefix_size;
+ conn->encoded_offset += base_prefix_size;
+ }
+ conn->encoded_offset += encoded_size;
+
+ if (base64_blocks_per_line == 0) {
+ input = decoded_input;
+ i_stream_ref(input);
+ } else {
+ input = i_stream_create_base64_encoder(decoded_input,
+ base64_blocks_per_line*4,
+ base64_have_crlf);
+ i_stream_set_name(input, t_strdup_printf("%s[base64:%u b/l%s]",
+ i_stream_get_name(decoded_input),
+ base64_blocks_per_line,
+ base64_have_crlf ? ",crlf" : ""));
+ }
+ input2 = i_stream_create_sized(input, encoded_size);
+ array_push_back(&conn->streams, &input2);
+ i_stream_unref(&input);
+ return 0;
+}
+
+static void
+istream_attachment_connector_free(struct istream_attachment_connector *conn)
+{
+ struct istream *stream;
+
+ array_foreach_elem(&conn->streams, stream)
+ i_stream_unref(&stream);
+ i_stream_unref(&conn->base_input);
+ pool_unref(&conn->pool);
+}
+
+struct istream *
+istream_attachment_connector_finish(struct istream_attachment_connector **_conn)
+{
+ struct istream_attachment_connector *conn = *_conn;
+ struct istream **inputs, *input;
+ uoff_t trailer_size;
+
+ *_conn = NULL;
+
+ if (conn->base_input_offset != conn->msg_size) {
+ i_assert(conn->base_input_offset < conn->msg_size);
+
+ if (conn->msg_size != UOFF_T_MAX) {
+ trailer_size = conn->msg_size - conn->encoded_offset;
+ input = i_stream_create_sized_range(conn->base_input,
+ conn->base_input_offset,
+ trailer_size);
+ i_stream_set_name(input, t_strdup_printf(
+ "%s trailer", i_stream_get_name(conn->base_input)));
+ } else {
+ input = i_stream_create_range(conn->base_input,
+ conn->base_input_offset,
+ UOFF_T_MAX);
+ }
+ array_push_back(&conn->streams, &input);
+ }
+ array_append_zero(&conn->streams);
+
+ inputs = array_front_modifiable(&conn->streams);
+ input = i_stream_create_concat(inputs);
+
+ istream_attachment_connector_free(conn);
+ return input;
+}
+
+void istream_attachment_connector_abort(struct istream_attachment_connector **_conn)
+{
+ struct istream_attachment_connector *conn = *_conn;
+
+ *_conn = NULL;
+
+ istream_attachment_connector_free(conn);
+}
diff --git a/src/lib-mail/istream-attachment-connector.h b/src/lib-mail/istream-attachment-connector.h
new file mode 100644
index 0000000..df829cd
--- /dev/null
+++ b/src/lib-mail/istream-attachment-connector.h
@@ -0,0 +1,28 @@
+#ifndef ISTREAM_ATTACHMENT_CONNECTOR_H
+#define ISTREAM_ATTACHMENT_CONNECTOR_H
+
+/* Start building a message stream. The base_input contains the message
+ without attachments. The final stream must be exactly msg_size bytes.
+ If the original msg_size isn't known, it can be set to UOFF_T_MAX. */
+struct istream_attachment_connector *
+istream_attachment_connector_begin(struct istream *base_input, uoff_t msg_size);
+
+/* Add the given input stream as attachment. The attachment starts at the given
+ start_offset in the (original) message. If base64_blocks_per_line is
+ non-zero, the input is base64-encoded with the given settings. The
+ (resulting base64-encoded) input must have exactly encoded_size bytes.
+
+ Returns 0 if the input was ok, -1 if we've already reached msg_size or
+ attachment offsets/sizes aren't valid. */
+int istream_attachment_connector_add(struct istream_attachment_connector *conn,
+ struct istream *decoded_input,
+ uoff_t start_offset, uoff_t encoded_size,
+ unsigned int base64_blocks_per_line,
+ bool base64_have_crlf,
+ const char **error_r);
+
+struct istream *
+istream_attachment_connector_finish(struct istream_attachment_connector **conn);
+void istream_attachment_connector_abort(struct istream_attachment_connector **conn);
+
+#endif
diff --git a/src/lib-mail/istream-attachment-extractor.c b/src/lib-mail/istream-attachment-extractor.c
new file mode 100644
index 0000000..7d4ac01
--- /dev/null
+++ b/src/lib-mail/istream-attachment-extractor.c
@@ -0,0 +1,740 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "ostream.h"
+#include "base64.h"
+#include "buffer.h"
+#include "str.h"
+#include "hash-format.h"
+#include "rfc822-parser.h"
+#include "message-parser.h"
+#include "istream-attachment-extractor.h"
+
+#define BASE64_ATTACHMENT_MAX_EXTRA_BYTES 1024
+
+enum mail_attachment_state {
+ MAIL_ATTACHMENT_STATE_NO,
+ MAIL_ATTACHMENT_STATE_MAYBE,
+ MAIL_ATTACHMENT_STATE_YES
+};
+
+enum base64_state {
+ BASE64_STATE_0 = 0,
+ BASE64_STATE_1,
+ BASE64_STATE_2,
+ BASE64_STATE_3,
+ BASE64_STATE_CR,
+ BASE64_STATE_EOB,
+ BASE64_STATE_EOM
+};
+
+struct attachment_istream_part {
+ char *content_type, *content_disposition;
+ enum mail_attachment_state state;
+ /* start offset of the message part in the original input stream */
+ uoff_t start_offset;
+
+ /* for saving attachments base64-decoded: */
+ enum base64_state base64_state;
+ unsigned int base64_line_blocks, cur_base64_blocks;
+ uoff_t base64_bytes;
+ bool base64_have_crlf; /* CRLF linefeeds */
+ bool base64_failed;
+
+ int temp_fd;
+ struct ostream *temp_output;
+ buffer_t *part_buf;
+};
+
+struct attachment_istream {
+ struct istream_private istream;
+ pool_t pool;
+
+ struct istream_attachment_settings set;
+ void *context;
+
+ struct message_parser_ctx *parser;
+ struct message_part *cur_part;
+ struct attachment_istream_part part;
+
+ bool retry_read;
+};
+
+static void stream_add_data(struct attachment_istream *astream,
+ const void *data, size_t size)
+{
+ if (size > 0) {
+ memcpy(i_stream_alloc(&astream->istream, size), data, size);
+ astream->istream.pos += size;
+ }
+}
+
+static void parse_content_type(struct attachment_istream *astream,
+ const struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *content_type;
+
+ if (astream->part.content_type != NULL)
+ return;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ T_BEGIN {
+ content_type = t_str_new(64);
+ (void)rfc822_parse_content_type(&parser, content_type);
+ astream->part.content_type = i_strdup(str_c(content_type));
+ } T_END;
+ rfc822_parser_deinit(&parser);
+}
+
+static void
+parse_content_disposition(struct attachment_istream *astream,
+ const struct message_header_line *hdr)
+{
+ /* just pass it without parsing to is_attachment() callback */
+ i_free(astream->part.content_disposition);
+ astream->part.content_disposition =
+ i_strndup(hdr->full_value, hdr->full_value_len);
+}
+
+static void astream_parse_header(struct attachment_istream *astream,
+ struct message_header_line *hdr)
+{
+ if (!hdr->continued) {
+ stream_add_data(astream, hdr->name, hdr->name_len);
+ stream_add_data(astream, hdr->middle, hdr->middle_len);
+ }
+ stream_add_data(astream, hdr->value, hdr->value_len);
+ if (!hdr->no_newline) {
+ if (hdr->crlf_newline)
+ stream_add_data(astream, "\r\n", 2);
+ else
+ stream_add_data(astream, "\n", 1);
+ }
+
+ if (hdr->continues) {
+ hdr->use_full_value = TRUE;
+ return;
+ }
+
+ if (strcasecmp(hdr->name, "Content-Type") == 0)
+ parse_content_type(astream, hdr);
+ else if (strcasecmp(hdr->name, "Content-Disposition") == 0)
+ parse_content_disposition(astream, hdr);
+}
+
+static bool astream_want_attachment(struct attachment_istream *astream,
+ struct message_part *part)
+{
+ struct istream_attachment_header ahdr;
+
+ if ((part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0) {
+ /* multiparts may contain attachments as children,
+ but they're never themselves */
+ return FALSE;
+ }
+ if (astream->set.want_attachment == NULL)
+ return TRUE;
+
+ i_zero(&ahdr);
+ ahdr.part = part;
+ ahdr.content_type = astream->part.content_type;
+ ahdr.content_disposition = astream->part.content_disposition;
+ return astream->set.want_attachment(&ahdr, astream->context);
+}
+
+static int astream_base64_decode_lf(struct attachment_istream_part *part)
+{
+ if (part->base64_have_crlf && part->base64_state != BASE64_STATE_CR) {
+ /* mixed LF vs CRLFs */
+ return -1;
+ }
+ part->base64_state = BASE64_STATE_0;
+ if (part->cur_base64_blocks < part->base64_line_blocks) {
+ /* last line */
+ part->base64_state = BASE64_STATE_EOM;
+ return 0;
+ } else if (part->base64_line_blocks == 0) {
+ /* first line */
+ if (part->cur_base64_blocks == 0)
+ return -1;
+ part->base64_line_blocks = part->cur_base64_blocks;
+ } else if (part->cur_base64_blocks == part->base64_line_blocks) {
+ /* line is ok */
+ } else {
+ return -1;
+ }
+ part->cur_base64_blocks = 0;
+ return 1;
+}
+
+static int
+astream_try_base64_decode_char(struct attachment_istream_part *part,
+ size_t pos, char chr)
+{
+ switch (part->base64_state) {
+ case BASE64_STATE_0:
+ if (base64_is_valid_char(chr))
+ part->base64_state++;
+ else if (chr == '\r')
+ part->base64_state = BASE64_STATE_CR;
+ else if (chr == '\n') {
+ return astream_base64_decode_lf(part);
+ } else {
+ return -1;
+ }
+ break;
+ case BASE64_STATE_1:
+ if (!base64_is_valid_char(chr))
+ return -1;
+ part->base64_state++;
+ break;
+ case BASE64_STATE_2:
+ if (base64_is_valid_char(chr))
+ part->base64_state++;
+ else if (chr == '=')
+ part->base64_state = BASE64_STATE_EOB;
+ else
+ return -1;
+ break;
+ case BASE64_STATE_3:
+ part->base64_bytes = part->temp_output->offset + pos + 1;
+ if (base64_is_valid_char(chr)) {
+ part->base64_state = BASE64_STATE_0;
+ part->cur_base64_blocks++;
+ } else if (chr == '=') {
+ part->base64_state = BASE64_STATE_EOM;
+ part->cur_base64_blocks++;
+
+ if (part->cur_base64_blocks > part->base64_line_blocks &&
+ part->base64_line_blocks > 0) {
+ /* too many blocks */
+ return -1;
+ }
+ return 0;
+ } else {
+ return -1;
+ }
+ break;
+ case BASE64_STATE_CR:
+ if (chr != '\n')
+ return -1;
+ if (!part->base64_have_crlf) {
+ if (part->base64_line_blocks != 0) {
+ /* mixed LF vs CRLFs */
+ return -1;
+ }
+ part->base64_have_crlf = TRUE;
+ }
+ return astream_base64_decode_lf(part);
+ case BASE64_STATE_EOB:
+ if (chr != '=')
+ return -1;
+
+ part->base64_bytes = part->temp_output->offset + pos + 1;
+ part->base64_state = BASE64_STATE_EOM;
+ part->cur_base64_blocks++;
+
+ if (part->cur_base64_blocks > part->base64_line_blocks &&
+ part->base64_line_blocks > 0) {
+ /* too many blocks */
+ return -1;
+ }
+ return 0;
+ case BASE64_STATE_EOM:
+ i_unreached();
+ }
+ return 1;
+}
+
+static void
+astream_try_base64_decode(struct attachment_istream_part *part,
+ const unsigned char *data, size_t size)
+{
+ size_t i;
+ int ret;
+
+ if (part->base64_failed || part->base64_state == BASE64_STATE_EOM)
+ return;
+
+ for (i = 0; i < size; i++) {
+ ret = astream_try_base64_decode_char(part, i, (char)data[i]);
+ if (ret <= 0) {
+ if (ret < 0)
+ part->base64_failed = TRUE;
+ break;
+ }
+ }
+}
+
+static int astream_open_output(struct attachment_istream *astream)
+{
+ int fd;
+
+ i_assert(astream->part.temp_fd == -1);
+
+ fd = astream->set.open_temp_fd(astream->context);
+ if (fd == -1)
+ return -1;
+
+ astream->part.temp_fd = fd;
+ astream->part.temp_output = o_stream_create_fd(fd, 0);
+ o_stream_cork(astream->part.temp_output);
+ return 0;
+}
+
+static void astream_add_body(struct attachment_istream *astream,
+ const struct message_block *block)
+{
+ struct attachment_istream_part *part = &astream->part;
+ buffer_t *part_buf;
+ size_t new_size;
+
+ switch (part->state) {
+ case MAIL_ATTACHMENT_STATE_NO:
+ stream_add_data(astream, block->data, block->size);
+ break;
+ case MAIL_ATTACHMENT_STATE_MAYBE:
+ /* we'll write data to in-memory buffer until we reach
+ attachment min_size */
+ if (part->part_buf == NULL) {
+ part->part_buf =
+ buffer_create_dynamic(default_pool,
+ astream->set.min_size);
+ }
+ part_buf = part->part_buf;
+ new_size = part_buf->used + block->size;
+ if (new_size < astream->set.min_size) {
+ buffer_append(part_buf, block->data, block->size);
+ break;
+ }
+ /* attachment is large enough. we'll first copy the buffered
+ data from memory to temp file */
+ if (astream_open_output(astream) < 0) {
+ /* failed, fallback to just saving it inline */
+ part->state = MAIL_ATTACHMENT_STATE_NO;
+ stream_add_data(astream, part_buf->data, part_buf->used);
+ stream_add_data(astream, block->data, block->size);
+ break;
+ }
+ part->state = MAIL_ATTACHMENT_STATE_YES;
+ astream_try_base64_decode(part, part_buf->data, part_buf->used);
+ hash_format_loop(astream->set.hash_format,
+ part_buf->data, part_buf->used);
+ o_stream_nsend(part->temp_output,
+ part_buf->data, part_buf->used);
+ buffer_set_used_size(part_buf, 0);
+ /* fall through - write the new data to temp file */
+ case MAIL_ATTACHMENT_STATE_YES:
+ astream_try_base64_decode(part, block->data, block->size);
+ hash_format_loop(astream->set.hash_format,
+ block->data, block->size);
+ o_stream_nsend(part->temp_output, block->data, block->size);
+ break;
+ }
+}
+
+static int astream_decode_base64(struct attachment_istream *astream,
+ buffer_t **extra_buf_r)
+{
+ struct attachment_istream_part *part = &astream->part;
+ struct base64_decoder b64dec;
+ struct istream *input, *base64_input;
+ struct ostream *output;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+ buffer_t *buf;
+ int outfd;
+ bool failed = FALSE;
+
+ *extra_buf_r = NULL;
+
+ if (part->base64_bytes < astream->set.min_size ||
+ part->temp_output->offset > part->base64_bytes +
+ BASE64_ATTACHMENT_MAX_EXTRA_BYTES) {
+ /* only a small part of the MIME part is base64-encoded. */
+ return -1;
+ }
+
+ if (part->base64_line_blocks == 0) {
+ /* only one line of base64 */
+ part->base64_line_blocks = part->cur_base64_blocks;
+ i_assert(part->base64_line_blocks > 0);
+ }
+
+ /* decode base64 data and write it to another temp file */
+ outfd = astream->set.open_temp_fd(astream->context);
+ if (outfd == -1)
+ return -1;
+
+ buf = buffer_create_dynamic(default_pool, 1024);
+ input = i_stream_create_fd(part->temp_fd, IO_BLOCK_SIZE);
+ base64_input = i_stream_create_limit(input, part->base64_bytes);
+ output = o_stream_create_fd_file(outfd, 0, FALSE);
+ o_stream_cork(output);
+
+ base64_decode_init(&b64dec, &base64_scheme, 0);
+ hash_format_reset(astream->set.hash_format);
+ size_t bytes_needed = 1;
+ while ((ret = i_stream_read_bytes(base64_input, &data, &size,
+ bytes_needed)) > 0) {
+ buffer_set_used_size(buf, 0);
+ if (base64_decode_more(&b64dec, data, size, &size, buf) < 0) {
+ i_error("istream-attachment: BUG: "
+ "Attachment base64 data unexpectedly broke");
+ failed = TRUE;
+ break;
+ }
+ i_stream_skip(base64_input, size);
+ o_stream_nsend(output, buf->data, buf->used);
+ hash_format_loop(astream->set.hash_format,
+ buf->data, buf->used);
+ bytes_needed = i_stream_get_data_size(base64_input) + 1;
+ }
+ if (ret != -1) {
+ i_assert(failed);
+ } else if (base64_input->stream_errno != 0) {
+ i_error("istream-attachment: read(%s) failed: %s",
+ i_stream_get_name(base64_input),
+ i_stream_get_error(base64_input));
+ failed = TRUE;
+ }
+ if (base64_decode_finish(&b64dec) < 0) {
+ i_error("istream-attachment: BUG: "
+ "Attachment base64 data unexpectedly broke");
+ failed = TRUE;
+ }
+ if (o_stream_finish(output) < 0) {
+ i_error("istream-attachment: write(%s) failed: %s",
+ o_stream_get_name(output), o_stream_get_error(output));
+ failed = TRUE;
+ }
+
+ buffer_free(&buf);
+ i_stream_unref(&base64_input);
+ o_stream_unref(&output);
+
+ if (input->v_offset != part->temp_output->offset && !failed) {
+ /* write the rest of the data to the message stream */
+ *extra_buf_r = buffer_create_dynamic(default_pool, 1024);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ buffer_append(*extra_buf_r, data, size);
+ i_stream_skip(input, size);
+ }
+ i_assert(ret == -1);
+ if (input->stream_errno != 0) {
+ i_error("istream-attachment: read(%s) failed: %s",
+ i_stream_get_name(input),
+ i_stream_get_error(input));
+ failed = TRUE;
+ }
+ }
+ i_stream_unref(&input);
+
+ if (failed) {
+ i_close_fd(&outfd);
+ return -1;
+ }
+
+ /* successfully wrote it. switch to using it. */
+ o_stream_destroy(&part->temp_output);
+ i_close_fd(&part->temp_fd);
+ part->temp_fd = outfd;
+ return 0;
+}
+
+static int
+astream_part_finish(struct attachment_istream *astream, const char **error_r)
+{
+ struct attachment_istream_part *part = &astream->part;
+ struct istream_attachment_info info;
+ struct istream *input;
+ struct ostream *output;
+ string_t *digest_str;
+ buffer_t *extra_buf = NULL;
+ const unsigned char *data;
+ size_t size;
+ int ret = 0;
+
+ if (o_stream_finish(part->temp_output) < 0) {
+ *error_r = t_strdup_printf("write(%s) failed: %s",
+ o_stream_get_name(part->temp_output),
+ o_stream_get_error(part->temp_output));
+ return -1;
+ }
+
+ i_zero(&info);
+ info.start_offset = astream->part.start_offset;
+ /* base64_bytes contains how many valid base64 bytes there are so far.
+ if the base64 ends properly, it'll specify how much of the MIME part
+ is saved as an attachment. the rest of the data (typically
+ linefeeds) is added back to main stream */
+ info.encoded_size = part->base64_bytes;
+ /* get the hash before base64-decoder resets it */
+ digest_str = t_str_new(128);
+ hash_format_write(astream->set.hash_format, digest_str);
+ info.hash = str_c(digest_str);
+
+ /* if it looks like we can decode base64 without any data loss,
+ do it and write the decoded data to another temp file. */
+ if (!part->base64_failed) {
+ if (part->base64_state == BASE64_STATE_0 &&
+ part->base64_bytes > 0) {
+ /* there is no trailing LF or '=' characters,
+ but it's not completely empty */
+ part->base64_state = BASE64_STATE_EOM;
+ }
+ if (part->base64_state == BASE64_STATE_EOM) {
+ /* base64 data looks ok. */
+ if (astream_decode_base64(astream, &extra_buf) < 0)
+ part->base64_failed = TRUE;
+ } else {
+ part->base64_failed = TRUE;
+ }
+ }
+
+ /* open attachment output file */
+ info.part = astream->cur_part;
+ if (!part->base64_failed) {
+ info.base64_blocks_per_line = part->base64_line_blocks;
+ info.base64_have_crlf = part->base64_have_crlf;
+ /* base64-decoder updated the hash, use it */
+ str_truncate(digest_str, 0);
+ hash_format_write(astream->set.hash_format, digest_str);
+ info.hash = str_c(digest_str);
+ } else {
+ /* couldn't decode base64, so write the entire MIME part
+ as attachment */
+ info.encoded_size = part->temp_output->offset;
+ }
+ if (astream->set.open_attachment_ostream(&info, &output, error_r,
+ astream->context) < 0) {
+ buffer_free(&extra_buf);
+ return -1;
+ }
+
+ /* copy data to attachment from temp file */
+ input = i_stream_create_fd(part->temp_fd, IO_BLOCK_SIZE);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ o_stream_nsend(output, data, size);
+ i_stream_skip(input, size);
+ }
+
+ if (input->stream_errno != 0) {
+ *error_r = t_strdup_printf("read(%s) failed: %s",
+ i_stream_get_name(input), i_stream_get_error(input));
+ ret = -1;
+ }
+ i_stream_destroy(&input);
+
+ if (astream->set.close_attachment_ostream(output, ret == 0, error_r,
+ astream->context) < 0)
+ ret = -1;
+ if (ret == 0 && extra_buf != NULL)
+ stream_add_data(astream, extra_buf->data, extra_buf->used);
+ buffer_free(&extra_buf);
+ return ret;
+}
+
+static void astream_part_reset(struct attachment_istream *astream)
+{
+ struct attachment_istream_part *part = &astream->part;
+
+ o_stream_destroy(&part->temp_output);
+ i_close_fd(&part->temp_fd);
+
+ i_free_and_null(part->content_type);
+ i_free_and_null(part->content_disposition);
+ buffer_free(&part->part_buf);
+
+ i_zero(part);
+ part->temp_fd = -1;
+ hash_format_reset(astream->set.hash_format);
+}
+
+static int
+astream_end_of_part(struct attachment_istream *astream, const char **error_r)
+{
+ struct attachment_istream_part *part = &astream->part;
+ size_t old_size;
+ int ret = 0;
+
+ /* MIME part changed. we're now parsing the end of a boundary,
+ possibly followed by message epilogue */
+ switch (part->state) {
+ case MAIL_ATTACHMENT_STATE_NO:
+ break;
+ case MAIL_ATTACHMENT_STATE_MAYBE:
+ /* MIME part wasn't large enough to be an attachment */
+ if (part->part_buf != NULL) {
+ stream_add_data(astream, part->part_buf->data,
+ part->part_buf->used);
+ ret = part->part_buf->used > 0 ? 1 : 0;
+ }
+ break;
+ case MAIL_ATTACHMENT_STATE_YES:
+ old_size = astream->istream.pos - astream->istream.skip;
+ if (astream_part_finish(astream, error_r) < 0)
+ ret = -1;
+ else {
+ /* finished base64 may have added a few more trailing
+ bytes to the stream */
+ ret = astream->istream.pos -
+ astream->istream.skip - old_size;
+ }
+ break;
+ }
+ part->state = MAIL_ATTACHMENT_STATE_NO;
+ astream_part_reset(astream);
+ return ret;
+}
+
+static int astream_read_next(struct attachment_istream *astream, bool *retry_r)
+{
+ struct istream_private *stream = &astream->istream;
+ struct message_block block;
+ size_t old_size, new_size;
+ const char *error;
+ int ret;
+
+ *retry_r = FALSE;
+
+ if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
+ return -2;
+
+ old_size = stream->pos - stream->skip;
+ switch (message_parser_parse_next_block(astream->parser, &block)) {
+ case -1:
+ /* done / error */
+ ret = astream_end_of_part(astream, &error);
+ if (ret > 0) {
+ /* final data */
+ new_size = stream->pos - stream->skip;
+ return new_size - old_size;
+ }
+ stream->istream.eof = TRUE;
+ stream->istream.stream_errno = stream->parent->stream_errno;
+
+ if (ret < 0) {
+ io_stream_set_error(&stream->iostream, "%s", error);
+ stream->istream.stream_errno = EIO;
+ }
+ astream->cur_part = NULL;
+ return -1;
+ case 0:
+ /* need more data */
+ return 0;
+ default:
+ break;
+ }
+
+ if (block.part != astream->cur_part && astream->cur_part != NULL) {
+ /* end of a MIME part */
+ if (astream_end_of_part(astream, &error) < 0) {
+ io_stream_set_error(&stream->iostream, "%s", error);
+ stream->istream.stream_errno = EIO;
+ return -1;
+ }
+ }
+ astream->cur_part = block.part;
+
+ if (block.hdr != NULL) {
+ /* parsing a header */
+ astream_parse_header(astream, block.hdr);
+ } else if (block.size == 0) {
+ /* end of headers */
+ if (astream_want_attachment(astream, block.part)) {
+ astream->part.state = MAIL_ATTACHMENT_STATE_MAYBE;
+ astream->part.start_offset = stream->parent->v_offset;
+ }
+ } else {
+ astream_add_body(astream, &block);
+ }
+ new_size = stream->pos - stream->skip;
+ *retry_r = new_size == old_size;
+ return new_size - old_size;
+}
+
+static ssize_t
+i_stream_attachment_extractor_read(struct istream_private *stream)
+{
+ struct attachment_istream *astream =
+ (struct attachment_istream *)stream;
+ bool retry;
+ ssize_t ret;
+
+ do {
+ ret = astream_read_next(astream, &retry);
+ } while (retry && astream->set.drain_parent_input);
+
+ astream->retry_read = retry;
+ return ret;
+}
+
+static void i_stream_attachment_extractor_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct attachment_istream *astream =
+ (struct attachment_istream *)stream;
+ struct message_part *parts;
+
+ if (astream->parser != NULL) {
+ message_parser_deinit(&astream->parser, &parts);
+ }
+ hash_format_deinit_free(&astream->set.hash_format);
+ pool_unref(&astream->pool);
+ if (close_parent)
+ i_stream_close(astream->istream.parent);
+}
+
+struct istream *
+i_stream_create_attachment_extractor(struct istream *input,
+ struct istream_attachment_settings *set,
+ void *context)
+{
+ const struct message_parser_settings parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS |
+ MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES,
+ };
+ struct attachment_istream *astream;
+
+ i_assert(set->min_size > 0);
+ i_assert(set->hash_format != NULL);
+ i_assert(set->open_attachment_ostream != NULL);
+ i_assert(set->close_attachment_ostream != NULL);
+
+ astream = i_new(struct attachment_istream, 1);
+ astream->part.temp_fd = -1;
+ astream->set = *set;
+ astream->context = context;
+ astream->retry_read = TRUE;
+
+ /* make sure the caller doesn't try to double-free this */
+ set->hash_format = NULL;
+
+ astream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ astream->istream.read = i_stream_attachment_extractor_read;
+ astream->istream.iostream.close = i_stream_attachment_extractor_close;
+
+ astream->istream.istream.readable_fd = FALSE;
+ astream->istream.istream.blocking = input->blocking;
+ astream->istream.istream.seekable = FALSE;
+
+ astream->pool = pool_alloconly_create("istream attachment", 1024);
+ astream->parser = message_parser_init(astream->pool, input, &parser_set);
+ return i_stream_create(&astream->istream, input,
+ i_stream_get_fd(input), 0);
+}
+
+bool i_stream_attachment_extractor_can_retry(struct istream *input)
+{
+ struct attachment_istream *astream =
+ (struct attachment_istream *)input->real_stream;
+
+ return astream->retry_read;
+}
diff --git a/src/lib-mail/istream-attachment-extractor.h b/src/lib-mail/istream-attachment-extractor.h
new file mode 100644
index 0000000..151566e
--- /dev/null
+++ b/src/lib-mail/istream-attachment-extractor.h
@@ -0,0 +1,62 @@
+#ifndef ISTREAM_ATTACHMENT_H
+#define ISTREAM_ATTACHMENT_H
+
+struct istream_attachment_header {
+ struct message_part *part;
+ const char *content_type, *content_disposition;
+};
+
+struct istream_attachment_info {
+ const char *hash;
+ /* offset within input stream where the attachment starts */
+ uoff_t start_offset;
+ /* original (base64-encoded) size of the attachment */
+ uoff_t encoded_size;
+
+ unsigned int base64_blocks_per_line;
+ bool base64_have_crlf;
+
+ const struct message_part *part;
+};
+
+struct istream_attachment_settings {
+ /* Minimum size of of a MIME part to be saved separately. */
+ uoff_t min_size;
+ /* Format to use when calculating attachment's hash. */
+ struct hash_format *hash_format;
+ /* Set this to TRUE if parent stream can be read from as long as
+ wanted. This is useful when parsing attachments, which the extractor
+ hides from read() output, so they would return a lot of 0.
+ On the other hand if you have a tee-istream, it's not a good idea
+ to let it get to "buffer full" state. */
+ bool drain_parent_input;
+
+ /* Returns TRUE if message part is wanted to be stored as separate
+ attachment. If NULL, assume we want the attachment. */
+ bool (*want_attachment)(const struct istream_attachment_header *hdr,
+ void *context);
+ /* Create a temporary file. */
+ int (*open_temp_fd)(void *context);
+ /* Create output stream for attachment */
+ int (*open_attachment_ostream)(struct istream_attachment_info *info,
+ struct ostream **output_r,
+ const char **error_r, void *context);
+ /* Finish output stream. If success==FALSE, *error contains the error
+ and the error shouldn't be replaced (other than maybe enhanced).
+ Otherwise, if close_attachment_ostream() fails and returns -1, it
+ should also set *error. */
+ int (*close_attachment_ostream)(struct ostream *output, bool success,
+ const char **error, void *context);
+};
+
+struct istream *
+i_stream_create_attachment_extractor(struct istream *input,
+ struct istream_attachment_settings *set,
+ void *context) ATTR_NULL(3);
+
+/* Returns TRUE if the last read returned 0 only because
+ drain_parent_input=FALSE and we didn't have anything to return, but
+ retrying a read from parent stream could give something the next time. */
+bool i_stream_attachment_extractor_can_retry(struct istream *input);
+
+#endif
diff --git a/src/lib-mail/istream-binary-converter.c b/src/lib-mail/istream-binary-converter.c
new file mode 100644
index 0000000..856b854
--- /dev/null
+++ b/src/lib-mail/istream-binary-converter.c
@@ -0,0 +1,309 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "base64.h"
+#include "istream-private.h"
+#include "message-parser.h"
+#include "istream-binary-converter.h"
+
+#define BASE64_BLOCK_INPUT_SIZE 3
+#define BASE64_BLOCK_SIZE 4
+#define BASE64_BLOCKS_PER_LINE (76/BASE64_BLOCK_SIZE)
+#define MAX_HDR_BUFFER_SIZE (1024*32)
+
+struct binary_converter_istream {
+ struct istream_private istream;
+
+ pool_t pool;
+ struct message_parser_ctx *parser;
+ struct message_part *convert_part;
+ char base64_delayed[BASE64_BLOCK_INPUT_SIZE-1];
+ unsigned int base64_delayed_len;
+ unsigned int base64_block_pos;
+
+ buffer_t *hdr_buf;
+ size_t cte_header_len;
+ bool content_type_seen:1;
+};
+
+static void stream_add_data(struct binary_converter_istream *bstream,
+ const void *data, size_t size);
+
+static bool part_can_convert(const struct message_part *part)
+{
+ /* some MUAs use "c-t-e: binary" for multiparts.
+ we don't want to convert them. */
+ return (part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0;
+}
+
+static void
+stream_finish_convert_decision(struct binary_converter_istream *bstream)
+{
+ buffer_t *buf = bstream->hdr_buf;
+ const unsigned char *data;
+
+ i_assert(bstream->convert_part != NULL);
+
+ bstream->hdr_buf = NULL;
+ if (!part_can_convert(bstream->convert_part)) {
+ bstream->convert_part = NULL;
+ stream_add_data(bstream, buf->data, buf->used);
+ } else {
+ stream_add_data(bstream,
+ "Content-Transfer-Encoding: base64\r\n", 35);
+
+ data = CONST_PTR_OFFSET(buf->data, bstream->cte_header_len);
+ stream_add_data(bstream, data,
+ buf->used - bstream->cte_header_len);
+ }
+ buffer_free(&buf);
+}
+
+static void stream_add_data(struct binary_converter_istream *bstream,
+ const void *data, size_t size)
+{
+ if (size == 0)
+ return;
+
+ if (bstream->hdr_buf != NULL) {
+ if (bstream->hdr_buf->used + size <= MAX_HDR_BUFFER_SIZE) {
+ buffer_append(bstream->hdr_buf, data, size);
+ return;
+ }
+ /* buffer is getting too large. just finish the decision. */
+ stream_finish_convert_decision(bstream);
+ }
+
+ memcpy(i_stream_alloc(&bstream->istream, size), data, size);
+ bstream->istream.pos += size;
+}
+
+static void stream_encode_base64(struct binary_converter_istream *bstream,
+ const void *_data, size_t size)
+{
+ struct istream_private *stream = &bstream->istream;
+ const unsigned char *data = _data;
+ buffer_t buf;
+ void *dest;
+ size_t encode_size, max_encoded_size;
+ unsigned char base64_block[BASE64_BLOCK_INPUT_SIZE];
+ unsigned int base64_block_len, missing_len, encode_blocks;
+
+ if (bstream->base64_delayed_len > 0) {
+ if (bstream->base64_delayed_len == 1 && size == 1) {
+ bstream->base64_delayed[1] = data[0];
+ bstream->base64_delayed_len++;
+ return;
+ }
+ memcpy(base64_block, bstream->base64_delayed,
+ bstream->base64_delayed_len);
+ base64_block_len = bstream->base64_delayed_len;
+ if (size == 0) {
+ /* finish base64 */
+ } else {
+ missing_len = BASE64_BLOCK_INPUT_SIZE - base64_block_len;
+ i_assert(size >= missing_len);
+ memcpy(base64_block + base64_block_len,
+ data, missing_len);
+ data += missing_len;
+ size -= missing_len;
+ base64_block_len = BASE64_BLOCK_INPUT_SIZE;
+ }
+
+ if (bstream->base64_block_pos == BASE64_BLOCKS_PER_LINE) {
+ memcpy(i_stream_alloc(stream, 2), "\r\n", 2);
+ stream->pos += 2;
+ bstream->base64_block_pos = 0;
+ }
+
+ dest = i_stream_alloc(stream, BASE64_BLOCK_SIZE);
+ buffer_create_from_data(&buf, dest, BASE64_BLOCK_SIZE);
+ base64_encode(base64_block, base64_block_len, &buf);
+ stream->pos += buf.used;
+ bstream->base64_block_pos++;
+ bstream->base64_delayed_len = 0;
+ }
+
+ while (size >= BASE64_BLOCK_INPUT_SIZE) {
+ if (bstream->base64_block_pos == BASE64_BLOCKS_PER_LINE) {
+ memcpy(i_stream_alloc(stream, 2), "\r\n", 2);
+ stream->pos += 2;
+ bstream->base64_block_pos = 0;
+ }
+
+ /* try to encode one full line of base64 blocks */
+ encode_size = I_MIN(size, BASE64_BLOCKS_PER_LINE*BASE64_BLOCK_SIZE);
+ if (encode_size % BASE64_BLOCK_INPUT_SIZE != 0)
+ encode_size -= encode_size % BASE64_BLOCK_INPUT_SIZE;
+ encode_blocks = encode_size/BASE64_BLOCK_INPUT_SIZE;
+ if (bstream->base64_block_pos + encode_blocks > BASE64_BLOCKS_PER_LINE) {
+ encode_blocks = BASE64_BLOCKS_PER_LINE -
+ bstream->base64_block_pos;
+ encode_size = encode_blocks * BASE64_BLOCK_INPUT_SIZE;
+ }
+
+ max_encoded_size = MAX_BASE64_ENCODED_SIZE(encode_size);
+ dest = i_stream_alloc(stream, max_encoded_size);
+ buffer_create_from_data(&buf, dest, max_encoded_size);
+ base64_encode(data, encode_size, &buf);
+ stream->pos += buf.used;
+ bstream->base64_block_pos += encode_blocks;
+
+ data += encode_size;
+ size -= encode_size;
+ }
+ if (size > 0) {
+ /* encode these when more data is available */
+ i_assert(size < BASE64_BLOCK_INPUT_SIZE);
+ memcpy(bstream->base64_delayed, data, size);
+ bstream->base64_delayed_len = size;
+ }
+}
+
+static void stream_add_hdr(struct binary_converter_istream *bstream,
+ const struct message_header_line *hdr)
+{
+ if (!hdr->continued) {
+ stream_add_data(bstream, hdr->name, hdr->name_len);
+ stream_add_data(bstream, hdr->middle, hdr->middle_len);
+ }
+
+ stream_add_data(bstream, hdr->value, hdr->value_len);
+ if (!hdr->no_newline)
+ stream_add_data(bstream, "\r\n", 2);
+}
+
+static ssize_t i_stream_binary_converter_read(struct istream_private *stream)
+{
+ /* @UNSAFE */
+ struct binary_converter_istream *bstream =
+ (struct binary_converter_istream *)stream;
+ struct message_block block;
+ size_t old_size, new_size;
+
+ if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
+ return -2;
+ old_size = stream->pos - stream->skip;
+
+ switch (message_parser_parse_next_block(bstream->parser, &block)) {
+ case -1:
+ /* done / error */
+ if (bstream->convert_part != NULL &&
+ bstream->base64_delayed_len > 0) {
+ /* flush any pending base64 output */
+ stream_encode_base64(bstream, "", 0);
+ new_size = stream->pos - stream->skip;
+ i_assert(old_size != new_size);
+ return new_size - old_size;
+ }
+ stream->istream.eof = TRUE;
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return -1;
+ case 0:
+ /* need more data */
+ return 0;
+ default:
+ break;
+ }
+
+ if (block.part != bstream->convert_part &&
+ bstream->convert_part != NULL) {
+ /* end of base64 encoded part */
+ stream_encode_base64(bstream, "", 0);
+ }
+
+ if (block.hdr != NULL) {
+ /* parsing a header */
+ if (strcasecmp(block.hdr->name, "Content-Type") == 0)
+ bstream->content_type_seen = TRUE;
+
+ if (strcasecmp(block.hdr->name, "Content-Transfer-Encoding") == 0 &&
+ !block.hdr->continued && !block.hdr->continues &&
+ block.hdr->value_len == 6 &&
+ i_memcasecmp(block.hdr->value, "binary", 6) == 0 &&
+ part_can_convert(block.part) &&
+ bstream->convert_part != block.part) {
+ /* looks like we want to convert this body part to
+ base64, but if we haven't seen Content-Type yet
+ delay the decision until we've read the rest of
+ the header */
+ i_assert(block.part != NULL);
+ bstream->convert_part = block.part;
+ bstream->base64_block_pos = 0;
+ if (!bstream->content_type_seen) {
+ i_assert(bstream->hdr_buf == NULL);
+ bstream->hdr_buf = buffer_create_dynamic(default_pool, 512);
+ stream_add_hdr(bstream, block.hdr);
+ bstream->cte_header_len = bstream->hdr_buf->used;
+ } else {
+ stream_add_data(bstream,
+ "Content-Transfer-Encoding: base64\r\n", 35);
+ }
+ } else if (block.hdr->eoh && bstream->hdr_buf != NULL) {
+ /* finish the decision about decoding */
+ stream_finish_convert_decision(bstream);
+ stream_add_data(bstream, "\r\n", 2);
+ } else {
+ stream_add_hdr(bstream, block.hdr);
+ }
+ } else if (block.size == 0) {
+ /* end of header */
+ if (bstream->hdr_buf != NULL) {
+ /* message has no body */
+ bstream->convert_part = NULL;
+ stream_add_data(bstream, bstream->hdr_buf->data,
+ bstream->hdr_buf->used);
+ buffer_free(&bstream->hdr_buf);
+ }
+ bstream->content_type_seen = FALSE;
+ } else if (block.part == bstream->convert_part) {
+ /* convert body part to base64 */
+ stream_encode_base64(bstream, block.data, block.size);
+ } else {
+ stream_add_data(bstream, block.data, block.size);
+ }
+ new_size = stream->pos - stream->skip;
+ if (new_size == old_size)
+ return i_stream_binary_converter_read(stream);
+ return new_size - old_size;
+}
+
+static void i_stream_binary_converter_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct binary_converter_istream *bstream =
+ (struct binary_converter_istream *)stream;
+ struct message_part *parts;
+
+ if (bstream->parser != NULL) {
+ message_parser_deinit(&bstream->parser, &parts);
+ }
+ pool_unref(&bstream->pool);
+ if (close_parent)
+ i_stream_close(bstream->istream.parent);
+}
+
+struct istream *i_stream_create_binary_converter(struct istream *input)
+{
+ const struct message_parser_settings parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS |
+ MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES,
+ };
+ struct binary_converter_istream *bstream;
+
+ bstream = i_new(struct binary_converter_istream, 1);
+ bstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ bstream->istream.read = i_stream_binary_converter_read;
+ bstream->istream.iostream.close = i_stream_binary_converter_close;
+
+ bstream->istream.istream.readable_fd = FALSE;
+ bstream->istream.istream.blocking = input->blocking;
+ bstream->istream.istream.seekable = FALSE;
+
+ bstream->pool = pool_alloconly_create("istream binary converter", 128);
+ bstream->parser = message_parser_init(bstream->pool, input, &parser_set);
+ return i_stream_create(&bstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-mail/istream-binary-converter.h b/src/lib-mail/istream-binary-converter.h
new file mode 100644
index 0000000..6567e6e
--- /dev/null
+++ b/src/lib-mail/istream-binary-converter.h
@@ -0,0 +1,6 @@
+#ifndef ISTREAM_BINARY_CONVERTER_H
+#define ISTREAM_BINARY_CONVERTER_H
+
+struct istream *i_stream_create_binary_converter(struct istream *input);
+
+#endif
diff --git a/src/lib-mail/istream-dot.c b/src/lib-mail/istream-dot.c
new file mode 100644
index 0000000..9c8f3f7
--- /dev/null
+++ b/src/lib-mail/istream-dot.c
@@ -0,0 +1,236 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "istream-dot.h"
+
+struct dot_istream {
+ struct istream_private istream;
+
+ char pending[3]; /* max. \r\n */
+
+ /* how far in string "\r\n.\r" are we */
+ unsigned int state;
+ /* state didn't actually start with \r */
+ bool state_no_cr:1;
+ /* state didn't contain \n either (only at the beginnign of stream) */
+ bool state_no_lf:1;
+ /* we've seen the "." line, keep returning EOF */
+ bool dot_eof:1;
+
+ bool send_last_lf:1;
+};
+
+static int i_stream_dot_read_some(struct dot_istream *dstream)
+{
+ struct istream_private *stream = &dstream->istream;
+ size_t size, avail;
+ ssize_t ret;
+
+ size = i_stream_get_data_size(stream->parent);
+ if (size == 0) {
+ ret = i_stream_read_memarea(stream->parent);
+ if (ret <= 0) {
+ i_assert(ret != -2); /* 0 sized buffer can't be full */
+ if (stream->parent->stream_errno != 0) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ } else if (ret < 0 && stream->parent->eof) {
+ /* we didn't see "." line */
+ io_stream_set_error(&stream->iostream,
+ "dot-input stream ends without '.' line");
+ stream->istream.stream_errno = EPIPE;
+ }
+ return ret;
+ }
+ size = i_stream_get_data_size(stream->parent);
+ i_assert(size != 0);
+ }
+
+ if (!i_stream_try_alloc(stream, size, &avail))
+ return -2;
+ return 1;
+}
+
+static bool flush_pending(struct dot_istream *dstream, size_t *destp)
+{
+ struct istream_private *stream = &dstream->istream;
+ size_t dest = *destp;
+ unsigned int i = 0;
+
+ for (; dstream->pending[i] != '\0' && dest < stream->buffer_size; i++)
+ stream->w_buffer[dest++] = dstream->pending[i];
+ memmove(dstream->pending, dstream->pending + i,
+ sizeof(dstream->pending) - i);
+ *destp = dest;
+ return dest < stream->buffer_size;
+}
+
+static bool flush_dot_state(struct dot_istream *dstream, size_t *destp)
+{
+ unsigned int i = 0;
+
+ if (!dstream->state_no_cr)
+ dstream->pending[i++] = '\r';
+ if (dstream->state_no_lf)
+ dstream->state_no_lf = FALSE;
+ else if (dstream->state > 1)
+ dstream->pending[i++] = '\n';
+ dstream->pending[i] = '\0';
+
+ if (dstream->state != 4)
+ dstream->state = 0;
+ else {
+ /* \r\n.\r seen, go back to \r state */
+ dstream->state = 1;
+ }
+ return flush_pending(dstream, destp);
+}
+
+static void i_stream_dot_eof(struct dot_istream *dstream, size_t *destp)
+{
+ if (dstream->send_last_lf) {
+ dstream->state = 2;
+ (void)flush_dot_state(dstream, destp);
+ }
+ dstream->dot_eof = TRUE;
+}
+
+static ssize_t
+i_stream_dot_return(struct istream_private *stream, size_t dest, ssize_t ret)
+{
+ if (dest != stream->pos) {
+ i_assert(dest > stream->pos);
+ ret = dest - stream->pos;
+ stream->pos = dest;
+ }
+ return ret;
+}
+
+static ssize_t i_stream_dot_read(struct istream_private *stream)
+{
+ /* @UNSAFE */
+ struct dot_istream *dstream = (struct dot_istream *)stream;
+ const unsigned char *data;
+ size_t i, dest, size, avail;
+ ssize_t ret, ret1;
+
+ if (dstream->pending[0] != '\0') {
+ if (!i_stream_try_alloc(stream, 1, &avail))
+ return -2;
+ dest = stream->pos;
+ (void)flush_pending(dstream, &dest);
+ } else {
+ dest = stream->pos;
+ }
+
+ if (dstream->dot_eof) {
+ stream->istream.eof = TRUE;
+ return i_stream_dot_return(stream, dest, -1);
+ }
+
+ /* we have to update stream->pos before reading more data */
+ ret1 = i_stream_dot_return(stream, dest, 0);
+ if ((ret = i_stream_dot_read_some(dstream)) <= 0) {
+ if (stream->istream.stream_errno != 0)
+ return -1;
+ if (ret1 != 0)
+ return ret1;
+ dest = stream->pos;
+ if (ret == -1 && dstream->state != 0)
+ (void)flush_dot_state(dstream, &dest);
+ return i_stream_dot_return(stream, dest, ret);
+ }
+ dest = stream->pos;
+
+ data = i_stream_get_data(stream->parent, &size);
+ for (i = 0; i < size && dest < stream->buffer_size; i++) {
+ switch (dstream->state) {
+ case 0:
+ break;
+ case 1:
+ /* CR seen */
+ if (data[i] == '\n')
+ dstream->state++;
+ else {
+ if (!flush_dot_state(dstream, &dest))
+ goto end;
+ }
+ break;
+ case 2:
+ /* [CR]LF seen */
+ if (data[i] == '.')
+ dstream->state++;
+ else {
+ if (!flush_dot_state(dstream, &dest))
+ goto end;
+ }
+ break;
+ case 3:
+ /* [CR]LF. seen */
+ if (data[i] == '\r')
+ dstream->state++;
+ else if (data[i] == '\n') {
+ /* EOF */
+ i_stream_dot_eof(dstream, &dest);
+ i++;
+ goto end;
+ } else {
+ /* drop the initial dot */
+ if (!flush_dot_state(dstream, &dest))
+ goto end;
+ }
+ break;
+ case 4:
+ /* [CR]LF.CR seen */
+ if (data[i] == '\n') {
+ /* EOF */
+ i_stream_dot_eof(dstream, &dest);
+ i++;
+ goto end;
+ } else {
+ /* drop the initial dot */
+ if (!flush_dot_state(dstream, &dest))
+ goto end;
+ }
+ }
+ if (dstream->state == 0) {
+ if (data[i] == '\r') {
+ dstream->state = 1;
+ dstream->state_no_cr = FALSE;
+ } else if (data[i] == '\n') {
+ dstream->state = 2;
+ dstream->state_no_cr = TRUE;
+ } else {
+ stream->w_buffer[dest++] = data[i];
+ }
+ }
+ }
+end:
+ i_stream_skip(stream->parent, i);
+
+ ret = i_stream_dot_return(stream, dest, 0) + ret1;
+ if (ret == 0)
+ return i_stream_dot_read(stream);
+ i_assert(ret > 0);
+ return ret;
+}
+
+struct istream *i_stream_create_dot(struct istream *input, bool send_last_lf)
+{
+ struct dot_istream *dstream;
+
+ dstream = i_new(struct dot_istream, 1);
+ dstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ dstream->istream.read = i_stream_dot_read;
+
+ dstream->istream.istream.readable_fd = FALSE;
+ dstream->istream.istream.blocking = input->blocking;
+ dstream->istream.istream.seekable = FALSE;
+ dstream->send_last_lf = send_last_lf;
+ dstream->state = 2;
+ dstream->state_no_cr = TRUE;
+ dstream->state_no_lf = TRUE;
+ return i_stream_create(&dstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-mail/istream-dot.h b/src/lib-mail/istream-dot.h
new file mode 100644
index 0000000..129bc70
--- /dev/null
+++ b/src/lib-mail/istream-dot.h
@@ -0,0 +1,9 @@
+#ifndef ISTREAM_DOT_H
+#define ISTREAM_DOT_H
+
+/* Create input stream for reading SMTP DATA style message: Drop initial "."
+ from lines beginning with it. Return EOF on line that contains only ".".
+ If send_last_lf=FALSE, the trailing [CR]LF before "." line isn't returned. */
+struct istream *i_stream_create_dot(struct istream *input, bool send_last_lf);
+
+#endif
diff --git a/src/lib-mail/istream-header-filter.c b/src/lib-mail/istream-header-filter.c
new file mode 100644
index 0000000..1783dbd
--- /dev/null
+++ b/src/lib-mail/istream-header-filter.c
@@ -0,0 +1,762 @@
+/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "memarea.h"
+#include "sort.h"
+#include "message-parser.h"
+#include "istream-private.h"
+#include "istream-header-filter.h"
+
+struct header_filter_istream_snapshot {
+ struct istream_snapshot snapshot;
+ struct header_filter_istream *mstream;
+ buffer_t *hdr_buf;
+};
+
+struct header_filter_istream {
+ struct istream_private istream;
+ pool_t pool;
+
+ struct message_header_parser_ctx *hdr_ctx;
+
+ const char **headers;
+ unsigned int headers_count;
+
+ header_filter_callback *callback;
+ void *context;
+
+ buffer_t *hdr_buf;
+ struct message_size header_size;
+ uoff_t skip_count;
+ uoff_t last_lf_offset;
+
+ unsigned int cur_line, parsed_lines;
+ ARRAY(unsigned int) match_change_lines;
+
+ bool header_read:1;
+ bool seen_eoh:1;
+ bool header_parsed:1;
+ bool headers_edited:1;
+ bool exclude:1;
+ bool crlf:1;
+ bool crlf_preserve:1;
+ bool hide_body:1;
+ bool add_missing_eoh:1;
+ bool end_body_with_lf:1;
+ bool last_lf_added:1;
+ bool last_orig_crlf:1;
+ bool last_added_newline:1;
+ bool eoh_not_matched:1;
+ bool callbacks_called:1;
+ bool prev_matched:1;
+ bool snapshot_pending:1;
+};
+
+header_filter_callback *null_header_filter_callback = NULL;
+
+static ssize_t i_stream_header_filter_read(struct istream_private *stream);
+
+static void i_stream_header_filter_destroy(struct iostream_private *stream)
+{
+ struct header_filter_istream *mstream =
+ (struct header_filter_istream *)stream;
+
+ if (mstream->hdr_ctx != NULL)
+ message_parse_header_deinit(&mstream->hdr_ctx);
+ if (array_is_created(&mstream->match_change_lines))
+ array_free(&mstream->match_change_lines);
+ if (!mstream->snapshot_pending)
+ buffer_free(&mstream->hdr_buf);
+ else {
+ /* Clear hdr_buf to make sure
+ i_stream_header_filter_snapshot_free() frees it. */
+ mstream->hdr_buf = NULL;
+ }
+ pool_unref(&mstream->pool);
+}
+
+static ssize_t
+read_mixed(struct header_filter_istream *mstream, size_t body_highwater_size)
+{
+ const unsigned char *data;
+ size_t pos;
+ ssize_t ret;
+
+ if (mstream->hide_body) {
+ mstream->istream.istream.eof = TRUE;
+ return -1;
+ }
+
+ data = i_stream_get_data(mstream->istream.parent, &pos);
+ if (pos <= body_highwater_size) {
+ i_assert(pos == body_highwater_size ||
+ (mstream->end_body_with_lf &&
+ pos+1 == body_highwater_size));
+
+ ret = i_stream_read_memarea(mstream->istream.parent);
+ mstream->istream.istream.stream_errno =
+ mstream->istream.parent->stream_errno;
+ mstream->istream.istream.eof = mstream->istream.parent->eof;
+
+ if (ret <= 0) {
+ data = mstream->hdr_buf->data;
+ pos = mstream->hdr_buf->used;
+ i_assert(pos > 0);
+
+ if (mstream->end_body_with_lf && data[pos-1] != '\n' &&
+ ret == -1 && mstream->istream.istream.eof) {
+ /* add missing trailing LF to body */
+ if (mstream->crlf)
+ buffer_append_c(mstream->hdr_buf, '\r');
+ buffer_append_c(mstream->hdr_buf, '\n');
+ mstream->istream.buffer = mstream->hdr_buf->data;
+ mstream->istream.pos = mstream->hdr_buf->used;
+ return mstream->hdr_buf->used - pos;
+ }
+ return ret;
+ }
+
+ data = i_stream_get_data(mstream->istream.parent, &pos);
+ }
+ buffer_append(mstream->hdr_buf, data + body_highwater_size,
+ pos - body_highwater_size);
+
+ mstream->istream.buffer = buffer_get_data(mstream->hdr_buf, &pos);
+ ret = (ssize_t)(pos - mstream->istream.pos - mstream->istream.skip);
+ i_assert(ret > 0);
+ mstream->istream.pos = pos;
+ return ret;
+}
+
+static int cmp_uint(const unsigned int *i1, const unsigned int *i2)
+{
+ return *i1 < *i2 ? -1 :
+ (*i1 > *i2 ? 1 : 0);
+}
+
+static bool match_line_changed(struct header_filter_istream *mstream)
+{
+ if (!array_is_created(&mstream->match_change_lines))
+ return FALSE;
+
+ return array_bsearch(&mstream->match_change_lines, &mstream->cur_line,
+ cmp_uint) != NULL;
+}
+
+static void add_eol(struct header_filter_istream *mstream, bool orig_crlf)
+{
+ if (mstream->crlf || (orig_crlf && mstream->crlf_preserve))
+ buffer_append(mstream->hdr_buf, "\r\n", 2);
+ else
+ buffer_append_c(mstream->hdr_buf, '\n');
+ mstream->last_orig_crlf = orig_crlf;
+ mstream->last_added_newline = TRUE;
+}
+
+static ssize_t hdr_stream_update_pos(struct header_filter_istream *mstream)
+{
+ ssize_t ret;
+ size_t pos;
+
+ mstream->istream.buffer = buffer_get_data(mstream->hdr_buf, &pos);
+ ret = (ssize_t)(pos - mstream->istream.pos - mstream->istream.skip);
+ i_assert(ret >= 0);
+ mstream->istream.pos = pos;
+ return ret;
+}
+
+static void hdr_buf_realloc_if_needed(struct header_filter_istream *mstream)
+{
+ if (!mstream->snapshot_pending)
+ return;
+
+ /* hdr_buf exists in a snapshot. Leave it be and create a copy of it
+ that we modify. */
+ buffer_t *old_buf = mstream->hdr_buf;
+ mstream->hdr_buf = buffer_create_dynamic(default_pool,
+ I_MAX(1024, old_buf->used));
+ buffer_append(mstream->hdr_buf, old_buf->data, old_buf->used);
+ mstream->snapshot_pending = FALSE;
+
+ mstream->istream.buffer = mstream->hdr_buf->data;
+}
+
+static ssize_t read_header(struct header_filter_istream *mstream)
+{
+ struct message_header_line *hdr;
+ uoff_t highwater_offset;
+ size_t max_buffer_size;
+ ssize_t ret, ret2;
+ int hdr_ret;
+
+ if (mstream->hdr_ctx == NULL) {
+ mstream->hdr_ctx =
+ message_parse_header_init(mstream->istream.parent,
+ NULL, 0);
+ }
+
+ /* remove skipped data from hdr_buf */
+ hdr_buf_realloc_if_needed(mstream);
+ buffer_copy(mstream->hdr_buf, 0,
+ mstream->hdr_buf, mstream->istream.skip, SIZE_MAX);
+
+ mstream->istream.pos -= mstream->istream.skip;
+ mstream->istream.skip = 0;
+ buffer_set_used_size(mstream->hdr_buf, mstream->istream.pos);
+
+ if (mstream->header_read) {
+ i_assert(mstream->istream.skip == 0);
+ highwater_offset = mstream->istream.istream.v_offset +
+ mstream->istream.pos;
+ if (highwater_offset >= mstream->header_size.virtual_size) {
+ /* we want to return mixed headers and body */
+ size_t body_highwater_size = highwater_offset -
+ mstream->header_size.virtual_size;
+ return read_mixed(mstream, body_highwater_size);
+ }
+ }
+
+ max_buffer_size = i_stream_get_max_buffer_size(&mstream->istream.istream);
+ if (mstream->hdr_buf->used >= max_buffer_size) {
+ i_assert(max_buffer_size > 0);
+ return -2;
+ }
+
+ while ((hdr_ret = message_parse_header_next(mstream->hdr_ctx,
+ &hdr)) > 0) {
+ bool matched;
+
+ if (!hdr->continued)
+ mstream->cur_line++;
+ if (hdr->eoh) {
+ mstream->seen_eoh = TRUE;
+ matched = FALSE;
+ if (mstream->header_parsed && !mstream->headers_edited) {
+ if (mstream->eoh_not_matched)
+ matched = !matched;
+ } else if (mstream->callback != NULL) {
+ mstream->callback(mstream, hdr, &matched,
+ mstream->context);
+ mstream->callbacks_called = TRUE;
+ }
+
+ if (matched) {
+ mstream->eoh_not_matched = TRUE;
+ continue;
+ }
+
+ add_eol(mstream, hdr->crlf_newline);
+ continue;
+ }
+
+ if (hdr->continued) {
+ /* Header line continued - use only the first line's
+ matched-result. Otherwise multiline headers might
+ end up being only partially picked, which wouldn't
+ be very good. However, allow callbacks to modify
+ the headers in any way they want. */
+ matched = mstream->prev_matched;
+ } else if (mstream->headers_count == 0) {
+ /* no include/exclude headers - default matching */
+ matched = FALSE;
+ } else {
+ matched = i_bsearch(hdr->name, mstream->headers,
+ mstream->headers_count,
+ sizeof(*mstream->headers),
+ bsearch_strcasecmp) != NULL;
+ }
+ if (mstream->callback == NULL) {
+ /* nothing gets excluded */
+ } else if (!mstream->header_parsed || mstream->headers_edited) {
+ /* first time in this line or we have actually modified
+ the header so we always want to call the callbacks */
+ bool orig_matched = matched;
+
+ mstream->parsed_lines = mstream->cur_line;
+ mstream->callback(mstream, hdr, &matched,
+ mstream->context);
+ mstream->callbacks_called = TRUE;
+ if (matched != orig_matched &&
+ !hdr->continued && !mstream->headers_edited) {
+ if (!array_is_created(&mstream->match_change_lines))
+ i_array_init(&mstream->match_change_lines, 8);
+ array_push_back(&mstream->match_change_lines,
+ &mstream->cur_line);
+ }
+ } else if (!hdr->continued) {
+ /* second time in this line. was it excluded by the
+ callback the first time? */
+ if (match_line_changed(mstream))
+ matched = !matched;
+ }
+ mstream->prev_matched = matched;
+
+ if (matched == mstream->exclude) {
+ /* ignore */
+ } else {
+ if (!hdr->continued) {
+ buffer_append(mstream->hdr_buf,
+ hdr->name, hdr->name_len);
+ buffer_append(mstream->hdr_buf,
+ hdr->middle, hdr->middle_len);
+ }
+ buffer_append(mstream->hdr_buf,
+ hdr->value, hdr->value_len);
+ if (!hdr->no_newline)
+ add_eol(mstream, hdr->crlf_newline);
+
+ if (mstream->skip_count >= mstream->hdr_buf->used) {
+ /* we need more */
+ mstream->skip_count -= mstream->hdr_buf->used;
+ buffer_set_used_size(mstream->hdr_buf, 0);
+ } else {
+ if (mstream->skip_count > 0) {
+ mstream->istream.skip =
+ mstream->skip_count;
+ mstream->skip_count = 0;
+ }
+ break;
+ }
+ }
+ if (mstream->hdr_buf->used >= max_buffer_size)
+ break;
+ }
+ if (mstream->hdr_buf->used > 0) {
+ const unsigned char *data = mstream->hdr_buf->data;
+ mstream->last_added_newline =
+ data[mstream->hdr_buf->used-1] == '\n';
+ }
+
+ if (hdr_ret < 0) {
+ if (mstream->istream.parent->stream_errno != 0) {
+ mstream->istream.istream.stream_errno =
+ mstream->istream.parent->stream_errno;
+ mstream->istream.istream.eof =
+ mstream->istream.parent->eof;
+ return -1;
+ }
+ if (!mstream->seen_eoh && mstream->add_missing_eoh) {
+ bool matched = FALSE;
+
+ mstream->seen_eoh = TRUE;
+
+ if (!mstream->last_added_newline)
+ add_eol(mstream, mstream->last_orig_crlf);
+
+ if (mstream->header_parsed && !mstream->headers_edited) {
+ if (mstream->eoh_not_matched)
+ matched = !matched;
+ } else if (mstream->callback != NULL) {
+ struct message_header_line fake_eoh_hdr = {
+ .eoh = TRUE,
+ .name = "",
+ };
+ mstream->callback(mstream, &fake_eoh_hdr,
+ &matched, mstream->context);
+ mstream->callbacks_called = TRUE;
+ }
+
+ if (matched) {
+ mstream->seen_eoh = FALSE;
+ } else {
+ add_eol(mstream, mstream->last_orig_crlf);
+ }
+ }
+ }
+
+ /* don't copy eof here because we're only returning headers here.
+ the body will be returned in separate read() call. */
+ ret = hdr_stream_update_pos(mstream);
+
+ if (hdr_ret == 0) {
+ /* need more data to finish parsing headers. we may have some
+ data already available though. */
+ return ret;
+ }
+
+ if (hdr == NULL) {
+ /* finished */
+ message_parse_header_deinit(&mstream->hdr_ctx);
+ mstream->hdr_ctx = NULL;
+
+ if ((!mstream->header_parsed || mstream->headers_edited ||
+ mstream->callbacks_called) &&
+ mstream->callback != NULL) {
+ bool matched = FALSE;
+ mstream->callback(mstream, NULL,
+ &matched, mstream->context);
+ /* check if the callback added more headers.
+ this is allowed only if EOH wasn't added yet. */
+ ret2 = hdr_stream_update_pos(mstream);
+ if (!mstream->seen_eoh)
+ ret += ret2;
+ else {
+ i_assert(ret2 == 0);
+ }
+ }
+ mstream->header_parsed = TRUE;
+ mstream->header_read = TRUE;
+ mstream->callbacks_called = FALSE;
+
+ mstream->header_size.physical_size =
+ mstream->istream.parent->v_offset;
+ mstream->header_size.virtual_size =
+ mstream->istream.istream.v_offset +
+ mstream->istream.pos;
+ }
+
+ if (ret == 0) {
+ /* we're at the end of headers. */
+ i_assert(hdr == NULL);
+ i_assert(mstream->istream.istream.v_offset +
+ mstream->istream.pos ==
+ mstream->header_size.virtual_size);
+
+ return i_stream_header_filter_read(&mstream->istream);
+ }
+
+ return ret;
+}
+
+static ssize_t
+handle_end_body_with_lf(struct header_filter_istream *mstream, ssize_t ret)
+{
+ struct istream_private *stream = &mstream->istream;
+ const unsigned char *data;
+ size_t size;
+ uoff_t last_offset;
+ bool last_lf;
+
+ data = i_stream_get_data(stream->parent, &size);
+ if (stream->parent->v_offset + size == 0 && size == 0)
+ last_offset = UOFF_T_MAX;
+ else
+ last_offset = stream->parent->v_offset + size - 1;
+
+ if (mstream->last_lf_offset == last_offset)
+ last_lf = TRUE;
+ else if (size > 0)
+ last_lf = data[size-1] == '\n';
+ else
+ last_lf = FALSE;
+
+ if (ret == -1 && stream->parent->eof && !last_lf) {
+ /* missing LF, need to add it */
+ i_assert(!mstream->last_lf_added);
+ i_assert(size == 0 || data[size-1] != '\n');
+
+ hdr_buf_realloc_if_needed(mstream);
+ buffer_set_used_size(mstream->hdr_buf, 0);
+ buffer_append(mstream->hdr_buf, data, size);
+ if (mstream->crlf)
+ buffer_append_c(mstream->hdr_buf, '\r');
+ buffer_append_c(mstream->hdr_buf, '\n');
+ mstream->last_lf_offset = last_offset;
+ mstream->last_lf_added = TRUE;
+
+ stream->skip = 0;
+ stream->pos = mstream->hdr_buf->used;
+ stream->buffer = mstream->hdr_buf->data;
+ return mstream->crlf ? 2 : 1;
+ } else {
+ mstream->last_lf_offset = last_lf ? last_offset : UOFF_T_MAX;
+ }
+ return ret;
+}
+
+static ssize_t i_stream_header_filter_read(struct istream_private *stream)
+{
+ struct header_filter_istream *mstream =
+ (struct header_filter_istream *)stream;
+ uoff_t v_offset;
+ ssize_t ret;
+
+ if (mstream->last_lf_added) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ if (!mstream->header_read ||
+ stream->istream.v_offset < mstream->header_size.virtual_size)
+ return read_header(mstream);
+
+ if (mstream->hide_body) {
+ stream->istream.eof = TRUE;
+ return -1;
+ }
+
+ v_offset = stream->parent_start_offset + stream->istream.v_offset -
+ mstream->header_size.virtual_size +
+ mstream->header_size.physical_size;
+ i_stream_seek(stream->parent, v_offset);
+ ret = i_stream_read_copy_from_parent(&stream->istream);
+ if (mstream->end_body_with_lf)
+ ret = handle_end_body_with_lf(mstream, ret);
+ return ret;
+}
+
+static void
+i_stream_header_filter_seek_to_header(struct header_filter_istream *mstream,
+ uoff_t v_offset)
+{
+ i_stream_seek(mstream->istream.parent,
+ mstream->istream.parent_start_offset);
+ mstream->istream.parent_expected_offset =
+ mstream->istream.parent_start_offset;
+ mstream->istream.access_counter =
+ mstream->istream.parent->real_stream->access_counter;
+
+ if (mstream->hdr_ctx != NULL)
+ message_parse_header_deinit(&mstream->hdr_ctx);
+ mstream->skip_count = v_offset;
+ mstream->cur_line = 0;
+ mstream->prev_matched = FALSE;
+ mstream->header_read = FALSE;
+ mstream->seen_eoh = FALSE;
+ mstream->last_added_newline = TRUE;
+}
+
+static int skip_header(struct header_filter_istream *mstream)
+{
+ size_t pos;
+
+ if (mstream->header_read)
+ return 0;
+
+ if (mstream->istream.access_counter !=
+ mstream->istream.parent->real_stream->access_counter) {
+ /* need to re-parse headers */
+ i_stream_header_filter_seek_to_header(mstream, 0);
+ }
+
+ while (!mstream->header_read &&
+ i_stream_read_memarea(&mstream->istream.istream) != -1) {
+ pos = i_stream_get_data_size(&mstream->istream.istream);
+ i_stream_skip(&mstream->istream.istream, pos);
+ }
+ return mstream->istream.istream.stream_errno != 0 ? -1 : 0;
+}
+
+static void
+stream_reset_to(struct header_filter_istream *mstream, uoff_t v_offset)
+{
+ hdr_buf_realloc_if_needed(mstream);
+ mstream->istream.istream.v_offset = v_offset;
+ mstream->istream.skip = mstream->istream.pos = 0;
+ mstream->istream.buffer = NULL;
+ buffer_set_used_size(mstream->hdr_buf, 0);
+}
+
+static void i_stream_header_filter_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark ATTR_UNUSED)
+{
+ struct header_filter_istream *mstream =
+ (struct header_filter_istream *)stream;
+
+ if (stream->istream.v_offset == v_offset) {
+ /* just reset the input buffer */
+ stream_reset_to(mstream, v_offset);
+ i_stream_seek(mstream->istream.parent,
+ mstream->istream.parent_expected_offset);
+ return;
+ }
+ /* if last_lf_added=TRUE, we're currently at EOF. So reset it only if
+ we're seeking backwards, otherwise we would just add a duplicate */
+ mstream->last_lf_added = FALSE;
+
+ if (v_offset == 0) {
+ /* seeking to beginning of headers. */
+ stream_reset_to(mstream, 0);
+ i_stream_header_filter_seek_to_header(mstream, 0);
+ return;
+ }
+
+ /* if we haven't parsed the whole header yet, we don't know if we
+ want to seek inside header or body. so make sure we've parsed the
+ header. */
+ if (skip_header(mstream) < 0)
+ return;
+ stream_reset_to(mstream, v_offset);
+
+ if (v_offset < mstream->header_size.virtual_size) {
+ /* seek into headers. we'll have to re-parse them, use
+ skip_count to set the wanted position */
+ i_stream_header_filter_seek_to_header(mstream, v_offset);
+ } else {
+ /* body */
+ v_offset -= mstream->header_size.virtual_size;
+ v_offset += mstream->header_size.physical_size;
+ i_stream_seek(stream->parent,
+ stream->parent_start_offset + v_offset);
+ }
+}
+
+static void ATTR_NORETURN
+i_stream_header_filter_sync(struct istream_private *stream ATTR_UNUSED)
+{
+ i_panic("istream-header-filter sync() not implemented");
+}
+
+static int
+i_stream_header_filter_stat(struct istream_private *stream, bool exact)
+{
+ struct header_filter_istream *mstream =
+ (struct header_filter_istream *)stream;
+ const struct stat *st;
+ uoff_t old_offset;
+
+ if (i_stream_stat(stream->parent, exact, &st) < 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ return -1;
+ }
+ stream->statbuf = *st;
+ if (stream->statbuf.st_size == -1 || !exact)
+ return 0;
+
+ /* fix the filtered header size */
+ old_offset = stream->istream.v_offset;
+ if (skip_header(mstream) < 0)
+ return -1;
+
+ if (mstream->hide_body) {
+ /* no body */
+ stream->statbuf.st_size = mstream->header_size.physical_size;
+ } else if (!mstream->end_body_with_lf) {
+ /* no last-LF */
+ } else if (mstream->last_lf_added) {
+ /* yes, we have added LF */
+ stream->statbuf.st_size += mstream->crlf ? 2 : 1;
+ } else if (mstream->last_lf_offset != UOFF_T_MAX) {
+ /* no, we didn't need to add LF */
+ } else {
+ /* check if we need to add LF */
+ i_stream_seek(stream->parent, st->st_size - 1);
+ (void)i_stream_read_memarea(stream->parent);
+ if (stream->parent->stream_errno != 0) {
+ stream->istream.stream_errno =
+ stream->parent->stream_errno;
+ return -1;
+ }
+ i_assert(stream->parent->eof);
+ ssize_t ret = handle_end_body_with_lf(mstream, -1);
+ if (ret > 0)
+ stream->statbuf.st_size += ret;
+ }
+
+ stream->statbuf.st_size -=
+ (off_t)mstream->header_size.physical_size -
+ (off_t)mstream->header_size.virtual_size;
+ i_stream_seek(&stream->istream, old_offset);
+ return 0;
+}
+
+static void
+i_stream_header_filter_snapshot_free(struct istream_snapshot *_snapshot)
+{
+ struct header_filter_istream_snapshot *snapshot =
+ container_of(_snapshot, struct header_filter_istream_snapshot, snapshot);
+
+ if (snapshot->mstream->hdr_buf != snapshot->hdr_buf)
+ buffer_free(&snapshot->hdr_buf);
+ else {
+ i_assert(snapshot->mstream->snapshot_pending);
+ snapshot->mstream->snapshot_pending = FALSE;
+ }
+ i_free(snapshot);
+}
+
+static struct istream_snapshot *
+i_stream_header_filter_snapshot(struct istream_private *stream,
+ struct istream_snapshot *prev_snapshot)
+{
+ struct header_filter_istream *mstream =
+ (struct header_filter_istream *)stream;
+ struct header_filter_istream_snapshot *snapshot;
+
+ if (stream->buffer != mstream->hdr_buf->data) {
+ /* reading body */
+ return i_stream_default_snapshot(stream, prev_snapshot);
+ }
+
+ /* snapshot the header buffer */
+ snapshot = i_new(struct header_filter_istream_snapshot, 1);
+ snapshot->mstream = mstream;
+ snapshot->hdr_buf = mstream->hdr_buf;
+ snapshot->snapshot.free = i_stream_header_filter_snapshot_free;
+ snapshot->snapshot.prev_snapshot = prev_snapshot;
+ mstream->snapshot_pending = TRUE;
+ return &snapshot->snapshot;
+}
+
+#undef i_stream_create_header_filter
+struct istream *
+i_stream_create_header_filter(struct istream *input,
+ enum header_filter_flags flags,
+ const char *const *headers,
+ unsigned int headers_count,
+ header_filter_callback *callback, void *context)
+{
+ struct header_filter_istream *mstream;
+ unsigned int i, j;
+ int ret;
+
+ i_assert((flags & (HEADER_FILTER_INCLUDE|HEADER_FILTER_EXCLUDE)) != 0);
+
+ mstream = i_new(struct header_filter_istream, 1);
+ mstream->pool = pool_alloconly_create(MEMPOOL_GROWING
+ "header filter stream", 256);
+ mstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+
+ mstream->headers = headers_count == 0 ? NULL :
+ p_new(mstream->pool, const char *, headers_count);
+ for (i = j = 0; i < headers_count; i++) {
+ ret = j == 0 ? -1 :
+ strcasecmp(mstream->headers[j-1], headers[i]);
+ if (ret == 0) {
+ /* drop duplicate */
+ continue;
+ }
+ i_assert(ret < 0);
+ mstream->headers[j++] = p_strdup(mstream->pool, headers[i]);
+ }
+ mstream->headers_count = j;
+ mstream->hdr_buf = buffer_create_dynamic(default_pool, 1024);
+
+ mstream->callback = callback;
+ mstream->context = context;
+ mstream->exclude = (flags & HEADER_FILTER_EXCLUDE) != 0;
+ if ((flags & HEADER_FILTER_CRLF_PRESERVE) != 0)
+ mstream->crlf_preserve = TRUE;
+ else if ((flags & HEADER_FILTER_NO_CR) != 0)
+ mstream->crlf = FALSE;
+ else
+ mstream->crlf = TRUE;
+ mstream->hide_body = (flags & HEADER_FILTER_HIDE_BODY) != 0;
+ mstream->add_missing_eoh = (flags & HEADER_FILTER_ADD_MISSING_EOH) != 0;
+ mstream->end_body_with_lf =
+ (flags & HEADER_FILTER_END_BODY_WITH_LF) != 0;
+ mstream->last_lf_offset = UOFF_T_MAX;
+ mstream->last_added_newline = TRUE;
+
+ mstream->istream.iostream.destroy = i_stream_header_filter_destroy;
+ mstream->istream.read = i_stream_header_filter_read;
+ mstream->istream.seek = i_stream_header_filter_seek;
+ mstream->istream.sync = i_stream_header_filter_sync;
+ mstream->istream.stat = i_stream_header_filter_stat;
+ mstream->istream.snapshot = i_stream_header_filter_snapshot;
+
+ mstream->istream.istream.readable_fd = FALSE;
+ mstream->istream.istream.blocking = input->blocking;
+ mstream->istream.istream.seekable = input->seekable;
+
+ return i_stream_create(&mstream->istream, input, -1, 0);
+}
+
+void i_stream_header_filter_add(struct header_filter_istream *input,
+ const void *data, size_t size)
+{
+ hdr_buf_realloc_if_needed(input);
+ buffer_append(input->hdr_buf, data, size);
+ input->headers_edited = TRUE;
+}
diff --git a/src/lib-mail/istream-header-filter.h b/src/lib-mail/istream-header-filter.h
new file mode 100644
index 0000000..7c5ca36
--- /dev/null
+++ b/src/lib-mail/istream-header-filter.h
@@ -0,0 +1,52 @@
+#ifndef ISTREAM_HEADER_FILTER_H
+#define ISTREAM_HEADER_FILTER_H
+
+struct header_filter_istream;
+
+enum header_filter_flags {
+ /* Include only specified headers in output.*/
+ HEADER_FILTER_INCLUDE = 0x01,
+ /* Exclude specified headers from output. */
+ HEADER_FILTER_EXCLUDE = 0x02,
+
+ /* Use LF linefeeds instead of CRLF. */
+ HEADER_FILTER_NO_CR = 0x04,
+ /* Return EOF at the beginning of message body. */
+ HEADER_FILTER_HIDE_BODY = 0x08,
+ /* If the empty "end of headers" line doesn't exist, add it. */
+ HEADER_FILTER_ADD_MISSING_EOH = 0x10,
+ /* If body doesn't end with [CR]LF, add it/them. */
+ HEADER_FILTER_END_BODY_WITH_LF = 0x20,
+ /* Preserve the original LF or CRLF. */
+ HEADER_FILTER_CRLF_PRESERVE = 0x40
+};
+
+struct message_header_line;
+
+typedef void header_filter_callback(struct header_filter_istream *input,
+ struct message_header_line *hdr,
+ bool *matched, void *context);
+
+extern header_filter_callback *null_header_filter_callback;
+
+/* NOTE: headers list must be sorted. */
+struct istream *
+i_stream_create_header_filter(struct istream *input,
+ enum header_filter_flags flags,
+ const char *const *headers,
+ unsigned int headers_count,
+ header_filter_callback *callback, void *context)
+ ATTR_NULL(6);
+#define i_stream_create_header_filter(input, flags, headers, headers_count, \
+ callback, context) \
+ i_stream_create_header_filter(input, flags, headers, headers_count - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct header_filter_istream *, \
+ struct message_header_line *, bool *, typeof(context))), \
+ (header_filter_callback *)callback, context)
+
+/* Add more data to headers. Should called from the filter callback. */
+void i_stream_header_filter_add(struct header_filter_istream *input,
+ const void *data, size_t size);
+
+#endif
diff --git a/src/lib-mail/istream-nonuls.c b/src/lib-mail/istream-nonuls.c
new file mode 100644
index 0000000..28f33b9
--- /dev/null
+++ b/src/lib-mail/istream-nonuls.c
@@ -0,0 +1,79 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream-private.h"
+#include "istream-nonuls.h"
+
+struct nonuls_istream {
+ struct istream_private istream;
+ char replace_chr;
+};
+
+static int i_stream_read_parent(struct istream_private *stream)
+{
+ ssize_t ret;
+
+ if (i_stream_get_data_size(stream->parent) > 0)
+ return 1;
+
+ ret = i_stream_read_memarea(stream->parent);
+ if (ret <= 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ return ret;
+ }
+ i_assert(i_stream_get_data_size(stream->parent) != 0);
+ return 1;
+}
+
+static ssize_t i_stream_nonuls_read(struct istream_private *stream)
+{
+ struct nonuls_istream *nstream = (struct nonuls_istream *)stream;
+ const unsigned char *data, *p;
+ size_t i, size, avail_size;
+ int ret;
+
+ if ((ret = i_stream_read_parent(stream)) <= 0)
+ return ret;
+
+ data = i_stream_get_data(stream->parent, &size);
+ if (!i_stream_try_alloc(stream, size, &avail_size))
+ return -2;
+ if (size > avail_size)
+ size = avail_size;
+ i_assert(size > 0);
+
+ p = memchr(data, '\0', size);
+ if (p == NULL) {
+ /* no NULs in this block */
+ memcpy(stream->w_buffer+stream->pos, data, size);
+ } else {
+ i = p-data;
+ memcpy(stream->w_buffer+stream->pos, data, i);
+ for (; i < size; i++) {
+ stream->w_buffer[stream->pos+i] = data[i] == '\0' ?
+ nstream->replace_chr : data[i];
+ }
+ }
+ stream->pos += size;
+ i_stream_skip(stream->parent, size);
+ return size;
+}
+
+struct istream *i_stream_create_nonuls(struct istream *input, char replace_chr)
+{
+ struct nonuls_istream *nstream;
+
+ nstream = i_new(struct nonuls_istream, 1);
+ nstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ nstream->istream.stream_size_passthrough = TRUE;
+
+ nstream->istream.read = i_stream_nonuls_read;
+
+ nstream->istream.istream.readable_fd = FALSE;
+ nstream->istream.istream.blocking = input->blocking;
+ nstream->istream.istream.seekable = FALSE;
+ nstream->replace_chr = replace_chr;
+ return i_stream_create(&nstream->istream, input,
+ i_stream_get_fd(input), 0);
+}
diff --git a/src/lib-mail/istream-nonuls.h b/src/lib-mail/istream-nonuls.h
new file mode 100644
index 0000000..73dd4c5
--- /dev/null
+++ b/src/lib-mail/istream-nonuls.h
@@ -0,0 +1,7 @@
+#ifndef ISTREAM_NONULS_H
+#define ISTREAM_NONULS_H
+
+/* Translate all NUL characters to the specified replace_chr. */
+struct istream *i_stream_create_nonuls(struct istream *input, char replace_chr);
+
+#endif
diff --git a/src/lib-mail/istream-qp-decoder.c b/src/lib-mail/istream-qp-decoder.c
new file mode 100644
index 0000000..7ee0580
--- /dev/null
+++ b/src/lib-mail/istream-qp-decoder.c
@@ -0,0 +1,140 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "qp-decoder.h"
+#include "istream-private.h"
+#include "istream-qp.h"
+
+struct qp_decoder_istream {
+ struct istream_private istream;
+ buffer_t *buf;
+ struct qp_decoder *qp;
+};
+
+static void i_stream_qp_decoder_close(struct iostream_private *stream,
+ bool close_parent)
+{
+ struct qp_decoder_istream *bstream =
+ (struct qp_decoder_istream *)stream;
+
+ if (bstream->qp != NULL)
+ qp_decoder_deinit(&bstream->qp);
+ buffer_free(&bstream->buf);
+ if (close_parent)
+ i_stream_close(bstream->istream.parent);
+}
+
+static ssize_t i_stream_qp_decoder_read(struct istream_private *stream)
+{
+ struct qp_decoder_istream *bstream =
+ (struct qp_decoder_istream *)stream;
+ const unsigned char *data;
+ size_t size, error_pos, max_buffer_size;
+ const char *error;
+ int ret;
+
+ max_buffer_size = i_stream_get_max_buffer_size(&stream->istream);
+ for (;;) {
+ /* remove skipped data from buffer */
+ 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 >= max_buffer_size) {
+ /* stream buffer still at maximum */
+ return -2;
+ }
+
+ /* if something is already decoded, 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 */
+ new_pos = I_MIN(bstream->buf->used, 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_memarea(stream->parent, &data, &size);
+ if (ret <= 0) {
+ stream->istream.stream_errno = stream->parent->stream_errno;
+ stream->istream.eof = stream->parent->eof;
+ if (ret != -1 || stream->istream.stream_errno != 0)
+ return ret;
+ /* end of quoted-printable stream. verify that the
+ ending is ok. */
+ if (qp_decoder_finish(bstream->qp, &error) == 0) {
+ i_assert(bstream->buf->used == 0);
+ return -1;
+ }
+ io_stream_set_error(&stream->iostream,
+ "Invalid quoted-printable input trailer: %s", error);
+ stream->istream.stream_errno = EPIPE;
+ return -1;
+ }
+ if (qp_decoder_more(bstream->qp, data, size,
+ &error_pos, &error) < 0) {
+ i_assert(error_pos < size);
+ io_stream_set_error(&stream->iostream,
+ "Invalid quoted-printable input 0x%s: %s",
+ binary_to_hex(data+error_pos, I_MIN(size-error_pos, 8)), error);
+ stream->istream.stream_errno = EINVAL;
+ return -1;
+ }
+ i_stream_skip(stream->parent, size);
+ }
+}
+
+static void
+i_stream_qp_decoder_seek(struct istream_private *stream,
+ uoff_t v_offset, bool mark)
+{
+ struct qp_decoder_istream *bstream =
+ (struct qp_decoder_istream *)stream;
+ const char *error;
+
+ 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);
+ (void)qp_decoder_finish(bstream->qp, &error);
+ buffer_set_used_size(bstream->buf, 0);
+ }
+ i_stream_default_seek_nonseekable(stream, v_offset, mark);
+}
+
+struct istream *i_stream_create_qp_decoder(struct istream *input)
+{
+ struct qp_decoder_istream *bstream;
+
+ bstream = i_new(struct qp_decoder_istream, 1);
+ bstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
+ bstream->buf = buffer_create_dynamic(default_pool, 128);
+ bstream->qp = qp_decoder_init(bstream->buf);
+
+ bstream->istream.iostream.close = i_stream_qp_decoder_close;
+ bstream->istream.read = i_stream_qp_decoder_read;
+ bstream->istream.seek = i_stream_qp_decoder_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), 0);
+}
diff --git a/src/lib-mail/istream-qp-encoder.c b/src/lib-mail/istream-qp-encoder.c
new file mode 100644
index 0000000..25b7589
--- /dev/null
+++ b/src/lib-mail/istream-qp-encoder.c
@@ -0,0 +1,127 @@
+/* Copyright (c) 2017-2018 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);
+ 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_memarea(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), 0);
+}
diff --git a/src/lib-mail/istream-qp.h b/src/lib-mail/istream-qp.h
new file mode 100644
index 0000000..464129b
--- /dev/null
+++ b/src/lib-mail/istream-qp.h
@@ -0,0 +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
diff --git a/src/lib-mail/mail-html2text.c b/src/lib-mail/mail-html2text.c
new file mode 100644
index 0000000..9332b64
--- /dev/null
+++ b/src/lib-mail/mail-html2text.c
@@ -0,0 +1,354 @@
+/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "unichar.h"
+#include "message-parser.h"
+#include "mail-html2text.h"
+
+/* Zero-width space (&#x200B;) apparently also belongs here, but that gets a
+ bit tricky to handle.. is it actually used anywhere? */
+#define HTML_WHITESPACE(c) \
+ ((c) == ' ' || (c) == '\t' || (c) == '\r' || (c) == '\n')
+
+enum html_state {
+ /* regular text */
+ HTML_STATE_TEXT,
+ /* tag outside "quoted string" */
+ HTML_STATE_TAG,
+ /* tag inside "double quoted string" */
+ HTML_STATE_TAG_DQUOTED,
+ /* tag -> "escape\ */
+ HTML_STATE_TAG_DQUOTED_ESCAPE,
+ /* tag inside 'single quoted string' */
+ HTML_STATE_TAG_SQUOTED,
+ /* tag -> 'escape\ */
+ HTML_STATE_TAG_SQUOTED_ESCAPE,
+ /* comment */
+ HTML_STATE_COMMENT,
+ /* comment is ending, we've seen "--" and now just waiting for ">" */
+ HTML_STATE_COMMENT_END,
+ /* (java)script */
+ HTML_STATE_SCRIPT,
+ /* CSS style */
+ HTML_STATE_STYLE,
+ /* <![CDATA[...]]> */
+ HTML_STATE_CDATA
+};
+
+struct mail_html2text {
+ enum mail_html2text_flags flags;
+ enum html_state state;
+ buffer_t *input;
+ unsigned int quote_level;
+ bool add_newline;
+};
+
+static struct {
+ const char *name;
+ unichar_t chr;
+} html_entities[] = {
+#include "html-entities.h"
+};
+
+struct mail_html2text *
+mail_html2text_init(enum mail_html2text_flags flags)
+{
+ struct mail_html2text *ht;
+
+ ht = i_new(struct mail_html2text, 1);
+ ht->flags = flags;
+ ht->input = buffer_create_dynamic(default_pool, 512);
+ return ht;
+}
+
+static size_t
+parse_tag_name(struct mail_html2text *ht,
+ const unsigned char *data, size_t size)
+{
+ size_t i;
+
+ if (size >= 3 && memcmp(data, "!--", 3) == 0) {
+ ht->state = HTML_STATE_COMMENT;
+ return 3 + 1;
+ }
+ if (size >= 7 && i_memcasecmp(data, "script", 6) == 0 &&
+ (HTML_WHITESPACE(data[6]) || data[6] == '>')) {
+ ht->state = HTML_STATE_SCRIPT;
+ return 7 + 1;
+ }
+ if (size >= 6 && i_memcasecmp(data, "style", 5) == 0 &&
+ (HTML_WHITESPACE(data[5]) || data[5] == '>')) {
+ ht->state = HTML_STATE_STYLE;
+ return 6 + 1;
+ }
+ if (size >= 8 && i_memcasecmp(data, "![CDATA[", 8) == 0) {
+ ht->state = HTML_STATE_CDATA;
+ return 8 + 1;
+ }
+
+ if (size >= 11 && i_memcasecmp(data, "blockquote", 10) == 0 &&
+ (HTML_WHITESPACE(data[10]) || data[10] == '>')) {
+ ht->quote_level++;
+ ht->state = HTML_STATE_TAG;
+ return 1;
+ } else if (ht->quote_level > 0 &&
+ size >= 12 && i_memcasecmp(data, "/blockquote>", 12) == 0) {
+ ht->quote_level--;
+ if ((ht->flags & MAIL_HTML2TEXT_FLAG_SKIP_QUOTED) == 0)
+ ht->add_newline = TRUE;
+ ht->state = HTML_STATE_TAG;
+ return 1;
+ }
+ if (size < 12) {
+ /* can we see the whole tag name? */
+ for (i = 0; i < size; i++) {
+ if (HTML_WHITESPACE(data[i]) || data[i] == '>')
+ break;
+ }
+ if (i == size) {
+ /* need more data */
+ return 0;
+ }
+ }
+ ht->state = HTML_STATE_TAG;
+ return 1;
+}
+
+static bool html_entity_get_unichar(const char *name, unichar_t *chr_r)
+{
+ unichar_t chr;
+
+ for (size_t i = 0; i < N_ELEMENTS(html_entities); i++) {
+ if (strcmp(html_entities[i].name, name) == 0) {
+ *chr_r = html_entities[i].chr;
+ return TRUE;
+ }
+ }
+
+ /* maybe it's just encoded binary byte
+ it can be &#nnn; or &#xnnn;
+ */
+ if (name[0] == '#' &&
+ ((name[1] == 'x' &&
+ str_to_uint32_hex(name+2, &chr) == 0) ||
+ str_to_uint32(name+1, &chr) == 0) &&
+ uni_is_valid_ucs4(chr)) {
+ *chr_r = chr;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static size_t parse_entity(const unsigned char *data, size_t size,
+ buffer_t *output)
+{
+ char entity[10];
+ unichar_t chr;
+ size_t i;
+
+ for (i = 0; i < size; i++) {
+ if (HTML_WHITESPACE(data[i]) || i >= sizeof(entity)) {
+ /* broken entity */
+ return 1;
+ }
+ if (data[i] == ';')
+ break;
+ }
+ if (i == size)
+ return 0;
+
+ i_assert(i < sizeof(entity));
+ memcpy(entity, data, i); entity[i] = '\0';
+
+ if (html_entity_get_unichar(entity, &chr))
+ uni_ucs4_to_utf8_c(chr, output);
+ return i + 1 + 1;
+}
+
+static void mail_html2text_add_space(buffer_t *output)
+{
+ const unsigned char *data = output->data;
+
+ if (output->used > 0 && data[output->used-1] != ' ' &&
+ data[output->used-1] != '\n')
+ buffer_append_c(output, ' ');
+}
+
+static size_t
+parse_data(struct mail_html2text *ht,
+ const unsigned char *data, size_t size, buffer_t *output)
+{
+ size_t i, ret;
+
+ for (i = 0; i < size; i++) {
+ unsigned char c = data[i];
+
+ switch (ht->state) {
+ case HTML_STATE_TEXT:
+ if (c == '<') {
+ ret = parse_tag_name(ht, data+i+1, size-i-1);
+ if (ret == 0)
+ return i;
+ i += ret - 1;
+ } else if (ht->quote_level > 0 &&
+ (ht->flags & MAIL_HTML2TEXT_FLAG_SKIP_QUOTED) != 0) {
+ break;
+ } else if (c == '&') {
+ ret = parse_entity(data+i+1, size-i-1, output);
+ if (ret == 0)
+ return i;
+ i += ret - 1;
+ } else {
+ buffer_append_c(output, c);
+ }
+ break;
+ case HTML_STATE_TAG:
+ if (c == '"')
+ ht->state = HTML_STATE_TAG_DQUOTED;
+ else if (c == '\'')
+ ht->state = HTML_STATE_TAG_SQUOTED;
+ else if (c == '>') {
+ ht->state = HTML_STATE_TEXT;
+ if (ht->quote_level > 0 &&
+ (ht->flags & MAIL_HTML2TEXT_FLAG_SKIP_QUOTED) == 0) {
+ buffer_append(output, "\n>", 2);
+ } else if (ht->add_newline) {
+ buffer_append_c(output, '\n');
+ }
+ ht->add_newline = FALSE;
+ mail_html2text_add_space(output);
+ }
+ break;
+ case HTML_STATE_TAG_DQUOTED:
+ if (c == '"')
+ ht->state = HTML_STATE_TAG;
+ else if (c == '\\')
+ ht->state = HTML_STATE_TAG_DQUOTED_ESCAPE;
+ break;
+ case HTML_STATE_TAG_DQUOTED_ESCAPE:
+ ht->state = HTML_STATE_TAG_DQUOTED;
+ break;
+ case HTML_STATE_TAG_SQUOTED:
+ if (c == '\'')
+ ht->state = HTML_STATE_TAG;
+ else if (c == '\\')
+ ht->state = HTML_STATE_TAG_SQUOTED_ESCAPE;
+ break;
+ case HTML_STATE_TAG_SQUOTED_ESCAPE:
+ ht->state = HTML_STATE_TAG_SQUOTED;
+ break;
+ case HTML_STATE_COMMENT:
+ if (c == '-') {
+ if (i+1 == size)
+ return i;
+ if (data[i+1] == '-') {
+ ht->state = HTML_STATE_COMMENT_END;
+ i++;
+ }
+ }
+ break;
+ case HTML_STATE_COMMENT_END:
+ if (c == '>')
+ ht->state = HTML_STATE_TEXT;
+ else if (!HTML_WHITESPACE(c))
+ ht->state = HTML_STATE_COMMENT;
+ break;
+ case HTML_STATE_SCRIPT:
+ if (c == '<') {
+ unsigned int max_len = I_MIN(size-i, 9);
+
+ if (i_memcasecmp(data+i, "</script>", max_len) == 0) {
+ if (max_len < 9)
+ return i;
+ mail_html2text_add_space(output);
+ ht->state = HTML_STATE_TEXT;
+ i += 8;
+ }
+ }
+ break;
+ case HTML_STATE_STYLE:
+ if (c == '<') {
+ unsigned int max_len = I_MIN(size-i, 8);
+
+ if (i_memcasecmp(data+i, "</style>", max_len) == 0) {
+ if (max_len < 8)
+ return i;
+ mail_html2text_add_space(output);
+ ht->state = HTML_STATE_TEXT;
+ i += 7;
+ }
+ }
+ break;
+ case HTML_STATE_CDATA:
+ if (c == ']') {
+ unsigned int max_len = I_MIN(size-i, 3);
+
+ if (i_memcasecmp(data+i, "]]>", max_len) == 0) {
+ if (max_len < 3)
+ return i;
+ ht->state = HTML_STATE_TEXT;
+ i += 2;
+ break;
+ }
+ }
+ if (ht->quote_level == 0 ||
+ (ht->flags & MAIL_HTML2TEXT_FLAG_SKIP_QUOTED) == 0)
+ buffer_append_c(output, c);
+ break;
+ }
+ }
+ return i;
+}
+
+void mail_html2text_more(struct mail_html2text *ht,
+ const unsigned char *data, size_t size,
+ buffer_t *output)
+{
+ size_t pos, inc_size, buf_orig_size;
+
+ i_assert(size > 0);
+
+ while (ht->input->used > 0) {
+ /* we didn't get enough input the last time to know
+ what to do. */
+ buf_orig_size = ht->input->used;
+
+ inc_size = I_MIN(size, 128);
+ buffer_append(ht->input, data, inc_size);
+ pos = parse_data(ht, ht->input->data,
+ ht->input->used, output);
+ if (pos == 0) {
+ /* we need to add more data into buffer */
+ data += inc_size;
+ size -= inc_size;
+ if (size == 0)
+ return;
+ } else if (pos >= buf_orig_size) {
+ /* we parsed forward */
+ data += pos - buf_orig_size;
+ size -= pos - buf_orig_size;
+ buffer_set_used_size(ht->input, 0);
+ } else {
+ /* invalid input - eat away what we parsed so far
+ and retry */
+ buffer_set_used_size(ht->input, buf_orig_size);
+ buffer_delete(ht->input, 0, pos);
+ }
+ }
+ pos = parse_data(ht, data, size, output);
+ buffer_append(ht->input, data + pos, size - pos);
+}
+
+void mail_html2text_deinit(struct mail_html2text **_ht)
+{
+ struct mail_html2text *ht = *_ht;
+
+ if (ht == NULL)
+ return;
+
+ *_ht = NULL;
+ buffer_free(&ht->input);
+ i_free(ht);
+}
diff --git a/src/lib-mail/mail-html2text.h b/src/lib-mail/mail-html2text.h
new file mode 100644
index 0000000..6af484e
--- /dev/null
+++ b/src/lib-mail/mail-html2text.h
@@ -0,0 +1,22 @@
+#ifndef MAIL_HTML2TEXT_H
+#define MAIL_HTML2TEXT_H
+
+enum mail_html2text_flags {
+ MAIL_HTML2TEXT_FLAG_SKIP_QUOTED = 0x01
+};
+
+struct mail_html2text *
+mail_html2text_init(enum mail_html2text_flags flags);
+void mail_html2text_more(struct mail_html2text *ht,
+ const unsigned char *data, size_t size,
+ buffer_t *output);
+void mail_html2text_deinit(struct mail_html2text **ht);
+
+static inline bool
+mail_html2text_content_type_match(const char *content_type)
+{
+ return strcasecmp(content_type, "text/html") == 0 ||
+ strcasecmp(content_type, "application/xhtml+xml") == 0;
+}
+
+#endif
diff --git a/src/lib-mail/mail-types.h b/src/lib-mail/mail-types.h
new file mode 100644
index 0000000..4703c79
--- /dev/null
+++ b/src/lib-mail/mail-types.h
@@ -0,0 +1,25 @@
+#ifndef MAIL_TYPES_H
+#define MAIL_TYPES_H
+
+enum mail_flags {
+ MAIL_ANSWERED = 0x01,
+ MAIL_FLAGGED = 0x02,
+ MAIL_DELETED = 0x04,
+ MAIL_SEEN = 0x08,
+ MAIL_DRAFT = 0x10,
+ MAIL_RECENT = 0x20,
+
+ MAIL_FLAGS_MASK = 0x3f,
+ MAIL_FLAGS_NONRECENT = (MAIL_FLAGS_MASK ^ MAIL_RECENT)
+};
+
+enum modify_type {
+ MODIFY_ADD,
+ MODIFY_REMOVE,
+ MODIFY_REPLACE
+};
+
+ARRAY_DEFINE_TYPE(keywords, const char *);
+ARRAY_DEFINE_TYPE(keyword_indexes, unsigned int);
+
+#endif
diff --git a/src/lib-mail/mail-user-hash.c b/src/lib-mail/mail-user-hash.c
new file mode 100644
index 0000000..88724a6
--- /dev/null
+++ b/src/lib-mail/mail-user-hash.c
@@ -0,0 +1,48 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "md5.h"
+#include "str.h"
+#include "var-expand.h"
+#include "mail-user-hash.h"
+
+bool mail_user_hash(const char *username, const char *format,
+ unsigned int *hash_r, const char **error_r)
+{
+ unsigned char md5[MD5_RESULTLEN];
+ unsigned int i, hash = 0;
+ int ret = 1;
+
+ if (strcmp(format, "%u") == 0) {
+ /* fast path */
+ md5_get_digest(username, strlen(username), md5);
+ } else if (strcmp(format, "%Lu") == 0) {
+ /* almost as fast path */
+ T_BEGIN {
+ md5_get_digest(t_str_lcase(username),
+ strlen(username), md5);
+ } T_END;
+ } else T_BEGIN {
+ const struct var_expand_table tab[] = {
+ { 'u', username, "user" },
+ { 'n', t_strcut(username, '@'), "username" },
+ { 'd', i_strchr_to_next(username, '@'), "domain" },
+ { '\0', NULL, NULL }
+ };
+ string_t *str = t_str_new(128);
+
+ ret = var_expand(str, format, tab, error_r);
+ i_assert(ret >= 0);
+ md5_get_digest(str_data(str), str_len(str), md5);
+ } T_END_PASS_STR_IF(ret == 0, error_r);
+ for (i = 0; i < sizeof(hash); i++)
+ hash = (hash << CHAR_BIT) | md5[i];
+ if (hash == 0) {
+ /* Make sure we don't return the hash as 0, since it's often
+ treated in a special way that won't work well. For example
+ trying to insert it into a hash table will assert-crash. */
+ hash = 1;
+ }
+ *hash_r = hash;
+ return ret > 0;
+}
diff --git a/src/lib-mail/mail-user-hash.h b/src/lib-mail/mail-user-hash.h
new file mode 100644
index 0000000..e3a9e59
--- /dev/null
+++ b/src/lib-mail/mail-user-hash.h
@@ -0,0 +1,10 @@
+#ifndef MAIL_USER_HASH
+#define MAIL_USER_HASH
+
+/* Get a hash for username, based on given format. The format can use
+ %n, %d and %u variables. The returned hash is never 0.
+ Returns TRUE if ok, FALSE if format is invalid. */
+bool mail_user_hash(const char *username, const char *format,
+ unsigned int *hash_r, const char **error_r);
+
+#endif
diff --git a/src/lib-mail/mbox-from.c b/src/lib-mail/mbox-from.c
new file mode 100644
index 0000000..452e32e
--- /dev/null
+++ b/src/lib-mail/mbox-from.c
@@ -0,0 +1,301 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "str.h"
+#include "utc-mktime.h"
+#include "utc-offset.h"
+#include "mbox-from.h"
+
+#include <time.h>
+#include <ctype.h>
+
+static const char *weekdays[] = {
+ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+static const char *months[] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+static int mbox_parse_month(const unsigned char *msg, struct tm *tm)
+{
+ int i;
+
+ for (i = 0; i < 12; i++) {
+ if (i_memcasecmp(months[i], msg, 3) == 0) {
+ tm->tm_mon = i;
+ break;
+ }
+ }
+
+ if (i == 12 && memcmp(msg, "???", 3) == 0) {
+ /* just a hack to parse one special mbox I have :) */
+ i = 0;
+ }
+
+ if (i == 12 || msg[3] != ' ')
+ return -1;
+ return 0;
+}
+
+static int mbox_parse_year(const unsigned char *msg, struct tm *tm)
+{
+ if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]) ||
+ !i_isdigit(msg[2]) || !i_isdigit(msg[3]))
+ return -1;
+
+ tm->tm_year = (msg[0]-'0') * 1000 + (msg[1]-'0') * 100 +
+ (msg[2]-'0') * 10 + (msg[3]-'0') - 1900;
+ return 0;
+}
+
+int mbox_from_parse(const unsigned char *msg, size_t size,
+ time_t *time_r, int *tz_offset_r, char **sender_r)
+{
+ const unsigned char *msg_start, *sender_end, *msg_end;
+ struct tm tm;
+ bool esc, alt_stamp, seen_timezone = FALSE;
+ int timezone_secs = 0;
+ time_t t;
+
+ *time_r = (time_t)-1;
+ *sender_r = NULL;
+
+ /* <sender> <date> <moreinfo> */
+ msg_start = msg;
+ msg_end = msg + size;
+
+ /* get sender */
+ if (msg < msg_end && *msg == '"') {
+ /* "x y z"@domain - skip the quoted part */
+ esc = FALSE;
+ msg++;
+ while (msg < msg_end && (*msg != '"' || esc)) {
+ if (*msg == '\r' || *msg == '\n')
+ return -1;
+ esc = *msg == '\\';
+ msg++;
+ }
+ msg++;
+ }
+
+ while (msg < msg_end && *msg != ' ') {
+ if (*msg == '\r' || *msg == '\n')
+ return -1;
+ msg++;
+ }
+ sender_end = msg;
+ while (msg < msg_end && *msg == ' ') msg++;
+
+ /* next 29 chars should be in the date in asctime() format, eg.
+ "Thu Nov 9 22:33:52 2001 +0300"
+
+ - Some put the timezone before the year
+ - Some use a named timezone before or after year, which we ignore
+ - Some don't include seconds (-3)
+ - Some don't include timezone (-5)
+ */
+ if (msg+29-3-5 > msg_end)
+ return -1;
+
+ i_zero(&tm);
+
+ /* skip weekday */
+ msg += 4;
+
+ /* month */
+ if (mbox_parse_month(msg, &tm) < 0) {
+ /* Try alternate timestamp: "Thu, 9 Nov 2002 22:33:52" */
+ alt_stamp = TRUE;
+ msg++;
+
+ if (!i_isdigit(msg[0]))
+ return -1;
+ tm.tm_mday = msg[0]-'0';
+ msg++;
+
+ if (i_isdigit(msg[0])) {
+ tm.tm_mday = tm.tm_mday*10 + msg[0]-'0';
+ msg++;
+ }
+ if (msg[0] != ' ')
+ return -1;
+ msg++;
+
+ if (mbox_parse_month(msg, &tm) < 0)
+ return -1;
+ msg += 4;
+
+ if (mbox_parse_year(msg, &tm) < 0)
+ return -1;
+ msg += 5;
+ } else {
+ alt_stamp = FALSE;
+ msg += 4;
+
+ /* day. single digit is usually preceded by extra space */
+ if (msg[0] == ' ')
+ msg++;
+ if (msg[1] == ' ') {
+ if (!i_isdigit(msg[0]))
+ return -1;
+ tm.tm_mday = msg[0]-'0';
+ msg += 2;
+ } else {
+ if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]) || msg[2] != ' ')
+ return -1;
+ tm.tm_mday = (msg[0]-'0') * 10 + (msg[1]-'0');
+ msg += 3;
+ }
+ }
+ if (tm.tm_mday == 0)
+ tm.tm_mday = 1;
+
+ /* hour */
+ if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]) || msg[2] != ':')
+ return -1;
+ tm.tm_hour = (msg[0]-'0') * 10 + (msg[1]-'0');
+ msg += 3;
+
+ /* minute */
+ if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]))
+ return -1;
+ tm.tm_min = (msg[0]-'0') * 10 + (msg[1]-'0');
+ msg += 2;
+
+ /* optional second */
+ if (msg[0] == ':') {
+ msg++;
+ if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]))
+ return -1;
+ tm.tm_sec = (msg[0]-'0') * 10 + (msg[1]-'0');
+ msg += 2;
+
+ if (!alt_stamp) {
+ if (msg[0] == ' ')
+ msg++;
+ else
+ return -1;
+ }
+ } else if (!alt_stamp) {
+ if (msg[0] != ' ')
+ return -1;
+ msg++;
+ }
+
+ /* optional named timezone */
+ if (alt_stamp)
+ ;
+ else if (!i_isdigit(msg[0]) || !i_isdigit(msg[1]) ||
+ !i_isdigit(msg[2]) || !i_isdigit(msg[3])) {
+ /* skip to next space */
+ while (msg < msg_end && *msg != ' ') {
+ if (*msg == '\r' || *msg == '\n')
+ return -1;
+ msg++;
+ }
+ if (msg+5 > msg_end)
+ return -1;
+ msg++;
+ } else if ((msg[0] == '-' || msg[0] == '+') &&
+ i_isdigit(msg[1]) && i_isdigit(msg[2]) &&
+ i_isdigit(msg[3]) && i_isdigit(msg[4]) && msg[5] == ' ') {
+ /* numeric timezone, use it */
+ seen_timezone = TRUE;
+ timezone_secs = (msg[1]-'0') * 10*60*60 + (msg[2]-'0') * 60*60 +
+ (msg[3]-'0') * 10 + (msg[4]-'0');
+ if (msg[0] == '-') timezone_secs = -timezone_secs;
+ msg += 6;
+ }
+
+ if (!alt_stamp) {
+ /* year */
+ if (mbox_parse_year(msg, &tm) < 0)
+ return -1;
+ msg += 4;
+ }
+
+ tm.tm_isdst = -1;
+ if (!seen_timezone && msg != msg_end &&
+ msg[0] == ' ' && (msg[1] == '-' || msg[1] == '+') &&
+ i_isdigit(msg[2]) && i_isdigit(msg[3]) &&
+ i_isdigit(msg[4]) && i_isdigit(msg[5])) {
+ seen_timezone = TRUE;
+ timezone_secs = (msg[2]-'0') * 10*60*60 + (msg[3]-'0') * 60*60 +
+ (msg[4]-'0') * 10 + (msg[5]-'0');
+ if (msg[1] == '-') timezone_secs = -timezone_secs;
+ }
+
+ if (seen_timezone) {
+ t = utc_mktime(&tm);
+ if (t == (time_t)-1)
+ return -1;
+
+ t -= timezone_secs;
+ *time_r = t;
+ *tz_offset_r = timezone_secs/60;
+ } else {
+ /* assume local timezone */
+ *time_r = mktime(&tm);
+ *tz_offset_r = utc_offset(localtime(time_r), *time_r);
+ }
+
+ *sender_r = i_strdup_until(msg_start, sender_end);
+ return 0;
+}
+
+const char *mbox_from_create(const char *sender, time_t timestamp)
+{
+ string_t *str;
+ struct tm *tm;
+ int year;
+
+ str = t_str_new(256);
+ str_append(str, "From ");
+ str_append(str, sender);
+ str_append(str, " ");
+
+ /* we could use simply asctime(), but i18n etc. may break it.
+ Example: "Thu Nov 29 22:33:52 2001" */
+ tm = localtime(&timestamp);
+
+ /* week day */
+ str_append(str, weekdays[tm->tm_wday]);
+ str_append_c(str, ' ');
+
+ /* month */
+ str_append(str, months[tm->tm_mon]);
+ str_append_c(str, ' ');
+
+ /* day */
+ str_append_c(str, (tm->tm_mday / 10) + '0');
+ str_append_c(str, (tm->tm_mday % 10) + '0');
+ str_append_c(str, ' ');
+
+ /* hour */
+ str_append_c(str, (tm->tm_hour / 10) + '0');
+ str_append_c(str, (tm->tm_hour % 10) + '0');
+ str_append_c(str, ':');
+
+ /* minute */
+ str_append_c(str, (tm->tm_min / 10) + '0');
+ str_append_c(str, (tm->tm_min % 10) + '0');
+ str_append_c(str, ':');
+
+ /* second */
+ str_append_c(str, (tm->tm_sec / 10) + '0');
+ str_append_c(str, (tm->tm_sec % 10) + '0');
+ str_append_c(str, ' ');
+
+ /* year */
+ year = tm->tm_year + 1900;
+ str_append_c(str, (year / 1000) + '0');
+ str_append_c(str, ((year / 100) % 10) + '0');
+ str_append_c(str, ((year / 10) % 10) + '0');
+ str_append_c(str, (year % 10) + '0');
+
+ str_append_c(str, '\n');
+ return str_c(str);
+}
diff --git a/src/lib-mail/mbox-from.h b/src/lib-mail/mbox-from.h
new file mode 100644
index 0000000..fbca066
--- /dev/null
+++ b/src/lib-mail/mbox-from.h
@@ -0,0 +1,12 @@
+#ifndef MBOX_FROM_H
+#define MBOX_FROM_H
+
+/* Parse time and sender from mbox-compatible From_-line. msg points to the
+ data after "From ". */
+int mbox_from_parse(const unsigned char *msg, size_t size,
+ time_t *time_r, int *tz_offset_r, char **sender_r);
+/* Return a mbox-compatible From_-line using given sender and time.
+ The returned string begins with "From ". */
+const char *mbox_from_create(const char *sender, time_t timestamp);
+
+#endif
diff --git a/src/lib-mail/message-address.c b/src/lib-mail/message-address.c
new file mode 100644
index 0000000..fb06afa
--- /dev/null
+++ b/src/lib-mail/message-address.c
@@ -0,0 +1,672 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "smtp-address.h"
+#include "message-parser.h"
+#include "message-address.h"
+#include "rfc822-parser.h"
+
+struct message_address_parser_context {
+ pool_t pool;
+ struct rfc822_parser_context parser;
+
+ struct message_address *first_addr, *last_addr, addr;
+ string_t *str;
+
+ bool fill_missing, non_strict_dots;
+};
+
+static void add_address(struct message_address_parser_context *ctx)
+{
+ struct message_address *addr;
+
+ addr = p_new(ctx->pool, struct message_address, 1);
+
+ memcpy(addr, &ctx->addr, sizeof(ctx->addr));
+ i_zero(&ctx->addr);
+
+ if (ctx->first_addr == NULL)
+ ctx->first_addr = addr;
+ else
+ ctx->last_addr->next = addr;
+ ctx->last_addr = addr;
+}
+
+/* quote with "" and escape all '\', '"' and "'" characters if need */
+static void str_append_maybe_escape(string_t *dest, const char *cstr, bool escape_dot)
+{
+ const char *p;
+
+ /* see if we need to quote it */
+ for (p = cstr; *p != '\0'; p++) {
+ if (!IS_ATEXT(*p) && (escape_dot || *p != '.'))
+ break;
+ }
+
+ if (*p == '\0') {
+ str_append_data(dest, cstr, (size_t) (p - cstr));
+ return;
+ }
+
+ /* see if we need to escape it */
+ for (p = cstr; *p != '\0'; p++) {
+ if (IS_ESCAPED_CHAR(*p))
+ break;
+ }
+
+ if (*p == '\0') {
+ /* only quote */
+ str_append_c(dest, '"');
+ str_append_data(dest, cstr, (size_t) (p - cstr));
+ str_append_c(dest, '"');
+ return;
+ }
+
+ /* quote and escape */
+ str_append_c(dest, '"');
+ str_append_data(dest, cstr, (size_t) (p - cstr));
+
+ for (; *p != '\0'; p++) {
+ if (IS_ESCAPED_CHAR(*p))
+ str_append_c(dest, '\\');
+ str_append_c(dest, *p);
+ }
+
+ str_append_c(dest, '"');
+}
+
+static int
+parse_nonstrict_dot_atom(struct rfc822_parser_context *ctx, string_t *str)
+{
+ int ret = -1;
+
+ do {
+ while (*ctx->data == '.') {
+ str_append_c(str, '.');
+ ctx->data++;
+ if (ctx->data == ctx->end) {
+ /* @domain is missing, but local-part
+ parsing was successful */
+ return 0;
+ }
+ ret = 1;
+ }
+ if (*ctx->data == '@')
+ break;
+ ret = rfc822_parse_atom(ctx, str);
+ } while (ret > 0 && *ctx->data == '.');
+ return ret;
+}
+
+static int parse_local_part(struct message_address_parser_context *ctx)
+{
+ int ret;
+
+ /*
+ local-part = dot-atom / quoted-string / obs-local-part
+ obs-local-part = word *("." word)
+ */
+ i_assert(ctx->parser.data < ctx->parser.end);
+
+ str_truncate(ctx->str, 0);
+ if (*ctx->parser.data == '"')
+ ret = rfc822_parse_quoted_string(&ctx->parser, ctx->str);
+ else if (!ctx->non_strict_dots)
+ ret = rfc822_parse_dot_atom(&ctx->parser, ctx->str);
+ else
+ ret = parse_nonstrict_dot_atom(&ctx->parser, ctx->str);
+ if (ret < 0)
+ return -1;
+
+ ctx->addr.mailbox = p_strdup(ctx->pool, str_c(ctx->str));
+ return ret;
+}
+
+static int parse_domain(struct message_address_parser_context *ctx)
+{
+ int ret;
+
+ str_truncate(ctx->str, 0);
+ if ((ret = rfc822_parse_domain(&ctx->parser, ctx->str)) < 0)
+ return -1;
+
+ ctx->addr.domain = p_strdup(ctx->pool, str_c(ctx->str));
+ return ret;
+}
+
+static int parse_domain_list(struct message_address_parser_context *ctx)
+{
+ int ret;
+
+ /* obs-domain-list = "@" domain *(*(CFWS / "," ) [CFWS] "@" domain) */
+ str_truncate(ctx->str, 0);
+ for (;;) {
+ if (ctx->parser.data >= ctx->parser.end)
+ return 0;
+
+ if (*ctx->parser.data != '@')
+ break;
+
+ if (str_len(ctx->str) > 0)
+ str_append_c(ctx->str, ',');
+
+ str_append_c(ctx->str, '@');
+ if ((ret = rfc822_parse_domain(&ctx->parser, ctx->str)) <= 0)
+ return ret;
+
+ while (rfc822_skip_lwsp(&ctx->parser) > 0 &&
+ *ctx->parser.data == ',')
+ ctx->parser.data++;
+ }
+ ctx->addr.route = p_strdup(ctx->pool, str_c(ctx->str));
+ return 1;
+}
+
+static int parse_angle_addr(struct message_address_parser_context *ctx,
+ bool parsing_path)
+{
+ /* "<" [ "@" route ":" ] local-part "@" domain ">" */
+ i_assert(*ctx->parser.data == '<');
+ ctx->parser.data++;
+
+ if (rfc822_skip_lwsp(&ctx->parser) <= 0)
+ return -1;
+
+ if (*ctx->parser.data == '@') {
+ if (parse_domain_list(ctx) > 0 && *ctx->parser.data == ':') {
+ ctx->parser.data++;
+ } else if (parsing_path && (ctx->parser.data >= ctx->parser.end || *ctx->parser.data != ':')) {
+ return -1;
+ } else {
+ if (ctx->fill_missing)
+ ctx->addr.route = "INVALID_ROUTE";
+ if (ctx->parser.data >= ctx->parser.end)
+ return -1;
+ /* try to continue anyway */
+ }
+ if (rfc822_skip_lwsp(&ctx->parser) <= 0)
+ return -1;
+ }
+
+ if (*ctx->parser.data == '>') {
+ /* <> address isn't valid */
+ } else {
+ if (parse_local_part(ctx) <= 0)
+ return -1;
+ if (*ctx->parser.data == '@') {
+ if (parse_domain(ctx) <= 0)
+ return -1;
+ }
+ }
+
+ if (*ctx->parser.data != '>')
+ return -1;
+ ctx->parser.data++;
+
+ return rfc822_skip_lwsp(&ctx->parser);
+}
+
+static int parse_name_addr(struct message_address_parser_context *ctx)
+{
+ /*
+ name-addr = [display-name] angle-addr
+ display-name = phrase
+ */
+ str_truncate(ctx->str, 0);
+ if (rfc822_parse_phrase(&ctx->parser, ctx->str) <= 0 ||
+ *ctx->parser.data != '<')
+ return -1;
+
+ ctx->addr.name = p_strdup(ctx->pool, str_c(ctx->str));
+ if (*ctx->addr.name == '\0') {
+ /* Cope with "<address>" without display name */
+ ctx->addr.name = NULL;
+ }
+ if (parse_angle_addr(ctx, FALSE) < 0) {
+ /* broken */
+ if (ctx->fill_missing)
+ ctx->addr.domain = "SYNTAX_ERROR";
+ ctx->addr.invalid_syntax = TRUE;
+ }
+ return ctx->parser.data < ctx->parser.end ? 1 : 0;
+}
+
+static int parse_addr_spec(struct message_address_parser_context *ctx)
+{
+ /* addr-spec = local-part "@" domain */
+ int ret, ret2 = -2;
+
+ i_assert(ctx->parser.data < ctx->parser.end);
+
+ str_truncate(ctx->parser.last_comment, 0);
+
+ bool quoted_string = *ctx->parser.data == '"';
+ ret = parse_local_part(ctx);
+ if (ret <= 0) {
+ /* end of input or parsing local-part failed */
+ ctx->addr.invalid_syntax = TRUE;
+ }
+ if (ret != 0 && ctx->parser.data < ctx->parser.end &&
+ *ctx->parser.data == '@') {
+ ret2 = parse_domain(ctx);
+ if (ret2 <= 0)
+ ret = ret2;
+ }
+
+ if (str_len(ctx->parser.last_comment) > 0)
+ ctx->addr.name = p_strdup(ctx->pool, str_c(ctx->parser.last_comment));
+ else if (ret2 == -2) {
+ /* So far we've read user without @domain and without
+ (Display Name). We'll assume that a single "user" (already
+ read into addr.mailbox) is a mailbox, but if it's followed
+ by anything else it's a display-name. */
+ str_append_c(ctx->str, ' ');
+ size_t orig_str_len = str_len(ctx->str);
+ (void)rfc822_parse_phrase(&ctx->parser, ctx->str);
+ if (str_len(ctx->str) != orig_str_len) {
+ ctx->addr.mailbox = NULL;
+ ctx->addr.name = p_strdup(ctx->pool, str_c(ctx->str));
+ } else {
+ if (!quoted_string)
+ ctx->addr.domain = "";
+ }
+ ctx->addr.invalid_syntax = TRUE;
+ ret = -1;
+ }
+ return ret;
+}
+
+static void add_fixed_address(struct message_address_parser_context *ctx)
+{
+ if (ctx->addr.mailbox == NULL) {
+ ctx->addr.mailbox = !ctx->fill_missing ? "" : "MISSING_MAILBOX";
+ ctx->addr.invalid_syntax = TRUE;
+ }
+ if (ctx->addr.domain == NULL || ctx->addr.domain[0] == '\0') {
+ ctx->addr.domain = !ctx->fill_missing ? "" : "MISSING_DOMAIN";
+ ctx->addr.invalid_syntax = TRUE;
+ }
+ add_address(ctx);
+}
+
+static int parse_mailbox(struct message_address_parser_context *ctx)
+{
+ const unsigned char *start;
+ int ret;
+
+ /* mailbox = name-addr / addr-spec */
+ start = ctx->parser.data;
+ if ((ret = parse_name_addr(ctx)) < 0) {
+ /* nope, should be addr-spec */
+ ctx->parser.data = start;
+ ret = parse_addr_spec(ctx);
+ if (ctx->addr.invalid_syntax && ctx->addr.name == NULL &&
+ ctx->addr.mailbox != NULL && ctx->addr.domain == NULL) {
+ ctx->addr.name = ctx->addr.mailbox;
+ ctx->addr.mailbox = NULL;
+ }
+ }
+
+ if (ret < 0)
+ ctx->addr.invalid_syntax = TRUE;
+ add_fixed_address(ctx);
+ return ret;
+}
+
+static int parse_group(struct message_address_parser_context *ctx)
+{
+ int ret;
+
+ /*
+ group = display-name ":" [mailbox-list / CFWS] ";" [CFWS]
+ display-name = phrase
+ */
+ str_truncate(ctx->str, 0);
+ if (rfc822_parse_phrase(&ctx->parser, ctx->str) <= 0 ||
+ *ctx->parser.data != ':')
+ return -1;
+
+ /* from now on don't return -1 even if there are problems, so that
+ the caller knows this is a group */
+ ctx->parser.data++;
+ if ((ret = rfc822_skip_lwsp(&ctx->parser)) <= 0)
+ ctx->addr.invalid_syntax = TRUE;
+
+ ctx->addr.mailbox = p_strdup(ctx->pool, str_c(ctx->str));
+ add_address(ctx);
+
+ if (ret > 0 && *ctx->parser.data != ';') {
+ for (;;) {
+ /* mailbox-list =
+ (mailbox *("," mailbox)) / obs-mbox-list */
+ if (parse_mailbox(ctx) <= 0) {
+ /* broken mailbox - try to continue anyway. */
+ }
+ if (ctx->parser.data >= ctx->parser.end ||
+ *ctx->parser.data != ',')
+ break;
+ ctx->parser.data++;
+ if (rfc822_skip_lwsp(&ctx->parser) <= 0) {
+ ret = -1;
+ break;
+ }
+ }
+ }
+ if (ret >= 0) {
+ if (ctx->parser.data >= ctx->parser.end ||
+ *ctx->parser.data != ';')
+ ret = -1;
+ else {
+ ctx->parser.data++;
+ ret = rfc822_skip_lwsp(&ctx->parser);
+ }
+ }
+ if (ret < 0)
+ ctx->addr.invalid_syntax = TRUE;
+
+ add_address(ctx);
+ return ret == 0 ? 0 : 1;
+}
+
+static int parse_address(struct message_address_parser_context *ctx)
+{
+ const unsigned char *start;
+ int ret;
+
+ /* address = mailbox / group */
+ start = ctx->parser.data;
+ if ((ret = parse_group(ctx)) < 0) {
+ /* not a group, try mailbox */
+ ctx->parser.data = start;
+ ret = parse_mailbox(ctx);
+ }
+ return ret;
+}
+
+static int parse_address_list(struct message_address_parser_context *ctx,
+ unsigned int max_addresses)
+{
+ int ret = 0;
+
+ /* address-list = (address *("," address)) / obs-addr-list */
+ while (max_addresses > 0) {
+ max_addresses--;
+ if ((ret = parse_address(ctx)) == 0)
+ break;
+ if (ctx->parser.data >= ctx->parser.end ||
+ *ctx->parser.data != ',') {
+ ret = -1;
+ break;
+ }
+ ctx->parser.data++;
+ if ((ret = rfc822_skip_lwsp(&ctx->parser)) <= 0) {
+ if (ret < 0) {
+ /* ends with some garbage */
+ add_fixed_address(ctx);
+ }
+ break;
+ }
+ }
+ return ret;
+}
+
+static int parse_path(struct message_address_parser_context *ctx)
+{
+ int ret;
+
+ if (rfc822_skip_lwsp(&ctx->parser) <= 0)
+ return -1;
+ if (*ctx->parser.data != '<') {
+ /* Cope with paths that omit < and >. This is a syntax
+ violation, but we allow it to account for a rather wide
+ selection of software that does not follow the standards.
+ */
+ if ((ret=parse_local_part(ctx)) > 0 &&
+ *ctx->parser.data == '@') {
+ ret = parse_domain(ctx);
+ }
+ } else {
+ ret = parse_angle_addr(ctx, TRUE);
+ }
+ if (ret < 0 || (ret=rfc822_skip_lwsp(&ctx->parser)) < 0 ||
+ ctx->parser.data != ctx->parser.end ||
+ (ctx->addr.mailbox != NULL &&
+ (ctx->addr.domain == NULL || *ctx->addr.domain == '\0')) ||
+ (ctx->addr.mailbox == NULL && ctx->addr.domain != NULL)) {
+ ctx->addr.invalid_syntax = TRUE;
+ ret = -1;
+ }
+ add_address(ctx);
+ return ret;
+}
+
+static struct message_address *
+message_address_parse_real(pool_t pool, const unsigned char *data, size_t size,
+ unsigned int max_addresses,
+ enum message_address_parse_flags flags)
+{
+ struct message_address_parser_context ctx;
+
+ i_zero(&ctx);
+
+ rfc822_parser_init(&ctx.parser, data, size, t_str_new(128));
+ ctx.parser.nul_replacement_str = RFC822_NUL_REPLACEMENT_STR;
+ ctx.pool = pool;
+ ctx.str = t_str_new(128);
+ ctx.fill_missing = (flags & MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING) != 0;
+ ctx.non_strict_dots = (flags & MESSAGE_ADDRESS_PARSE_FLAG_STRICT_DOTS) == 0;
+
+ if (rfc822_skip_lwsp(&ctx.parser) <= 0) {
+ /* no addresses */
+ } else {
+ (void)parse_address_list(&ctx, max_addresses);
+ }
+ rfc822_parser_deinit(&ctx.parser);
+ return ctx.first_addr;
+}
+
+static int
+message_address_parse_path_real(pool_t pool, const unsigned char *data,
+ size_t size, struct message_address **addr_r)
+{
+ struct message_address_parser_context ctx;
+ int ret;
+
+ i_zero(&ctx);
+ *addr_r = NULL;
+
+ rfc822_parser_init(&ctx.parser, data, size, NULL);
+ ctx.pool = pool;
+ ctx.str = t_str_new(128);
+
+ ret = parse_path(&ctx);
+
+ rfc822_parser_deinit(&ctx.parser);
+ *addr_r = ctx.first_addr;
+ return (ret < 0 ? -1 : 0);
+}
+
+struct message_address *
+message_address_parse(pool_t pool, const unsigned char *data, size_t size,
+ unsigned int max_addresses,
+ enum message_address_parse_flags flags)
+{
+ struct message_address *addr;
+
+ if (pool->datastack_pool) {
+ return message_address_parse_real(pool, data, size,
+ max_addresses, flags);
+ }
+ T_BEGIN {
+ addr = message_address_parse_real(pool, data, size,
+ max_addresses, flags);
+ } T_END;
+ return addr;
+}
+
+int message_address_parse_path(pool_t pool, const unsigned char *data,
+ size_t size, struct message_address **addr_r)
+{
+ int ret;
+
+ if (pool->datastack_pool) {
+ return message_address_parse_path_real(pool, data, size, addr_r);
+ }
+ T_BEGIN {
+ ret = message_address_parse_path_real(pool, data, size, addr_r);
+ } T_END;
+ return ret;
+}
+
+void message_address_write(string_t *str, const struct message_address *addr)
+{
+ const char *tmp;
+ bool first = TRUE, in_group = FALSE;
+
+ if (addr == NULL)
+ return;
+
+ /* <> path */
+ if (addr->mailbox == NULL && addr->domain == NULL) {
+ i_assert(addr->next == NULL);
+ str_append(str, "<>");
+ return;
+ }
+
+ /* a) mailbox@domain
+ b) name <@route:mailbox@domain>
+ c) group: .. ; */
+
+ while (addr != NULL) {
+ if (first)
+ first = FALSE;
+ else
+ str_append(str, ", ");
+
+ if (addr->domain == NULL) {
+ if (!in_group) {
+ /* beginning of group. mailbox is the group
+ name, others are NULL. */
+ if (addr->mailbox != NULL && *addr->mailbox != '\0') {
+ /* check for MIME encoded-word */
+ if (strstr(addr->mailbox, "=?") != NULL)
+ /* MIME encoded-word MUST NOT appear within a 'quoted-string'
+ so escaping and quoting of phrase is not possible, instead
+ use obsolete RFC822 phrase syntax which allow spaces */
+ str_append(str, addr->mailbox);
+ else
+ str_append_maybe_escape(str, addr->mailbox, TRUE);
+ } else {
+ /* empty group name needs to be quoted */
+ str_append(str, "\"\"");
+ }
+ str_append(str, ": ");
+ first = TRUE;
+ } else {
+ /* end of group. all fields should be NULL. */
+ i_assert(addr->mailbox == NULL);
+
+ /* cut out the ", " */
+ tmp = str_c(str)+str_len(str)-2;
+ i_assert((tmp[0] == ',' || tmp[0] == ':') && tmp[1] == ' ');
+ if (tmp[0] == ',' && tmp[1] == ' ')
+ str_truncate(str, str_len(str)-2);
+ else if (tmp[0] == ':' && tmp[1] == ' ')
+ str_truncate(str, str_len(str)-1);
+ str_append_c(str, ';');
+ }
+
+ in_group = !in_group;
+ } else {
+ /* "Display Name" <mailbox@domain> */
+ i_assert(addr->mailbox != NULL);
+
+ if (addr->name != NULL) {
+ /* check for MIME encoded-word */
+ if (strstr(addr->name, "=?") != NULL)
+ /* MIME encoded-word MUST NOT appear within a 'quoted-string'
+ so escaping and quoting of phrase is not possible, instead
+ use obsolete RFC822 phrase syntax which allow spaces */
+ str_append(str, addr->name);
+ else
+ str_append_maybe_escape(str, addr->name, TRUE);
+ }
+ if (addr->route != NULL ||
+ addr->mailbox[0] != '\0' ||
+ addr->domain[0] != '\0') {
+ if (addr->name != NULL && addr->name[0] != '\0')
+ str_append_c(str, ' ');
+ str_append_c(str, '<');
+ if (addr->route != NULL) {
+ str_append(str, addr->route);
+ str_append_c(str, ':');
+ }
+ if (addr->mailbox[0] == '\0')
+ str_append(str, "\"\"");
+ else
+ str_append_maybe_escape(str, addr->mailbox, FALSE);
+ if (addr->domain[0] != '\0') {
+ str_append_c(str, '@');
+ str_append(str, addr->domain);
+ }
+ str_append_c(str, '>');
+ }
+ }
+
+ addr = addr->next;
+ }
+}
+
+const char *message_address_to_string(const struct message_address *addr)
+{
+ string_t *str = t_str_new(256);
+ message_address_write(str, addr);
+ return str_c(str);
+}
+
+const char *message_address_first_to_string(const struct message_address *addr)
+{
+ struct message_address first_addr;
+
+ first_addr = *addr;
+ first_addr.next = NULL;
+ first_addr.route = NULL;
+ return message_address_to_string(&first_addr);
+}
+
+void message_address_init(struct message_address *addr,
+ const char *name, const char *mailbox, const char *domain)
+{
+ i_zero(addr);
+ addr->name = name;
+ addr->mailbox = mailbox;
+ addr->domain = domain;
+}
+
+void message_address_init_from_smtp(struct message_address *addr,
+ const char *name, const struct smtp_address *smtp_addr)
+{
+ i_zero(addr);
+ addr->name = name;
+ addr->mailbox = smtp_addr->localpart;
+ addr->domain = smtp_addr->domain;
+}
+
+static const char *address_headers[] = {
+ "From", "Sender", "Reply-To",
+ "To", "Cc", "Bcc",
+ "Resent-From", "Resent-Sender", "Resent-To", "Resent-Cc", "Resent-Bcc"
+};
+
+bool message_header_is_address(const char *hdr_name)
+{
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(address_headers); i++) {
+ if (strcasecmp(hdr_name, address_headers[i]) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
diff --git a/src/lib-mail/message-address.h b/src/lib-mail/message-address.h
new file mode 100644
index 0000000..8370397
--- /dev/null
+++ b/src/lib-mail/message-address.h
@@ -0,0 +1,61 @@
+#ifndef MESSAGE_ADDRESS_H
+#define MESSAGE_ADDRESS_H
+
+struct smtp_address;
+
+enum message_address_parse_flags {
+ /* If enabled, missing mailbox and domain are set to MISSING_MAILBOX
+ and MISSING_DOMAIN strings. Otherwise they're set to "". */
+ MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING = BIT(0),
+ /* Require local-part to strictly adhere to RFC5322 when parsing dots.
+ For example ".user", "us..ser" and "user." will be invalid. This
+ isn't enabled by default, because these kind of invalid addresses
+ are commonly used in Japan. */
+ MESSAGE_ADDRESS_PARSE_FLAG_STRICT_DOTS = BIT(1),
+};
+
+/* group: ... ; will be stored like:
+ {name = NULL, NULL, "group", NULL}, ..., {NULL, NULL, NULL, NULL}
+*/
+struct message_address {
+ struct message_address *next;
+
+ /* display-name */
+ const char *name;
+ /* route string contains the @ prefix */
+ const char *route;
+ /* local-part */
+ const char *mailbox;
+ const char *domain;
+ /* there were errors when parsing this address */
+ bool invalid_syntax;
+};
+
+/* Parse message addresses from given data. Note that giving an empty string
+ will return NULL since there are no addresses. */
+struct message_address *
+message_address_parse(pool_t pool, const unsigned char *data, size_t size,
+ unsigned int max_addresses,
+ enum message_address_parse_flags flags);
+
+/* Parse RFC 5322 "path" (Return-Path header) from given data. Returns -1 if
+ the path is invalid and 0 otherwise.
+ */
+int message_address_parse_path(pool_t pool, const unsigned char *data,
+ size_t size, struct message_address **addr_r);
+
+void message_address_init(struct message_address *addr,
+ const char *name, const char *mailbox, const char *domain)
+ ATTR_NULL(1);
+void message_address_init_from_smtp(struct message_address *addr,
+ const char *name, const struct smtp_address *smtp_addr)
+ ATTR_NULL(1);
+
+void message_address_write(string_t *str, const struct message_address *addr);
+const char *message_address_to_string(const struct message_address *addr);
+const char *message_address_first_to_string(const struct message_address *addr);
+
+/* Returns TRUE if header is known to be an address */
+bool message_header_is_address(const char *hdr_name);
+
+#endif
diff --git a/src/lib-mail/message-binary-part.c b/src/lib-mail/message-binary-part.c
new file mode 100644
index 0000000..20416ff
--- /dev/null
+++ b/src/lib-mail/message-binary-part.c
@@ -0,0 +1,44 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "numpack.h"
+#include "message-binary-part.h"
+
+void message_binary_part_serialize(const struct message_binary_part *parts,
+ buffer_t *dest)
+{
+ const struct message_binary_part *part;
+
+ for (part = parts; part != NULL; part = part->next) {
+ numpack_encode(dest, part->physical_pos);
+ numpack_encode(dest, part->binary_hdr_size);
+ numpack_encode(dest, part->binary_body_size);
+ numpack_encode(dest, part->binary_body_lines_count);
+ }
+}
+
+int message_binary_part_deserialize(pool_t pool, const void *data, size_t size,
+ struct message_binary_part **parts_r)
+{
+ const uint8_t *p = data, *end = p + size;
+ uint64_t n1, n2, n3, n4;
+ struct message_binary_part *part = NULL, *prev_part = NULL;
+
+ while (p != end) {
+ part = p_new(pool, struct message_binary_part, 1);
+ part->next = prev_part;
+ prev_part = part;
+ if (numpack_decode(&p, end, &n1) < 0 ||
+ numpack_decode(&p, end, &n2) < 0 ||
+ numpack_decode(&p, end, &n3) < 0 ||
+ numpack_decode(&p, end, &n4) < 0 ||
+ n4 > UINT_MAX)
+ return -1;
+ part->physical_pos = n1;
+ part->binary_hdr_size = n2;
+ part->binary_body_size = n3;
+ part->binary_body_lines_count = n4;
+ }
+ *parts_r = part;
+ return 0;
+}
diff --git a/src/lib-mail/message-binary-part.h b/src/lib-mail/message-binary-part.h
new file mode 100644
index 0000000..f30033a
--- /dev/null
+++ b/src/lib-mail/message-binary-part.h
@@ -0,0 +1,29 @@
+#ifndef MESSAGE_BINARY_PART_H
+#define MESSAGE_BINARY_PART_H
+
+struct message_binary_part {
+ struct message_binary_part *next;
+
+ /* Absolute position from beginning of message. This can be used to
+ match the part to struct message_part. */
+ uoff_t physical_pos;
+ /* Decoded binary header/body size. The binary header size may differ
+ from message_part's, because Content-Transfer-Encoding is changed to
+ "binary". */
+ uoff_t binary_hdr_size;
+ uoff_t binary_body_size;
+ /* BODYSTRUCTURE for text/ and message/rfc822 parts includes lines
+ count. Decoding may change these numbers. */
+ unsigned int binary_body_lines_count;
+};
+
+/* Serialize message binary_part. */
+void message_binary_part_serialize(const struct message_binary_part *parts,
+ buffer_t *dest);
+
+/* Generate struct message_binary_part from serialized data. Returns 0 if ok,
+ -1 if any problems are detected. */
+int message_binary_part_deserialize(pool_t pool, const void *data, size_t size,
+ struct message_binary_part **parts_r);
+
+#endif
diff --git a/src/lib-mail/message-date.c b/src/lib-mail/message-date.c
new file mode 100644
index 0000000..7a7529a
--- /dev/null
+++ b/src/lib-mail/message-date.c
@@ -0,0 +1,283 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "utc-offset.h"
+#include "utc-mktime.h"
+#include "rfc822-parser.h"
+#include "message-date.h"
+
+#include <ctype.h>
+
+/* RFC specifies ':' as the only allowed separator,
+ but be forgiving also for some broken ones */
+#define IS_TIME_SEP(c) \
+ ((c) == ':' || (c) == '.')
+
+struct message_date_parser_context {
+ struct rfc822_parser_context parser;
+ string_t *str;
+};
+
+static const char *month_names[] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+static const char *weekday_names[] = {
+ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+static int parse_timezone(const unsigned char *str, size_t len)
+{
+ int offset;
+ char chr;
+
+ if (len == 5 && (*str == '+' || *str == '-')) {
+ /* numeric offset */
+ if (!i_isdigit(str[1]) || !i_isdigit(str[2]) ||
+ !i_isdigit(str[3]) || !i_isdigit(str[4]))
+ return 0;
+
+ offset = ((str[1]-'0') * 10 + (str[2]-'0')) * 60 +
+ (str[3]-'0') * 10 + (str[4]-'0');
+ return *str == '+' ? offset : -offset;
+ }
+
+ if (len == 1) {
+ /* military zone - handle them the correct way, not as
+ RFC822 says. RFC2822 though suggests that they'd be
+ considered as unspecified.. */
+ chr = i_toupper(*str);
+ if (chr < 'J')
+ return (*str-'A'+1) * 60;
+ if (chr == 'J')
+ return 0;
+ if (chr <= 'M')
+ return (*str-'A') * 60;
+ if (chr < 'Z')
+ return ('M'-*str) * 60;
+ return 0;
+ }
+
+ if (len == 2 && i_toupper(str[0]) == 'U' && i_toupper(str[1]) == 'T') {
+ /* UT - Universal Time */
+ return 0;
+ }
+
+ if (len == 3) {
+ /* GMT | [ECMP][DS]T */
+ if (str[2] != 'T')
+ return 0;
+
+ switch (i_toupper(*str)) {
+ case 'E':
+ offset = -5 * 60;
+ break;
+ case 'C':
+ offset = -6 * 60;
+ break;
+ case 'M':
+ offset = -7 * 60;
+ break;
+ case 'P':
+ offset = -8 * 60;
+ break;
+ default:
+ /* GMT and others */
+ return 0;
+ }
+
+ if (i_toupper(str[1]) == 'D')
+ return offset + 60;
+ if (i_toupper(str[1]) == 'S')
+ return offset;
+ }
+
+ return 0;
+}
+
+static int next_token(struct message_date_parser_context *ctx,
+ const unsigned char **value, size_t *value_len)
+{
+ int ret;
+
+ str_truncate(ctx->str, 0);
+ ret = ctx->parser.data >= ctx->parser.end ? 0 :
+ rfc822_parse_atom(&ctx->parser, ctx->str);
+
+ *value = str_data(ctx->str);
+ *value_len = str_len(ctx->str);
+ return ret < 0 ? -1 : *value_len > 0;
+}
+
+static bool
+message_date_parser_tokens(struct message_date_parser_context *ctx,
+ time_t *timestamp_r, int *timezone_offset_r)
+{
+ struct tm tm;
+ const unsigned char *value;
+ size_t i, len;
+ int ret;
+
+ /* [weekday_name "," ] dd month_name [yy]yy hh:mi[:ss] timezone */
+ i_zero(&tm);
+
+ rfc822_skip_lwsp(&ctx->parser);
+
+ /* skip the optional weekday */
+ if (next_token(ctx, &value, &len) <= 0)
+ return FALSE;
+ if (len == 3) {
+ if (*ctx->parser.data != ',')
+ return FALSE;
+ ctx->parser.data++;
+ rfc822_skip_lwsp(&ctx->parser);
+
+ if (next_token(ctx, &value, &len) <= 0)
+ return FALSE;
+ }
+
+ /* dd */
+ if (len < 1 || len > 2 || !i_isdigit(value[0]))
+ return FALSE;
+
+ tm.tm_mday = value[0]-'0';
+ if (len == 2) {
+ if (!i_isdigit(value[1]))
+ return FALSE;
+ tm.tm_mday = (tm.tm_mday * 10) + (value[1]-'0');
+ }
+
+ /* month name */
+ if (next_token(ctx, &value, &len) <= 0 || len < 3)
+ return FALSE;
+
+ for (i = 0; i < 12; i++) {
+ if (i_memcasecmp(month_names[i], value, 3) == 0) {
+ tm.tm_mon = i;
+ break;
+ }
+ }
+ if (i == 12)
+ return FALSE;
+
+ /* [yy]yy */
+ if (next_token(ctx, &value, &len) <= 0 || (len != 2 && len != 4))
+ return FALSE;
+
+ for (i = 0; i < len; i++) {
+ if (!i_isdigit(value[i]))
+ return FALSE;
+ tm.tm_year = tm.tm_year * 10 + (value[i]-'0');
+ }
+
+ if (len == 2) {
+ /* two digit year, assume 1970+ */
+ if (tm.tm_year < 70)
+ tm.tm_year += 100;
+ } else {
+ if (tm.tm_year < 1900)
+ return FALSE;
+ tm.tm_year -= 1900;
+ }
+
+ /* hh, allow also single digit */
+ if (next_token(ctx, &value, &len) <= 0 ||
+ len < 1 || len > 2 || !i_isdigit(value[0]))
+ return FALSE;
+ tm.tm_hour = value[0]-'0';
+ if (len == 2) {
+ if (!i_isdigit(value[1]))
+ return FALSE;
+ tm.tm_hour = tm.tm_hour * 10 + (value[1]-'0');
+ }
+
+ /* :mm (may be the last token) */
+ if (!IS_TIME_SEP(*ctx->parser.data))
+ return FALSE;
+ ctx->parser.data++;
+ rfc822_skip_lwsp(&ctx->parser);
+
+ if (next_token(ctx, &value, &len) < 0 || len != 2 ||
+ !i_isdigit(value[0]) || !i_isdigit(value[1]))
+ return FALSE;
+ tm.tm_min = (value[0]-'0') * 10 + (value[1]-'0');
+
+ /* [:ss] */
+ if (ctx->parser.data < ctx->parser.end &&
+ IS_TIME_SEP(*ctx->parser.data)) {
+ ctx->parser.data++;
+ rfc822_skip_lwsp(&ctx->parser);
+
+ if (next_token(ctx, &value, &len) <= 0 || len != 2 ||
+ !i_isdigit(value[0]) || !i_isdigit(value[1]))
+ return FALSE;
+ tm.tm_sec = (value[0]-'0') * 10 + (value[1]-'0');
+ }
+
+ if ((ret = next_token(ctx, &value, &len)) < 0)
+ return FALSE;
+ if (ret == 0) {
+ /* missing timezone */
+ *timezone_offset_r = 0;
+ } else {
+ /* timezone. invalid timezones are treated as GMT, because
+ we may not know all the possible timezones that are used
+ and it's better to give at least a mostly correct reply.
+ FIXME: perhaps some different strict version of this
+ function would be useful? */
+ *timezone_offset_r = parse_timezone(value, len);
+ }
+
+ tm.tm_isdst = -1;
+ *timestamp_r = utc_mktime(&tm);
+ if (*timestamp_r == (time_t)-1)
+ return FALSE;
+
+ *timestamp_r -= *timezone_offset_r * 60;
+
+ return TRUE;
+}
+
+bool message_date_parse(const unsigned char *data, size_t size,
+ time_t *timestamp_r, int *timezone_offset_r)
+{
+ bool success;
+
+ T_BEGIN {
+ struct message_date_parser_context ctx;
+
+ rfc822_parser_init(&ctx.parser, data, size, NULL);
+ ctx.str = t_str_new(128);
+ success = message_date_parser_tokens(&ctx, timestamp_r,
+ timezone_offset_r);
+ rfc822_parser_deinit(&ctx.parser);
+ } T_END;
+
+ return success;
+}
+
+const char *message_date_create(time_t timestamp)
+{
+ struct tm *tm;
+ int offset;
+ bool negative;
+
+ tm = localtime(&timestamp);
+ offset = utc_offset(tm, timestamp);
+ if (offset >= 0)
+ negative = FALSE;
+ else {
+ negative = TRUE;
+ offset = -offset;
+ }
+
+ return t_strdup_printf("%s, %02d %s %04d %02d:%02d:%02d %c%02d%02d",
+ weekday_names[tm->tm_wday],
+ tm->tm_mday,
+ month_names[tm->tm_mon],
+ tm->tm_year+1900,
+ tm->tm_hour, tm->tm_min, tm->tm_sec,
+ negative ? '-' : '+', offset / 60, offset % 60);
+}
diff --git a/src/lib-mail/message-date.h b/src/lib-mail/message-date.h
new file mode 100644
index 0000000..fb1364b
--- /dev/null
+++ b/src/lib-mail/message-date.h
@@ -0,0 +1,12 @@
+#ifndef MESSAGE_DATE_H
+#define MESSAGE_DATE_H
+
+/* Parses RFC2822 date/time string. timezone_offset is filled with the
+ timezone's difference to UTC in minutes. */
+bool message_date_parse(const unsigned char *data, size_t size,
+ time_t *timestamp_r, int *timezone_offset_r);
+
+/* Create RFC2822 date/time string from given time in local timezone. */
+const char *message_date_create(time_t timestamp);
+
+#endif
diff --git a/src/lib-mail/message-decoder.c b/src/lib-mail/message-decoder.c
new file mode 100644
index 0000000..845b3d5
--- /dev/null
+++ b/src/lib-mail/message-decoder.c
@@ -0,0 +1,390 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "base64.h"
+#include "str.h"
+#include "unichar.h"
+#include "charset-utf8.h"
+#include "qp-decoder.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+#include "message-parser.h"
+#include "message-header-decode.h"
+#include "message-decoder.h"
+
+struct message_decoder_context {
+ enum message_decoder_flags flags;
+ normalizer_func_t *normalizer;
+ struct message_part *prev_part;
+
+ struct message_header_line hdr;
+ buffer_t *buf, *buf2;
+
+ char *charset_trans_charset;
+ struct charset_translation *charset_trans;
+ char translation_buf[CHARSET_MAX_PENDING_BUF_SIZE];
+ size_t translation_size;
+
+ struct qp_decoder *qp;
+ struct base64_decoder base64_decoder;
+
+ char *content_type, *content_charset;
+ enum message_cte message_cte;
+
+ bool binary_input:1;
+};
+
+static void
+message_decode_body_init_charset(struct message_decoder_context *ctx,
+ struct message_part *part);
+
+struct message_decoder_context *
+message_decoder_init(normalizer_func_t *normalizer,
+ enum message_decoder_flags flags)
+{
+ struct message_decoder_context *ctx;
+
+ ctx = i_new(struct message_decoder_context, 1);
+ ctx->flags = flags;
+ ctx->normalizer = normalizer;
+ ctx->buf = buffer_create_dynamic(default_pool, 8192);
+ ctx->buf2 = buffer_create_dynamic(default_pool, 8192);
+ base64_decode_init(&ctx->base64_decoder, &base64_scheme, 0);
+ return ctx;
+}
+
+void message_decoder_deinit(struct message_decoder_context **_ctx)
+{
+ struct message_decoder_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ if (ctx->charset_trans != NULL)
+ charset_to_utf8_end(&ctx->charset_trans);
+ if (ctx->qp != NULL)
+ qp_decoder_deinit(&ctx->qp);
+
+ buffer_free(&ctx->buf);
+ buffer_free(&ctx->buf2);
+ i_free(ctx->charset_trans_charset);
+ i_free(ctx->content_type);
+ i_free(ctx->content_charset);
+ i_free(ctx);
+}
+
+void message_decoder_set_return_binary(struct message_decoder_context *ctx,
+ bool set)
+{
+ if (set)
+ ctx->flags |= MESSAGE_DECODER_FLAG_RETURN_BINARY;
+ else
+ ctx->flags &= ENUM_NEGATE(MESSAGE_DECODER_FLAG_RETURN_BINARY);
+ message_decode_body_init_charset(ctx, ctx->prev_part);
+}
+
+enum message_cte message_decoder_parse_cte(const struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ enum message_cte message_cte;
+ string_t *value;
+
+ value = t_str_new(64);
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+
+ rfc822_skip_lwsp(&parser);
+
+ /* Ensure we do not accidentically accept confused values like
+ 'base64 binary' or embedded NULs */
+ if (rfc822_parse_mime_token(&parser, value) == 1) {
+ rfc822_skip_lwsp(&parser);
+ /* RFC 2045 does not permit parameters for CTE,
+ but in case someone uses them, we accept
+ parameter separator ';' to be lenient. */
+ if (*parser.data != ';')
+ return MESSAGE_CTE_UNKNOWN;
+ }
+
+ message_cte = MESSAGE_CTE_UNKNOWN;
+ switch (str_len(value)) {
+ case 4:
+ if (i_memcasecmp(str_data(value), "7bit", 4) == 0 ||
+ i_memcasecmp(str_data(value), "8bit", 4) == 0)
+ message_cte = MESSAGE_CTE_78BIT;
+ break;
+ case 6:
+ if (i_memcasecmp(str_data(value), "base64", 6) == 0)
+ message_cte = MESSAGE_CTE_BASE64;
+ else if (i_memcasecmp(str_data(value), "binary", 6) == 0)
+ message_cte = MESSAGE_CTE_BINARY;
+ break;
+ case 16:
+ if (i_memcasecmp(str_data(value), "quoted-printable", 16) == 0)
+ message_cte = MESSAGE_CTE_QP;
+ break;
+ }
+ rfc822_parser_deinit(&parser);
+ return message_cte;
+}
+
+static void
+parse_content_type(struct message_decoder_context *ctx,
+ struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ const char *const *results;
+ string_t *str;
+ int ret;
+
+ if (ctx->content_type != NULL)
+ return;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+ str = t_str_new(64);
+ ret = rfc822_parse_content_type(&parser, str);
+ ctx->content_type = i_strdup(str_c(str));
+ if (ret < 0) {
+ rfc822_parser_deinit(&parser);
+ return;
+ }
+
+ rfc2231_parse(&parser, &results);
+ for (; *results != NULL; results += 2) {
+ if (strcasecmp(results[0], "charset") == 0) {
+ ctx->content_charset = i_strdup(results[1]);
+ break;
+ }
+ }
+ rfc822_parser_deinit(&parser);
+}
+
+static bool message_decode_header(struct message_decoder_context *ctx,
+ struct message_header_line *hdr,
+ struct message_block *output)
+{
+ size_t value_len;
+
+ if (hdr->continues) {
+ hdr->use_full_value = TRUE;
+ return FALSE;
+ }
+
+ T_BEGIN {
+ if (hdr->name_len == 12 &&
+ strcasecmp(hdr->name, "Content-Type") == 0)
+ parse_content_type(ctx, hdr);
+ if (hdr->name_len == 25 &&
+ strcasecmp(hdr->name, "Content-Transfer-Encoding") == 0)
+ ctx->message_cte = message_decoder_parse_cte(hdr);
+ } T_END;
+
+ buffer_set_used_size(ctx->buf, 0);
+ message_header_decode_utf8(hdr->full_value, hdr->full_value_len,
+ ctx->buf, ctx->normalizer);
+ value_len = ctx->buf->used;
+
+ if (ctx->normalizer != NULL) {
+ (void)ctx->normalizer(hdr->name, hdr->name_len, ctx->buf);
+ buffer_append_c(ctx->buf, '\0');
+ } else {
+ if (!uni_utf8_get_valid_data((const unsigned char *)hdr->name,
+ hdr->name_len, ctx->buf))
+ buffer_append_c(ctx->buf, '\0');
+ }
+
+ ctx->hdr = *hdr;
+ ctx->hdr.full_value = ctx->buf->data;
+ ctx->hdr.full_value_len = value_len;
+ ctx->hdr.value_len = 0;
+ if (ctx->buf->used != value_len) {
+ ctx->hdr.name = CONST_PTR_OFFSET(ctx->buf->data,
+ ctx->hdr.full_value_len);
+ ctx->hdr.name_len = ctx->buf->used - 1 - value_len;
+ }
+
+ output->hdr = &ctx->hdr;
+ return TRUE;
+}
+
+static void translation_buf_decode(struct message_decoder_context *ctx,
+ const unsigned char **data, size_t *size)
+{
+ unsigned char trans_buf[CHARSET_MAX_PENDING_BUF_SIZE+1];
+ size_t data_wanted, skip;
+ size_t trans_size, orig_size;
+
+ /* @UNSAFE: move the previously untranslated bytes to trans_buf
+ and see if we have now enough data to get the next character
+ translated */
+ memcpy(trans_buf, ctx->translation_buf, ctx->translation_size);
+ data_wanted = sizeof(trans_buf) - ctx->translation_size;
+ if (data_wanted > *size)
+ data_wanted = *size;
+ memcpy(trans_buf + ctx->translation_size, *data, data_wanted);
+
+ orig_size = trans_size = ctx->translation_size + data_wanted;
+ (void)charset_to_utf8(ctx->charset_trans, trans_buf,
+ &trans_size, ctx->buf2);
+
+ if (trans_size <= ctx->translation_size) {
+ /* need more data to finish the translation. */
+ i_assert(orig_size < CHARSET_MAX_PENDING_BUF_SIZE);
+ memcpy(ctx->translation_buf, trans_buf, orig_size);
+ ctx->translation_size = orig_size;
+ *data += *size;
+ *size = 0;
+ return;
+ }
+ skip = trans_size - ctx->translation_size;
+
+ i_assert(*size >= skip);
+ *data += skip;
+ *size -= skip;
+
+ ctx->translation_size = 0;
+}
+
+static void
+message_decode_body_init_charset(struct message_decoder_context *ctx,
+ struct message_part *part)
+{
+ ctx->binary_input = ctx->content_charset == NULL &&
+ (ctx->flags & MESSAGE_DECODER_FLAG_RETURN_BINARY) != 0 &&
+ (part->flags & (MESSAGE_PART_FLAG_TEXT |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) == 0;
+
+ if (ctx->binary_input)
+ return;
+
+ if (ctx->charset_trans != NULL && ctx->content_charset != NULL &&
+ strcasecmp(ctx->content_charset, ctx->charset_trans_charset) == 0) {
+ /* already have the correct translation selected */
+ charset_to_utf8_reset(ctx->charset_trans);
+ return;
+ }
+
+ if (ctx->charset_trans != NULL)
+ charset_to_utf8_end(&ctx->charset_trans);
+ i_free_and_null(ctx->charset_trans_charset);
+
+ ctx->charset_trans_charset = i_strdup(ctx->content_charset != NULL ?
+ ctx->content_charset : "UTF-8");
+ if (charset_to_utf8_begin(ctx->charset_trans_charset, ctx->normalizer,
+ &ctx->charset_trans) < 0)
+ ctx->charset_trans = charset_utf8_to_utf8_begin(ctx->normalizer);
+}
+
+static bool message_decode_body(struct message_decoder_context *ctx,
+ struct message_block *input,
+ struct message_block *output)
+{
+ const unsigned char *data = NULL;
+ size_t pos, size = 0;
+ const char *error;
+
+ switch (ctx->message_cte) {
+ case MESSAGE_CTE_UNKNOWN:
+ /* just skip this body */
+ return FALSE;
+
+ case MESSAGE_CTE_78BIT:
+ case MESSAGE_CTE_BINARY:
+ data = input->data;
+ size = input->size;
+ break;
+ case MESSAGE_CTE_QP: {
+ buffer_set_used_size(ctx->buf, 0);
+ if (ctx->qp == NULL)
+ ctx->qp = qp_decoder_init(ctx->buf);
+ (void)qp_decoder_more(ctx->qp, input->data, input->size,
+ &pos, &error);
+ data = ctx->buf->data;
+ size = ctx->buf->used;
+ break;
+ }
+ case MESSAGE_CTE_BASE64:
+ buffer_set_used_size(ctx->buf, 0);
+ if (!base64_decode_is_finished(&ctx->base64_decoder)) {
+ if (base64_decode_more(&ctx->base64_decoder,
+ input->data, input->size,
+ &pos, ctx->buf) <= 0) {
+ /* ignore the rest of the input in this
+ MIME part */
+ (void)base64_decode_finish(&ctx->base64_decoder);
+ }
+ }
+ data = ctx->buf->data;
+ size = ctx->buf->used;
+ break;
+ }
+
+ if (ctx->binary_input) {
+ output->data = data;
+ output->size = size;
+ } else {
+ buffer_set_used_size(ctx->buf2, 0);
+ if (ctx->translation_size != 0)
+ translation_buf_decode(ctx, &data, &size);
+
+ pos = size;
+ (void)charset_to_utf8(ctx->charset_trans,
+ data, &pos, ctx->buf2);
+ if (pos != size) {
+ ctx->translation_size = size - pos;
+ i_assert(ctx->translation_size <=
+ sizeof(ctx->translation_buf));
+ memcpy(ctx->translation_buf, data + pos,
+ ctx->translation_size);
+ }
+ output->data = ctx->buf2->data;
+ output->size = ctx->buf2->used;
+ }
+
+ output->hdr = NULL;
+ return TRUE;
+}
+
+bool message_decoder_decode_next_block(struct message_decoder_context *ctx,
+ struct message_block *input,
+ struct message_block *output)
+{
+ if (input->part != ctx->prev_part) {
+ /* MIME part changed. */
+ message_decoder_decode_reset(ctx);
+ }
+
+ output->part = input->part;
+ ctx->prev_part = input->part;
+
+ if (input->hdr != NULL) {
+ output->size = 0;
+ return message_decode_header(ctx, input->hdr, output);
+ } else if (input->size != 0)
+ return message_decode_body(ctx, input, output);
+ else {
+ output->hdr = NULL;
+ output->size = 0;
+ message_decode_body_init_charset(ctx, input->part);
+ return TRUE;
+ }
+}
+
+const char *
+message_decoder_current_content_type(struct message_decoder_context *ctx)
+{
+ return ctx->content_type;
+}
+
+void message_decoder_decode_reset(struct message_decoder_context *ctx)
+{
+ const char *error;
+
+ base64_decode_reset(&ctx->base64_decoder);
+
+ if (ctx->qp != NULL)
+ (void)qp_decoder_finish(ctx->qp, &error);
+ i_free_and_null(ctx->content_type);
+ i_free_and_null(ctx->content_charset);
+ ctx->message_cte = MESSAGE_CTE_78BIT;
+}
diff --git a/src/lib-mail/message-decoder.h b/src/lib-mail/message-decoder.h
new file mode 100644
index 0000000..9928ae6
--- /dev/null
+++ b/src/lib-mail/message-decoder.h
@@ -0,0 +1,54 @@
+#ifndef MESSAGE_DECODER_H
+#define MESSAGE_DECODER_H
+
+#include "unichar.h"
+
+struct message_header_line;
+
+enum message_cte {
+ MESSAGE_CTE_UNKNOWN = 0,
+ MESSAGE_CTE_78BIT,
+ MESSAGE_CTE_BINARY,
+ MESSAGE_CTE_QP,
+ MESSAGE_CTE_BASE64
+};
+
+enum message_decoder_flags {
+ /* Return binary MIME parts as-is without any conversion. */
+ MESSAGE_DECODER_FLAG_RETURN_BINARY = 0x02
+};
+
+struct message_block;
+
+/* Decode message's contents as UTF-8, both the headers and the MIME bodies.
+ The bodies are decoded from quoted-printable and base64 formats if needed. */
+struct message_decoder_context *
+message_decoder_init(normalizer_func_t *normalizer,
+ enum message_decoder_flags flags);
+void message_decoder_deinit(struct message_decoder_context **ctx);
+
+/* Change the MESSAGE_DECODER_FLAG_RETURN_BINARY flag */
+void message_decoder_set_return_binary(struct message_decoder_context *ctx,
+ bool set);
+
+/* Decode input and return decoded output. Headers are returned only in their
+ full multiline forms.
+
+ Returns TRUE if output is given, FALSE if more data is needed. If the input
+ ends in a partial character, it's returned in the next output. */
+bool message_decoder_decode_next_block(struct message_decoder_context *ctx,
+ struct message_block *input,
+ struct message_block *output);
+
+/* Returns the parsed Content-Type of the current MIME part. If there is no
+ explicit Content-Type, returns NULL. */
+const char *
+message_decoder_current_content_type(struct message_decoder_context *ctx);
+
+/* Call whenever message changes */
+void message_decoder_decode_reset(struct message_decoder_context *ctx);
+
+/* Decode Content-Transfer-Encoding header. */
+enum message_cte message_decoder_parse_cte(const struct message_header_line *hdr);
+
+#endif
diff --git a/src/lib-mail/message-header-decode.c b/src/lib-mail/message-header-decode.c
new file mode 100644
index 0000000..18f6ca2
--- /dev/null
+++ b/src/lib-mail/message-header-decode.c
@@ -0,0 +1,188 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base64.h"
+#include "buffer.h"
+#include "unichar.h"
+#include "charset-utf8.h"
+#include "quoted-printable.h"
+#include "message-header-decode.h"
+
+static size_t
+message_header_decode_encoded(const unsigned char *data, size_t size,
+ buffer_t *decodebuf, size_t *charsetlen_r)
+{
+#define QCOUNT 3
+ unsigned int num = 0;
+ size_t i, start_pos[QCOUNT] = {0, 0, 0};
+
+ /* data should contain "charset?encoding?text?=" */
+ for (i = 0; i < size; i++) {
+ if (data[i] == '?') {
+ start_pos[num++] = i;
+ if (num == QCOUNT)
+ break;
+ }
+ }
+
+ if (i+1 >= size || data[i+1] != '=') {
+ /* invalid block */
+ return 0;
+ }
+
+ i_assert(num == QCOUNT);
+
+ buffer_append(decodebuf, data, start_pos[0]);
+ buffer_append_c(decodebuf, '\0');
+ *charsetlen_r = decodebuf->used;
+
+ switch (data[start_pos[0]+1]) {
+ case 'q':
+ case 'Q':
+ if (quoted_printable_q_decode(data + start_pos[1] + 1,
+ start_pos[2] - start_pos[1] - 1,
+ decodebuf) < 0) {
+ /* we skipped over some invalid data */
+ }
+ break;
+ case 'b':
+ case 'B':
+ if (base64_decode(data + start_pos[1] + 1,
+ start_pos[2] - start_pos[1] - 1,
+ NULL, decodebuf) < 0) {
+ /* contains invalid data. show what we got so far. */
+ }
+ break;
+ default:
+ /* unknown encoding */
+ return 0;
+ }
+
+ return start_pos[2] + 2;
+}
+
+static bool is_only_lwsp(const unsigned char *data, size_t size)
+{
+ size_t i;
+
+ for (i = 0; i < size; i++) {
+ if (!(data[i] == ' ' || data[i] == '\t' ||
+ data[i] == '\r' || data[i] == '\n'))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void message_header_decode(const unsigned char *data, size_t size,
+ message_header_decode_callback_t *callback,
+ void *context)
+{
+ buffer_t *decodebuf = NULL;
+ size_t charsetlen = 0;
+ size_t pos, start_pos, ret;
+
+ /* =?charset?Q|B?text?= */
+ start_pos = 0;
+ for (pos = 0; pos + 1 < size; ) {
+ if (data[pos] != '=' || data[pos+1] != '?') {
+ pos++;
+ continue;
+ }
+
+ /* encoded string beginning */
+ if (pos != start_pos &&
+ !is_only_lwsp(data+start_pos, pos-start_pos)) {
+ /* send the unencoded data so far */
+ if (!callback(data + start_pos, pos - start_pos,
+ NULL, context)) {
+ start_pos = size;
+ break;
+ }
+ }
+
+ if (decodebuf == NULL) {
+ decodebuf = buffer_create_dynamic(default_pool,
+ size - pos);
+ } else {
+ buffer_set_used_size(decodebuf, 0);
+ }
+
+ pos += 2;
+ ret = message_header_decode_encoded(data + pos, size - pos,
+ decodebuf, &charsetlen);
+ if (ret == 0) {
+ start_pos = pos-2;
+ continue;
+ }
+ pos += ret;
+
+ if (decodebuf->used > charsetlen) {
+ /* decodebuf contains <charset> NUL <text> */
+ if (!callback(CONST_PTR_OFFSET(decodebuf->data,
+ charsetlen),
+ decodebuf->used - charsetlen,
+ decodebuf->data, context)) {
+ start_pos = size;
+ break;
+ }
+ }
+
+ start_pos = pos;
+ }
+
+ if (size != start_pos) {
+ i_assert(size > start_pos);
+ (void)callback(data + start_pos, size - start_pos,
+ NULL, context);
+ }
+ buffer_free(&decodebuf);
+}
+
+struct decode_utf8_context {
+ buffer_t *dest;
+ normalizer_func_t *normalizer;
+ bool changed:1;
+};
+
+static bool
+decode_utf8_callback(const unsigned char *data, size_t size,
+ const char *charset, void *context)
+{
+ struct decode_utf8_context *ctx = context;
+ struct charset_translation *t;
+
+ if (charset == NULL || charset_is_utf8(charset)) {
+ /* ASCII / UTF-8 */
+ if (ctx->normalizer != NULL) {
+ (void)ctx->normalizer(data, size, ctx->dest);
+ } else {
+ if (uni_utf8_get_valid_data(data, size, ctx->dest))
+ buffer_append(ctx->dest, data, size);
+ }
+ return TRUE;
+ }
+
+ if (charset_to_utf8_begin(charset, ctx->normalizer, &t) < 0) {
+ /* data probably still contains some valid ASCII characters.
+ append them. */
+ if (uni_utf8_get_valid_data(data, size, ctx->dest))
+ buffer_append(ctx->dest, data, size);
+ return TRUE;
+ }
+
+ /* ignore any errors */
+ (void)charset_to_utf8(t, data, &size, ctx->dest);
+ charset_to_utf8_end(&t);
+ return TRUE;
+}
+
+void message_header_decode_utf8(const unsigned char *data, size_t size,
+ buffer_t *dest, normalizer_func_t *normalizer)
+{
+ struct decode_utf8_context ctx;
+
+ i_zero(&ctx);
+ ctx.dest = dest;
+ ctx.normalizer = normalizer;
+ message_header_decode(data, size, decode_utf8_callback, &ctx);
+}
diff --git a/src/lib-mail/message-header-decode.h b/src/lib-mail/message-header-decode.h
new file mode 100644
index 0000000..5f7a5ad
--- /dev/null
+++ b/src/lib-mail/message-header-decode.h
@@ -0,0 +1,22 @@
+#ifndef MESSAGE_HEADER_DECODE_H
+#define MESSAGE_HEADER_DECODE_H
+
+#include "unichar.h"
+
+/* Return FALSE if you wish to stop decoding. charset is NULL when it's not
+ RFC2047-encoded. */
+typedef bool message_header_decode_callback_t(const unsigned char *data,
+ size_t size, const char *charset,
+ void *context);
+
+/* Decode RFC2047 encoded words. Call specified function for each
+ decoded block. */
+void message_header_decode(const unsigned char *data, size_t size,
+ message_header_decode_callback_t *callback,
+ void *context);
+
+/* Append decoded RFC2047 header as UTF-8 to given buffer. */
+void message_header_decode_utf8(const unsigned char *data, size_t size,
+ buffer_t *dest, normalizer_func_t *normalizer);
+
+#endif
diff --git a/src/lib-mail/message-header-encode.c b/src/lib-mail/message-header-encode.c
new file mode 100644
index 0000000..a9410a8
--- /dev/null
+++ b/src/lib-mail/message-header-encode.c
@@ -0,0 +1,406 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "unichar.h"
+#include "base64.h"
+#include "message-header-encode.h"
+
+#define MIME_WRAPPER_LEN (strlen("=?utf-8?q?""?="))
+#define MIME_MAX_LINE_LEN 76
+
+#define IS_LWSP(c) \
+ ((c) == ' ' || (c) == '\t' || (c) == '\n')
+
+static bool
+input_idx_need_encoding(const unsigned char *input, size_t i, size_t len)
+{
+ switch (input[i]) {
+ case '\r':
+ if (i+1 == len || input[i+1] != '\n')
+ return TRUE;
+ i++;
+ /* fall through - verify the LF as well */
+ case '\n':
+ if (i+1 == len) {
+ /* trailing LF - we need to drop it */
+ return TRUE;
+ }
+ i_assert(i+1 < len);
+ if (input[i+1] != '\t' && input[i+1] != ' ') {
+ /* LF not followed by whitespace - we need to
+ add the whitespace */
+ return TRUE;
+ }
+ break;
+ case '\t':
+ /* TAB doesn't need to be encoded */
+ break;
+ case '=':
+ /* <LWSP>=? - we need to check backwards a bit to see if
+ there is LWSP (note that we don't want to return TRUE for
+ the LWSP itself yet, so we need to do this backwards
+ check) */
+ if ((i == 0 || IS_LWSP(input[i-1])) && i+2 <= len &&
+ memcmp(input + i, "=?", 2) == 0)
+ return TRUE;
+ break;
+ default:
+ /* 8bit chars */
+ if ((input[i] & 0x80) != 0)
+ return TRUE;
+ /* control chars */
+ if (input[i] < 32)
+ return TRUE;
+ break;
+ }
+ return FALSE;
+}
+
+void message_header_encode_q(const unsigned char *input, size_t len,
+ string_t *output, size_t first_line_len)
+{
+ static const unsigned char *rep_char =
+ (const unsigned char *)UNICODE_REPLACEMENT_CHAR_UTF8;
+ static const unsigned int rep_char_len =
+ UNICODE_REPLACEMENT_CHAR_UTF8_LEN;
+ size_t line_len_left;
+ bool invalid_char = FALSE;
+
+ if (len == 0)
+ return;
+
+ line_len_left = MIME_MAX_LINE_LEN - MIME_WRAPPER_LEN;
+
+ if (first_line_len >= MIME_MAX_LINE_LEN - MIME_WRAPPER_LEN - 3) {
+ str_append(output, "\n\t");
+ line_len_left--;
+ } else {
+ line_len_left -= first_line_len;
+ }
+
+ str_append(output, "=?utf-8?q?");
+ for (;;) {
+ unichar_t ch;
+ int nch = 1;
+ size_t n_in, n_out = 0, j;
+
+ /* Determine how many bytes are to be consumed from input and
+ written to output. */
+ switch (input[0]) {
+ case ' ':
+ /* Space is translated to a single '_'. */
+ n_out = 1;
+ n_in = 1;
+ break;
+ case '=':
+ case '?':
+ case '_':
+ /* Special characters are escaped. */
+ n_in = 1;
+ n_out = 3;
+ break;
+ default:
+ nch = uni_utf8_get_char_n(input, len, &ch);
+ if (nch <= 0) {
+ /* Invalid UTF-8 character */
+ n_in = 1;
+ if (!invalid_char) {
+ /* First octet of bad stuff; will emit
+ replacement character. */
+ n_out = rep_char_len * 3;
+ } else {
+ /* Emit only one replacement char for
+ a burst of bad stuff. */
+ n_out = 0;
+ }
+ } else if (nch > 1) {
+ /* Unicode characters are escaped as several
+ escape sequences for each octet. */
+ n_in = nch;
+ n_out = nch * 3;
+ } else if (ch < 0x20 || ch > 0x7e) {
+ /* Control characters are escaped. */
+ i_assert(ch < 0x80);
+ n_in = 1;
+ n_out = 3;
+ } else {
+ /* Other ASCII characters are written to output
+ directly. */
+ n_in = 1;
+ n_out = 1;
+ }
+ }
+ invalid_char = (nch <= 0);
+
+ /* Start a new line once unsufficient space is available to
+ write more to the current line. */
+ if (line_len_left < n_out) {
+ str_append(output, "?=\n\t=?utf-8?q?");
+ line_len_left = MIME_MAX_LINE_LEN -
+ MIME_WRAPPER_LEN - 1;
+ }
+
+ /* Encode the character */
+ if (input[0] == ' ') {
+ /* Write special escape sequence for space character */
+ str_append_c(output, '_');
+ } else if (invalid_char) {
+ /* Write replacement character for invalid UTF-8 code
+ point. */
+ for (j = 0; n_out > 0 && j < rep_char_len; j++)
+ str_printfa(output, "=%02X", rep_char[j]);
+ } else if (n_out > 1) {
+ /* Write one or more escape sequences for a special
+ character, a control character, or a valid UTF-8
+ code point. */
+ for (j = 0; j < n_in; j++)
+ str_printfa(output, "=%02X", input[j]);
+ } else {
+ /* Write other ASCII characters directly to output. */
+ str_append_c(output, input[0]);
+ }
+
+ /* Update sizes and pointers */
+ i_assert(len >= n_in);
+ line_len_left -= n_out;
+ input += n_in;
+ len -= n_in;
+
+ if (len == 0)
+ break;
+ }
+ str_append(output, "?=");
+}
+
+void message_header_encode_b(const unsigned char *input, size_t len,
+ string_t *output, size_t first_line_len)
+{
+ static const unsigned char *rep_char =
+ (const unsigned char *)UNICODE_REPLACEMENT_CHAR_UTF8;
+ static const unsigned int rep_char_len =
+ UNICODE_REPLACEMENT_CHAR_UTF8_LEN;
+ struct base64_encoder b64enc;
+ size_t line_len_left;
+
+ if (len == 0)
+ return;
+
+ line_len_left = MIME_MAX_LINE_LEN - MIME_WRAPPER_LEN;
+
+ if (first_line_len >= MIME_MAX_LINE_LEN - MIME_WRAPPER_LEN - 3) {
+ str_append(output, "\n\t");
+ line_len_left--;
+ } else {
+ line_len_left -= first_line_len;
+ }
+
+ str_append(output, "=?utf-8?b?");
+ base64_encode_init(&b64enc, &base64_scheme, 0, 0);
+ for (;;) {
+ unichar_t ch;
+ size_t space, max, old_bufsize, n_in, n_out;
+ int nch = 1;
+
+ /* Determine how many octets can be encoded on (the remainder
+ of) this line */
+ space = base64_encode_get_full_space(&b64enc, line_len_left);
+ max = I_MIN(space, len);
+
+ /* Check UTF-8 code points in the input and determine a proper
+ boundary for the end of this fragment if the encoded size
+ exceeds the maximum (remaining) line length. */
+ for (n_in = 0; n_in < max;) {
+ nch = uni_utf8_get_char_n(&input[n_in],
+ len - n_in, &ch);
+ if (nch <= 0)
+ break;
+ if ((n_in + nch) > max)
+ break;
+ n_in += nch;
+ }
+
+ /* Encode this fragment up until the maximum fragment size or
+ the first invalid UTF-8 code point in the input. */
+ if (n_in > 0) {
+ old_bufsize = output->used;
+ if (!base64_encode_more(&b64enc, input, n_in,
+ &n_in, output))
+ i_unreached();
+ n_out = output->used - old_bufsize;
+
+ /* Update sizes and pointers */
+ i_assert(len >= n_in);
+ i_assert(line_len_left >= n_out);
+ input += n_in;
+ len -= n_in;
+ line_len_left -= n_out;
+ }
+
+ /* Determine whether a repacement character needs to be written
+ and how much space there is left for it on the current line.
+ */
+ space = 0;
+ if (nch <= 0) {
+ space = base64_encode_get_full_space(
+ &b64enc, line_len_left);
+ }
+
+ /* Start a new line once insufficient space is available. */
+ if ((nch > 0 && len > 0) ||
+ (nch <= 0 && space < rep_char_len)) {
+ old_bufsize = output->used;
+ if (!base64_encode_finish(&b64enc, output))
+ i_unreached();
+ n_out = output->used - old_bufsize;
+ i_assert(line_len_left >= n_out);
+
+ str_append(output, "?=\n\t=?utf-8?b?");
+ line_len_left = MIME_MAX_LINE_LEN -
+ MIME_WRAPPER_LEN - 1;
+ base64_encode_reset(&b64enc);
+ }
+
+ /* Write replacement character if needed. */
+ n_in = 0;
+ n_out = 0;
+ if (nch <= 0) {
+ old_bufsize = output->used;
+ if (!base64_encode_more(&b64enc, rep_char, rep_char_len,
+ NULL, output))
+ i_unreached();
+
+ n_in = 1;
+ n_out = output->used - old_bufsize;
+
+ /* Skip more invalid characters in the input. */
+ for (; n_in < len; n_in++) {
+ nch = uni_utf8_get_char_n(&input[n_in],
+ len - n_in, &ch);
+ if (nch > 0)
+ break;
+ }
+ }
+
+ /* Update sizes and pointers */
+ i_assert(line_len_left >= n_out);
+ input += n_in;
+ len -= n_in;
+ line_len_left -= n_out;
+
+ if (len == 0)
+ break;
+ }
+ if (!base64_encode_finish(&b64enc, output))
+ i_unreached();
+ str_append(output, "?=");
+}
+
+void message_header_encode(const char *input, string_t *output)
+{
+ message_header_encode_data((const void *)input, strlen(input), output);
+}
+
+void message_header_encode_data(const unsigned char *input, size_t len,
+ string_t *output)
+{
+ size_t i, j, first_line_len, cur_line_len, last_idx;
+ size_t enc_chars, enc_len, base64_len, q_len;
+ const unsigned char *next_line_input;
+ size_t next_line_len = 0;
+ bool use_q, cr;
+
+ /* find the first word that needs encoding */
+ for (i = 0; i < len; i++) {
+ if (input_idx_need_encoding(input, i, len))
+ break;
+ }
+ if (i == len) {
+ /* no encoding necessary */
+ str_append_data(output, input, len);
+ return;
+ }
+ /* go back to the beginning of the word so it is fully encoded */
+ if (input[i] != '\r' && input[i] != '\n') {
+ while (i > 0 && !IS_LWSP(input[i-1]))
+ i--;
+ }
+
+ /* write the prefix */
+ str_append_data(output, input, i);
+ first_line_len = j = i;
+ while (j > 0 && input[j-1] != '\n') j--;
+ if (j != 0)
+ first_line_len = j;
+
+ input += i;
+ len -= i;
+
+ /* we'll encode data only up to the next LF, the rest is handled
+ recursively. */
+ next_line_input = memchr(input, '\n', len);
+ if (next_line_input != NULL) {
+ cur_line_len = next_line_input - input;
+ if (cur_line_len > 0 && input[cur_line_len-1] == '\r') {
+ cur_line_len--;
+ next_line_input = input + cur_line_len;
+ }
+ next_line_len = len - cur_line_len;
+ len = cur_line_len;
+ }
+
+ /* find the last word that needs encoding */
+ last_idx = 0; enc_chars = 0;
+ for (i = 0; i < len; i++) {
+ if (input_idx_need_encoding(input, i, len)) {
+ last_idx = i + 1;
+ enc_chars++;
+ }
+ }
+ while (last_idx < len && !IS_LWSP(input[last_idx]))
+ last_idx++;
+
+ /* figure out if we should use Q or B encoding. Prefer Q if it's not
+ too much larger. */
+ enc_len = last_idx;
+ base64_len = MAX_BASE64_ENCODED_SIZE(enc_len);
+ q_len = enc_len + enc_chars*3;
+ use_q = q_len*2/3 <= base64_len;
+
+ /* and do it */
+ if (enc_len == 0)
+ ;
+ else if (use_q)
+ message_header_encode_q(input, enc_len, output, first_line_len);
+ else
+ message_header_encode_b(input, enc_len, output, first_line_len);
+ str_append_data(output, input + last_idx, len - last_idx);
+
+ if (next_line_input != NULL) {
+ /* we're at [CR]LF */
+ i = 0;
+ if (next_line_input[0] == '\r') {
+ cr = TRUE;
+ i++;
+ } else {
+ cr = FALSE;
+ }
+ i_assert(next_line_input[i] == '\n');
+ if (++i == next_line_len)
+ return; /* drop trailing [CR]LF */
+
+ if (cr)
+ str_append_c(output, '\r');
+ str_append_c(output, '\n');
+
+ if (next_line_input[i] == ' ' || next_line_input[i] == '\t') {
+ str_append_c(output, next_line_input[i]);
+ i++;
+ } else {
+ /* make it valid folding whitespace by adding a TAB */
+ str_append_c(output, '\t');
+ }
+ message_header_encode_data(next_line_input+i, next_line_len-i,
+ output);
+ }
+}
diff --git a/src/lib-mail/message-header-encode.h b/src/lib-mail/message-header-encode.h
new file mode 100644
index 0000000..fdd3b19
--- /dev/null
+++ b/src/lib-mail/message-header-encode.h
@@ -0,0 +1,27 @@
+#ifndef MESSAGE_HEADER_ENCODE_H
+#define MESSAGE_HEADER_ENCODE_H
+
+/* Encode UTF-8 input into output wherever necessary using either Q or B
+ encoding depending on which takes less space (approximately). The encoded
+ output is split into multiple lines if necessary (max 76 chars/line).
+ Existing folding whitespace is preserved. Bare [CR]LF will be preserved by
+ adding a TAB after it to make it a valid folding whitespace. All the control
+ characters are encoded, including NUL, CR and LF. Sequences of one or more
+ invalid UTF-8 characters are replaced by a single Unicode replacement
+ character (U+fffd). */
+void message_header_encode(const char *input, string_t *output);
+void message_header_encode_data(const unsigned char *input, size_t len,
+ string_t *output);
+
+/* Encode the whole UTF-8 input using "Q" or "B" encoding into output.
+ The output is split into multiple lines if necessary (max 76 chars/line).
+ The first line's length is given as parameter. All the control characters
+ are encoded, including NUL, CR and LF. Sequences of one or more invalid UTF-8
+ characters are replaced by a single Unicode replacement character (U+fffd).
+ */
+void message_header_encode_q(const unsigned char *input, size_t len,
+ string_t *output, size_t first_line_len);
+void message_header_encode_b(const unsigned char *input, size_t len,
+ string_t *output, size_t first_line_len);
+
+#endif
diff --git a/src/lib-mail/message-header-hash.c b/src/lib-mail/message-header-hash.c
new file mode 100644
index 0000000..05eb791
--- /dev/null
+++ b/src/lib-mail/message-header-hash.c
@@ -0,0 +1,72 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "hash-method.h"
+#include "message-header-hash.h"
+
+void message_header_hash_more(struct message_header_hash_context *ctx,
+ const struct hash_method *method, void *context,
+ unsigned int version,
+ const unsigned char *data, size_t size)
+{
+ size_t i, start;
+
+ i_assert(version >= 1 && version <= MESSAGE_HEADER_HASH_MAX_VERSION);
+
+ if (version == 1) {
+ method->loop(context, data, size);
+ return;
+ }
+ /* - Dovecot IMAP replaces NULs with 0x80 character.
+ - Dovecot POP3 with outlook-no-nuls workaround replaces NULs
+ with 0x80 character.
+ - Zimbra replaces 8bit chars with '?' in header fetches,
+ but not body fetches.
+ - Yahoo replaces 8bit chars with '?' in partial header
+ fetches, but not POP3 TOP. UTF-8 character sequence writes only a
+ single '?'
+
+ So we'll just replace all control and 8bit chars with '?' and
+ remove any repeated '?', which hopefully will satisfy everybody.
+
+ Also:
+ - Zimbra removes trailing spaces and tabs from IMAP BODY[HEADER],
+ but not IMAP BODY[] or POP3 TOP. Just strip away all spaces with
+ version 3 and tabs also with version 4.
+ */
+ for (i = start = 0; i < size; i++) {
+ bool cur_is_questionmark = FALSE;
+
+ switch (data[i]) {
+ case ' ':
+ if (version >= 3) {
+ /* strip away spaces */
+ method->loop(context, data + start, i-start);
+ start = i+1;
+ }
+ break;
+ case '\t':
+ if (version >= 4) {
+ /* strip away tabs */
+ method->loop(context, data + start, i-start);
+ start = i+1;
+ }
+ break;
+ case '\n':
+ break;
+ default:
+ if (data[i] < 0x20 || data[i] >= 0x7f || data[i] == '?') {
+ /* remove repeated '?' */
+ if (start < i || !ctx->prev_was_questionmark) {
+ method->loop(context, data + start, i-start);
+ method->loop(context, "?", 1);
+ }
+ start = i+1;
+ cur_is_questionmark = TRUE;
+ }
+ break;
+ }
+ ctx->prev_was_questionmark = cur_is_questionmark;
+ }
+ method->loop(context, data + start, i-start);
+}
diff --git a/src/lib-mail/message-header-hash.h b/src/lib-mail/message-header-hash.h
new file mode 100644
index 0000000..633d0c1
--- /dev/null
+++ b/src/lib-mail/message-header-hash.h
@@ -0,0 +1,18 @@
+#ifndef MESSAGE_HEADER_HASH_H
+#define MESSAGE_HEADER_HASH_H
+
+#define MESSAGE_HEADER_HASH_MAX_VERSION 4
+
+struct hash_method;
+
+struct message_header_hash_context {
+ bool prev_was_questionmark;
+};
+
+/* Initialize ctx with zeros. */
+void message_header_hash_more(struct message_header_hash_context *ctx,
+ const struct hash_method *method, void *context,
+ unsigned int version,
+ const unsigned char *data, size_t size);
+
+#endif
diff --git a/src/lib-mail/message-header-parser.c b/src/lib-mail/message-header-parser.c
new file mode 100644
index 0000000..c5026f1
--- /dev/null
+++ b/src/lib-mail/message-header-parser.c
@@ -0,0 +1,474 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "unichar.h"
+#include "message-size.h"
+#include "message-header-parser.h"
+
+/* RFC 5322 2.1.1 and 2.2 */
+#define MESSAGE_HEADER_NAME_MAX_LEN 1000
+
+struct message_header_parser_ctx {
+ struct message_header_line line;
+
+ struct istream *input;
+ struct message_size *hdr_size;
+
+ string_t *name;
+ buffer_t *value_buf;
+
+ enum message_header_parser_flags flags;
+ bool skip_line:1;
+ bool has_nuls:1;
+};
+
+struct message_header_parser_ctx *
+message_parse_header_init(struct istream *input, struct message_size *hdr_size,
+ enum message_header_parser_flags flags)
+{
+ struct message_header_parser_ctx *ctx;
+
+ ctx = i_new(struct message_header_parser_ctx, 1);
+ ctx->input = input;
+ ctx->hdr_size = hdr_size;
+ ctx->name = str_new(default_pool, 128);
+ ctx->flags = flags;
+ ctx->value_buf = buffer_create_dynamic(default_pool, 4096);
+ i_stream_ref(input);
+
+ if (hdr_size != NULL)
+ i_zero(hdr_size);
+ return ctx;
+}
+
+void message_parse_header_deinit(struct message_header_parser_ctx **_ctx)
+{
+ struct message_header_parser_ctx *ctx = *_ctx;
+
+ i_stream_unref(&ctx->input);
+ buffer_free(&ctx->value_buf);
+ str_free(&ctx->name);
+ i_free(ctx);
+
+ *_ctx = NULL;
+}
+
+int message_parse_header_next(struct message_header_parser_ctx *ctx,
+ struct message_header_line **hdr_r)
+{
+ struct message_header_line *line = &ctx->line;
+ const unsigned char *msg;
+ size_t i, size, startpos, colon_pos, parse_size, skip = 0;
+ int ret;
+ bool continued, continues, last_no_newline, last_crlf;
+ bool no_newline, crlf_newline;
+
+ *hdr_r = NULL;
+ if (line->eoh)
+ return -1;
+
+ if (line->continues)
+ colon_pos = 0;
+ else {
+ /* new header line */
+ line->name_offset = ctx->input->v_offset;
+ colon_pos = UINT_MAX;
+ buffer_set_used_size(ctx->value_buf, 0);
+ }
+
+ no_newline = FALSE;
+ crlf_newline = FALSE;
+ continued = line->continues;
+ continues = FALSE;
+
+ for (startpos = 0;;) {
+ ret = i_stream_read_bytes(ctx->input, &msg, &size, startpos+2);
+ if (ret >= 0) {
+ /* we want to know one byte in advance to find out
+ if it's multiline header */
+ parse_size = size == 0 ? 0 : size-1;
+ } else {
+ parse_size = size;
+ }
+
+ if (ret <= 0 && startpos == parse_size) {
+ if (ret == -1) {
+ if (startpos > 0) {
+ /* header ended unexpectedly. */
+ no_newline = TRUE;
+ skip = startpos;
+ break;
+ }
+ /* error / EOF with no bytes */
+ i_assert(skip == 0);
+ return -1;
+ }
+
+ if (size > 0 && !ctx->skip_line && !continued &&
+ (msg[0] == '\n' ||
+ (msg[0] == '\r' && size > 1 && msg[1] == '\n'))) {
+ /* end of headers - this mostly happens just
+ with mbox where headers are read separately
+ from body */
+ size = 0;
+ if (ctx->hdr_size != NULL)
+ ctx->hdr_size->lines++;
+ if (msg[0] == '\r') {
+ skip = 2;
+ crlf_newline = TRUE;
+ } else {
+ skip = 1;
+ if (ctx->hdr_size != NULL)
+ ctx->hdr_size->virtual_size++;
+ }
+ break;
+ }
+ if (ret == 0 && !ctx->input->eof) {
+ /* stream is nonblocking - need more data */
+ i_assert(skip == 0);
+ return 0;
+ }
+ i_assert(size > 0);
+
+ /* a) line is larger than input buffer
+ b) header ended unexpectedly */
+ if (ret == -2) {
+ /* go back to last LWSP if found. */
+ size_t min_pos = !continued ? colon_pos : 0;
+ for (i = size-1; i > min_pos; i--) {
+ if (IS_LWSP(msg[i])) {
+ size = i;
+ break;
+ }
+ }
+ if (i == min_pos && (msg[size-1] == '\r' ||
+ msg[size-1] == '\n')) {
+ /* we may or may not have a full header,
+ but we don't know until we get the
+ next character. leave out the
+ linefeed and finish the header on
+ the next run. */
+ size--;
+ if (size > 0 && msg[size-1] == '\r')
+ size--;
+ }
+ /* the buffer really has to be more than 2 to
+ avoid CRLF looping forever */
+ i_assert(size > 0);
+
+ continues = TRUE;
+ }
+ no_newline = TRUE;
+ skip = size;
+ break;
+ }
+
+ /* find ':' */
+ if (colon_pos == UINT_MAX) {
+ for (i = startpos; i < parse_size; i++) {
+ if (msg[i] > ':')
+ continue;
+
+ if (msg[i] == ':' && !ctx->skip_line) {
+ colon_pos = i;
+ line->full_value_offset =
+ ctx->input->v_offset + i + 1;
+ break;
+ }
+ if (msg[i] == '\n') {
+ /* end of headers, or error */
+ break;
+ }
+
+ if (msg[i] == '\0')
+ ctx->has_nuls = TRUE;
+ }
+ } else {
+ i = startpos;
+ }
+
+ /* find '\n' */
+ for (; i < parse_size; i++) {
+ if (msg[i] <= '\n') {
+ if (msg[i] == '\n')
+ break;
+ if (msg[i] == '\0')
+ ctx->has_nuls = TRUE;
+ }
+ }
+
+ if (i < parse_size && i+1 == size && ret == -2) {
+ /* we don't know if the line continues. */
+ i++;
+ } else if (i < parse_size) {
+ /* got a line */
+ if (ctx->skip_line) {
+ /* skipping a line with a huge header name */
+ if (ctx->hdr_size != NULL) {
+ ctx->hdr_size->lines++;
+ ctx->hdr_size->physical_size += i + 1;
+ ctx->hdr_size->virtual_size += i + 1;
+ }
+ if (i == 0 || msg[i-1] != '\r') {
+ /* missing CR */
+ if (ctx->hdr_size != NULL)
+ ctx->hdr_size->virtual_size++;
+ }
+
+ i_stream_skip(ctx->input, i + 1);
+ startpos = 0;
+ ctx->skip_line = FALSE;
+ continue;
+ }
+ continues = i+1 < size && IS_LWSP(msg[i+1]);
+
+ if (ctx->hdr_size != NULL)
+ ctx->hdr_size->lines++;
+ if (i == 0 || msg[i-1] != '\r') {
+ /* missing CR */
+ if (ctx->hdr_size != NULL)
+ ctx->hdr_size->virtual_size++;
+ size = i;
+ } else {
+ size = i-1;
+ crlf_newline = TRUE;
+ }
+
+ skip = i+1;
+ break;
+ }
+
+ startpos = i;
+ }
+
+ last_crlf = line->crlf_newline &&
+ (ctx->flags & MESSAGE_HEADER_PARSER_FLAG_DROP_CR) == 0;
+ last_no_newline = line->no_newline ||
+ (ctx->flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0;
+
+ line->continues = continues;
+ line->continued = continued;
+ line->crlf_newline = crlf_newline;
+ line->no_newline = no_newline;
+ if (size == 0 && !continued) {
+ /* end of headers */
+ line->eoh = TRUE;
+ line->name_len = line->value_len = line->full_value_len = 0;
+ line->name = ""; line->value = line->full_value = NULL;
+ line->middle = NULL; line->middle_len = 0;
+ line->full_value_offset = line->name_offset;
+ line->continues = FALSE;
+ } else if (line->continued) {
+ line->value = msg;
+ line->value_len = size;
+ } else if (colon_pos == UINT_MAX) {
+ /* missing ':', assume the whole line is value */
+ line->value = msg;
+ line->value_len = size;
+ line->full_value_offset = line->name_offset;
+
+ line->name = "";
+ line->name_len = 0;
+
+ line->middle = uchar_empty_ptr;
+ line->middle_len = 0;
+ } else {
+ size_t pos;
+
+ line->value = msg + colon_pos+1;
+ line->value_len = size - colon_pos - 1;
+ if ((ctx->flags & MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP) != 0) {
+ /* get value. skip all LWSP after ':'. Note that
+ RFC2822 doesn't say we should, but history behind
+ it..
+
+ Exception to this is if the value consists only of
+ LWSP, then skip only the one LWSP after ':'. */
+ for (pos = 0; pos < line->value_len; pos++) {
+ if (!IS_LWSP(line->value[pos]))
+ break;
+ }
+
+ if (pos == line->value_len) {
+ /* everything was LWSP */
+ if (line->value_len > 0 &&
+ IS_LWSP(line->value[0]))
+ pos = 1;
+ }
+ } else {
+ pos = line->value_len > 0 &&
+ IS_LWSP(line->value[0]) ? 1 : 0;
+ }
+
+ line->value += pos;
+ line->value_len -= pos;
+ line->full_value_offset += pos;
+
+ /* get name, skip LWSP before ':' */
+ while (colon_pos > 0 && IS_LWSP(msg[colon_pos-1]))
+ colon_pos--;
+
+ /* Treat overlong header names as if the full header line was
+ a value. Callers can usually handle large values better than
+ large names. */
+ if (colon_pos > MESSAGE_HEADER_NAME_MAX_LEN) {
+ line->name = "";
+ line->name_len = 0;
+ line->middle = uchar_empty_ptr;
+ line->middle_len = 0;
+ line->value = msg;
+ line->value_len = size;
+ line->full_value_offset = line->name_offset;
+ } else {
+ str_truncate(ctx->name, 0);
+ /* use buffer_append() so the name won't be truncated if there
+ are NULs. */
+ buffer_append(ctx->name, msg, colon_pos);
+ str_append_c(ctx->name, '\0');
+
+ /* keep middle stored also in ctx->name so it's available
+ with use_full_value */
+ line->middle = msg + colon_pos;
+ line->middle_len = (size_t)(line->value - line->middle);
+ str_append_data(ctx->name, line->middle, line->middle_len);
+
+ line->name = str_c(ctx->name);
+ line->name_len = colon_pos;
+ line->middle = str_data(ctx->name) + line->name_len + 1;
+ }
+ }
+
+ if (!line->continued) {
+ /* first header line. make a copy of the line since we can't
+ really trust input stream not to lose it. */
+ buffer_append(ctx->value_buf, line->value, line->value_len);
+ line->value = line->full_value = ctx->value_buf->data;
+ line->full_value_len = line->value_len;
+ } else if (line->use_full_value) {
+ /* continue saving the full value. */
+ if (last_no_newline) {
+ /* line is longer than fit into our buffer, so we
+ were forced to break it into multiple
+ message_header_lines */
+ } else {
+ if (last_crlf)
+ buffer_append_c(ctx->value_buf, '\r');
+ buffer_append_c(ctx->value_buf, '\n');
+ }
+ if ((ctx->flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0 &&
+ line->value_len > 0 && line->value[0] != ' ' &&
+ IS_LWSP(line->value[0])) {
+ buffer_append_c(ctx->value_buf, ' ');
+ buffer_append(ctx->value_buf,
+ line->value + 1, line->value_len - 1);
+ } else {
+ buffer_append(ctx->value_buf,
+ line->value, line->value_len);
+ }
+ line->full_value = ctx->value_buf->data;
+ line->full_value_len = ctx->value_buf->used;
+ } else {
+ /* we didn't want full_value, and this is a continued line. */
+ line->full_value = NULL;
+ line->full_value_len = 0;
+ }
+
+ /* always reset it */
+ line->use_full_value = FALSE;
+
+ if (ctx->hdr_size != NULL) {
+ ctx->hdr_size->physical_size += skip;
+ ctx->hdr_size->virtual_size += skip;
+ }
+ i_stream_skip(ctx->input, skip);
+
+ *hdr_r = line;
+ return 1;
+}
+
+bool message_parse_header_has_nuls(const struct message_header_parser_ctx *ctx)
+{
+ return ctx->has_nuls;
+}
+
+#undef message_parse_header
+void message_parse_header(struct istream *input, struct message_size *hdr_size,
+ enum message_header_parser_flags flags,
+ message_header_callback_t *callback, void *context)
+{
+ struct message_header_parser_ctx *hdr_ctx;
+ struct message_header_line *hdr;
+ int ret;
+
+ hdr_ctx = message_parse_header_init(input, hdr_size, flags);
+ while ((ret = message_parse_header_next(hdr_ctx, &hdr)) > 0)
+ callback(hdr, context);
+ i_assert(ret != 0);
+ message_parse_header_deinit(&hdr_ctx);
+
+ /* call after the final skipping */
+ callback(NULL, context);
+}
+
+void message_header_line_write(buffer_t *output,
+ const struct message_header_line *hdr)
+{
+ if (!hdr->continued) {
+ buffer_append(output, hdr->name, strlen(hdr->name));
+ buffer_append(output, hdr->middle, hdr->middle_len);
+ }
+ buffer_append(output, hdr->value, hdr->value_len);
+ if (!hdr->no_newline) {
+ if (hdr->crlf_newline)
+ buffer_append_c(output, '\r');
+ buffer_append_c(output, '\n');
+ }
+}
+
+const char *
+message_header_strdup(pool_t pool, const unsigned char *data, size_t size)
+{
+ if (memchr(data, '\0', size) == NULL) {
+ /* fast path */
+ char *dest = p_malloc(pool, size+1);
+ memcpy(dest, data, size);
+ return dest;
+ }
+
+ /* slow path - this could be made faster, but it should be
+ rare so keep it simple */
+ string_t *str = str_new(pool, size+2);
+ for (size_t i = 0; i < size; i++) {
+ if (data[i] != '\0')
+ str_append_c(str, data[i]);
+ else
+ str_append(str, UNICODE_REPLACEMENT_CHAR_UTF8);
+ }
+ return str_c(str);
+}
+
+bool message_header_name_is_valid(const char *name)
+{
+ /*
+ field-name = 1*ftext
+
+ ftext = %d33-57 / ; Printable US-ASCII
+ %d59-126 ; characters not including
+ ; ":".
+ */
+ for (unsigned int i = 0; name[i] != '\0'; i++) {
+ unsigned char c = name[i];
+ if (c >= 33 && c <= 57) {
+ /* before ":" */
+ } else if (c >= 59 && c <= 126) {
+ /* after ":" */
+ } else {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
diff --git a/src/lib-mail/message-header-parser.h b/src/lib-mail/message-header-parser.h
new file mode 100644
index 0000000..ce0825c
--- /dev/null
+++ b/src/lib-mail/message-header-parser.h
@@ -0,0 +1,85 @@
+#ifndef MESSAGE_HEADER_PARSER_H
+#define MESSAGE_HEADER_PARSER_H
+
+#define IS_LWSP(c) \
+ ((c) == ' ' || (c) == '\t')
+
+struct message_size;
+struct message_header_parser_ctx;
+
+enum message_header_parser_flags {
+ /* Don't add LWSP after "header: " to value. */
+ MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP = 0x01,
+ /* Don't add CRs to full_value even if input had them */
+ MESSAGE_HEADER_PARSER_FLAG_DROP_CR = 0x02,
+ /* Convert [CR+]LF+LWSP to a space character in full_value */
+ MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE = 0x04
+};
+
+struct message_header_line {
+ const char *name;
+ size_t name_len;
+
+ const unsigned char *value;
+ size_t value_len;
+
+ const unsigned char *full_value;
+ size_t full_value_len;
+
+ const unsigned char *middle;
+ size_t middle_len;
+
+ uoff_t name_offset, full_value_offset;
+
+ bool continues:1; /* multiline header, continues in next line */
+ bool continued:1; /* multiline header, continues */
+ bool eoh:1; /* "end of headers" line */
+ bool no_newline:1; /* no \n after this line */
+ bool crlf_newline:1; /* newline was \r\n */
+ bool use_full_value:1; /* set if you want full_value */
+};
+
+/* called once with hdr = NULL at the end of headers */
+typedef void message_header_callback_t(struct message_header_line *hdr,
+ void *context);
+
+struct message_header_parser_ctx *
+message_parse_header_init(struct istream *input, struct message_size *hdr_size,
+ enum message_header_parser_flags flags) ATTR_NULL(2);
+void message_parse_header_deinit(struct message_header_parser_ctx **ctx);
+
+/* Read and return next header line. Returns 1 if header is returned, 0 if
+ input stream is non-blocking and more data needs to be read, -1 when all is
+ done or error occurred (see stream's error status). */
+int message_parse_header_next(struct message_header_parser_ctx *ctx,
+ struct message_header_line **hdr_r);
+
+/* Returns TRUE if the parser has seen NUL characters. */
+bool message_parse_header_has_nuls(const struct message_header_parser_ctx *ctx)
+ ATTR_PURE;
+
+/* Read and parse the header from the given stream. */
+void message_parse_header(struct istream *input, struct message_size *hdr_size,
+ enum message_header_parser_flags flags,
+ message_header_callback_t *callback, void *context)
+ ATTR_NULL(2);
+#define message_parse_header(input, hdr_size, flags, callback, context) \
+ message_parse_header(input, hdr_size, flags - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct message_header_line *hdr, typeof(context))), \
+ (message_header_callback_t *)callback, context)
+
+/* Write the header line to buffer exactly as it was read, including the
+ newline. */
+void message_header_line_write(buffer_t *output,
+ const struct message_header_line *hdr);
+
+/* Duplicate the given header value data and return it. Replaces any NULs with
+ UNICODE_REPLACEMENT_CHAR_UTF8. */
+const char *
+message_header_strdup(pool_t pool, const unsigned char *data, size_t size);
+
+/* Returns TRUE if message header name is valid. */
+bool message_header_name_is_valid(const char *name);
+
+#endif
diff --git a/src/lib-mail/message-id.c b/src/lib-mail/message-id.c
new file mode 100644
index 0000000..68e2be0
--- /dev/null
+++ b/src/lib-mail/message-id.c
@@ -0,0 +1,129 @@
+/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "rfc822-parser.h"
+#include "message-id.h"
+
+static bool get_untokenized_msgid(const char **msgid_p, string_t *msgid)
+{
+ struct rfc822_parser_context parser;
+ int ret;
+ bool success = FALSE;
+
+ rfc822_parser_init(&parser, (const unsigned char *)*msgid_p,
+ strlen(*msgid_p), NULL);
+
+ /*
+ msg-id = [CFWS] "<" id-left "@" id-right ">" [CFWS]
+ id-left = dot-atom-text / no-fold-quote / obs-id-left
+ id-right = dot-atom-text / no-fold-literal / obs-id-right
+ no-fold-quote = DQUOTE *(qtext / quoted-pair) DQUOTE
+ no-fold-literal = "[" *(dtext / quoted-pair) "]"
+ */
+
+ rfc822_skip_lwsp(&parser);
+
+ if (*parser.data == '"')
+ ret = rfc822_parse_quoted_string(&parser, msgid);
+ else
+ ret = rfc822_parse_dot_atom(&parser, msgid);
+ if (ret > 0 && *parser.data == '@') {
+ str_append_c(msgid, '@');
+ parser.data++;
+ rfc822_skip_lwsp(&parser);
+
+ if (rfc822_parse_dot_atom(&parser, msgid) > 0 &&
+ *parser.data == '>') {
+ *msgid_p = (const char *)parser.data + 1;
+ success = TRUE;
+ }
+ }
+ rfc822_parser_deinit(&parser);
+ return success;
+}
+
+static void strip_lwsp(char *str)
+{
+ /* @UNSAFE */
+ char *dest;
+
+ /* find the first lwsp */
+ while (*str != ' ' && *str != '\t' && *str != '\r' && *str != '\n') {
+ if (*str == '\0')
+ return;
+ str++;
+ }
+
+ for (dest = str; *str != '\0'; str++) {
+ if (*str != ' ' && *str != '\t' && *str != '\r' && *str != '\n')
+ *dest++ = *str;
+ }
+ *dest = '\0';
+}
+
+const char *message_id_get_next(const char **msgid_p)
+{
+ const char *msgid = *msgid_p;
+ const char *p;
+ string_t *str = NULL;
+ bool found_at;
+
+ if (*msgid_p == NULL)
+ return NULL;
+
+ for (;;) {
+ /* skip until '<' */
+ while (*msgid != '<') {
+ if (*msgid == '\0') {
+ *msgid_p = msgid;
+ return NULL;
+ }
+ msgid++;
+ }
+ msgid++;
+
+ /* check it through quickly to see if it's already normalized */
+ p = msgid; found_at = FALSE;
+ for (;; p++) {
+ if ((unsigned char)*p >= 'A') /* matches most */
+ continue;
+
+ if (*p == '@')
+ found_at = TRUE;
+ if (*p == '>' || *p == '"' || *p == '(' || *p == '[')
+ break;
+
+ if (*p == '\0') {
+ *msgid_p = p;
+ return NULL;
+ }
+ }
+
+ if (*p == '>') {
+ *msgid_p = p+1;
+ if (found_at) {
+ char *s;
+
+ s = p_strdup_until(unsafe_data_stack_pool,
+ msgid, p);
+ strip_lwsp(s);
+ return s;
+ }
+ } else {
+ /* ok, do it the slow way */
+ *msgid_p = msgid;
+
+ if (str == NULL) {
+ /* allocate only once, so we don't leak
+ with multiple invalid message IDs */
+ str = t_str_new(256);
+ }
+ if (get_untokenized_msgid(msgid_p, str))
+ return str_c(str);
+ }
+
+ /* invalid message id, see if there's another valid one */
+ msgid = *msgid_p;
+ }
+}
diff --git a/src/lib-mail/message-id.h b/src/lib-mail/message-id.h
new file mode 100644
index 0000000..fcfff44
--- /dev/null
+++ b/src/lib-mail/message-id.h
@@ -0,0 +1,8 @@
+#ifndef MESSAGE_ID_H
+#define MESSAGE_ID_H
+
+/* Returns the next valid message ID from a given Message-ID header.
+ The return value is allocated from data stack. */
+const char *message_id_get_next(const char **msgid_p);
+
+#endif
diff --git a/src/lib-mail/message-parser-from-parts.c b/src/lib-mail/message-parser-from-parts.c
new file mode 100644
index 0000000..8e21ec8
--- /dev/null
+++ b/src/lib-mail/message-parser-from-parts.c
@@ -0,0 +1,365 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "message-parser-private.h"
+
+static int preparsed_parse_epilogue_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+static int preparsed_parse_next_header_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+
+static int preparsed_parse_eof(struct message_parser_ctx *ctx ATTR_UNUSED,
+ struct message_block *block_r ATTR_UNUSED)
+{
+ return -1;
+}
+
+static void preparsed_skip_to_next(struct message_parser_ctx *ctx)
+{
+ ctx->parse_next_block = preparsed_parse_next_header_init;
+ while (ctx->part != NULL) {
+ if (ctx->part->next != NULL) {
+ ctx->part = ctx->part->next;
+ break;
+ }
+
+ /* parse epilogue of multipart parent if requested */
+ if (ctx->part->parent != NULL &&
+ (ctx->part->parent->flags & MESSAGE_PART_FLAG_MULTIPART) != 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS) != 0) {
+ /* check for presence of epilogue */
+ uoff_t part_end = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size +
+ ctx->part->body_size.physical_size;
+ uoff_t parent_end = ctx->part->parent->physical_pos +
+ ctx->part->parent->header_size.physical_size +
+ ctx->part->parent->body_size.physical_size;
+
+ if (parent_end > part_end) {
+ ctx->parse_next_block = preparsed_parse_epilogue_init;
+ break;
+ }
+ }
+ ctx->part = ctx->part->parent;
+ }
+ if (ctx->part == NULL)
+ ctx->parse_next_block = preparsed_parse_eof;
+}
+
+static int preparsed_parse_body_finish(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ i_stream_skip(ctx->input, ctx->skip);
+ ctx->skip = 0;
+
+ preparsed_skip_to_next(ctx);
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int preparsed_parse_prologue_finish(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ i_stream_skip(ctx->input, ctx->skip);
+ ctx->skip = 0;
+
+ ctx->parse_next_block = preparsed_parse_next_header_init;
+ ctx->part = ctx->part->children;
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int preparsed_parse_body_more(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t end_offset = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size +
+ ctx->part->body_size.physical_size;
+ bool full;
+ int ret;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ if (ctx->input->v_offset + block_r->size >= end_offset) {
+ block_r->size = end_offset - ctx->input->v_offset;
+ ctx->parse_next_block = preparsed_parse_body_finish;
+ }
+ ctx->skip = block_r->size;
+ return 1;
+}
+
+static int preparsed_parse_prologue_more(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t boundary_min_start, end_offset;
+ const unsigned char *cur;
+ bool full;
+ int ret;
+
+ i_assert(ctx->part->children != NULL);
+ end_offset = ctx->part->children->physical_pos;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ if (ctx->input->v_offset + block_r->size >= end_offset) {
+ /* we've got the full prologue: clip off the initial boundary */
+ block_r->size = end_offset - ctx->input->v_offset;
+ cur = block_r->data + block_r->size - 1;
+
+ /* [\r]\n--boundary[\r]\n */
+ if (block_r->size < 5 || *cur != '\n') {
+ ctx->broken_reason = "Prologue boundary end not at expected position";
+ return -1;
+ }
+
+ cur--;
+ if (*cur == '\r') cur--;
+
+ /* find newline just before boundary */
+ for (; cur >= block_r->data; cur--) {
+ if (*cur == '\n') break;
+ }
+
+ if (cur[0] != '\n' || cur[1] != '-' || cur[2] != '-') {
+ ctx->broken_reason = "Prologue boundary beginning not at expected position";
+ return -1;
+ }
+
+ if (cur != block_r->data && cur[-1] == '\r') cur--;
+
+ /* clip boundary */
+ block_r->size = cur - block_r->data;
+
+ ctx->parse_next_block = preparsed_parse_prologue_finish;
+ ctx->skip = block_r->size;
+ return 1;
+ }
+
+ /* retain enough data in the stream buffer to contain initial boundary */
+ if (end_offset > BOUNDARY_END_MAX_LEN)
+ boundary_min_start = end_offset - BOUNDARY_END_MAX_LEN;
+ else
+ boundary_min_start = 0;
+
+ if (ctx->input->v_offset + block_r->size >= boundary_min_start) {
+ if (boundary_min_start <= ctx->input->v_offset)
+ return 0;
+ block_r->size = boundary_min_start - ctx->input->v_offset;
+ }
+ ctx->skip = block_r->size;
+ return 1;
+}
+
+static int preparsed_parse_epilogue_more(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t end_offset = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size +
+ ctx->part->body_size.physical_size;
+ bool full;
+ int ret;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ if (ctx->input->v_offset + block_r->size >= end_offset) {
+ block_r->size = end_offset - ctx->input->v_offset;
+ ctx->parse_next_block = preparsed_parse_body_finish;
+ }
+ ctx->skip = block_r->size;
+ return 1;
+}
+
+static int preparsed_parse_epilogue_boundary(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t end_offset = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size +
+ ctx->part->body_size.physical_size;
+ const unsigned char *data, *cur;
+ size_t size;
+ bool full;
+ int ret;
+
+ if (end_offset - ctx->input->v_offset < 7) {
+ ctx->broken_reason = "Epilogue position is wrong";
+ return -1;
+ }
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ /* [\r]\n--boundary--[\r]\n */
+ if (block_r->size < 7) {
+ ctx->want_count = 7;
+ return 0;
+ }
+
+ data = block_r->data;
+ size = block_r->size;
+ cur = data;
+
+ if (*cur == '\r') cur++;
+
+ if (cur[0] != '\n' || cur[1] != '-' || data[2] != '-') {
+ ctx->broken_reason = "Epilogue boundary start not at expected position";
+ return -1;
+ }
+
+ /* find the end of the line */
+ cur += 3;
+ if ((cur = memchr(cur, '\n', size - (cur-data))) == NULL) {
+ if (end_offset < ctx->input->v_offset + size) {
+ ctx->broken_reason = "Epilogue boundary end not at expected position";
+ return -1;
+ } else if (ctx->input->v_offset + size < end_offset &&
+ size < BOUNDARY_END_MAX_LEN &&
+ !ctx->input->eof && !full) {
+ ctx->want_count = BOUNDARY_END_MAX_LEN;
+ return 0;
+ }
+ }
+
+ block_r->size = 0;
+ ctx->parse_next_block = preparsed_parse_epilogue_more;
+ ctx->skip = cur - data + 1;
+ return 0;
+}
+
+static int preparsed_parse_body_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t offset = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size;
+
+ if (offset < ctx->input->v_offset) {
+ /* header was actually larger than the cached size suggested */
+ ctx->broken_reason = "Header larger than its cached size";
+ return -1;
+ }
+ i_stream_skip(ctx->input, offset - ctx->input->v_offset);
+
+ /* multipart messages may begin with --boundary--, which makes them
+ not have any children. */
+ if ((ctx->part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0 ||
+ ctx->part->children == NULL)
+ ctx->parse_next_block = preparsed_parse_body_more;
+ else
+ ctx->parse_next_block = preparsed_parse_prologue_more;
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int preparsed_parse_epilogue_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ uoff_t offset = ctx->part->physical_pos +
+ ctx->part->header_size.physical_size +
+ ctx->part->body_size.physical_size;
+
+ ctx->part = ctx->part->parent;
+
+ if (offset < ctx->input->v_offset) {
+ /* last child was actually larger than the cached size
+ suggested */
+ ctx->broken_reason = "Part larger than its cached size";
+ return -1;
+ }
+ i_stream_skip(ctx->input, offset - ctx->input->v_offset);
+
+ ctx->parse_next_block = preparsed_parse_epilogue_boundary;
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int preparsed_parse_finish_header(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ if (ctx->part->children != NULL) {
+ if ((ctx->part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS) != 0)
+ ctx->parse_next_block = preparsed_parse_body_init;
+ else {
+ ctx->parse_next_block = preparsed_parse_next_header_init;
+ ctx->part = ctx->part->children;
+ }
+ } else if ((ctx->flags & MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK) == 0) {
+ ctx->parse_next_block = preparsed_parse_body_init;
+ } else {
+ preparsed_skip_to_next(ctx);
+ }
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int preparsed_parse_next_header(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ struct message_header_line *hdr;
+ int ret;
+
+ ret = message_parse_header_next(ctx->hdr_parser_ctx, &hdr);
+ if (ret == 0 || (ret < 0 && ctx->input->stream_errno != 0)) {
+ ctx->want_count = i_stream_get_data_size(ctx->input) + 1;
+ return ret;
+ }
+
+ if (hdr != NULL) {
+ block_r->hdr = hdr;
+ block_r->size = 0;
+ return 1;
+ }
+ message_parse_header_deinit(&ctx->hdr_parser_ctx);
+
+ ctx->parse_next_block = preparsed_parse_finish_header;
+
+ /* return empty block as end of headers */
+ block_r->hdr = NULL;
+ block_r->size = 0;
+
+ i_assert(ctx->skip == 0);
+ if (ctx->input->v_offset != ctx->part->physical_pos +
+ ctx->part->header_size.physical_size) {
+ ctx->broken_reason = "Cached header size mismatch";
+ return -1;
+ }
+ return 1;
+}
+
+static int preparsed_parse_next_header_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ struct istream *hdr_input;
+
+ i_assert(ctx->hdr_parser_ctx == NULL);
+
+ i_assert(ctx->part->physical_pos >= ctx->input->v_offset);
+ i_stream_skip(ctx->input, ctx->part->physical_pos -
+ ctx->input->v_offset);
+
+ /* the header may become truncated by --boundaries. limit the header
+ stream's size to what it's supposed to be to avoid duplicating (and
+ keeping in sync!) all the same complicated logic as in
+ parse_next_header(). */
+ hdr_input = i_stream_create_limit(ctx->input, ctx->part->header_size.physical_size);
+ ctx->hdr_parser_ctx =
+ message_parse_header_init(hdr_input, NULL, ctx->hdr_flags);
+ i_stream_unref(&hdr_input);
+
+ ctx->parse_next_block = preparsed_parse_next_header;
+ return preparsed_parse_next_header(ctx, block_r);
+}
+
+struct message_parser_ctx *
+message_parser_init_from_parts(struct message_part *parts,
+ struct istream *input,
+ const struct message_parser_settings *set)
+{
+ struct message_parser_ctx *ctx;
+
+ i_assert(parts != NULL);
+
+ ctx = message_parser_init_int(input, set);
+ ctx->preparsed = TRUE;
+ ctx->parts = ctx->part = parts;
+ ctx->parse_next_block = preparsed_parse_next_header_init;
+ return ctx;
+}
diff --git a/src/lib-mail/message-parser-private.h b/src/lib-mail/message-parser-private.h
new file mode 100644
index 0000000..41c32da
--- /dev/null
+++ b/src/lib-mail/message-parser-private.h
@@ -0,0 +1,62 @@
+#ifndef MESSAGE_PARSER_PRIVATE_H
+#define MESSAGE_PARSER_PRIVATE_H
+
+#include "message-parser.h"
+
+/* RFC-2046 requires boundaries are max. 70 chars + "--" prefix + "--" suffix.
+ We'll add a bit more just in case. */
+#define BOUNDARY_STRING_MAX_LEN (70 + 10)
+#define BOUNDARY_END_MAX_LEN (BOUNDARY_STRING_MAX_LEN + 2 + 2)
+
+struct message_boundary {
+ struct message_boundary *next;
+
+ struct message_part *part;
+ char *boundary;
+ size_t len;
+
+ bool epilogue_found:1;
+};
+
+struct message_parser_ctx {
+ pool_t part_pool;
+ struct istream *input;
+ struct message_part *parts, *part;
+ const char *broken_reason;
+ unsigned int nested_parts_count;
+ unsigned int total_parts_count;
+
+ enum message_header_parser_flags hdr_flags;
+ enum message_parser_flags flags;
+ unsigned int max_nested_mime_parts;
+ unsigned int max_total_mime_parts;
+
+ char *last_boundary;
+ struct message_boundary *boundaries;
+
+ struct message_part **next_part;
+ ARRAY(struct message_part **) next_part_stack;
+
+ size_t skip;
+ unsigned char last_chr;
+ unsigned int want_count;
+
+ struct message_header_parser_ctx *hdr_parser_ctx;
+ unsigned int prev_hdr_newline_size;
+
+ int (*parse_next_block)(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+
+ bool part_seen_content_type:1;
+ bool multipart:1;
+ bool preparsed:1;
+ bool eof:1;
+};
+
+struct message_parser_ctx *
+message_parser_init_int(struct istream *input,
+ const struct message_parser_settings *set);
+int message_parser_read_more(struct message_parser_ctx *ctx,
+ struct message_block *block_r, bool *full_r);
+
+#endif
diff --git a/src/lib-mail/message-parser.c b/src/lib-mail/message-parser.c
new file mode 100644
index 0000000..9a9c9a3
--- /dev/null
+++ b/src/lib-mail/message-parser.c
@@ -0,0 +1,907 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "istream.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+#include "message-parser-private.h"
+
+message_part_header_callback_t *null_message_part_header_callback = NULL;
+
+static int parse_next_header_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+static int parse_next_body_to_boundary(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+static int parse_next_body_to_eof(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+
+static struct message_boundary *
+boundary_find(struct message_boundary *boundaries,
+ const unsigned char *data, size_t len, bool trailing_dashes)
+{
+ struct message_boundary *best = NULL;
+
+ /* As MIME spec says: search from latest one to oldest one so that we
+ don't break if the same boundary is used in nested parts. Also the
+ full message line doesn't have to match the boundary, only the
+ beginning. However, if there are multiple prefixes whose beginning
+ matches, use the longest matching one. */
+ while (boundaries != NULL) {
+ if (boundaries->len <= len &&
+ memcmp(boundaries->boundary, data, boundaries->len) == 0 &&
+ (best == NULL || best->len < boundaries->len)) {
+ best = boundaries;
+ /* If we see "foo--", it could either mean that there
+ is a boundary named "foo" that ends now or there's
+ a boundary "foo--" which continues. */
+ if (best->len == len ||
+ (best->len == len-2 && trailing_dashes)) {
+ /* This is exactly the wanted boundary. There
+ can't be a better one. */
+ break;
+ }
+ }
+
+ boundaries = boundaries->next;
+ }
+
+ return best;
+}
+
+static void parse_body_add_block(struct message_parser_ctx *ctx,
+ struct message_block *block)
+{
+ unsigned int missing_cr_count = 0;
+ const unsigned char *cur, *next, *data = block->data;
+
+ i_assert(block->size > 0);
+
+ block->hdr = NULL;
+
+ /* check if we have NULs */
+ if (memchr(data, '\0', block->size) != NULL)
+ ctx->part->flags |= MESSAGE_PART_FLAG_HAS_NULS;
+
+ /* count number of lines and missing CRs */
+ if (*data == '\n') {
+ ctx->part->body_size.lines++;
+ if (ctx->last_chr != '\r')
+ missing_cr_count++;
+ }
+
+ cur = data + 1;
+ while ((next = memchr(cur, '\n', block->size - (cur - data))) != NULL) {
+ ctx->part->body_size.lines++;
+ if (next[-1] != '\r')
+ missing_cr_count++;
+
+ cur = next + 1;
+ }
+ ctx->last_chr = data[block->size - 1];
+ ctx->skip += block->size;
+
+ ctx->part->body_size.physical_size += block->size;
+ ctx->part->body_size.virtual_size += block->size + missing_cr_count;
+}
+
+int message_parser_read_more(struct message_parser_ctx *ctx,
+ struct message_block *block_r, bool *full_r)
+{
+ int ret;
+
+ if (ctx->skip > 0) {
+ i_stream_skip(ctx->input, ctx->skip);
+ ctx->skip = 0;
+ }
+
+ *full_r = FALSE;
+ ret = i_stream_read_bytes(ctx->input, &block_r->data,
+ &block_r->size, ctx->want_count + 1);
+ if (ret <= 0) {
+ switch (ret) {
+ case 0:
+ if (!ctx->input->eof) {
+ i_assert(!ctx->input->blocking);
+ return 0;
+ }
+ break;
+ case -1:
+ i_assert(ctx->input->eof ||
+ ctx->input->stream_errno != 0);
+ ctx->eof = TRUE;
+ if (block_r->size != 0) {
+ /* EOF, but we still have some data.
+ return it. */
+ return 1;
+ }
+ return -1;
+ case -2:
+ *full_r = TRUE;
+ break;
+ default:
+ i_unreached();
+ }
+ }
+
+ if (!*full_r) {
+ /* reset number of wanted characters if we actually got them */
+ ctx->want_count = 1;
+ }
+ return 1;
+}
+
+static void
+message_part_append(struct message_parser_ctx *ctx)
+{
+ struct message_part *parent = ctx->part;
+ struct message_part *part;
+
+ i_assert(!ctx->preparsed);
+ i_assert(parent != NULL);
+ i_assert((parent->flags & (MESSAGE_PART_FLAG_MULTIPART |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) != 0);
+
+ part = p_new(ctx->part_pool, struct message_part, 1);
+ part->parent = parent;
+
+ /* set child position */
+ part->physical_pos =
+ parent->physical_pos +
+ parent->body_size.physical_size +
+ parent->header_size.physical_size;
+
+ /* add to parent's linked list */
+ *ctx->next_part = part;
+ /* update the parent's end-of-linked-list pointer */
+ struct message_part **next_part = &part->next;
+ array_push_back(&ctx->next_part_stack, &next_part);
+ /* This part is now the new parent for the next message_part_append()
+ call. Its linked list begins with the children pointer. */
+ ctx->next_part = &part->children;
+
+ ctx->part = part;
+ ctx->nested_parts_count++;
+ ctx->total_parts_count++;
+ i_assert(ctx->nested_parts_count < ctx->max_nested_mime_parts);
+ i_assert(ctx->total_parts_count <= ctx->max_total_mime_parts);
+}
+
+static void message_part_finish(struct message_parser_ctx *ctx)
+{
+ struct message_part **const *parent_next_partp;
+
+ if (!ctx->preparsed) {
+ i_assert(ctx->nested_parts_count > 0);
+ ctx->nested_parts_count--;
+
+ parent_next_partp = array_back(&ctx->next_part_stack);
+ array_pop_back(&ctx->next_part_stack);
+ ctx->next_part = *parent_next_partp;
+ }
+
+ message_size_add(&ctx->part->parent->body_size, &ctx->part->body_size);
+ message_size_add(&ctx->part->parent->body_size, &ctx->part->header_size);
+ ctx->part->parent->children_count += 1 + ctx->part->children_count;
+ ctx->part = ctx->part->parent;
+}
+
+static void message_boundary_free(struct message_boundary *b)
+{
+ i_free(b->boundary);
+ i_free(b);
+}
+
+static void
+boundary_remove_until(struct message_parser_ctx *ctx,
+ struct message_boundary *boundary)
+{
+ while (ctx->boundaries != boundary) {
+ struct message_boundary *cur = ctx->boundaries;
+
+ i_assert(cur != NULL);
+ ctx->boundaries = cur->next;
+ message_boundary_free(cur);
+
+ }
+ ctx->boundaries = boundary;
+}
+
+static void parse_next_body_multipart_init(struct message_parser_ctx *ctx)
+{
+ struct message_boundary *b;
+
+ b = i_new(struct message_boundary, 1);
+ b->part = ctx->part;
+ b->boundary = ctx->last_boundary;
+ ctx->last_boundary = NULL;
+ b->len = strlen(b->boundary);
+
+ b->next = ctx->boundaries;
+ ctx->boundaries = b;
+}
+
+static int parse_next_body_message_rfc822_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ message_part_append(ctx);
+ return parse_next_header_init(ctx, block_r);
+}
+
+static int
+boundary_line_find(struct message_parser_ctx *ctx,
+ const unsigned char *data, size_t size, bool full,
+ struct message_boundary **boundary_r)
+{
+ *boundary_r = NULL;
+
+ if (size < 2) {
+ i_assert(!full);
+
+ if (ctx->input->eof)
+ return -1;
+ ctx->want_count = 2;
+ return 0;
+ }
+
+ if (data[0] != '-' || data[1] != '-') {
+ /* not a boundary, just skip this line */
+ return -1;
+ }
+
+ if (ctx->total_parts_count >= ctx->max_total_mime_parts) {
+ /* can't add any more MIME parts. just stop trying to find
+ more boundaries. */
+ ctx->part->flags |= MESSAGE_PART_FLAG_OVERFLOW;
+ return -1;
+ }
+
+ /* need to find the end of line */
+ data += 2;
+ size -= 2;
+ const unsigned char *lf_pos = memchr(data, '\n', size);
+ if (lf_pos == NULL &&
+ size+2 < BOUNDARY_END_MAX_LEN &&
+ !ctx->input->eof && !full) {
+ /* no LF found */
+ ctx->want_count = BOUNDARY_END_MAX_LEN;
+ return 0;
+ }
+ size_t find_size = size;
+ bool trailing_dashes = FALSE;
+
+ if (lf_pos != NULL) {
+ find_size = lf_pos - data;
+ if (find_size > 0 && data[find_size-1] == '\r')
+ find_size--;
+ if (find_size > 2 && data[find_size-1] == '-' &&
+ data[find_size-2] == '-')
+ trailing_dashes = TRUE;
+ } else if (find_size > BOUNDARY_END_MAX_LEN)
+ find_size = BOUNDARY_END_MAX_LEN;
+
+ *boundary_r = boundary_find(ctx->boundaries, data, find_size,
+ trailing_dashes);
+ if (*boundary_r == NULL)
+ return -1;
+
+ (*boundary_r)->epilogue_found =
+ size >= (*boundary_r)->len + 2 &&
+ memcmp(data + (*boundary_r)->len, "--", 2) == 0;
+ return 1;
+}
+
+static int parse_next_mime_header_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ message_part_append(ctx);
+ ctx->part->flags |= MESSAGE_PART_FLAG_IS_MIME;
+
+ return parse_next_header_init(ctx, block_r);
+}
+
+static int parse_next_body_skip_boundary_line(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ const unsigned char *ptr;
+ int ret;
+ bool full;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ ptr = memchr(block_r->data, '\n', block_r->size);
+ if (ptr == NULL) {
+ parse_body_add_block(ctx, block_r);
+ if (block_r->size > 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES) != 0)
+ return 1;
+ return 0;
+ }
+
+ /* found the LF */
+ block_r->size = (ptr - block_r->data) + 1;
+ parse_body_add_block(ctx, block_r);
+
+ if (ctx->boundaries == NULL || ctx->boundaries->part != ctx->part) {
+ /* epilogue */
+ if (ctx->boundaries != NULL)
+ ctx->parse_next_block = parse_next_body_to_boundary;
+ else
+ ctx->parse_next_block = parse_next_body_to_eof;
+ } else {
+ /* a new MIME part begins */
+ ctx->parse_next_block = parse_next_mime_header_init;
+ }
+ if (block_r->size > 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES) != 0)
+ return 1;
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int parse_part_finish(struct message_parser_ctx *ctx,
+ struct message_boundary *boundary,
+ struct message_block *block_r, bool first_line)
+{
+ size_t line_size;
+ size_t boundary_len = boundary->len;
+ bool boundary_epilogue_found = boundary->epilogue_found;
+
+ i_assert(ctx->last_boundary == NULL);
+
+ /* get back to parent MIME part, summing the child MIME part sizes
+ into parent's body sizes */
+ while (ctx->part != boundary->part) {
+ message_part_finish(ctx);
+ i_assert(ctx->part != NULL);
+ }
+
+ if (boundary->epilogue_found) {
+ /* this boundary isn't needed anymore */
+ boundary_remove_until(ctx, boundary->next);
+ } else {
+ /* forget about the boundaries we possibly skipped */
+ boundary_remove_until(ctx, boundary);
+ }
+
+ /* the boundary itself should already be in buffer. add that. */
+ block_r->data = i_stream_get_data(ctx->input, &block_r->size);
+ i_assert(block_r->size >= ctx->skip);
+ block_r->data += ctx->skip;
+ /* [[\r]\n]--<boundary>[--] */
+ if (first_line)
+ line_size = 0;
+ else if (block_r->data[0] == '\r') {
+ i_assert(block_r->data[1] == '\n');
+ line_size = 2;
+ } else {
+ i_assert(block_r->data[0] == '\n');
+ line_size = 1;
+ }
+ line_size += 2 + boundary_len + (boundary_epilogue_found ? 2 : 0);
+ i_assert(block_r->size >= ctx->skip + line_size);
+ block_r->size = line_size;
+ parse_body_add_block(ctx, block_r);
+
+ ctx->parse_next_block = parse_next_body_skip_boundary_line;
+
+ if ((ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES) != 0)
+ return 1;
+ return ctx->parse_next_block(ctx, block_r);
+}
+
+static int parse_next_body_to_boundary(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ struct message_boundary *boundary = NULL;
+ const unsigned char *data, *cur, *next, *end;
+ size_t boundary_start;
+ int ret;
+ bool full;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ data = block_r->data;
+ if (ctx->last_chr == '\n') {
+ /* handle boundary in first line of message. alternatively
+ it's an empty line. */
+ ret = boundary_line_find(ctx, block_r->data,
+ block_r->size, full, &boundary);
+ if (ret >= 0) {
+ return ret == 0 ? 0 :
+ parse_part_finish(ctx, boundary, block_r, TRUE);
+ }
+ }
+
+ i_assert(block_r->size > 0);
+ boundary_start = 0;
+
+ /* skip to beginning of the next line. the first line was
+ handled already. */
+ cur = data; end = data + block_r->size;
+ while ((next = memchr(cur, '\n', end - cur)) != NULL) {
+ cur = next + 1;
+
+ boundary_start = next - data;
+ if (next > data && next[-1] == '\r')
+ boundary_start--;
+
+ if (boundary_start != 0) {
+ /* we can at least skip data until the first [CR]LF.
+ input buffer can't be full anymore. */
+ full = FALSE;
+ }
+
+ ret = boundary_line_find(ctx, cur, end - cur, full, &boundary);
+ if (ret >= 0) {
+ /* found / need more data */
+ if (ret == 0 && boundary_start == 0)
+ ctx->want_count += cur - block_r->data;
+ break;
+ }
+ }
+
+ if (next != NULL) {
+ /* found / need more data */
+ i_assert(ret >= 0);
+ i_assert(!(ret == 0 && full));
+ } else if (boundary_start == 0) {
+ /* no linefeeds in this block. we can just skip it. */
+ ret = 0;
+ if (block_r->data[block_r->size-1] == '\r' && !ctx->eof) {
+ /* this may be the beginning of the \r\n--boundary */
+ block_r->size--;
+ }
+ boundary_start = block_r->size;
+ } else {
+ /* the boundary wasn't found from this data block,
+ we'll need more data. */
+ ret = 0;
+ ctx->want_count = (block_r->size - boundary_start) + 1;
+ }
+
+ if (ret > 0 || (ret == 0 && !ctx->eof)) {
+ /* a) we found the boundary
+ b) we need more data and haven't reached EOF yet
+ so leave CR+LF + last line to buffer */
+ block_r->size = boundary_start;
+ }
+ if (block_r->size != 0) {
+ parse_body_add_block(ctx, block_r);
+
+ if ((ctx->part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS) == 0)
+ return 0;
+
+ return 1;
+ }
+ return ret <= 0 ? ret :
+ parse_part_finish(ctx, boundary, block_r, FALSE);
+}
+
+static int parse_next_body_to_eof(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ bool full;
+ int ret;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) <= 0)
+ return ret;
+
+ parse_body_add_block(ctx, block_r);
+
+ if ((ctx->part->flags & MESSAGE_PART_FLAG_MULTIPART) != 0 &&
+ (ctx->flags & MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS) == 0)
+ return 0;
+
+ return 1;
+}
+
+static void parse_content_type(struct message_parser_ctx *ctx,
+ struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ const char *const *results;
+ string_t *content_type;
+ int ret;
+
+ if (ctx->part_seen_content_type)
+ return;
+ ctx->part_seen_content_type = TRUE;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ content_type = t_str_new(64);
+ ret = rfc822_parse_content_type(&parser, content_type);
+
+ if (strcasecmp(str_c(content_type), "message/rfc822") == 0)
+ ctx->part->flags |= MESSAGE_PART_FLAG_MESSAGE_RFC822;
+ else if (strncasecmp(str_c(content_type), "text", 4) == 0 &&
+ (str_len(content_type) == 4 ||
+ str_data(content_type)[4] == '/'))
+ ctx->part->flags |= MESSAGE_PART_FLAG_TEXT;
+ else if (strncasecmp(str_c(content_type), "multipart/", 10) == 0) {
+ ctx->part->flags |= MESSAGE_PART_FLAG_MULTIPART;
+
+ if (strcasecmp(str_c(content_type)+10, "digest") == 0)
+ ctx->part->flags |= MESSAGE_PART_FLAG_MULTIPART_DIGEST;
+ }
+
+ if (ret < 0 ||
+ (ctx->part->flags & MESSAGE_PART_FLAG_MULTIPART) == 0 ||
+ ctx->last_boundary != NULL) {
+ rfc822_parser_deinit(&parser);
+ return;
+ }
+
+ rfc2231_parse(&parser, &results);
+ for (; *results != NULL; results += 2) {
+ if (strcasecmp(results[0], "boundary") == 0) {
+ /* truncate excessively long boundaries */
+ i_free(ctx->last_boundary);
+ ctx->last_boundary =
+ i_strndup(results[1], BOUNDARY_STRING_MAX_LEN);
+ break;
+ }
+ }
+ rfc822_parser_deinit(&parser);
+}
+
+static bool block_is_at_eoh(const struct message_block *block)
+{
+ if (block->size < 1)
+ return FALSE;
+ if (block->data[0] == '\n')
+ return TRUE;
+ if (block->data[0] == '\r') {
+ if (block->size < 2)
+ return FALSE;
+ if (block->data[1] == '\n')
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static bool parse_too_many_nested_mime_parts(struct message_parser_ctx *ctx)
+{
+ return ctx->nested_parts_count+1 >= ctx->max_nested_mime_parts;
+}
+
+#define MUTEX_FLAGS \
+ (MESSAGE_PART_FLAG_MESSAGE_RFC822 | MESSAGE_PART_FLAG_MULTIPART)
+
+static int parse_next_header(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ struct message_part *part = ctx->part;
+ struct message_header_line *hdr;
+ struct message_boundary *boundary;
+ bool full;
+ int ret;
+
+ if ((ret = message_parser_read_more(ctx, block_r, &full)) == 0)
+ return ret;
+
+ if (ret > 0 && block_is_at_eoh(block_r) &&
+ ctx->last_boundary != NULL &&
+ (part->flags & MESSAGE_PART_FLAG_IS_MIME) != 0) {
+ /* we are at the end of headers and we've determined that we're
+ going to start a multipart. add the boundary already here
+ at this point so we can reliably determine whether the
+ "\n--boundary" belongs to us or to a previous boundary.
+ this is a problem if the boundary prefixes are identical,
+ because MIME requires only the prefix to match. */
+ if (!parse_too_many_nested_mime_parts(ctx)) {
+ parse_next_body_multipart_init(ctx);
+ ctx->multipart = TRUE;
+ } else {
+ part->flags |= MESSAGE_PART_FLAG_OVERFLOW;
+ part->flags &= ENUM_NEGATE(MESSAGE_PART_FLAG_MULTIPART);
+ }
+ }
+
+ /* before parsing the header see if we can find a --boundary from here.
+ we're guaranteed to be at the beginning of the line here. */
+ if (ret > 0) {
+ ret = ctx->boundaries == NULL ? -1 :
+ boundary_line_find(ctx, block_r->data,
+ block_r->size, full, &boundary);
+ if (ret > 0 && boundary->part == ctx->part) {
+ /* our own body begins with our own --boundary.
+ we don't want to handle that yet. */
+ ret = -1;
+ }
+ }
+ if (ret < 0) {
+ /* no boundary */
+ ret = message_parse_header_next(ctx->hdr_parser_ctx, &hdr);
+ if (ret == 0 || (ret < 0 && ctx->input->stream_errno != 0)) {
+ ctx->want_count = i_stream_get_data_size(ctx->input) + 1;
+ return ret;
+ }
+ } else if (ret == 0) {
+ /* need more data */
+ return 0;
+ } else {
+ /* boundary found. stop parsing headers here. The previous
+ [CR]LF belongs to the MIME boundary though. */
+ if (ctx->prev_hdr_newline_size > 0) {
+ i_assert(ctx->part->header_size.lines > 0);
+ /* remove the newline size from the MIME header */
+ ctx->part->header_size.lines--;
+ ctx->part->header_size.physical_size -=
+ ctx->prev_hdr_newline_size;
+ ctx->part->header_size.virtual_size -= 2;
+ /* add the newline size to the parent's body */
+ ctx->part->parent->body_size.lines++;
+ ctx->part->parent->body_size.physical_size +=
+ ctx->prev_hdr_newline_size;
+ ctx->part->parent->body_size.virtual_size += 2;
+ }
+ hdr = NULL;
+ }
+
+ if (hdr != NULL) {
+ if (hdr->eoh)
+ ;
+ else if (strcasecmp(hdr->name, "Mime-Version") == 0) {
+ /* it's MIME. Content-* headers are valid */
+ part->flags |= MESSAGE_PART_FLAG_IS_MIME;
+ } else if (strcasecmp(hdr->name, "Content-Type") == 0) {
+ if ((ctx->flags &
+ MESSAGE_PARSER_FLAG_MIME_VERSION_STRICT) == 0)
+ part->flags |= MESSAGE_PART_FLAG_IS_MIME;
+
+ if (hdr->continues)
+ hdr->use_full_value = TRUE;
+ else T_BEGIN {
+ parse_content_type(ctx, hdr);
+ } T_END;
+ }
+
+ block_r->hdr = hdr;
+ block_r->size = 0;
+ ctx->prev_hdr_newline_size = hdr->no_newline ? 0 :
+ (hdr->crlf_newline ? 2 : 1);
+ return 1;
+ }
+
+ /* end of headers */
+ if ((part->flags & MESSAGE_PART_FLAG_IS_MIME) == 0) {
+ /* It's not MIME. Reset everything we found from
+ Content-Type. */
+ i_assert(!ctx->multipart);
+ part->flags = 0;
+ }
+ i_free(ctx->last_boundary);
+
+ if (!ctx->part_seen_content_type ||
+ (part->flags & MESSAGE_PART_FLAG_IS_MIME) == 0) {
+ if (part->parent != NULL &&
+ (part->parent->flags &
+ MESSAGE_PART_FLAG_MULTIPART_DIGEST) != 0) {
+ /* when there's no content-type specified and we're
+ below multipart/digest, assume message/rfc822
+ content-type */
+ part->flags |= MESSAGE_PART_FLAG_MESSAGE_RFC822;
+ } else {
+ /* otherwise we default to text/plain */
+ part->flags |= MESSAGE_PART_FLAG_TEXT;
+ }
+ }
+
+ if (message_parse_header_has_nuls(ctx->hdr_parser_ctx))
+ part->flags |= MESSAGE_PART_FLAG_HAS_NULS;
+ message_parse_header_deinit(&ctx->hdr_parser_ctx);
+
+ i_assert((part->flags & MUTEX_FLAGS) != MUTEX_FLAGS);
+
+ ctx->last_chr = '\n';
+ if (ctx->multipart) {
+ i_assert(ctx->last_boundary == NULL);
+ ctx->multipart = FALSE;
+ ctx->parse_next_block = parse_next_body_to_boundary;
+ } else if ((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) == 0) {
+ /* Not message/rfc822 */
+ if (ctx->boundaries != NULL)
+ ctx->parse_next_block = parse_next_body_to_boundary;
+ else
+ ctx->parse_next_block = parse_next_body_to_eof;
+ } else if (!parse_too_many_nested_mime_parts(ctx) &&
+ ctx->total_parts_count < ctx->max_total_mime_parts) {
+ /* message/rfc822 - not reached MIME part limits yet */
+ ctx->parse_next_block = parse_next_body_message_rfc822_init;
+ } else {
+ /* message/rfc822 - already reached MIME part limits */
+ part->flags |= MESSAGE_PART_FLAG_OVERFLOW;
+ part->flags &= ENUM_NEGATE(MESSAGE_PART_FLAG_MESSAGE_RFC822);
+ if (ctx->boundaries != NULL)
+ ctx->parse_next_block = parse_next_body_to_boundary;
+ else
+ ctx->parse_next_block = parse_next_body_to_eof;
+ }
+
+ ctx->want_count = 1;
+
+ /* return empty block as end of headers */
+ block_r->hdr = NULL;
+ block_r->size = 0;
+ return 1;
+}
+
+static int parse_next_header_init(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ i_assert(ctx->hdr_parser_ctx == NULL);
+
+ ctx->hdr_parser_ctx =
+ message_parse_header_init(ctx->input, &ctx->part->header_size,
+ ctx->hdr_flags);
+ ctx->part_seen_content_type = FALSE;
+ ctx->prev_hdr_newline_size = 0;
+
+ ctx->parse_next_block = parse_next_header;
+ return parse_next_header(ctx, block_r);
+}
+
+struct message_parser_ctx *
+message_parser_init_int(struct istream *input,
+ const struct message_parser_settings *set)
+{
+ struct message_parser_ctx *ctx;
+
+ ctx = i_new(struct message_parser_ctx, 1);
+ ctx->hdr_flags = set->hdr_flags;
+ ctx->flags = set->flags;
+ ctx->max_nested_mime_parts = set->max_nested_mime_parts != 0 ?
+ set->max_nested_mime_parts :
+ MESSAGE_PARSER_DEFAULT_MAX_NESTED_MIME_PARTS;
+ ctx->max_total_mime_parts = set->max_total_mime_parts != 0 ?
+ set->max_total_mime_parts :
+ MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS;
+ ctx->input = input;
+ i_stream_ref(input);
+ return ctx;
+}
+
+struct message_parser_ctx *
+message_parser_init(pool_t part_pool, struct istream *input,
+ const struct message_parser_settings *set)
+{
+ struct message_parser_ctx *ctx;
+
+ ctx = message_parser_init_int(input, set);
+ ctx->part_pool = part_pool;
+ ctx->parts = ctx->part = p_new(part_pool, struct message_part, 1);
+ ctx->next_part = &ctx->part->children;
+ ctx->parse_next_block = parse_next_header_init;
+ ctx->total_parts_count = 1;
+ i_array_init(&ctx->next_part_stack, 4);
+ return ctx;
+}
+
+void message_parser_deinit(struct message_parser_ctx **_ctx,
+ struct message_part **parts_r)
+{
+ const char *error;
+
+ i_assert((**_ctx).preparsed == FALSE);
+ if (message_parser_deinit_from_parts(_ctx, parts_r, &error) < 0)
+ i_panic("message_parser_deinit_from_parts: %s", error);
+}
+
+int message_parser_deinit_from_parts(struct message_parser_ctx **_ctx,
+ struct message_part **parts_r,
+ const char **error_r)
+{
+ struct message_parser_ctx *ctx = *_ctx;
+ int ret = ctx->broken_reason != NULL ? -1 : 0;
+
+ *_ctx = NULL;
+ *parts_r = ctx->parts;
+ *error_r = ctx->broken_reason;
+
+ if (ctx->hdr_parser_ctx != NULL)
+ message_parse_header_deinit(&ctx->hdr_parser_ctx);
+ if (ctx->part != NULL) {
+ /* If the whole message has been parsed, the parts are
+ usually finished in message_parser_parse_next_block().
+ However, it's possible that the caller finishes reading
+ through the istream without calling
+ message_parser_parse_next_block() afterwards. In that case
+ we still need to finish these parts. */
+ while (ctx->part->parent != NULL)
+ message_part_finish(ctx);
+ }
+ boundary_remove_until(ctx, NULL);
+ i_assert(ctx->nested_parts_count == 0);
+
+ i_stream_unref(&ctx->input);
+ array_free(&ctx->next_part_stack);
+ i_free(ctx->last_boundary);
+ i_free(ctx);
+ i_assert(ret < 0 || *parts_r != NULL);
+ return ret;
+}
+
+int message_parser_parse_next_block(struct message_parser_ctx *ctx,
+ struct message_block *block_r)
+{
+ int ret;
+ bool eof = FALSE, full;
+
+ i_zero(block_r);
+
+ while ((ret = ctx->parse_next_block(ctx, block_r)) == 0) {
+ ret = message_parser_read_more(ctx, block_r, &full);
+ if (ret == 0) {
+ i_assert(!ctx->input->blocking);
+ return 0;
+ }
+ if (ret == -1) {
+ i_assert(!eof);
+ eof = TRUE;
+ }
+ }
+
+ block_r->part = ctx->part;
+
+ if (ret < 0 && ctx->part != NULL) {
+ /* Successful EOF or unexpected failure */
+ i_assert(ctx->input->eof || ctx->input->closed ||
+ ctx->input->stream_errno != 0 ||
+ ctx->broken_reason != NULL);
+ while (ctx->part->parent != NULL)
+ message_part_finish(ctx);
+ }
+
+ if (block_r->size == 0) {
+ /* data isn't supposed to be read, so make sure it's NULL */
+ block_r->data = NULL;
+ }
+ return ret;
+}
+
+#undef message_parser_parse_header
+void message_parser_parse_header(struct message_parser_ctx *ctx,
+ struct message_size *hdr_size,
+ message_part_header_callback_t *callback,
+ void *context)
+{
+ struct message_block block;
+ int ret;
+
+ while ((ret = message_parser_parse_next_block(ctx, &block)) > 0) {
+ callback(block.part, block.hdr, context);
+
+ if (block.hdr == NULL)
+ break;
+ }
+ i_assert(ret != 0);
+ i_assert(ctx->part != NULL);
+
+ if (ret < 0) {
+ /* well, can't return error so fake end of headers */
+ callback(ctx->part, NULL, context);
+ }
+
+ *hdr_size = ctx->part->header_size;
+}
+
+#undef message_parser_parse_body
+void message_parser_parse_body(struct message_parser_ctx *ctx,
+ message_part_header_callback_t *hdr_callback,
+ void *context)
+{
+ struct message_block block;
+ int ret;
+
+ while ((ret = message_parser_parse_next_block(ctx, &block)) > 0) {
+ if (block.size == 0 && hdr_callback != NULL)
+ hdr_callback(block.part, block.hdr, context);
+ }
+ i_assert(ret != 0);
+}
diff --git a/src/lib-mail/message-parser.h b/src/lib-mail/message-parser.h
new file mode 100644
index 0000000..f19e526
--- /dev/null
+++ b/src/lib-mail/message-parser.h
@@ -0,0 +1,112 @@
+#ifndef MESSAGE_PARSER_H
+#define MESSAGE_PARSER_H
+
+#include "message-header-parser.h"
+#include "message-part.h"
+
+enum message_parser_flags {
+ /* Don't return message bodies in message_blocks. */
+ MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK = 0x01,
+ /* Buggy software creates Content-Type: headers without Mime-Version:
+ header. By default we allow this and assume message is MIME if
+ Content-Type: is found. This flag disables this. */
+ MESSAGE_PARSER_FLAG_MIME_VERSION_STRICT = 0x02,
+ /* Return multipart (preamble and epilogue) blocks */
+ MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS = 0x04,
+ /* Return --boundary lines */
+ MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES = 0x08
+};
+
+#define MESSAGE_PARSER_DEFAULT_MAX_NESTED_MIME_PARTS 100
+#define MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS 10000
+
+struct message_parser_settings {
+ enum message_header_parser_flags hdr_flags;
+ enum message_parser_flags flags;
+
+ /* Maximum nested MIME parts.
+ 0 = MESSAGE_PARSER_DEFAULT_MAX_NESTED_MIME_PARTS. */
+ unsigned int max_nested_mime_parts;
+ /* Maximum MIME parts in total.
+ 0 = MESSAGE_PARSER_DEFAULT_MAX_TOTAL_MIME_PARTS. */
+ unsigned int max_total_mime_parts;
+};
+
+struct message_parser_ctx;
+
+struct message_block {
+ /* Message part this block belongs to */
+ struct message_part *part;
+
+ /* non-NULL if a header line was read */
+ struct message_header_line *hdr;
+
+ /* hdr = NULL, size = 0 block returned at the end of headers for the
+ empty line between header and body (unless the header is truncated).
+ Later on data and size>0 is returned for blocks of mail body that
+ is read (see message_parser_flags for what is actually returned) */
+ const unsigned char *data;
+ size_t size;
+};
+
+/* called once with hdr = NULL at the end of headers */
+typedef void message_part_header_callback_t(struct message_part *part,
+ struct message_header_line *hdr,
+ void *context);
+
+extern message_part_header_callback_t *null_message_part_header_callback;
+
+/* Initialize message parser. part_spool specifies where struct message_parts
+ are allocated from. */
+struct message_parser_ctx *
+message_parser_init(pool_t part_pool, struct istream *input,
+ const struct message_parser_settings *set);
+/* Deinitialize message parser. The ctx must NOT have been created by
+ message_parser_init_from_parts(). */
+void message_parser_deinit(struct message_parser_ctx **ctx,
+ struct message_part **parts_r);
+/* Use preparsed parts to speed up parsing. */
+struct message_parser_ctx *
+message_parser_init_from_parts(struct message_part *parts,
+ struct istream *input,
+ const struct message_parser_settings *set);
+/* Same as message_parser_deinit(), but return an error message describing
+ why the preparsed parts didn't match the message. This can also safely be
+ called even when preparsed parts weren't used - it'll always just return
+ success in that case. */
+int message_parser_deinit_from_parts(struct message_parser_ctx **_ctx,
+ struct message_part **parts_r,
+ const char **error_r);
+
+/* Read the next block of a message. Returns 1 if block is returned, 0 if
+ input stream is non-blocking and more data needs to be read, -1 when all is
+ done or error occurred (see stream's error status). */
+int message_parser_parse_next_block(struct message_parser_ctx *ctx,
+ struct message_block *block_r);
+
+/* Read and parse header. */
+void message_parser_parse_header(struct message_parser_ctx *ctx,
+ struct message_size *hdr_size,
+ message_part_header_callback_t *callback,
+ void *context) ATTR_NULL(4);
+#define message_parser_parse_header(ctx, hdr_size, callback, context) \
+ message_parser_parse_header(ctx, hdr_size - \
+ CALLBACK_TYPECHECK(callback, void (*)( \
+ struct message_part *, \
+ struct message_header_line *, typeof(context))), \
+ (message_part_header_callback_t *)callback, context)
+
+/* Read and parse body. If message is a MIME multipart or message/rfc822
+ message, hdr_callback is called for all headers. body_callback is called
+ for the body content. */
+void message_parser_parse_body(struct message_parser_ctx *ctx,
+ message_part_header_callback_t *hdr_callback,
+ void *context) ATTR_NULL(3);
+#define message_parser_parse_body(ctx, callback, context) \
+ message_parser_parse_body(ctx, \
+ (message_part_header_callback_t *)callback, \
+ (void *)((uintptr_t)context - CALLBACK_TYPECHECK(callback, \
+ void (*)(struct message_part *, \
+ struct message_header_line *, typeof(context)))))
+
+#endif
diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c
new file mode 100644
index 0000000..a5771f8
--- /dev/null
+++ b/src/lib-mail/message-part-data.c
@@ -0,0 +1,594 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "wildcard-match.h"
+#include "array.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+#include "message-address.h"
+#include "message-header-parser.h"
+
+#include "message-part-data.h"
+
+const char *message_part_envelope_headers[] = {
+ "Date", "Subject", "From", "Sender", "Reply-To",
+ "To", "Cc", "Bcc", "In-Reply-To", "Message-ID",
+ NULL
+};
+
+/*
+ *
+ */
+
+bool message_part_data_is_plain_7bit(const struct message_part *part)
+{
+ const struct message_part_data *data = part->data;
+
+ i_assert(data != NULL);
+ i_assert(part->parent == NULL);
+
+ /* if content-type is text/xxx we don't have to check any
+ multipart stuff */
+ if ((part->flags & MESSAGE_PART_FLAG_TEXT) == 0)
+ return FALSE;
+ if (part->next != NULL || part->children != NULL)
+ return FALSE; /* shouldn't happen normally.. */
+
+ /* must be text/plain */
+ if (data->content_subtype != NULL &&
+ strcasecmp(data->content_subtype, "plain") != 0)
+ return FALSE;
+
+ /* only allowed parameter is charset=us-ascii, which is also default */
+ if (data->content_type_params_count == 0) {
+ /* charset defaults to us-ascii */
+ } else if (data->content_type_params_count != 1 ||
+ strcasecmp(data->content_type_params[0].name, "charset") != 0 ||
+ strcasecmp(data->content_type_params[0].value,
+ MESSAGE_PART_DEFAULT_CHARSET) != 0)
+ return FALSE;
+
+ if (data->content_id != NULL ||
+ data->content_description != NULL)
+ return FALSE;
+
+ if (data->content_transfer_encoding != NULL &&
+ strcasecmp(data->content_transfer_encoding, "7bit") != 0)
+ return FALSE;
+
+ /* BODYSTRUCTURE checks: */
+ if (data->content_md5 != NULL ||
+ data->content_disposition != NULL ||
+ data->content_language != NULL ||
+ data->content_location != NULL)
+ return FALSE;
+
+ return TRUE;
+}
+
+bool message_part_data_get_filename(const struct message_part *part,
+ const char **filename_r)
+{
+ const struct message_part_data *data = part->data;
+ const struct message_part_param *params;
+ unsigned int params_count, i;
+
+ i_assert(data != NULL);
+
+ params = data->content_disposition_params;
+ params_count = data->content_disposition_params_count;
+
+ if (data->content_disposition != NULL &&
+ strcasecmp(data->content_disposition, "attachment") != 0) {
+ return FALSE;
+ }
+ for (i = 0; i < params_count; i++) {
+ if (strcasecmp(params[i].name, "filename") == 0 &&
+ params[i].value != NULL) {
+ *filename_r = params[i].value;
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+/*
+ * Header parsing
+ */
+
+/* Message part envelope */
+
+enum envelope_field {
+ ENVELOPE_FIELD_DATE = 0,
+ ENVELOPE_FIELD_SUBJECT,
+ ENVELOPE_FIELD_FROM,
+ ENVELOPE_FIELD_SENDER,
+ ENVELOPE_FIELD_REPLY_TO,
+ ENVELOPE_FIELD_TO,
+ ENVELOPE_FIELD_CC,
+ ENVELOPE_FIELD_BCC,
+ ENVELOPE_FIELD_IN_REPLY_TO,
+ ENVELOPE_FIELD_MESSAGE_ID,
+
+ ENVELOPE_FIELD_UNKNOWN
+};
+
+static enum envelope_field
+envelope_get_field(const char *name)
+{
+ switch (*name) {
+ case 'B':
+ case 'b':
+ if (strcasecmp(name, "Bcc") == 0)
+ return ENVELOPE_FIELD_BCC;
+ break;
+ case 'C':
+ case 'c':
+ if (strcasecmp(name, "Cc") == 0)
+ return ENVELOPE_FIELD_CC;
+ break;
+ case 'D':
+ case 'd':
+ if (strcasecmp(name, "Date") == 0)
+ return ENVELOPE_FIELD_DATE;
+ break;
+ case 'F':
+ case 'f':
+ if (strcasecmp(name, "From") == 0)
+ return ENVELOPE_FIELD_FROM;
+ break;
+ case 'I':
+ case 'i':
+ if (strcasecmp(name, "In-reply-to") == 0)
+ return ENVELOPE_FIELD_IN_REPLY_TO;
+ break;
+ case 'M':
+ case 'm':
+ if (strcasecmp(name, "Message-id") == 0)
+ return ENVELOPE_FIELD_MESSAGE_ID;
+ break;
+ case 'R':
+ case 'r':
+ if (strcasecmp(name, "Reply-to") == 0)
+ return ENVELOPE_FIELD_REPLY_TO;
+ break;
+ case 'S':
+ case 's':
+ if (strcasecmp(name, "Subject") == 0)
+ return ENVELOPE_FIELD_SUBJECT;
+ if (strcasecmp(name, "Sender") == 0)
+ return ENVELOPE_FIELD_SENDER;
+ break;
+ case 'T':
+ case 't':
+ if (strcasecmp(name, "To") == 0)
+ return ENVELOPE_FIELD_TO;
+ break;
+ }
+
+ return ENVELOPE_FIELD_UNKNOWN;
+}
+
+void message_part_envelope_parse_from_header(pool_t pool,
+ struct message_part_envelope **data,
+ struct message_header_line *hdr)
+{
+ struct message_part_envelope *d;
+ enum envelope_field field;
+ struct message_address **addr_p, *addr;
+ const char **str_p;
+
+ if (*data == NULL) {
+ *data = p_new(pool, struct message_part_envelope, 1);
+ }
+
+ if (hdr == NULL)
+ return;
+ field = envelope_get_field(hdr->name);
+ if (field == ENVELOPE_FIELD_UNKNOWN)
+ return;
+
+ if (hdr->continues) {
+ /* wait for full value */
+ hdr->use_full_value = TRUE;
+ return;
+ }
+
+ d = *data;
+ addr_p = NULL; str_p = NULL;
+ switch (field) {
+ case ENVELOPE_FIELD_DATE:
+ str_p = &d->date;
+ break;
+ case ENVELOPE_FIELD_SUBJECT:
+ str_p = &d->subject;
+ break;
+ case ENVELOPE_FIELD_MESSAGE_ID:
+ str_p = &d->message_id;
+ break;
+ case ENVELOPE_FIELD_IN_REPLY_TO:
+ str_p = &d->in_reply_to;
+ break;
+
+ case ENVELOPE_FIELD_CC:
+ addr_p = &d->cc;
+ break;
+ case ENVELOPE_FIELD_BCC:
+ addr_p = &d->bcc;
+ break;
+ case ENVELOPE_FIELD_FROM:
+ addr_p = &d->from;
+ break;
+ case ENVELOPE_FIELD_SENDER:
+ addr_p = &d->sender;
+ break;
+ case ENVELOPE_FIELD_TO:
+ addr_p = &d->to;
+ break;
+ case ENVELOPE_FIELD_REPLY_TO:
+ addr_p = &d->reply_to;
+ break;
+ case ENVELOPE_FIELD_UNKNOWN:
+ i_unreached();
+ }
+
+ if (addr_p != NULL) {
+ addr = message_address_parse(pool, hdr->full_value,
+ hdr->full_value_len,
+ UINT_MAX,
+ MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING);
+ /* Merge multiple headers the same as if they were comma
+ separated in a single line. This is better from security
+ point of view, because attacker could intentionally write
+ addresses in a way that e.g. the first From header is
+ validated while MUA only shows the second From header. */
+ while (*addr_p != NULL)
+ addr_p = &(*addr_p)->next;
+ *addr_p = addr;
+ } else if (str_p != NULL) {
+ *str_p = message_header_strdup(pool, hdr->full_value,
+ hdr->full_value_len);
+ }
+}
+
+/* Message part data */
+
+static void
+parse_mime_parameters(struct rfc822_parser_context *parser,
+ pool_t pool, const struct message_part_param **params_r,
+ unsigned int *params_count_r)
+{
+ const char *const *results;
+ struct message_part_param *params;
+ unsigned int params_count, i;
+
+ rfc2231_parse(parser, &results);
+
+ params_count = str_array_length(results);
+ i_assert((params_count % 2) == 0);
+ params_count /= 2;
+
+ if (params_count > 0) {
+ params = p_new(pool, struct message_part_param, params_count);
+ for (i = 0; i < params_count; i++) {
+ params[i].name = p_strdup(pool, results[i*2+0]);
+ params[i].value = p_strdup(pool, results[i*2+1]);
+ }
+ *params_r = params;
+ }
+
+ *params_count_r = params_count;
+}
+
+static void
+parse_content_type(struct message_part_data *data,
+ pool_t pool, struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *str;
+ const char *value;
+ unsigned int i;
+ int ret;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ str = t_str_new(256);
+ ret = rfc822_parse_content_type(&parser, str);
+
+ /* Save content type and subtype */
+ value = str_c(str);
+ for (i = 0; value[i] != '\0'; i++) {
+ if (value[i] == '/') {
+ data->content_subtype = p_strdup(pool, value + i+1);
+ break;
+ }
+ }
+ str_truncate(str, i);
+ data->content_type = p_strdup(pool, str_c(str));
+ if (data->content_subtype == NULL) {
+ /* The Content-Type is invalid. Don't leave it NULL so that
+ callers can assume that if content_type != NULL,
+ content_subtype != NULL also. */
+ data->content_subtype = p_strdup(pool, "");
+ }
+
+ if (ret < 0) {
+ /* Content-Type is broken, but we wanted to get it as well as
+ we could. Don't try to read the parameters anymore though.
+
+ We don't completely ignore a broken Content-Type, because
+ then it would be written as text/plain. This would cause a
+ mismatch with the message_part's MESSAGE_PART_FLAG_TEXT. */
+ return;
+ }
+
+ parse_mime_parameters(&parser, pool,
+ &data->content_type_params,
+ &data->content_type_params_count);
+ rfc822_parser_deinit(&parser);
+}
+
+static void
+parse_content_transfer_encoding(struct message_part_data *data,
+ pool_t pool, struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *str;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ str = t_str_new(256);
+ if (rfc822_parse_mime_token(&parser, str) >= 0 &&
+ rfc822_skip_lwsp(&parser) == 0 && str_len(str) > 0) {
+ data->content_transfer_encoding =
+ p_strdup(pool, str_c(str));
+ }
+ rfc822_parser_deinit(&parser);
+}
+
+static void
+parse_content_disposition(struct message_part_data *data,
+ pool_t pool, struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *str;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ str = t_str_new(256);
+ if (rfc822_parse_mime_token(&parser, str) < 0) {
+ rfc822_parser_deinit(&parser);
+ return;
+ }
+ data->content_disposition = p_strdup(pool, str_c(str));
+
+ parse_mime_parameters(&parser, pool,
+ &data->content_disposition_params,
+ &data->content_disposition_params_count);
+ rfc822_parser_deinit(&parser);
+}
+
+static void
+parse_content_language(struct message_part_data *data,
+ pool_t pool, const unsigned char *value, size_t value_len)
+{
+ struct rfc822_parser_context parser;
+ ARRAY_TYPE(const_string) langs;
+ string_t *str;
+
+ /* Language-Header = "Content-Language" ":" 1#Language-tag
+ Language-Tag = Primary-tag *( "-" Subtag )
+ Primary-tag = 1*8ALPHA
+ Subtag = 1*8ALPHA */
+
+ rfc822_parser_init(&parser, value, value_len, NULL);
+
+ t_array_init(&langs, 16);
+ str = t_str_new(128);
+
+ rfc822_skip_lwsp(&parser);
+ while (rfc822_parse_atom(&parser, str) >= 0) {
+ const char *lang = p_strdup(pool, str_c(str));
+
+ array_push_back(&langs, &lang);
+ str_truncate(str, 0);
+
+ if (parser.data >= parser.end || *parser.data != ',')
+ break;
+ parser.data++;
+ rfc822_skip_lwsp(&parser);
+ }
+ rfc822_parser_deinit(&parser);
+
+ if (array_count(&langs) > 0) {
+ array_append_zero(&langs);
+ data->content_language =
+ p_strarray_dup(pool, array_front(&langs));
+ }
+}
+
+static void
+parse_content_header(struct message_part_data *data,
+ pool_t pool, struct message_header_line *hdr)
+{
+ const char *name = hdr->name + strlen("Content-");
+
+ if (hdr->continues) {
+ hdr->use_full_value = TRUE;
+ return;
+ }
+
+ switch (*name) {
+ case 'i':
+ case 'I':
+ if (strcasecmp(name, "ID") == 0 && data->content_id == NULL)
+ data->content_id =
+ message_header_strdup(pool, hdr->full_value,
+ hdr->full_value_len);
+ break;
+
+ case 'm':
+ case 'M':
+ if (strcasecmp(name, "MD5") == 0 && data->content_md5 == NULL)
+ data->content_md5 =
+ message_header_strdup(pool, hdr->full_value,
+ hdr->full_value_len);
+ break;
+
+ case 't':
+ case 'T':
+ if (strcasecmp(name, "Type") == 0 && data->content_type == NULL)
+ parse_content_type(data, pool, hdr);
+ else if (strcasecmp(name, "Transfer-Encoding") == 0 &&
+ data->content_transfer_encoding == NULL)
+ parse_content_transfer_encoding(data, pool, hdr);
+ break;
+
+ case 'l':
+ case 'L':
+ if (strcasecmp(name, "Language") == 0 &&
+ data->content_language == NULL) {
+ parse_content_language(data, pool,
+ hdr->full_value, hdr->full_value_len);
+ } else if (strcasecmp(name, "Location") == 0 &&
+ data->content_location == NULL) {
+ data->content_location =
+ message_header_strdup(pool, hdr->full_value,
+ hdr->full_value_len);
+ }
+ break;
+
+ case 'd':
+ case 'D':
+ if (strcasecmp(name, "Description") == 0 &&
+ data->content_description == NULL)
+ data->content_description =
+ message_header_strdup(pool, hdr->full_value,
+ hdr->full_value_len);
+ else if (strcasecmp(name, "Disposition") == 0 &&
+ data->content_disposition_params == NULL)
+ parse_content_disposition(data, pool, hdr);
+ break;
+ }
+}
+
+void message_part_data_parse_from_header(pool_t pool,
+ struct message_part *part,
+ struct message_header_line *hdr)
+{
+ struct message_part_data *part_data;
+ struct message_part_envelope *envelope;
+ bool parent_rfc822;
+
+ if (hdr == NULL) {
+ if (part->data == NULL) {
+ /* no Content-* headers. add an empty context
+ structure anyway. */
+ part->data = p_new(pool, struct message_part_data, 1);
+ } else if ((part->flags & MESSAGE_PART_FLAG_IS_MIME) == 0) {
+ /* If there was no Mime-Version, forget all
+ the Content-stuff */
+ part_data = part->data;
+ envelope = part_data->envelope;
+
+ i_zero(part_data);
+ part_data->envelope = envelope;
+ }
+ return;
+ }
+
+ if (hdr->eoh)
+ return;
+
+ parent_rfc822 = part->parent != NULL &&
+ (part->parent->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0;
+ if (!parent_rfc822 && strncasecmp(hdr->name, "Content-", 8) != 0)
+ return;
+
+ if (part->data == NULL) {
+ /* initialize message part data */
+ part->data = p_new(pool, struct message_part_data, 1);
+ }
+ part_data = part->data;
+
+ if (strncasecmp(hdr->name, "Content-", 8) == 0) {
+ T_BEGIN {
+ parse_content_header(part_data, pool, hdr);
+ } T_END;
+ }
+
+ if (parent_rfc822) {
+ /* message/rfc822, we need the envelope */
+ message_part_envelope_parse_from_header(pool, &part_data->envelope, hdr);
+ }
+}
+
+bool message_part_has_content_types(struct message_part *part,
+ const char *const *types)
+{
+ struct message_part_data *data = part->data;
+ bool ret = TRUE;
+ const char *const *ptr;
+ const char *content_type;
+
+ i_assert(data != NULL);
+
+ if (data->content_type == NULL)
+ return FALSE;
+ else if (data->content_subtype == NULL)
+ content_type = t_strdup_printf("%s/", data->content_type);
+ else
+ content_type = t_strdup_printf("%s/%s", data->content_type,
+ data->content_subtype);
+ for(ptr = types; *ptr != NULL; ptr++) {
+ bool exclude = (**ptr == '!');
+ if (wildcard_match_icase(content_type, (*ptr)+(exclude?1:0)))
+ ret = !exclude;
+ }
+
+ return ret;
+}
+
+bool message_part_has_parameter(struct message_part *part, const char *parameter,
+ bool has_value)
+{
+ struct message_part_data *data = part->data;
+
+ i_assert(data != NULL);
+
+ for (unsigned int i = 0; i < data->content_disposition_params_count; i++) {
+ const struct message_part_param *param =
+ &data->content_disposition_params[i];
+ if (strcasecmp(param->name, parameter) == 0 &&
+ (!has_value || *param->value != '\0')) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+bool message_part_is_attachment(struct message_part *part,
+ const struct message_part_attachment_settings *set)
+{
+ struct message_part_data *data = part->data;
+
+ i_assert(data != NULL);
+
+ /* see if the content-type is excluded */
+ if (set->content_type_filter != NULL &&
+ !message_part_has_content_types(part, set->content_type_filter))
+ return FALSE;
+
+ /* accept any attachment, or any inlined attachment with filename,
+ unless inlined ones are excluded */
+ if (null_strcasecmp(data->content_disposition, "attachment") == 0 ||
+ (!set->exclude_inlined &&
+ null_strcasecmp(data->content_disposition, "inline") == 0 &&
+ message_part_has_parameter(part, "filename", FALSE)))
+ return TRUE;
+ return FALSE;
+}
diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h
new file mode 100644
index 0000000..5ff9ffe
--- /dev/null
+++ b/src/lib-mail/message-part-data.h
@@ -0,0 +1,101 @@
+#ifndef MESSAGE_PART_DATA_H
+#define MESSAGE_PART_DATA_H
+
+#include "message-part.h"
+
+#define MESSAGE_PART_DEFAULT_CHARSET "us-ascii"
+
+struct message_header_line;
+
+struct message_part_param {
+ const char *name;
+ const char *value;
+};
+
+struct message_part_envelope {
+ const char *date, *subject;
+ struct message_address *from, *sender, *reply_to;
+ struct message_address *to, *cc, *bcc;
+
+ const char *in_reply_to, *message_id;
+};
+
+struct message_part_data {
+ const char *content_type, *content_subtype;
+ const struct message_part_param *content_type_params;
+ unsigned int content_type_params_count;
+
+ const char *content_transfer_encoding;
+ const char *content_id;
+ const char *content_description;
+ const char *content_disposition;
+ const struct message_part_param *content_disposition_params;
+ unsigned int content_disposition_params_count;
+ const char *content_md5;
+ const char *const *content_language;
+ const char *content_location;
+
+ struct message_part_envelope *envelope;
+};
+
+struct message_part_attachment_settings {
+ /* By default, all attachments with content-disposition=attachment
+ or content-disposition=inline;filename=... are consired as an
+ attachment.
+
+ If content_type_filter is set to an array of masks, then
+ anything starting with ! is excluded, and anything without
+ is considered negating exclusion. Setting foo/bar alone will */
+// not do anything, but setting !foo/*, foo/bar, will exclude
+ /* all attachments with foo/anything content type, but will
+ accept foo/bar.
+
+ Setting exclude_inlined, will exclude **any** inlined attachment
+ regardless of what content_type_filter is.
+ */
+ const char *const *content_type_filter;
+ bool exclude_inlined;
+};
+
+extern const char *message_part_envelope_headers[];
+
+/*
+ *
+ */
+
+/* Returns TRUE if this message part has content-type "text/plain",
+ charset "us-ascii" and content-transfer-encoding "7bit" */
+bool message_part_data_is_plain_7bit(const struct message_part *part)
+ ATTR_PURE;
+
+/* Returns TRUE if this message part has a filename. The filename is
+ returned in filename_r. */
+bool message_part_data_get_filename(const struct message_part *part,
+ const char **filename_r);
+
+/* See message_part_attachment_settings */
+bool message_part_has_content_types(struct message_part *part, const char *const *types);
+
+/* Returns TRUE if message part has given parameter, and has non-empty
+ value if has_value is TRUE. */
+bool message_part_has_parameter(struct message_part *part, const char *parameter,
+ bool has_value);
+
+/* Check if part is attachment according to given settings */
+bool message_part_is_attachment(struct message_part *part,
+ const struct message_part_attachment_settings *set);
+/*
+ * Header parsing
+ */
+
+/* Update envelope data based from given header field */
+void message_part_envelope_parse_from_header(pool_t pool,
+ struct message_part_envelope **_data,
+ struct message_header_line *hdr);
+
+/* Parse a single header. Note that this modifies part->context. */
+void message_part_data_parse_from_header(pool_t pool,
+ struct message_part *part,
+ struct message_header_line *hdr);
+
+#endif
diff --git a/src/lib-mail/message-part-serialize.c b/src/lib-mail/message-part-serialize.c
new file mode 100644
index 0000000..23f3a01
--- /dev/null
+++ b/src/lib-mail/message-part-serialize.c
@@ -0,0 +1,269 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "message-parser.h"
+#include "message-part-serialize.h"
+
+/*
+ root part
+ root's first children
+ children's first children
+ ...
+ root's next children
+ ...
+
+ part
+ unsigned int flags
+ (not root part)
+ uoff_t physical_pos
+ uoff_t header_physical_size
+ uoff_t header_virtual_size
+ uoff_t body_physical_size
+ uoff_t body_virtual_size
+ (flags & (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_MESSAGE_RFC822))
+ unsigned int body_lines
+ (flags & (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_MESSAGE_RFC822))
+ unsigned int children_count
+
+*/
+
+#define MINIMUM_SERIALIZED_SIZE \
+ (sizeof(unsigned int) + sizeof(uoff_t) * 4)
+
+struct deserialize_context {
+ pool_t pool;
+ const unsigned char *data, *end;
+
+ uoff_t pos;
+ const char *error;
+};
+
+static void part_serialize(struct message_part *part, buffer_t *dest,
+ unsigned int *children_count_r)
+{
+ unsigned int count, children_count;
+ size_t children_offset;
+ bool root = part->parent == NULL;
+
+ count = 0;
+ while (part != NULL) {
+ /* create serialized part */
+ buffer_append(dest, &part->flags, sizeof(part->flags));
+ if (root)
+ root = FALSE;
+ else {
+ buffer_append(dest, &part->physical_pos,
+ sizeof(part->physical_pos));
+ }
+ buffer_append(dest, &part->header_size.physical_size,
+ sizeof(part->header_size.physical_size));
+ buffer_append(dest, &part->header_size.virtual_size,
+ sizeof(part->header_size.virtual_size));
+ buffer_append(dest, &part->body_size.physical_size,
+ sizeof(part->body_size.physical_size));
+ buffer_append(dest, &part->body_size.virtual_size,
+ sizeof(part->body_size.virtual_size));
+
+ if ((part->flags & (MESSAGE_PART_FLAG_TEXT |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) != 0) {
+ buffer_append(dest, &part->body_size.lines,
+ sizeof(part->body_size.lines));
+ }
+
+ if ((part->flags & (MESSAGE_PART_FLAG_MULTIPART |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) != 0) {
+ children_offset = dest->used;
+ children_count = 0;
+ buffer_append(dest, &children_count,
+ sizeof(children_count));
+
+ if (part->children != NULL) {
+ part_serialize(part->children, dest,
+ &children_count);
+
+ buffer_write(dest, children_offset,
+ &children_count,
+ sizeof(children_count));
+ }
+ } else {
+ i_assert(part->children == NULL);
+ }
+
+ count++;
+ part = part->next;
+ }
+
+ *children_count_r = count;
+}
+
+void message_part_serialize(struct message_part *part, buffer_t *dest)
+{
+ unsigned int children_count;
+
+ part_serialize(part, dest, &children_count);
+}
+
+static bool read_next(struct deserialize_context *ctx,
+ void *buffer, size_t buffer_size)
+{
+ if (ctx->data + buffer_size > ctx->end) {
+ ctx->error = "Not enough data";
+ return FALSE;
+ }
+
+ memcpy(buffer, ctx->data, buffer_size);
+ ctx->data += buffer_size;
+ return TRUE;
+}
+
+static bool ATTR_NULL(2)
+message_part_deserialize_part(struct deserialize_context *ctx,
+ struct message_part *parent,
+ unsigned int siblings,
+ struct message_part **part_r)
+{
+ struct message_part *p, *part, *first_part, **next_part;
+ unsigned int children_count;
+ uoff_t pos;
+ bool root = parent == NULL;
+
+ first_part = NULL;
+ next_part = NULL;
+ while (siblings > 0) {
+ siblings--;
+
+ part = p_new(ctx->pool, struct message_part, 1);
+ part->parent = parent;
+ for (p = parent; p != NULL; p = p->parent)
+ p->children_count++;
+
+ if (!read_next(ctx, &part->flags, sizeof(part->flags)))
+ return FALSE;
+
+ if (root)
+ root = FALSE;
+ else {
+ if (!read_next(ctx, &part->physical_pos,
+ sizeof(part->physical_pos)))
+ return FALSE;
+ }
+
+ if (part->physical_pos < ctx->pos) {
+ ctx->error = "physical_pos less than expected";
+ return FALSE;
+ }
+
+ if (!read_next(ctx, &part->header_size.physical_size,
+ sizeof(part->header_size.physical_size)))
+ return FALSE;
+
+ if (!read_next(ctx, &part->header_size.virtual_size,
+ sizeof(part->header_size.virtual_size)))
+ return FALSE;
+
+ if (part->header_size.virtual_size <
+ part->header_size.physical_size) {
+ ctx->error = "header_size.virtual_size too small";
+ return FALSE;
+ }
+
+ if (!read_next(ctx, &part->body_size.physical_size,
+ sizeof(part->body_size.physical_size)))
+ return FALSE;
+
+ if (!read_next(ctx, &part->body_size.virtual_size,
+ sizeof(part->body_size.virtual_size)))
+ return FALSE;
+
+ if ((part->flags & (MESSAGE_PART_FLAG_TEXT |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) != 0) {
+ if (!read_next(ctx, &part->body_size.lines,
+ sizeof(part->body_size.lines)))
+ return FALSE;
+ }
+
+ if (part->body_size.virtual_size <
+ part->body_size.physical_size) {
+ ctx->error = "body_size.virtual_size too small";
+ return FALSE;
+ }
+
+ if ((part->flags & (MESSAGE_PART_FLAG_MULTIPART |
+ MESSAGE_PART_FLAG_MESSAGE_RFC822)) != 0) {
+ if (!read_next(ctx, &children_count,
+ sizeof(children_count)))
+ return FALSE;
+ } else {
+ children_count = 0;
+ }
+
+ if ((part->flags & MESSAGE_PART_FLAG_MESSAGE_RFC822) != 0) {
+ /* Only one child is possible */
+ if (children_count == 0) {
+ ctx->error =
+ "message/rfc822 part has no children";
+ return FALSE;
+ }
+ if (children_count != 1) {
+ ctx->error = "message/rfc822 part "
+ "has multiple children";
+ return FALSE;
+ }
+ }
+
+ if (children_count > 0) {
+ /* our children must be after our physical_pos+header
+ and the last child must be within our size. */
+ ctx->pos = part->physical_pos +
+ part->header_size.physical_size;
+ pos = ctx->pos + part->body_size.physical_size;
+
+ if (!message_part_deserialize_part(ctx, part,
+ children_count,
+ &part->children))
+ return FALSE;
+
+ if (ctx->pos > pos) {
+ ctx->error =
+ "child part location exceeds our size";
+ return FALSE;
+ }
+ ctx->pos = pos; /* save it for above check for parent */
+ }
+
+ if (first_part == NULL)
+ first_part = part;
+ if (next_part != NULL)
+ *next_part = part;
+ next_part = &part->next;
+ }
+
+ *part_r = first_part;
+ return TRUE;
+}
+
+struct message_part *
+message_part_deserialize(pool_t pool, const void *data, size_t size,
+ const char **error_r)
+{
+ struct deserialize_context ctx;
+ struct message_part *part;
+
+ i_zero(&ctx);
+ ctx.pool = pool;
+ ctx.data = data;
+ ctx.end = ctx.data + size;
+
+ if (!message_part_deserialize_part(&ctx, NULL, 1, &part)) {
+ *error_r = ctx.error;
+ return NULL;
+ }
+
+ if (ctx.data != ctx.end) {
+ *error_r = "Too much data";
+ return NULL;
+ }
+
+ return part;
+}
diff --git a/src/lib-mail/message-part-serialize.h b/src/lib-mail/message-part-serialize.h
new file mode 100644
index 0000000..197c3b6
--- /dev/null
+++ b/src/lib-mail/message-part-serialize.h
@@ -0,0 +1,16 @@
+#ifndef MESSAGE_PART_SERIALIZE_H
+#define MESSAGE_PART_SERIALIZE_H
+
+struct message_part;
+struct message_size;
+
+/* Serialize message part. */
+void message_part_serialize(struct message_part *part, buffer_t *dest);
+
+/* Generate struct message_part from serialized data. Returns NULL and sets
+ error if any problems are detected. */
+struct message_part *
+message_part_deserialize(pool_t pool, const void *data, size_t size,
+ const char **error_r);
+
+#endif
diff --git a/src/lib-mail/message-part.c b/src/lib-mail/message-part.c
new file mode 100644
index 0000000..340dbbb
--- /dev/null
+++ b/src/lib-mail/message-part.c
@@ -0,0 +1,103 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "message-part.h"
+
+static const struct message_part *
+message_part_root(const struct message_part *part)
+{
+ while (part->parent != NULL)
+ part = part->parent;
+ return part;
+}
+
+static bool message_part_find(const struct message_part *siblings,
+ const struct message_part *part,
+ unsigned int *n)
+{
+ const struct message_part *p;
+
+ for (p = siblings; p != NULL; p = p->next) {
+ if (p == part)
+ return TRUE;
+ *n += 1;
+ if (message_part_find(p->children, part, n))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+unsigned int message_part_to_idx(const struct message_part *part)
+{
+ const struct message_part *root;
+ unsigned int n = 0;
+
+ root = message_part_root(part);
+ if (!message_part_find(root, part, &n))
+ i_unreached();
+ return n;
+}
+
+static struct message_part *
+message_sub_part_by_idx(struct message_part *parts,
+ unsigned int idx)
+{
+ struct message_part *part = parts;
+
+ for (; part != NULL && idx > 0; part = part->next) {
+ if (part->children_count >= idx)
+ return message_sub_part_by_idx(part->children, idx-1);
+ idx -= part->children_count + 1;
+ }
+ return part;
+}
+
+struct message_part *
+message_part_by_idx(struct message_part *parts, unsigned int idx)
+{
+ i_assert(parts->parent == NULL);
+
+ return message_sub_part_by_idx(parts, idx);
+}
+
+bool message_part_is_equal(const struct message_part *p1,
+ const struct message_part *p2)
+{
+ /* This cannot be p1 && p2, because then we would return
+ TRUE when either part is NULL, and we should return FALSE */
+ while (p1 != NULL || p2 != NULL) {
+ /* If either part is NULL, return false */
+ if ((p1 != NULL) != (p2 != NULL))
+ return FALSE;
+
+ /* Expect that both either have children, or both
+ do not have children */
+ if ((p1->children != NULL) != (p2->children != NULL))
+ return FALSE;
+
+ /* If there are children, ensure they are equal */
+ if (p1->children != NULL) {
+ if (!message_part_is_equal(p1->children, p2->children))
+ return FALSE;
+ }
+
+ /* If any of these properties differ, then parts are not equal */
+ if (p1->physical_pos != p2->physical_pos ||
+ p1->header_size.physical_size != p2->header_size.physical_size ||
+ p1->header_size.virtual_size != p2->header_size.virtual_size ||
+ p1->header_size.lines != p2->header_size.lines ||
+ p1->body_size.physical_size != p2->body_size.physical_size ||
+ p1->body_size.virtual_size != p2->body_size.virtual_size ||
+ p1->body_size.lines != p2->body_size.lines ||
+ p1->children_count != p2->children_count ||
+ p1->flags != p2->flags)
+ return FALSE;
+
+ /* Move forward */
+ p1 = p1->next;
+ p2 = p2->next;
+ }
+
+ /* Parts are equal */
+ return TRUE;
+}
diff --git a/src/lib-mail/message-part.h b/src/lib-mail/message-part.h
new file mode 100644
index 0000000..d8f99ab
--- /dev/null
+++ b/src/lib-mail/message-part.h
@@ -0,0 +1,67 @@
+#ifndef MESSAGE_PART_H
+#define MESSAGE_PART_H
+
+#include "message-size.h"
+
+struct message_part_data;
+
+/* Note that these flags are used directly by message-parser-serialize, so
+ existing flags can't be changed without breaking backwards compatibility */
+enum message_part_flags {
+ MESSAGE_PART_FLAG_MULTIPART = 0x01,
+ MESSAGE_PART_FLAG_MULTIPART_DIGEST = 0x02,
+ MESSAGE_PART_FLAG_MESSAGE_RFC822 = 0x04,
+
+ /* content-type: text/... */
+ MESSAGE_PART_FLAG_TEXT = 0x08,
+
+ MESSAGE_PART_FLAG_UNUSED = 0x10,
+
+ /* message part header or body contains NULs */
+ MESSAGE_PART_FLAG_HAS_NULS = 0x20,
+
+ /* Mime-Version header exists. */
+ MESSAGE_PART_FLAG_IS_MIME = 0x40,
+ /* Message parsing was aborted because there were too many MIME parts.
+ This MIME part points to a blob which wasn't actually parsed to
+ see if it would contain further MIME parts. */
+ MESSAGE_PART_FLAG_OVERFLOW = 0x80,
+};
+
+struct message_part {
+ struct message_part *parent;
+ struct message_part *next;
+ struct message_part *children;
+
+ uoff_t physical_pos; /* absolute position from beginning of message */
+ struct message_size header_size;
+ struct message_size body_size;
+
+ struct message_part_data *data;
+
+ /* total number of message_parts under children */
+ unsigned int children_count;
+ enum message_part_flags flags;
+ void *context;
+};
+
+/* Return index number for the message part. The indexes are in the same order
+ as they exist in the flat RFC822 message. The root part is 0, its first
+ child is 1 and so on. */
+unsigned int message_part_to_idx(const struct message_part *part);
+/* Find message part by its index number, or return NULL if the index
+ doesn't exist. */
+struct message_part *
+message_part_by_idx(struct message_part *parts, unsigned int idx);
+
+/* Returns TRUE when message parts are considered equal. Equality is determined
+ to be TRUE, when
+
+ - both parts are NULL
+ - both parts are not NULL, and
+ - both parts children are equal
+ - both parts have same position, sizes, line counts and flags. */
+bool message_part_is_equal(const struct message_part *p1,
+ const struct message_part *p2) ATTR_NULL(1, 2);
+
+#endif
diff --git a/src/lib-mail/message-search.c b/src/lib-mail/message-search.c
new file mode 100644
index 0000000..5f54485
--- /dev/null
+++ b/src/lib-mail/message-search.c
@@ -0,0 +1,246 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "istream.h"
+#include "str.h"
+#include "str-find.h"
+#include "rfc822-parser.h"
+#include "message-decoder.h"
+#include "message-parser.h"
+#include "message-search.h"
+
+struct message_search_context {
+ enum message_search_flags flags;
+ normalizer_func_t *normalizer;
+
+ struct str_find_context *str_find_ctx;
+ struct message_part *prev_part;
+
+ struct message_decoder_context *decoder;
+ bool content_type_text:1; /* text/any or message/any */
+};
+
+struct message_search_context *
+message_search_init(const char *normalized_key_utf8,
+ normalizer_func_t *normalizer,
+ enum message_search_flags flags)
+{
+ struct message_search_context *ctx;
+
+ i_assert(*normalized_key_utf8 != '\0');
+
+ ctx = i_new(struct message_search_context, 1);
+ ctx->flags = flags;
+ ctx->decoder = message_decoder_init(normalizer, 0);
+ ctx->str_find_ctx = str_find_init(default_pool, normalized_key_utf8);
+ return ctx;
+}
+
+void message_search_deinit(struct message_search_context **_ctx)
+{
+ struct message_search_context *ctx = *_ctx;
+
+ *_ctx = NULL;
+ str_find_deinit(&ctx->str_find_ctx);
+ message_decoder_deinit(&ctx->decoder);
+ i_free(ctx);
+}
+
+static void parse_content_type(struct message_search_context *ctx,
+ struct message_header_line *hdr)
+{
+ struct rfc822_parser_context parser;
+ string_t *content_type;
+
+ rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
+ rfc822_skip_lwsp(&parser);
+
+ content_type = t_str_new(64);
+ (void)rfc822_parse_content_type(&parser, content_type);
+ ctx->content_type_text =
+ strncasecmp(str_c(content_type), "text/", 5) == 0 ||
+ strncasecmp(str_c(content_type), "message/", 8) == 0;
+ rfc822_parser_deinit(&parser);
+}
+
+static void handle_header(struct message_search_context *ctx,
+ struct message_header_line *hdr)
+{
+ if (hdr->name_len == 12 &&
+ strcasecmp(hdr->name, "Content-Type") == 0) {
+ if (hdr->continues) {
+ hdr->use_full_value = TRUE;
+ return;
+ }
+ T_BEGIN {
+ parse_content_type(ctx, hdr);
+ } T_END;
+ }
+}
+
+static bool search_header(struct message_search_context *ctx,
+ const struct message_header_line *hdr)
+{
+ static const unsigned char crlf[2] = { '\r', '\n' };
+
+ return str_find_more(ctx->str_find_ctx,
+ (const unsigned char *)hdr->name, hdr->name_len) ||
+ str_find_more(ctx->str_find_ctx,
+ hdr->middle, hdr->middle_len) ||
+ str_find_more(ctx->str_find_ctx, hdr->full_value,
+ hdr->full_value_len) ||
+ (!hdr->no_newline &&
+ str_find_more(ctx->str_find_ctx, crlf, 2));
+}
+
+static bool message_search_more_decoded2(struct message_search_context *ctx,
+ struct message_block *block)
+{
+ if (block->hdr != NULL) {
+ if (search_header(ctx, block->hdr))
+ return TRUE;
+ } else {
+ if (str_find_more(ctx->str_find_ctx, block->data, block->size))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+bool message_search_more(struct message_search_context *ctx,
+ struct message_block *raw_block)
+{
+ struct message_block decoded_block;
+
+ return message_search_more_get_decoded(ctx, raw_block, &decoded_block);
+}
+
+bool message_search_more_get_decoded(struct message_search_context *ctx,
+ struct message_block *raw_block,
+ struct message_block *decoded_block_r)
+{
+ struct message_header_line *hdr = raw_block->hdr;
+ struct message_block decoded_block;
+
+ i_zero(decoded_block_r);
+ decoded_block_r->part = raw_block->part;
+
+ if (raw_block->part != ctx->prev_part) {
+ /* part changes. we must change this before looking at
+ content type */
+ message_search_reset(ctx);
+ ctx->prev_part = raw_block->part;
+
+ if (hdr == NULL) {
+ /* we're returning to a multipart message. */
+ ctx->content_type_text = FALSE;
+ }
+ }
+
+ if (hdr != NULL) {
+ handle_header(ctx, hdr);
+ if ((ctx->flags & MESSAGE_SEARCH_FLAG_SKIP_HEADERS) != 0) {
+ /* we want to search only message bodies, but
+ but decoder needs some headers so that it can
+ decode the body properly. */
+ if (hdr->name_len != 12 && hdr->name_len != 25)
+ return FALSE;
+ if (strcasecmp(hdr->name, "Content-Type") != 0 &&
+ strcasecmp(hdr->name,
+ "Content-Transfer-Encoding") != 0)
+ return FALSE;
+ }
+ } else {
+ /* body */
+ if (!ctx->content_type_text)
+ return FALSE;
+ }
+ if (!message_decoder_decode_next_block(ctx->decoder, raw_block,
+ &decoded_block))
+ return FALSE;
+
+ if (decoded_block.hdr != NULL &&
+ (ctx->flags & MESSAGE_SEARCH_FLAG_SKIP_HEADERS) != 0) {
+ /* Content-* header */
+ return FALSE;
+ }
+
+ *decoded_block_r = decoded_block;
+ return message_search_more_decoded2(ctx, &decoded_block);
+}
+
+bool message_search_more_decoded(struct message_search_context *ctx,
+ struct message_block *block)
+{
+ if (block->part != ctx->prev_part) {
+ /* part changes */
+ message_search_reset(ctx);
+ ctx->prev_part = block->part;
+ }
+
+ return message_search_more_decoded2(ctx, block);
+}
+
+void message_search_reset(struct message_search_context *ctx)
+{
+ /* Content-Type defaults to text/plain */
+ ctx->content_type_text = TRUE;
+
+ ctx->prev_part = NULL;
+ str_find_reset(ctx->str_find_ctx);
+ message_decoder_decode_reset(ctx->decoder);
+}
+
+static int
+message_search_msg_real(struct message_search_context *ctx,
+ struct istream *input, struct message_part *parts,
+ const char **error_r)
+{
+ const struct message_parser_settings parser_set = {
+ .hdr_flags = MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE,
+ };
+ struct message_parser_ctx *parser_ctx;
+ struct message_block raw_block;
+ struct message_part *new_parts;
+ int ret;
+
+ message_search_reset(ctx);
+
+ if (parts != NULL) {
+ parser_ctx = message_parser_init_from_parts(parts,
+ input, &parser_set);
+ } else {
+ parser_ctx = message_parser_init(pool_datastack_create(),
+ input, &parser_set);
+ }
+
+ while ((ret = message_parser_parse_next_block(parser_ctx,
+ &raw_block)) > 0) {
+ if (message_search_more(ctx, &raw_block)) {
+ ret = 1;
+ break;
+ }
+ }
+ i_assert(ret != 0);
+ if (ret < 0 && input->stream_errno == 0) {
+ /* normal exit */
+ ret = 0;
+ }
+ if (message_parser_deinit_from_parts(&parser_ctx, &new_parts, error_r) < 0) {
+ /* broken parts */
+ ret = -1;
+ }
+ return ret;
+}
+
+int message_search_msg(struct message_search_context *ctx,
+ struct istream *input, struct message_part *parts,
+ const char **error_r)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = message_search_msg_real(ctx, input, parts, error_r);
+ } T_END_PASS_STR_IF(ret < 0, error_r);
+ return ret;
+}
diff --git a/src/lib-mail/message-search.h b/src/lib-mail/message-search.h
new file mode 100644
index 0000000..7d3786a
--- /dev/null
+++ b/src/lib-mail/message-search.h
@@ -0,0 +1,40 @@
+#ifndef MESSAGE_SEARCH_H
+#define MESSAGE_SEARCH_H
+
+struct message_block;
+struct message_part;
+struct message_search_context;
+
+enum message_search_flags {
+ /* Skip the main header and all the MIME headers. */
+ MESSAGE_SEARCH_FLAG_SKIP_HEADERS = 0x01
+};
+
+/* The key must be given in UTF-8 charset */
+struct message_search_context *
+message_search_init(const char *normalized_key_utf8,
+ normalizer_func_t *normalizer,
+ enum message_search_flags flags);
+void message_search_deinit(struct message_search_context **ctx);
+
+/* Returns TRUE if key is found from input buffer, FALSE if not. */
+bool message_search_more(struct message_search_context *ctx,
+ struct message_block *raw_block);
+/* Same as message_search_more(), but return the decoded block. If the same
+ input is being fed to multiple searches, this avoids duplicating the work
+ by doing the following searches with message_search_more_decoded() */
+bool message_search_more_get_decoded(struct message_search_context *ctx,
+ struct message_block *raw_block,
+ struct message_block *decoded_block_r);
+/* The data has already passed through decoder. */
+bool message_search_more_decoded(struct message_search_context *ctx,
+ struct message_block *block);
+void message_search_reset(struct message_search_context *ctx);
+/* Search a full message. Returns 1 if match was found, 0 if not,
+ -1 if error (if stream_error == 0, the parts contained broken data) */
+int message_search_msg(struct message_search_context *ctx,
+ struct istream *input, struct message_part *parts,
+ const char **error_r)
+ ATTR_NULL(3);
+
+#endif
diff --git a/src/lib-mail/message-size.c b/src/lib-mail/message-size.c
new file mode 100644
index 0000000..472abeb
--- /dev/null
+++ b/src/lib-mail/message-size.c
@@ -0,0 +1,174 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "message-parser.h"
+#include "message-size.h"
+
+int message_get_header_size(struct istream *input, struct message_size *hdr,
+ bool *has_nuls_r)
+{
+ const unsigned char *msg;
+ size_t i, size, startpos, missing_cr_count;
+ int ret;
+
+ memset(hdr, 0, sizeof(struct message_size));
+ *has_nuls_r = FALSE;
+
+ missing_cr_count = 0; startpos = 0;
+ while ((ret = i_stream_read_bytes(input, &msg, &size, startpos + 1)) > 0) {
+ for (i = startpos; i < size; i++) {
+ if (msg[i] != '\n') {
+ if (msg[i] == '\0')
+ *has_nuls_r = TRUE;
+ continue;
+ }
+
+ hdr->lines++;
+ if (i == 0 || msg[i-1] != '\r') {
+ /* missing CR */
+ missing_cr_count++;
+ }
+
+ if (i == 0 || (i == 1 && msg[i-1] == '\r')) {
+ /* no headers at all */
+ break;
+ }
+
+ if ((i > 0 && msg[i-1] == '\n') ||
+ (i > 1 && msg[i-2] == '\n' && msg[i-1] == '\r')) {
+ /* \n\n or \n\r\n - end of headers */
+ break;
+ }
+ }
+
+ if (i < size) {
+ /* end of header */
+ startpos = i+1;
+ break;
+ }
+
+ /* leave the last two characters, they may be \r\n */
+ startpos = size == 1 ? 1 : 2;
+ i_stream_skip(input, i - startpos);
+
+ hdr->physical_size += i - startpos;
+ }
+ i_assert(ret == -1 || ret > 0);
+
+ ret = input->stream_errno != 0 ? -1 : 0;
+ i_stream_skip(input, startpos);
+ hdr->physical_size += startpos;
+
+ hdr->virtual_size = hdr->physical_size + missing_cr_count;
+ i_assert(hdr->virtual_size >= hdr->physical_size);
+ return ret;
+}
+
+int message_get_body_size(struct istream *input, struct message_size *body,
+ bool *has_nuls_r)
+{
+ const unsigned char *msg;
+ size_t i, size, missing_cr_count;
+ int ret;
+
+ memset(body, 0, sizeof(struct message_size));
+ *has_nuls_r = FALSE;
+
+ missing_cr_count = 0;
+ if ((ret = i_stream_read_more(input, &msg, &size)) <= 0) {
+ i_assert(ret == -1);
+ return ret < 0 && input->stream_errno != 0 ? -1 : 0;
+ }
+
+ if (msg[0] == '\n')
+ missing_cr_count++;
+
+ do {
+ for (i = 1; i < size; i++) {
+ if (msg[i] > '\n')
+ continue;
+
+ if (msg[i] == '\n') {
+ if (msg[i-1] != '\r') {
+ /* missing CR */
+ missing_cr_count++;
+ }
+
+ /* increase after making sure we didn't break
+ at virtual \r */
+ body->lines++;
+ } else if (msg[i] == '\0') {
+ *has_nuls_r = TRUE;
+ }
+ }
+
+ /* leave the last character, it may be \r */
+ i_stream_skip(input, i - 1);
+ body->physical_size += i - 1;
+ } while ((ret = i_stream_read_bytes(input, &msg, &size, 2)) > 0);
+ i_assert(ret == -1);
+
+ ret = input->stream_errno != 0 ? -1 : 0;
+
+ i_stream_skip(input, 1);
+ body->physical_size++;
+
+ body->virtual_size = body->physical_size + missing_cr_count;
+ i_assert(body->virtual_size >= body->physical_size);
+ return ret;
+}
+
+void message_size_add(struct message_size *dest,
+ const struct message_size *src)
+{
+ dest->virtual_size += src->virtual_size;
+ dest->physical_size += src->physical_size;
+ dest->lines += src->lines;
+}
+
+int message_skip_virtual(struct istream *input, uoff_t virtual_skip,
+ bool *last_cr_r)
+{
+ const unsigned char *msg;
+ size_t i, size;
+ bool cr_skipped = FALSE;
+ int ret;
+
+ *last_cr_r = FALSE;
+ if (virtual_skip == 0)
+ return 0;
+
+ while ((ret = i_stream_read_more(input, &msg, &size)) > 0) {
+ for (i = 0; i < size && virtual_skip > 0; i++) {
+ virtual_skip--;
+
+ if (msg[i] == '\r') {
+ /* CR */
+ if (virtual_skip == 0)
+ *last_cr_r = TRUE;
+ } else if (msg[i] == '\n') {
+ /* LF */
+ if ((i == 0 && !cr_skipped) ||
+ (i > 0 && msg[i-1] != '\r')) {
+ if (virtual_skip == 0) {
+ /* CR/LF boundary */
+ *last_cr_r = TRUE;
+ break;
+ }
+
+ virtual_skip--;
+ }
+ }
+ }
+ i_stream_skip(input, i);
+
+ if (i < size)
+ return 0;
+
+ i_assert(i > 0);
+ cr_skipped = msg[i-1] == '\r';
+ }
+ i_assert(ret == -1);
+ return input->stream_errno == 0 ? 0 : -1;
+}
diff --git a/src/lib-mail/message-size.h b/src/lib-mail/message-size.h
new file mode 100644
index 0000000..5fe6b8a
--- /dev/null
+++ b/src/lib-mail/message-size.h
@@ -0,0 +1,28 @@
+#ifndef MESSAGE_SIZE_H
+#define MESSAGE_SIZE_H
+
+struct message_size {
+ uoff_t physical_size;
+ uoff_t virtual_size;
+ unsigned int lines;
+};
+
+/* Calculate size of message header. Leave the input point to first
+ character in body. */
+int message_get_header_size(struct istream *input, struct message_size *hdr,
+ bool *has_nuls_r);
+/* Calculate size of message body. */
+int message_get_body_size(struct istream *input, struct message_size *body,
+ bool *has_nuls_r);
+
+/* Sum contents of src into dest. */
+void message_size_add(struct message_size *dest,
+ const struct message_size *src);
+
+/* Skip number of virtual bytes from buffer. last_cr_r is set to TRUE if the
+ last character we skipped was '\r', meaning that the next character should
+ be '\n', which shouldn't be treated as "\r\n". */
+int message_skip_virtual(struct istream *input, uoff_t virtual_skip,
+ bool *last_cr_r);
+
+#endif
diff --git a/src/lib-mail/message-snippet.c b/src/lib-mail/message-snippet.c
new file mode 100644
index 0000000..2982e2e
--- /dev/null
+++ b/src/lib-mail/message-snippet.c
@@ -0,0 +1,207 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "istream.h"
+#include "mail-html2text.h"
+#include "message-parser.h"
+#include "message-decoder.h"
+#include "message-snippet.h"
+
+#include <ctype.h>
+
+enum snippet_state {
+ /* beginning of the line */
+ SNIPPET_STATE_NEWLINE = 0,
+ /* within normal text */
+ SNIPPET_STATE_NORMAL,
+ /* within quoted text - skip until EOL */
+ SNIPPET_STATE_QUOTED
+};
+
+struct snippet_data {
+ string_t *snippet;
+ unsigned int chars_left;
+};
+
+struct snippet_context {
+ struct snippet_data snippet;
+ struct snippet_data quoted_snippet;
+ enum snippet_state state;
+ bool add_whitespace;
+ struct mail_html2text *html2text;
+ buffer_t *plain_output;
+};
+
+static void snippet_add_content(struct snippet_context *ctx,
+ struct snippet_data *target,
+ const unsigned char *data, size_t size,
+ size_t *count_r)
+{
+ i_assert(target != NULL);
+ if (size == 0)
+ return;
+ if (size >= 3 &&
+ ((data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF) ||
+ (data[0] == 0xBF && data[1] == 0xBB && data[2] == 0xEF))) {
+ *count_r = 3;
+ return;
+ }
+ if (data[0] == '\0') {
+ /* skip NULs without increasing snippet size */
+ return;
+ }
+ if (i_isspace(*data)) {
+ /* skip any leading whitespace */
+ if (str_len(target->snippet) > 0)
+ ctx->add_whitespace = TRUE;
+ if (data[0] == '\n')
+ ctx->state = SNIPPET_STATE_NEWLINE;
+ return;
+ }
+ if (target->chars_left == 0)
+ return;
+ target->chars_left--;
+ if (ctx->add_whitespace) {
+ if (target->chars_left == 0) {
+ /* don't add a trailing whitespace */
+ return;
+ }
+ str_append_c(target->snippet, ' ');
+ ctx->add_whitespace = FALSE;
+ target->chars_left--;
+ }
+ *count_r = uni_utf8_char_bytes(data[0]);
+ i_assert(*count_r <= size);
+ str_append_data(target->snippet, data, *count_r);
+}
+
+static bool snippet_generate(struct snippet_context *ctx,
+ const unsigned char *data, size_t size)
+{
+ size_t i, count;
+ struct snippet_data *target;
+
+ if (ctx->html2text != NULL) {
+ buffer_set_used_size(ctx->plain_output, 0);
+ mail_html2text_more(ctx->html2text, data, size,
+ ctx->plain_output);
+ data = ctx->plain_output->data;
+ size = ctx->plain_output->used;
+ }
+
+ if (ctx->state == SNIPPET_STATE_QUOTED)
+ target = &ctx->quoted_snippet;
+ else
+ target = &ctx->snippet;
+
+ /* message-decoder should feed us only valid and complete
+ UTF-8 input */
+
+ for (i = 0; i < size; i += count) {
+ count = 1;
+ switch (ctx->state) {
+ case SNIPPET_STATE_NEWLINE:
+ if (data[i] == '>') {
+ ctx->state = SNIPPET_STATE_QUOTED;
+ i++;
+ target = &ctx->quoted_snippet;
+ } else {
+ ctx->state = SNIPPET_STATE_NORMAL;
+ target = &ctx->snippet;
+ }
+ /* fallthrough */
+ case SNIPPET_STATE_NORMAL:
+ case SNIPPET_STATE_QUOTED:
+ snippet_add_content(ctx, target, CONST_PTR_OFFSET(data, i),
+ size-i, &count);
+ /* break here if we have enough non-quoted data,
+ quoted data does not need to break here as it's
+ only used if the actual snippet is left empty. */
+ if (ctx->snippet.chars_left == 0)
+ return FALSE;
+ break;
+ }
+ }
+ return TRUE;
+}
+
+static void snippet_copy(const char *src, string_t *dst)
+{
+ while (*src != '\0' && i_isspace(*src)) src++;
+ str_append(dst, src);
+}
+
+int message_snippet_generate(struct istream *input,
+ unsigned int max_snippet_chars,
+ string_t *snippet)
+{
+ const struct message_parser_settings parser_set = { .flags = 0 };
+ struct message_parser_ctx *parser;
+ struct message_part *parts;
+ struct message_part *skip_part = NULL;
+ struct message_decoder_context *decoder;
+ struct message_block raw_block, block;
+ struct snippet_context ctx;
+ pool_t pool;
+ int ret;
+
+ i_zero(&ctx);
+ pool = pool_alloconly_create("message snippet", 2048);
+ ctx.snippet.snippet = str_new(pool, max_snippet_chars);
+ ctx.snippet.chars_left = max_snippet_chars;
+ ctx.quoted_snippet.snippet = str_new(pool, max_snippet_chars);
+ ctx.quoted_snippet.chars_left = max_snippet_chars - 1; /* -1 for '>' */
+ parser = message_parser_init(pool_datastack_create(), input, &parser_set);
+ decoder = message_decoder_init(NULL, 0);
+ while ((ret = message_parser_parse_next_block(parser, &raw_block)) > 0) {
+ if (raw_block.part == skip_part)
+ continue;
+ if (!message_decoder_decode_next_block(decoder, &raw_block, &block))
+ continue;
+ if (block.size == 0) {
+ const char *ct;
+
+ if (block.hdr != NULL)
+ continue;
+
+ /* We already have a snippet, don't look for more in
+ subsequent parts. */
+ if (ctx.snippet.snippet->used != 0 ||
+ ctx.quoted_snippet.snippet->used != 0)
+ break;
+
+ skip_part = NULL;
+
+ /* end of headers - verify that we can use this
+ Content-Type. we get here only once, because we
+ always handle only one non-multipart MIME part. */
+ ct = message_decoder_current_content_type(decoder);
+ if (ct == NULL)
+ /* text/plain */ ;
+ else if (mail_html2text_content_type_match(ct)) {
+ mail_html2text_deinit(&ctx.html2text);
+ ctx.html2text = mail_html2text_init(0);
+ if (ctx.plain_output == NULL) {
+ ctx.plain_output =
+ buffer_create_dynamic(pool, 1024);
+ }
+ } else if (strncasecmp(ct, "text/", 5) != 0)
+ skip_part = raw_block.part;
+ } else if (!snippet_generate(&ctx, block.data, block.size))
+ break;
+ }
+ i_assert(ret != 0);
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+ mail_html2text_deinit(&ctx.html2text);
+ if (ctx.snippet.snippet->used != 0)
+ snippet_copy(str_c(ctx.snippet.snippet), snippet);
+ else if (ctx.quoted_snippet.snippet->used != 0) {
+ str_append_c(snippet, '>');
+ snippet_copy(str_c(ctx.quoted_snippet.snippet), snippet);
+ }
+ pool_unref(&pool);
+ return input->stream_errno == 0 ? 0 : -1;
+}
diff --git a/src/lib-mail/message-snippet.h b/src/lib-mail/message-snippet.h
new file mode 100644
index 0000000..fe9c3b6
--- /dev/null
+++ b/src/lib-mail/message-snippet.h
@@ -0,0 +1,14 @@
+#ifndef MESSAGE_SNIPPET_H
+#define MESSAGE_SNIPPET_H
+
+/* Generate UTF-8 text snippet from the beginning of the given mail input
+ stream. The stream is expected to start at the MIME part's headers whose
+ snippet is being generated. Returns 0 if ok, -1 if I/O error.
+
+ Currently only Content-Type: text/ is supported, others will result in an
+ empty string. */
+int message_snippet_generate(struct istream *input,
+ unsigned int max_snippet_chars,
+ string_t *snippet);
+
+#endif
diff --git a/src/lib-mail/ostream-dot.c b/src/lib-mail/ostream-dot.c
new file mode 100644
index 0000000..f5bfa72
--- /dev/null
+++ b/src/lib-mail/ostream-dot.c
@@ -0,0 +1,235 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "ostream-private.h"
+#include "ostream-dot.h"
+
+enum dot_ostream_state {
+ STREAM_STATE_INIT = 0,
+ STREAM_STATE_NONE,
+ STREAM_STATE_CR,
+ STREAM_STATE_CRLF,
+ STREAM_STATE_DONE
+};
+
+struct dot_ostream {
+ struct ostream_private ostream;
+
+ enum dot_ostream_state state;
+ bool force_extra_crlf;
+};
+
+static int o_stream_dot_finish(struct ostream_private *stream)
+{
+ struct dot_ostream *dstream = (struct dot_ostream *)stream;
+ int ret;
+
+ if (dstream->state == STREAM_STATE_DONE)
+ return 1;
+
+ if (o_stream_get_buffer_avail_size(stream->parent) < 5) {
+ /* make space for the dot line */
+ if ((ret = o_stream_flush(stream->parent)) <= 0) {
+ if (ret < 0)
+ o_stream_copy_error_from_parent(stream);
+ return ret;
+ }
+ }
+
+ if (dstream->state == STREAM_STATE_CRLF &&
+ !dstream->force_extra_crlf) {
+ ret = o_stream_send(stream->parent, ".\r\n", 3);
+ i_assert(ret == 3);
+ } else {
+ ret = o_stream_send(stream->parent, "\r\n.\r\n", 5);
+ i_assert(ret == 5);
+ }
+ dstream->state = STREAM_STATE_DONE;
+ return 1;
+}
+
+static int
+o_stream_dot_flush(struct ostream_private *stream)
+{
+ int ret;
+
+ if (stream->finished) {
+ if ((ret = o_stream_dot_finish(stream)) <= 0)
+ return ret;
+ }
+
+ return o_stream_flush_parent(stream);
+}
+
+static void
+o_stream_dot_close(struct iostream_private *stream, bool close_parent)
+{
+ struct dot_ostream *dstream = (struct dot_ostream *)stream;
+
+ if (close_parent)
+ o_stream_close(dstream->ostream.parent);
+}
+
+static ssize_t
+o_stream_dot_sendv(struct ostream_private *stream,
+ const struct const_iovec *iov, unsigned int iov_count)
+{
+ struct dot_ostream *dstream = (struct dot_ostream *)stream;
+ ARRAY(struct const_iovec) iov_arr;
+ const struct const_iovec *iov_new;
+ size_t max_bytes, sent, added;
+ unsigned int count, i;
+ ssize_t ret;
+
+ i_assert(dstream->state != STREAM_STATE_DONE);
+
+ if ((ret=o_stream_flush(stream->parent)) <= 0) {
+ /* error / we still couldn't flush existing data to
+ parent stream. */
+ if (ret < 0)
+ o_stream_copy_error_from_parent(stream);
+ return ret;
+ }
+
+ /* check for dots */
+ t_array_init(&iov_arr, iov_count + 32);
+ max_bytes = o_stream_get_buffer_avail_size(stream->parent);
+ i_assert(max_bytes > 0); /* FIXME: not supported currently */
+
+ sent = added = 0;
+ for (i = 0; i < iov_count && max_bytes > 0; i++) {
+ size_t size = iov[i].iov_len, chunk;
+ const char *data = iov[i].iov_base, *p, *pend;
+ struct const_iovec iovn;
+
+ p = data;
+ pend = CONST_PTR_OFFSET(data, size);
+ for (; p < pend && (size_t)(p-data)+2 < max_bytes; p++) {
+ char add = 0;
+
+ switch (dstream->state) {
+ /* none */
+ case STREAM_STATE_NONE:
+ switch (*p) {
+ case '\n':
+ dstream->state = STREAM_STATE_CRLF;
+ /* add missing CR */
+ add = '\r';
+ break;
+ case '\r':
+ dstream->state = STREAM_STATE_CR;
+ break;
+ }
+ break;
+ /* got CR */
+ case STREAM_STATE_CR:
+ switch (*p) {
+ case '\r':
+ break;
+ case '\n':
+ dstream->state = STREAM_STATE_CRLF;
+ break;
+ default:
+ dstream->state = STREAM_STATE_NONE;
+ break;
+ }
+ break;
+ /* got CRLF, or the first line */
+ case STREAM_STATE_INIT:
+ case STREAM_STATE_CRLF:
+ switch (*p) {
+ case '\r':
+ dstream->state = STREAM_STATE_CR;
+ break;
+ case '\n':
+ dstream->state = STREAM_STATE_CRLF;
+ /* add missing CR */
+ add = '\r';
+ break;
+ case '.':
+ /* add dot */
+ add = '.';
+ /* fall through */
+ default:
+ dstream->state = STREAM_STATE_NONE;
+ break;
+ }
+ break;
+ case STREAM_STATE_DONE:
+ i_unreached();
+ }
+
+ if (add != 0) {
+ chunk = (size_t)(p - data);
+ if (chunk > 0) {
+ /* forward chunk to new iovec */
+ iovn.iov_base = data;
+ iovn.iov_len = chunk;
+ array_push_back(&iov_arr, &iovn);
+ data = p;
+ i_assert(max_bytes >= chunk);
+ max_bytes -= chunk;
+ sent += chunk;
+ }
+ /* insert byte (substitute one with pair) */
+ data++;
+ iovn.iov_base = (add == '\r' ? "\r\n" : "..");
+ iovn.iov_len = 2;
+ array_push_back(&iov_arr, &iovn);
+ i_assert(max_bytes >= 2);
+ max_bytes -= 2;
+ added++;
+ sent++;
+ }
+ }
+
+ if (max_bytes == 0)
+ break;
+ chunk = ((size_t)(p-data) >= max_bytes ?
+ max_bytes : (size_t)(p - data));
+ if (chunk > 0) {
+ iovn.iov_base = data;
+ iovn.iov_len = chunk;
+ array_push_back(&iov_arr, &iovn);
+ i_assert(max_bytes >= chunk);
+ max_bytes -= chunk;
+ sent += chunk;
+ }
+ }
+
+ /* send */
+ iov_new = array_get(&iov_arr, &count);
+ if (count == 0) {
+ ret = 0;
+ } else if ((ret=o_stream_sendv(stream->parent, iov_new, count)) <= 0) {
+ i_assert(ret < 0);
+ o_stream_copy_error_from_parent(stream);
+ return -1;
+ }
+
+ /* all must be sent */
+ i_assert((size_t)ret == sent + added);
+
+ stream->ostream.offset += sent;
+ return sent;
+}
+
+struct ostream *
+o_stream_create_dot(struct ostream *output, bool force_extra_crlf)
+{
+ struct dot_ostream *dstream;
+
+ dstream = i_new(struct dot_ostream, 1);
+ dstream->ostream.sendv = o_stream_dot_sendv;
+ dstream->ostream.iostream.close = o_stream_dot_close;
+ dstream->ostream.flush = o_stream_dot_flush;
+ dstream->ostream.max_buffer_size = output->real_stream->max_buffer_size;
+ dstream->force_extra_crlf = force_extra_crlf;
+ (void)o_stream_create(&dstream->ostream, output, o_stream_get_fd(output));
+ /* ostream-dot is always used inside another ostream that shouldn't
+ get finished when the "." line is written. Disable it here so all
+ of the callers don't have to set this. */
+ o_stream_set_finish_also_parent(&dstream->ostream.ostream, FALSE);
+ return &dstream->ostream.ostream;
+}
diff --git a/src/lib-mail/ostream-dot.h b/src/lib-mail/ostream-dot.h
new file mode 100644
index 0000000..4e22089
--- /dev/null
+++ b/src/lib-mail/ostream-dot.h
@@ -0,0 +1,15 @@
+#ifndef OSTREAM_DOT_H
+#define OSTREAM_DOT_H
+
+/* Create output stream for writing SMTP DATA style message: Add additional "."
+ to lines that start with ".". Write a line that contains only "." upon
+ o_stream_flush(). (This is also called at close(), but it shouldn't be
+ relied on since it could fail due to output buffer being full.)
+
+ If output ends with CRLF, force_extra_crlf controls whether additional CRLF
+ is written before the "." line. This parameter should match
+ i_stream_create_dot()'s send_last_lf parameter (reversed). */
+struct ostream *o_stream_create_dot(struct ostream *output,
+ bool force_extra_crlf);
+
+#endif
diff --git a/src/lib-mail/qp-decoder.c b/src/lib-mail/qp-decoder.c
new file mode 100644
index 0000000..7684803
--- /dev/null
+++ b/src/lib-mail/qp-decoder.c
@@ -0,0 +1,285 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "qp-decoder.h"
+
+/* quoted-printable lines can be max 76 characters. if we've seen more than
+ that much whitespace, it means there really shouldn't be anything else left
+ in the line except trailing whitespace. */
+#define QP_MAX_WHITESPACE_LEN 76
+
+#define QP_IS_TRAILING_WHITESPACE(c) \
+ ((c) == ' ' || (c) == '\t')
+
+enum qp_state {
+ STATE_TEXT = 0,
+ STATE_WHITESPACE,
+ STATE_EQUALS,
+ STATE_EQUALS_WHITESPACE,
+ STATE_HEX2,
+ STATE_CR,
+ STATE_SOFTCR
+};
+
+struct qp_decoder {
+ buffer_t *dest;
+ buffer_t *whitespace;
+ enum qp_state state;
+ char hexchar;
+};
+
+struct qp_decoder *qp_decoder_init(buffer_t *dest)
+{
+ struct qp_decoder *qp;
+
+ qp = i_new(struct qp_decoder, 1);
+ qp->dest = dest;
+ qp->whitespace = buffer_create_dynamic(default_pool, 80);
+ return qp;
+}
+
+void qp_decoder_deinit(struct qp_decoder **_qp)
+{
+ struct qp_decoder *qp = *_qp;
+
+ buffer_free(&qp->whitespace);
+ i_free(qp);
+}
+
+static size_t
+qp_decoder_more_text(struct qp_decoder *qp, const unsigned char *src,
+ size_t src_size)
+{
+ size_t i, start = 0, ret = src_size;
+
+ for (i = 0; i < src_size; i++) {
+ if (src[i] > '=') {
+ /* fast path */
+ continue;
+ }
+ switch (src[i]) {
+ case '=':
+ qp->state = STATE_EQUALS;
+ break;
+ case '\r':
+ qp->state = STATE_CR;
+ break;
+ case '\n':
+ /* LF without preceding CR */
+ buffer_append(qp->dest, src+start, i-start);
+ buffer_append(qp->dest, "\r\n", 2);
+ start = i+1;
+ continue;
+ case ' ':
+ case '\t':
+ i_assert(qp->whitespace->used == 0);
+ qp->state = STATE_WHITESPACE;
+ buffer_append_c(qp->whitespace, src[i]);
+ break;
+ default:
+ continue;
+ }
+ ret = i+1;
+ break;
+ }
+ buffer_append(qp->dest, src+start, i-start);
+ return ret;
+}
+
+static void qp_decoder_invalid(struct qp_decoder *qp, const char **error_r)
+{
+ switch (qp->state) {
+ case STATE_EQUALS:
+ buffer_append_c(qp->dest, '=');
+ *error_r = "'=' not followed by two hex digits";
+ break;
+ case STATE_HEX2:
+ buffer_append_c(qp->dest, '=');
+ buffer_append_c(qp->dest, qp->hexchar);
+ *error_r = "'=<hex>' not followed by a hex digit";
+ break;
+ case STATE_EQUALS_WHITESPACE:
+ buffer_append_c(qp->dest, '=');
+ buffer_append_buf(qp->dest, qp->whitespace, 0, SIZE_MAX);
+ buffer_set_used_size(qp->whitespace, 0);
+ *error_r = "'=<whitespace>' not followed by newline";
+ break;
+ case STATE_CR:
+ buffer_append_buf(qp->dest, qp->whitespace, 0, SIZE_MAX);
+ buffer_set_used_size(qp->whitespace, 0);
+ buffer_append_c(qp->dest, '\r');
+ *error_r = "CR not followed by LF";
+ break;
+ case STATE_SOFTCR:
+ buffer_append_c(qp->dest, '=');
+ buffer_append_buf(qp->dest, qp->whitespace, 0, SIZE_MAX);
+ buffer_set_used_size(qp->whitespace, 0);
+ buffer_append_c(qp->dest, '\r');
+ *error_r = "CR not followed by LF";
+ break;
+ case STATE_TEXT:
+ case STATE_WHITESPACE:
+ i_unreached();
+ }
+ qp->state = STATE_TEXT;
+ i_assert(*error_r != NULL);
+}
+
+int qp_decoder_more(struct qp_decoder *qp, const unsigned char *src,
+ size_t src_size, size_t *invalid_src_pos_r,
+ const char **error_r)
+{
+ const char *error;
+ size_t i;
+
+ *invalid_src_pos_r = SIZE_MAX;
+ *error_r = NULL;
+
+ for (i = 0; i < src_size; ) {
+ switch (qp->state) {
+ case STATE_TEXT:
+ i += qp_decoder_more_text(qp, src+i, src_size-i);
+ /* don't increment i any more than we already did,
+ so continue instead of break */
+ continue;
+ case STATE_WHITESPACE:
+ if (QP_IS_TRAILING_WHITESPACE(src[i])) {
+ /* more whitespace */
+ if (qp->whitespace->used <= QP_MAX_WHITESPACE_LEN)
+ buffer_append_c(qp->whitespace, src[i]);
+ } else if (src[i] == '\r') {
+ qp->state = STATE_CR;
+ } else if (src[i] == '\n') {
+ /* drop the trailing whitespace */
+ buffer_append(qp->dest, "\r\n", 2);
+ buffer_set_used_size(qp->whitespace, 0);
+ } else {
+ /* this wasn't trailing whitespace.
+ put it back. */
+ buffer_append_buf(qp->dest, qp->whitespace,
+ 0, SIZE_MAX);
+ if (qp->whitespace->used > QP_MAX_WHITESPACE_LEN) {
+ /* we already truncated some of the
+ whitespace away, because the line
+ is too long */
+ if (*invalid_src_pos_r == SIZE_MAX) {
+ *invalid_src_pos_r = i;
+ *error_r = "Too much whitespace";
+ }
+ }
+ buffer_set_used_size(qp->whitespace, 0);
+ qp->state = STATE_TEXT;
+ continue; /* don't increment i */
+ }
+ break;
+ case STATE_EQUALS:
+ if ((src[i] >= '0' && src[i] <= '9') ||
+ (src[i] >= 'A' && src[i] <= 'F') ||
+ /* lowercase hex isn't strictly valid, but allow */
+ (src[i] >= 'a' && src[i] <= 'f')) {
+ qp->hexchar = src[i];
+ qp->state = STATE_HEX2;
+ } else if (QP_IS_TRAILING_WHITESPACE(src[i])) {
+ i_assert(qp->whitespace->used == 0);
+ buffer_append_c(qp->whitespace, src[i]);
+ qp->state = STATE_EQUALS_WHITESPACE;
+ } else if (src[i] == '\r')
+ qp->state = STATE_SOFTCR;
+ else if (src[i] == '\n') {
+ qp->state = STATE_TEXT;
+ } else {
+ /* invalid input */
+ qp_decoder_invalid(qp, &error);
+ if (*invalid_src_pos_r == SIZE_MAX) {
+ *invalid_src_pos_r = i;
+ *error_r = error;
+ }
+ continue; /* don't increment i */
+ }
+ break;
+ case STATE_HEX2:
+ if ((src[i] >= '0' && src[i] <= '9') ||
+ (src[i] >= 'A' && src[i] <= 'F') ||
+ (src[i] >= 'a' && src[i] <= 'f')) {
+ char data[3];
+
+ data[0] = qp->hexchar;
+ data[1] = src[i];
+ data[2] = '\0';
+ if (hex_to_binary(data, qp->dest) < 0)
+ i_unreached();
+ qp->state = STATE_TEXT;
+ } else {
+ /* invalid input */
+ qp_decoder_invalid(qp, &error);
+ if (*invalid_src_pos_r == SIZE_MAX) {
+ *invalid_src_pos_r = i;
+ *error_r = error;
+ }
+ continue; /* don't increment i */
+ }
+ break;
+ case STATE_EQUALS_WHITESPACE:
+ if (QP_IS_TRAILING_WHITESPACE(src[i])) {
+ if (qp->whitespace->used <= QP_MAX_WHITESPACE_LEN)
+ buffer_append_c(qp->whitespace, src[i]);
+ else {
+ /* if this isn't going to get truncated
+ anyway, it's going to be an error */
+ }
+ } else if (src[i] == '\r')
+ qp->state = STATE_SOFTCR;
+ else if (src[i] == '\n') {
+ buffer_set_used_size(qp->whitespace, 0);
+ qp->state = STATE_TEXT;
+ } else {
+ /* =<whitespace> not followed by [CR]LF
+ is invalid. */
+ qp_decoder_invalid(qp, &error);
+ if (*invalid_src_pos_r == SIZE_MAX) {
+ *invalid_src_pos_r = i;
+ *error_r = error;
+ }
+ continue; /* don't increment i */
+ }
+ break;
+ case STATE_CR:
+ case STATE_SOFTCR:
+ if (src[i] == '\n') {
+ buffer_set_used_size(qp->whitespace, 0);
+ if (qp->state != STATE_SOFTCR)
+ buffer_append(qp->dest, "\r\n", 2);
+ qp->state = STATE_TEXT;
+ } else {
+ qp_decoder_invalid(qp, &error);
+ if (*invalid_src_pos_r == SIZE_MAX) {
+ *invalid_src_pos_r = i;
+ *error_r = error;
+ }
+ continue; /* don't increment i */
+ }
+ break;
+ }
+ i++;
+ }
+ i_assert((*invalid_src_pos_r == SIZE_MAX) == (*error_r == NULL));
+ return *invalid_src_pos_r == SIZE_MAX ? 0 : -1;
+}
+
+int qp_decoder_finish(struct qp_decoder *qp, const char **error_r)
+{
+ int ret;
+
+ if (qp->state == STATE_TEXT || qp->state == STATE_WHITESPACE) {
+ ret = 0;
+ *error_r = NULL;
+ } else {
+ qp_decoder_invalid(qp, error_r);
+ ret = -1;
+ }
+ qp->state = STATE_TEXT;
+ buffer_set_used_size(qp->whitespace, 0);
+ return ret;
+}
diff --git a/src/lib-mail/qp-decoder.h b/src/lib-mail/qp-decoder.h
new file mode 100644
index 0000000..f69711f
--- /dev/null
+++ b/src/lib-mail/qp-decoder.h
@@ -0,0 +1,19 @@
+#ifndef QP_DECODER_H
+#define QP_DECODER_H
+
+/* Initialize quoted-printable decoder. Write all the decoded output to dest. */
+struct qp_decoder *qp_decoder_init(buffer_t *dest);
+void qp_decoder_deinit(struct qp_decoder **qp);
+
+/* Translate more quoted printable data into binary. Returns 0 if input was
+ valid, -1 if there were some decoding errors (which were skipped over).
+ LFs without preceding CR are returned as CRLF (but =0A isn't). */
+int qp_decoder_more(struct qp_decoder *qp, const unsigned char *src,
+ size_t src_size, size_t *invalid_src_pos_r,
+ const char **error_r);
+/* Finish decoding any pending input. Returns the same as qp_decoder_more().
+ This function also resets the entire decoder state, so the same decoder can
+ be used to decode more data if wanted. */
+int qp_decoder_finish(struct qp_decoder *qp, const char **error_r);
+
+#endif
diff --git a/src/lib-mail/qp-encoder.c b/src/lib-mail/qp-encoder.c
new file mode 100644
index 0000000..37a4412
--- /dev/null
+++ b/src/lib-mail/qp-encoder.c
@@ -0,0 +1,162 @@
+/* Copyright (c) 2017-2018 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>
+
+enum qp_encoder_last_char {
+ QP_ENCODER_LAST_ANY = 0,
+ QP_ENCODER_LAST_CR,
+ QP_ENCODER_LAST_WHITE_SPACE,
+};
+
+struct qp_encoder {
+ const char *linebreak;
+ string_t *dest;
+ size_t line_len;
+ size_t max_len;
+ enum qp_encoder_flag flags;
+ enum qp_encoder_last_char last_char;
+ bool add_header_preamble: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->last_char == QP_ENCODER_LAST_CR)) {
+ str_append_c(qp->dest, '\r');
+ str_append_c(qp->dest, '\n');
+ /* reset line length here */
+ qp->line_len = 0;
+ qp->last_char = QP_ENCODER_LAST_ANY;
+ continue;
+ } else if (qp->last_char == QP_ENCODER_LAST_CR) {
+ qp_encode_or_break(qp, '\r');
+ qp->last_char = QP_ENCODER_LAST_ANY;
+ }
+
+ if (c == ' ' || c == '\t')
+ qp->last_char = QP_ENCODER_LAST_WHITE_SPACE;
+ else if (c == '\r')
+ qp->last_char = QP_ENCODER_LAST_CR;
+ else
+ qp->last_char = QP_ENCODER_LAST_ANY;
+
+ if (c != '\r')
+ qp_encode_or_break(qp, c);
+ }
+}
+
+void qp_encoder_finish(struct qp_encoder *qp)
+{
+ switch (qp->last_char) {
+ case QP_ENCODER_LAST_CR:
+ /* Last char was CR which was not yet encoded */
+ qp_encode_or_break(qp, '\r');
+ break;
+ case QP_ENCODER_LAST_WHITE_SPACE:
+ /* Last char was a white space, it must be followed by
+ * a printable char. */
+ str_append_c(qp->dest, '=');
+ break;
+ default:
+ break;
+ }
+
+ 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->last_char = QP_ENCODER_LAST_ANY;
+}
diff --git a/src/lib-mail/qp-encoder.h b/src/lib-mail/qp-encoder.h
new file mode 100644
index 0000000..f4a6148
--- /dev/null
+++ b/src/lib-mail/qp-encoder.h
@@ -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
diff --git a/src/lib-mail/quoted-printable.c b/src/lib-mail/quoted-printable.c
new file mode 100644
index 0000000..9bfbe35
--- /dev/null
+++ b/src/lib-mail/quoted-printable.c
@@ -0,0 +1,49 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "hex-binary.h"
+#include "quoted-printable.h"
+
+int quoted_printable_q_decode(const unsigned char *src, size_t src_size,
+ buffer_t *dest)
+{
+ char hexbuf[3];
+ size_t src_pos, next;
+ bool errors = FALSE;
+
+ hexbuf[2] = '\0';
+
+ next = 0;
+ for (src_pos = 0; src_pos < src_size; src_pos++) {
+ if (src[src_pos] != '_' && src[src_pos] != '=')
+ continue;
+
+ buffer_append(dest, src + next, src_pos - next);
+ next = src_pos;
+
+ if (src[src_pos] == '_') {
+ buffer_append_c(dest, ' ');
+ next++;
+ continue;
+ }
+
+ if (src_pos+2 >= src_size)
+ break;
+
+ /* =<hex> */
+ hexbuf[0] = src[src_pos+1];
+ hexbuf[1] = src[src_pos+2];
+
+ if (hex_to_binary(hexbuf, dest) == 0) {
+ src_pos += 2;
+ next = src_pos+1;
+ } else {
+ /* non-hex data, show as-is */
+ errors = TRUE;
+ next = src_pos;
+ }
+ }
+ buffer_append(dest, src + next, src_size - next);
+ return errors ? -1 : 0;
+}
diff --git a/src/lib-mail/quoted-printable.h b/src/lib-mail/quoted-printable.h
new file mode 100644
index 0000000..2233912
--- /dev/null
+++ b/src/lib-mail/quoted-printable.h
@@ -0,0 +1,8 @@
+#ifndef QUOTED_PRINTABLE_H
+#define QUOTED_PRINTABLE_H
+
+/* Decode MIME "Q" encoding. */
+int quoted_printable_q_decode(const unsigned char *src, size_t src_size,
+ buffer_t *dest);
+
+#endif
diff --git a/src/lib-mail/rfc2231-parser.c b/src/lib-mail/rfc2231-parser.c
new file mode 100644
index 0000000..119d48d
--- /dev/null
+++ b/src/lib-mail/rfc2231-parser.c
@@ -0,0 +1,179 @@
+/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+
+
+struct rfc2231_parameter {
+ const char *key, *value;
+ unsigned int idx;
+ bool extended;
+};
+
+static int rfc2231_parameter_cmp(const struct rfc2231_parameter *r1,
+ const struct rfc2231_parameter *r2)
+{
+ int ret;
+
+ ret = strcmp(r1->key, r2->key);
+ if (ret != 0)
+ return ret;
+
+ return r1->idx < r2->idx ? -1 :
+ (r1-> idx > r2->idx ? 1 : 0);
+}
+
+static void rfc2231_escape(string_t *dest, const char *src)
+{
+ for (; *src != '\0'; src++) {
+ if (*src == '%')
+ str_append(dest, "%25");
+ else
+ str_append_c(dest, *src);
+ }
+}
+
+int rfc2231_parse(struct rfc822_parser_context *ctx,
+ const char *const **result_r)
+{
+ ARRAY_TYPE(const_string) result;
+ ARRAY(struct rfc2231_parameter) rfc2231_params_arr;
+ struct rfc2231_parameter rfc2231_param;
+ const struct rfc2231_parameter *rfc2231_params;
+ const char *key, *p, *p2;
+ string_t *str;
+ unsigned int i, j, count, next, next_idx;
+ bool ok, have_extended, broken = FALSE;
+ const char *prev_replacement_str;
+ int ret;
+
+ /* Temporarily replace the nul_replacement_char while we're parsing
+ the content-params. It'll be restored before we return. */
+ prev_replacement_str = ctx->nul_replacement_str;
+ ctx->nul_replacement_str = RFC822_NUL_REPLACEMENT_STR;
+
+ /* Get a list of all parameters. RFC 2231 uses key*<n>[*]=value pairs,
+ which we want to merge to a key[*]=value pair. Save them to a
+ separate array. */
+ i_zero(&rfc2231_param);
+ t_array_init(&result, 8);
+ t_array_init(&rfc2231_params_arr, 8);
+ str = t_str_new(64);
+ while ((ret = rfc822_parse_content_param(ctx, &key, str)) != 0) {
+ if (ret < 0) {
+ /* try to continue anyway.. */
+ broken = TRUE;
+ if (ctx->data >= ctx->end)
+ break;
+ ctx->data++;
+ continue;
+ }
+ p = strchr(key, '*');
+ if (p != NULL) {
+ p2 = p;
+ if (p[1] != '\0') {
+ p++;
+ rfc2231_param.idx = 0;
+ for (; *p >= '0' && *p <= '9'; p++) {
+ rfc2231_param.idx =
+ rfc2231_param.idx*10 + *p - '0';
+ }
+ }
+ if (*p != '*')
+ rfc2231_param.extended = FALSE;
+ else {
+ rfc2231_param.extended = TRUE;
+ p++;
+ }
+ if (*p != '\0')
+ p = NULL;
+ else {
+ rfc2231_param.key = t_strdup_until(key, p2);
+ rfc2231_param.value = t_strdup(str_c(str));
+ array_push_back(&rfc2231_params_arr,
+ &rfc2231_param);
+ }
+ }
+ if (p == NULL) {
+ const char *value = t_strdup(str_c(str));
+ array_push_back(&result, &key);
+ array_push_back(&result, &value);
+ }
+ }
+ ctx->nul_replacement_str = prev_replacement_str;
+
+ if (array_count(&rfc2231_params_arr) == 0) {
+ /* No RFC 2231 parameters */
+ array_append_zero(&result); /* NULL-terminate */
+ *result_r = array_front(&result);
+ return broken ? -1 : 0;
+ }
+
+ /* Merge the RFC 2231 parameters. Since their order isn't guaranteed to
+ be ascending, start by sorting them. */
+ array_sort(&rfc2231_params_arr, rfc2231_parameter_cmp);
+ rfc2231_params = array_get(&rfc2231_params_arr, &count);
+
+ /* keys are now sorted primarily by their name and secondarily by
+ their index. If any indexes are missing, fallback to assuming
+ these aren't RFC 2231 encoded parameters. */
+ for (i = 0; i < count; i = next) {
+ ok = TRUE;
+ have_extended = FALSE;
+ next_idx = 0;
+ for (j = i; j < count; j++) {
+ if (strcasecmp(rfc2231_params[i].key,
+ rfc2231_params[j].key) != 0)
+ break;
+ if (rfc2231_params[j].idx != next_idx) {
+ /* missing indexes */
+ ok = FALSE;
+ }
+ if (rfc2231_params[j].extended)
+ have_extended = TRUE;
+ next_idx++;
+ }
+ next = j;
+
+ if (!ok) {
+ /* missing indexes */
+ for (j = i; j < next; j++) {
+ key = t_strdup_printf(
+ rfc2231_params[j].extended ?
+ "%s*%u*" : "%s*%u",
+ rfc2231_params[j].key,
+ rfc2231_params[j].idx);
+ array_push_back(&result, &key);
+ array_push_back(&result,
+ &rfc2231_params[j].value);
+ }
+ } else {
+ /* everything was successful */
+ str_truncate(str, 0);
+ if (!rfc2231_params[i].extended && have_extended)
+ str_append(str, "''");
+ for (j = i; j < next; j++) {
+ if (!rfc2231_params[j].extended &&
+ have_extended) {
+ rfc2231_escape(str,
+ rfc2231_params[j].value);
+ } else {
+ str_append(str,
+ rfc2231_params[j].value);
+ }
+ }
+ key = rfc2231_params[i].key;
+ if (have_extended)
+ key = t_strconcat(key, "*", NULL);
+ const char *value = t_strdup(str_c(str));
+ array_push_back(&result, &key);
+ array_push_back(&result, &value);
+ }
+ }
+ array_append_zero(&result); /* NULL-terminate */
+ *result_r = array_front(&result);
+ return broken ? -1 : 0;
+}
diff --git a/src/lib-mail/rfc2231-parser.h b/src/lib-mail/rfc2231-parser.h
new file mode 100644
index 0000000..95cadc2
--- /dev/null
+++ b/src/lib-mail/rfc2231-parser.h
@@ -0,0 +1,13 @@
+#ifndef RFC2231_PARSER_H
+#define RFC2231_PARSER_H
+
+/* Parse all content parameters using rfc822_parse_content_param() and return
+ them as a NULL-terminated [key, value] array. RFC 2231-style continuations
+ are merged to a single key. NULs are converted into unicode replacement
+ character (U+FFFD). Returns -1 if some of the input was invalid (but valid
+ key/value pairs are still returned), 0 if everything looked ok. */
+int ATTR_NOWARN_UNUSED_RESULT
+rfc2231_parse(struct rfc822_parser_context *ctx,
+ const char *const **result_r);
+
+#endif
diff --git a/src/lib-mail/rfc822-parser.c b/src/lib-mail/rfc822-parser.c
new file mode 100644
index 0000000..c8595b4
--- /dev/null
+++ b/src/lib-mail/rfc822-parser.c
@@ -0,0 +1,522 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strescape.h"
+#include "rfc822-parser.h"
+
+/*
+ atext = ALPHA / DIGIT / ; Any character except controls,
+ "!" / "#" / ; SP, and specials.
+ "$" / "%" / ; Used for atoms
+ "&" / "'" /
+ "*" / "+" /
+ "-" / "/" /
+ "=" / "?" /
+ "^" / "_" /
+ "`" / "{" /
+ "|" / "}" /
+ "~"
+
+ MIME:
+
+ token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
+ or tspecials>
+ tspecials := "(" / ")" / "<" / ">" / "@" /
+ "," / ";" / ":" / "\" / <">
+ "/" / "[" / "]" / "?" / "="
+
+ So token is same as dot-atom, except stops also at '/', '?' and '='.
+*/
+
+/* atext chars are marked with 1, alpha and digits with 2,
+ atext-but-mime-tspecials with 4 */
+unsigned char rfc822_atext_chars[256] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0-15 */
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16-31 */
+ 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 4, /* 32-47 */
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 4, 0, 4, /* 48-63 */
+ 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 64-79 */
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, /* 80-95 */
+ 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, /* 96-111 */
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 0, /* 112-127 */
+
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2
+};
+
+void rfc822_parser_init(struct rfc822_parser_context *ctx,
+ const unsigned char *data, size_t size,
+ string_t *last_comment)
+{
+ i_zero(ctx);
+ ctx->data = data;
+ ctx->end = data + size;
+ ctx->last_comment = last_comment;
+}
+
+int rfc822_skip_comment(struct rfc822_parser_context *ctx)
+{
+ const unsigned char *start;
+ size_t len;
+ int level = 1;
+
+ i_assert(*ctx->data == '(');
+
+ if (ctx->last_comment != NULL)
+ str_truncate(ctx->last_comment, 0);
+
+ start = ++ctx->data;
+ for (; ctx->data < ctx->end; ctx->data++) {
+ switch (*ctx->data) {
+ case '\0':
+ if (ctx->last_comment != NULL &&
+ ctx->nul_replacement_str != NULL) {
+ str_append_data(ctx->last_comment, start,
+ ctx->data - start);
+ str_append(ctx->last_comment,
+ ctx->nul_replacement_str);
+ start = ctx->data + 1;
+ }
+ break;
+ case '(':
+ level++;
+ break;
+ case ')':
+ if (--level == 0) {
+ if (ctx->last_comment != NULL) {
+ str_append_data(ctx->last_comment, start,
+ ctx->data - start);
+ }
+ ctx->data++;
+ return ctx->data < ctx->end ? 1 : 0;
+ }
+ break;
+ case '\n':
+ /* folding whitespace, remove the (CR)LF */
+ if (ctx->last_comment == NULL)
+ break;
+ len = ctx->data - start;
+ if (len > 0 && start[len-1] == '\r')
+ len--;
+ str_append_data(ctx->last_comment, start, len);
+ start = ctx->data + 1;
+ break;
+ case '\\':
+ ctx->data++;
+ if (ctx->data >= ctx->end)
+ return -1;
+
+ if (*ctx->data == '\r' || *ctx->data == '\n' ||
+ *ctx->data == '\0') {
+ /* quoted-pair doesn't allow CR/LF/NUL.
+ They are part of the obs-qp though, so don't
+ return them as error. */
+ ctx->data--;
+ break;
+ }
+ if (ctx->last_comment != NULL) {
+ str_append_data(ctx->last_comment, start,
+ ctx->data - start - 1);
+ }
+ start = ctx->data;
+ break;
+ }
+ }
+
+ /* missing ')' */
+ return -1;
+}
+
+int rfc822_skip_lwsp(struct rfc822_parser_context *ctx)
+{
+ for (; ctx->data < ctx->end;) {
+ if (*ctx->data == ' ' || *ctx->data == '\t' ||
+ *ctx->data == '\r' || *ctx->data == '\n') {
+ ctx->data++;
+ continue;
+ }
+
+ if (*ctx->data != '(')
+ break;
+
+ if (rfc822_skip_comment(ctx) < 0)
+ return -1;
+ }
+ return ctx->data < ctx->end ? 1 : 0;
+}
+
+int rfc822_parse_atom(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+
+ /*
+ atom = [CFWS] 1*atext [CFWS]
+ atext =
+ ; Any character except controls, SP, and specials.
+ */
+ if (ctx->data >= ctx->end || !IS_ATEXT(*ctx->data))
+ return -1;
+
+ for (start = ctx->data++; ctx->data < ctx->end; ctx->data++) {
+ if (IS_ATEXT(*ctx->data))
+ continue;
+
+ str_append_data(str, start, ctx->data - start);
+ return rfc822_skip_lwsp(ctx);
+ }
+
+ str_append_data(str, start, ctx->data - start);
+ return 0;
+}
+
+int rfc822_parse_dot_atom(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+ int ret;
+
+ /*
+ dot-atom = [CFWS] dot-atom-text [CFWS]
+ dot-atom-text = 1*atext *("." 1*atext)
+
+ atext =
+ ; Any character except controls, SP, and specials.
+
+ For RFC-822 compatibility allow LWSP around '.'
+ */
+ if (ctx->data >= ctx->end || !IS_ATEXT(*ctx->data))
+ return -1;
+
+ for (start = ctx->data++; ctx->data < ctx->end; ) {
+ if (IS_ATEXT(*ctx->data)) {
+ ctx->data++;
+ continue;
+ }
+
+ if (start == ctx->data)
+ return -1;
+ str_append_data(str, start, ctx->data - start);
+
+ if ((ret = rfc822_skip_lwsp(ctx)) <= 0)
+ return ret;
+
+ if (*ctx->data != '.')
+ return 1;
+
+ ctx->data++;
+ str_append_c(str, '.');
+
+ if (rfc822_skip_lwsp(ctx) <= 0)
+ return -1;
+ start = ctx->data;
+ }
+
+ i_assert(start != ctx->data);
+ str_append_data(str, start, ctx->data - start);
+ return 0;
+}
+
+int rfc822_parse_mime_token(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+
+ for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
+ if (IS_ATEXT_NON_TSPECIAL(*ctx->data) || *ctx->data == '.')
+ continue;
+
+ str_append_data(str, start, ctx->data - start);
+ return rfc822_skip_lwsp(ctx);
+ }
+
+ str_append_data(str, start, ctx->data - start);
+ return 0;
+}
+
+int rfc822_parse_quoted_string(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+ size_t len;
+
+ i_assert(ctx->data < ctx->end);
+ i_assert(*ctx->data == '"');
+ ctx->data++;
+
+ for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
+ switch (*ctx->data) {
+ case '\0':
+ if (ctx->nul_replacement_str != NULL) {
+ str_append_data(str, start, ctx->data - start);
+ str_append(str, ctx->nul_replacement_str);
+ start = ctx->data + 1;
+ }
+ break;
+ case '"':
+ str_append_data(str, start, ctx->data - start);
+ ctx->data++;
+ return rfc822_skip_lwsp(ctx);
+ case '\n':
+ /* folding whitespace, remove the (CR)LF */
+ len = ctx->data - start;
+ if (len > 0 && start[len-1] == '\r')
+ len--;
+ str_append_data(str, start, len);
+ start = ctx->data + 1;
+ break;
+ case '\\':
+ ctx->data++;
+ if (ctx->data >= ctx->end)
+ return -1;
+
+ if (*ctx->data == '\r' || *ctx->data == '\n' ||
+ *ctx->data == '\0') {
+ /* quoted-pair doesn't allow CR/LF/NUL.
+ They are part of the obs-qp though, so don't
+ return them as error. */
+ ctx->data--;
+ break;
+ }
+ str_append_data(str, start, ctx->data - start - 1);
+ start = ctx->data;
+ break;
+ }
+ }
+
+ /* missing '"' */
+ return -1;
+}
+
+static int
+rfc822_parse_atom_or_dot(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+
+ /*
+ atom = [CFWS] 1*atext [CFWS]
+ atext =
+ ; Any character except controls, SP, and specials.
+
+ The difference between this function and rfc822_parse_dot_atom()
+ is that this doesn't just silently skip over all the whitespace.
+ */
+ for (start = ctx->data; ctx->data < ctx->end; ctx->data++) {
+ if (IS_ATEXT(*ctx->data) || *ctx->data == '.')
+ continue;
+
+ str_append_data(str, start, ctx->data - start);
+ return rfc822_skip_lwsp(ctx);
+ }
+
+ str_append_data(str, start, ctx->data - start);
+ return 0;
+}
+
+int rfc822_parse_phrase(struct rfc822_parser_context *ctx, string_t *str)
+{
+ int ret;
+
+ /*
+ phrase = 1*word / obs-phrase
+ word = atom / quoted-string
+ obs-phrase = word *(word / "." / CFWS)
+ */
+
+ if (ctx->data >= ctx->end)
+ return 0;
+ if (*ctx->data == '.')
+ return -1;
+
+ for (;;) {
+ if (*ctx->data == '"')
+ ret = rfc822_parse_quoted_string(ctx, str);
+ else
+ ret = rfc822_parse_atom_or_dot(ctx, str);
+
+ if (ret <= 0)
+ return ret;
+
+ if (!IS_ATEXT(*ctx->data) && *ctx->data != '"'
+ && *ctx->data != '.')
+ break;
+ str_append_c(str, ' ');
+ }
+ return rfc822_skip_lwsp(ctx);
+}
+
+static int
+rfc822_parse_domain_literal(struct rfc822_parser_context *ctx, string_t *str)
+{
+ const unsigned char *start;
+ size_t len;
+
+ /*
+ domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
+ dcontent = dtext / quoted-pair
+ dtext = NO-WS-CTL / ; Non white space controls
+ %d33-90 / ; The rest of the US-ASCII
+ %d94-126 ; characters not including "[",
+ ; "]", or "\"
+ */
+ i_assert(ctx->data < ctx->end);
+ i_assert(*ctx->data == '[');
+
+ for (start = ctx->data++; ctx->data < ctx->end; ctx->data++) {
+ switch (*ctx->data) {
+ case '\0':
+ if (ctx->nul_replacement_str != NULL) {
+ str_append_data(str, start, ctx->data - start);
+ str_append(str, ctx->nul_replacement_str);
+ start = ctx->data + 1;
+ }
+ break;
+ case '[':
+ /* not allowed */
+ return -1;
+ case ']':
+ str_append_data(str, start, ctx->data - start + 1);
+ ctx->data++;
+ return rfc822_skip_lwsp(ctx);
+ case '\n':
+ /* folding whitespace, remove the (CR)LF */
+ len = ctx->data - start;
+ if (len > 0 && start[len-1] == '\r')
+ len--;
+ str_append_data(str, start, len);
+ start = ctx->data + 1;
+ break;
+ case '\\':
+ /* note: the '\' is preserved in the output */
+ ctx->data++;
+ if (ctx->data >= ctx->end)
+ return -1;
+
+ if (*ctx->data == '\r' || *ctx->data == '\n' ||
+ *ctx->data == '\0') {
+ /* quoted-pair doesn't allow CR/LF/NUL.
+ They are part of the obs-qp though, so don't
+ return them as error. */
+ str_append_data(str, start, ctx->data - start);
+ start = ctx->data;
+ ctx->data--;
+ break;
+ }
+ }
+ }
+
+ /* missing ']' */
+ return -1;
+}
+
+int rfc822_parse_domain(struct rfc822_parser_context *ctx, string_t *str)
+{
+ /*
+ domain = dot-atom / domain-literal / obs-domain
+ domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
+ obs-domain = atom *("." atom)
+ */
+ i_assert(ctx->data < ctx->end);
+ i_assert(*ctx->data == '@');
+ ctx->data++;
+
+ if (rfc822_skip_lwsp(ctx) <= 0)
+ return -1;
+
+ if (*ctx->data == '[')
+ return rfc822_parse_domain_literal(ctx, str);
+ else
+ return rfc822_parse_dot_atom(ctx, str);
+}
+
+int rfc822_parse_content_type(struct rfc822_parser_context *ctx, string_t *str)
+{
+ size_t str_pos_0 = str->used;
+ if (rfc822_skip_lwsp(ctx) <= 0)
+ return -1;
+
+ /* get main type, require at least one byte */
+ if (rfc822_parse_mime_token(ctx, str) <= 0 ||
+ str->used == str_pos_0)
+ return -1;
+
+ /* skip over "/" */
+ if (*ctx->data != '/') {
+ str_truncate(str, str_pos_0);
+ return -1;
+ }
+ ctx->data++;
+ if (rfc822_skip_lwsp(ctx) <= 0) {
+ str_truncate(str, str_pos_0);
+ return -1;
+ }
+ str_append_c(str, '/');
+
+ size_t str_pos = str->used;
+ /* get subtype, require at least one byte,
+ and check the next separator to avoid accepting
+ invalid values. */
+ int ret;
+ if ((ret = rfc822_parse_mime_token(ctx, str)) < 0 ||
+ str->used == str_pos ||
+ (ctx->data != ctx->end && *ctx->data != ';')) {
+ str_truncate(str, str_pos_0);
+ return -1;
+ }
+ return ret;
+}
+
+int rfc822_parse_content_param(struct rfc822_parser_context *ctx,
+ const char **key_r, string_t *value)
+{
+ string_t *key;
+ int ret;
+
+ /* .. := *(";" parameter)
+ parameter := attribute "=" value
+ attribute := token
+ value := token / quoted-string
+ */
+ *key_r = NULL;
+ str_truncate(value, 0);
+
+ if (ctx->data >= ctx->end)
+ return 0;
+ if (*ctx->data != ';')
+ return -1;
+ ctx->data++;
+
+ if (rfc822_skip_lwsp(ctx) <= 0)
+ return -1;
+
+ key = t_str_new(64);
+ if (rfc822_parse_mime_token(ctx, key) <= 0)
+ return -1;
+
+ if (*ctx->data != '=')
+ return -1;
+ ctx->data++;
+
+ if ((ret = rfc822_skip_lwsp(ctx)) <= 0) {
+ /* broken / no value */
+ } else if (*ctx->data == '"') {
+ ret = rfc822_parse_quoted_string(ctx, value);
+ } else if (ctx->data < ctx->end && *ctx->data == '=') {
+ /* workaround for broken input:
+ name==?utf-8?b?...?= */
+ while (ctx->data < ctx->end && *ctx->data != ';' &&
+ *ctx->data != ' ' && *ctx->data != '\t' &&
+ *ctx->data != '\r' && *ctx->data != '\n') {
+ str_append_c(value, *ctx->data);
+ ctx->data++;
+ }
+ } else {
+ ret = rfc822_parse_mime_token(ctx, value);
+ }
+
+ *key_r = str_c(key);
+ return ret < 0 ? -1 : 1;
+}
diff --git a/src/lib-mail/rfc822-parser.h b/src/lib-mail/rfc822-parser.h
new file mode 100644
index 0000000..c001f76
--- /dev/null
+++ b/src/lib-mail/rfc822-parser.h
@@ -0,0 +1,71 @@
+#ifndef RFC822_PARSER_H
+#define RFC822_PARSER_H
+
+#include "unichar.h"
+
+/* This can be used as a common NUL replacement character */
+#define RFC822_NUL_REPLACEMENT_STR UNICODE_REPLACEMENT_CHAR_UTF8
+
+struct rfc822_parser_context {
+ const unsigned char *data, *end;
+ string_t *last_comment;
+
+ /* Replace NUL characters with this string */
+ const char *nul_replacement_str;
+};
+
+#define IS_ATEXT(c) \
+ (rfc822_atext_chars[(int)(unsigned char)(c)] != 0)
+#define IS_ATEXT_NON_TSPECIAL(c) \
+ ((rfc822_atext_chars[(int)(unsigned char)(c)] & 3) != 0)
+extern unsigned char rfc822_atext_chars[256];
+
+/* Parse given data using RFC 822 token parser. */
+void rfc822_parser_init(struct rfc822_parser_context *ctx,
+ const unsigned char *data, size_t size,
+ string_t *last_comment) ATTR_NULL(4);
+static inline void rfc822_parser_deinit(struct rfc822_parser_context *ctx)
+{
+ /* make sure the parsing didn't trigger a bug that caused reading
+ past the end pointer. */
+ i_assert(ctx->data <= ctx->end);
+ /* make sure the parser is no longer accessed */
+ ctx->data = ctx->end = NULL;
+}
+
+/* The functions below return 1 = more data available, 0 = no more data
+ available (but a value might have been returned now), -1 = invalid input.
+
+ LWSP is automatically skipped after value, but not before it. So typically
+ you begin with skipping LWSP and then start using the parse functions. */
+
+/* Parse comment. Assumes parser's data points to '(' */
+int rfc822_skip_comment(struct rfc822_parser_context *ctx);
+/* Skip LWSP if there is any */
+int ATTR_NOWARN_UNUSED_RESULT
+rfc822_skip_lwsp(struct rfc822_parser_context *ctx);
+/* Stop at next non-atext char */
+int rfc822_parse_atom(struct rfc822_parser_context *ctx, string_t *str);
+/* Like parse_atom() but don't stop at '.' */
+int rfc822_parse_dot_atom(struct rfc822_parser_context *ctx, string_t *str);
+/* Like parse_dot_atom() but stops for '/', '?' and '='.
+ Also it doesn't allow LWSP around '.' chars. */
+int rfc822_parse_mime_token(struct rfc822_parser_context *ctx, string_t *str);
+/* "quoted string" */
+int rfc822_parse_quoted_string(struct rfc822_parser_context *ctx,
+ string_t *str);
+/* atom or quoted-string */
+int rfc822_parse_phrase(struct rfc822_parser_context *ctx, string_t *str);
+/* dot-atom / domain-literal */
+int rfc822_parse_domain(struct rfc822_parser_context *ctx, string_t *str);
+
+/* Parse Content-Type header's type/subtype. */
+int rfc822_parse_content_type(struct rfc822_parser_context *ctx, string_t *str);
+/* For Content-Type style parameter parsing. Expect ";" key "=" value.
+ value is unescaped if needed. The returned key is allocated from data
+ stack. The value string is truncated for each call. Returns 1 = key/value
+ set, 0 = no more data, -1 = invalid input. */
+int rfc822_parse_content_param(struct rfc822_parser_context *ctx,
+ const char **key_r, string_t *value);
+
+#endif
diff --git a/src/lib-mail/test-istream-attachment.c b/src/lib-mail/test-istream-attachment.c
new file mode 100644
index 0000000..ac3096e
--- /dev/null
+++ b/src/lib-mail/test-istream-attachment.c
@@ -0,0 +1,486 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "sha1.h"
+#include "hash-format.h"
+#include "safe-mkstemp.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "istream-attachment-extractor.h"
+#include "istream-attachment-connector.h"
+#include "ostream.h"
+#include "test-common.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#define BINARY_TEXT_LONG "we have\ra lot \nof \0binary stuff in here\n" \
+"b adjig sadjg jasidgjiaehga3wht8a3w8ghxjc dsgad hasdghsd gasd ds" \
+"jdsoga sjdga0w3tjhawjgsertniq3n5oqerjqw2r89q23h awhrqh835r8a"
+#define BINARY_TEXT_LONG_BASE64 \
+"d2UgaGF2ZQ1hIGxvdCAKb2YgAGJpbmFyeSBzdHVmZiBpbiBoZXJlCmIgYWRqaWcgc2FkamcgamFz\r\n" \
+"aWRnamlhZWhnYTN3aHQ4YTN3OGdoeGpjIGRzZ2FkIGhhc2RnaHNkIGdhc2QgZHNqZHNvZ2Egc2pk\r\n" \
+"Z2EwdzN0amhhd2pnc2VydG5pcTNuNW9xZXJqcXcycjg5cTIzaCBhd2hycWg4MzVyOGE="
+
+#define BINARY_TEXT_SHORT "eh"
+#define BINARY_TEXT_SHORT_BASE64 "ZWg="
+
+static const char mail_input[] =
+"MIME-Version: 1.0\r\n"
+"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
+"\r\n"
+"mime header\r\n"
+"\r\n--bound\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+BINARY_TEXT_LONG_BASE64
+"\r\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"\r\n"
+BINARY_TEXT_SHORT_BASE64
+"\r\n--bound--\r\n";
+
+static const char mail_output[] =
+"MIME-Version: 1.0\r\n"
+"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
+"\r\n"
+"mime header\r\n"
+"\r\n--bound\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+"\r\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"\r\n"
+"\r\n--bound--\r\n";
+
+static const char *mail_broken_input_body_prefix =
+"MIME-Version: 1.0\r\n"
+"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
+"\r\n"
+"--bound\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n";
+
+static const char *mail_broken_input_bodies[] = {
+ /* broken base64 input */
+ "Zm9vCg=\n",
+ "Zm9vCg\n",
+ "Zm9vC\n",
+ /* extra whitespace */
+ "Zm9v\n Zm9v\n",
+ "Zm9v \nZm9v\n",
+ /* mixed LF vs CRLFs */
+ "Zm9vYmFy\r\nZm9vYmFy\n",
+ "Zm9vYmFy\nZm9vYmFy\r\n",
+ /* line length increases */
+ "Zm9v\nZm9vYmFy\n",
+ "Zm9v\nZm9vCg==",
+ "Zm9v\nZm9vYgo="
+};
+
+static const char *mail_nonbroken_input_bodies[] = {
+ /* suffixes with explicit '=' end */
+ "Zm9vCg==",
+ "Zm9vCg==\n",
+ "Zm9vCg==\r\n",
+ "Zm9vCg==\nfoo\n",
+ "Zm9vCg==\r\nfoo\n",
+ "Zm9vCg== \t\t\n\n",
+ /* suffixes with shorter line length */
+ "Zm9vYmFy\nZm9v\n",
+ "Zm9vYmFy\r\nZm9v\r\n",
+ "Zm9vYmFy\nZm9v\nfoo\n",
+ "Zm9vYmFy\r\nZm9v\r\nfoo\n",
+ "Zm9vYmFy\nZm9v\n \t\t\n\n",
+ /* suffixes with empty line */
+ "Zm9v\n\n",
+ "Zm9v\r\n\r\n",
+ "Zm9v\n\nfoo\n"
+ "Zm9v\r\n\nfoo\n"
+ "Zm9v\r\n\r\nfoo\n"
+#if 0
+ /* the whitespace here could be handled as suffixes, but for now
+ they're not: */
+ "Zm9v ",
+ "Zm9v \n"
+#endif
+};
+
+struct attachment {
+ size_t buffer_offset;
+ uoff_t start_offset;
+ uoff_t encoded_size, decoded_size;
+ unsigned int base64_blocks_per_line;
+};
+
+static buffer_t *attachment_data;
+static ARRAY(struct attachment) attachments;
+
+static int test_open_temp_fd(void *context ATTR_UNUSED)
+{
+ string_t *str = t_str_new(128);
+ int fd;
+
+ str_append(str, "/tmp/dovecot-test.");
+ fd = safe_mkstemp(str, 0600, (uid_t)-1, (gid_t)-1);
+ if (fd == -1)
+ i_fatal("safe_mkstemp(%s) failed: %m", str_c(str));
+ i_unlink(str_c(str));
+ return fd;
+}
+
+static int test_open_attachment_ostream(struct istream_attachment_info *info,
+ struct ostream **output_r,
+ const char **error_r ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ struct attachment *a;
+
+ if (attachment_data == NULL)
+ attachment_data = buffer_create_dynamic(default_pool, 1024);
+ if (!array_is_created(&attachments))
+ i_array_init(&attachments, 8);
+ a = array_append_space(&attachments);
+ a->buffer_offset = attachment_data->used;
+ a->start_offset = info->start_offset;
+ a->encoded_size = info->encoded_size;
+ a->base64_blocks_per_line = info->base64_blocks_per_line;
+ test_assert(strlen(info->hash) == 160/8*2); /* sha1 size */
+
+ *output_r = o_stream_create_buffer(attachment_data);
+ if (o_stream_seek(*output_r, a->buffer_offset) < 0)
+ i_unreached();
+ return 0;
+}
+
+static int
+test_open_attachment_ostream_error(struct istream_attachment_info *info ATTR_UNUSED,
+ struct ostream **output_r ATTR_UNUSED,
+ const char **error_r,
+ void *context ATTR_UNUSED)
+{
+ *error_r = "test open error";
+ return -1;
+}
+
+static int test_close_attachment_ostream(struct ostream *output, bool success,
+ const char **error_r ATTR_UNUSED,
+ void *context ATTR_UNUSED)
+{
+ struct attachment *a;
+
+ i_assert(success);
+
+ a = array_back_modifiable(&attachments);
+ a->decoded_size = output->offset - a->buffer_offset;
+
+ if (o_stream_finish(output) < 0)
+ i_unreached();
+ o_stream_destroy(&output);
+ return 0;
+}
+
+static int
+test_close_attachment_ostream_error(struct ostream *output,
+ bool success, const char **error,
+ void *context ATTR_UNUSED)
+{
+ if (success)
+ *error = "test output error";
+ o_stream_abort(output);
+ o_stream_destroy(&output);
+ return -1;
+}
+
+static struct istream *
+test_build_original_istream(struct istream *base_input, uoff_t msg_size)
+{
+ struct istream_attachment_connector *conn;
+ const unsigned char *data = attachment_data->data;
+ const struct attachment *a;
+ struct istream *input;
+ uoff_t data_size = attachment_data->used;
+ const char *error;
+
+ conn = istream_attachment_connector_begin(base_input, msg_size);
+ array_foreach(&attachments, a) {
+ input = i_stream_create_from_data(data, a->decoded_size);
+ if (istream_attachment_connector_add(conn, input,
+ a->start_offset, a->encoded_size,
+ a->base64_blocks_per_line, TRUE, &error) < 0)
+ i_unreached();
+ i_stream_unref(&input);
+
+ i_assert(a->decoded_size <= data_size);
+ data += a->decoded_size;
+ data_size -= a->decoded_size;
+ }
+ i_assert(data_size == 0);
+ return istream_attachment_connector_finish(&conn);
+}
+
+static void
+get_istream_attachment_settings(struct istream_attachment_settings *set_r)
+{
+ const char *error;
+
+ i_zero(set_r);
+ set_r->min_size = 1;
+ set_r->drain_parent_input = TRUE;
+ set_r->open_temp_fd = test_open_temp_fd;
+ set_r->open_attachment_ostream = test_open_attachment_ostream;
+ set_r->close_attachment_ostream= test_close_attachment_ostream;
+ if (hash_format_init("%{sha1}", &set_r->hash_format, &error) < 0)
+ i_unreached();
+}
+
+static int test_input_stream(struct istream *file_input)
+{
+ struct istream_attachment_settings set;
+ struct istream *input, *input2;
+ const unsigned char *data;
+ size_t size;
+ struct sha1_ctxt hash;
+ uoff_t msg_size, orig_msg_size;
+ buffer_t *base_buf;
+ unsigned char hash_file[SHA1_RESULTLEN], hash_attached[SHA1_RESULTLEN];
+ int ret = 0;
+
+ /* get hash when directly reading input */
+ input = i_stream_create_crlf(file_input);
+ sha1_init(&hash);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ sha1_loop(&hash, data, size);
+ i_stream_skip(input, size);
+ }
+ sha1_result(&hash, hash_file);
+ msg_size = orig_msg_size = input->v_offset;
+ i_stream_unref(&input);
+
+ /* read through attachment extractor */
+ get_istream_attachment_settings(&set);
+
+ i_stream_seek(file_input, 0);
+ input = i_stream_create_crlf(file_input);
+ input2 = i_stream_create_attachment_extractor(input, &set, NULL);
+ i_stream_unref(&input);
+ base_buf = buffer_create_dynamic(default_pool, 1024);
+ while (i_stream_read_more(input2, &data, &size) > 0) {
+ buffer_append(base_buf, data, size);
+ i_stream_skip(input2, size);
+ }
+ i_stream_unref(&input2);
+
+ /* rebuild the original stream and see if the hash matches */
+ for (unsigned int i = 0; i < 2; i++) {
+ input2 = i_stream_create_from_data(base_buf->data, base_buf->used);
+ input = test_build_original_istream(input2, msg_size);
+ i_stream_unref(&input2);
+
+ sha1_init(&hash);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ sha1_loop(&hash, data, size);
+ i_stream_skip(input, size);
+ }
+ test_assert_idx(input->eof && input->stream_errno == 0, i);
+ sha1_result(&hash, hash_attached);
+ i_stream_unref(&input);
+
+ if (memcmp(hash_file, hash_attached, SHA1_RESULTLEN) != 0)
+ ret = -1;
+
+ /* try again without knowing the message's size */
+ msg_size = UOFF_T_MAX;
+ }
+
+ /* try with a wrong message size */
+ for (int i = 0; i < 2; i++) {
+ input2 = i_stream_create_from_data(base_buf->data, base_buf->used);
+ input = test_build_original_istream(input2,
+ i == 0 ? orig_msg_size + 1 : orig_msg_size - 1);
+ i_stream_unref(&input2);
+ while (i_stream_read_more(input, &data, &size) > 0)
+ i_stream_skip(input, size);
+ test_assert(input->stream_errno == (i == 0 ? EPIPE : EINVAL));
+ i_stream_unref(&input);
+ }
+
+ buffer_free(&base_buf);
+ buffer_free(&attachment_data);
+ if (array_is_created(&attachments))
+ array_free(&attachments);
+ return ret;
+}
+
+static void test_istream_attachment(void)
+{
+ struct istream_attachment_settings set;
+ struct istream *datainput, *input;
+ const unsigned char *data;
+ size_t i, size;
+ int ret;
+
+ test_begin("istream attachment");
+ datainput = test_istream_create_data(mail_input, sizeof(mail_input));
+ test_istream_set_allow_eof(datainput, FALSE);
+
+ get_istream_attachment_settings(&set);
+ input = i_stream_create_attachment_extractor(datainput, &set, NULL);
+
+ for (i = 1; i <= sizeof(mail_input); i++) {
+ test_istream_set_size(datainput, i);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert(ret == 0);
+ }
+ test_istream_set_allow_eof(datainput, TRUE);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert(ret == -1);
+
+ data = i_stream_get_data(input, &size);
+ test_assert(size == sizeof(mail_output) &&
+ memcmp(data, mail_output, size) == 0);
+
+ data = attachment_data->data;
+ test_assert(attachment_data->used ==
+ sizeof(BINARY_TEXT_LONG)-1 + strlen(BINARY_TEXT_SHORT));
+ test_assert(memcmp(data, BINARY_TEXT_LONG, sizeof(BINARY_TEXT_LONG)-1) == 0);
+ test_assert(memcmp(data + sizeof(BINARY_TEXT_LONG)-1,
+ BINARY_TEXT_SHORT, strlen(BINARY_TEXT_SHORT)) == 0);
+ i_stream_unref(&input);
+ i_stream_unref(&datainput);
+
+ buffer_free(&attachment_data);
+ if (array_is_created(&attachments))
+ array_free(&attachments);
+ test_end();
+}
+
+static bool test_istream_attachment_extractor_one(const char *body, int err_type)
+{
+ const size_t prefix_len = strlen(mail_broken_input_body_prefix);
+ struct istream_attachment_settings set;
+ struct istream *datainput, *input;
+ char *mail_text;
+ const unsigned char *data;
+ size_t size;
+ int ret;
+ bool unchanged;
+
+ mail_text = i_strconcat(mail_broken_input_body_prefix, body, NULL);
+ datainput = test_istream_create_data(mail_text, strlen(mail_text));
+
+ get_istream_attachment_settings(&set);
+ if (err_type == 1)
+ set.open_attachment_ostream = test_open_attachment_ostream_error;
+ else if (err_type == 2)
+ set.close_attachment_ostream = test_close_attachment_ostream_error;
+ input = i_stream_create_attachment_extractor(datainput, &set, NULL);
+
+ while ((ret = i_stream_read(input)) > 0) ;
+ if (err_type != 0) {
+ test_assert(ret == -1 && input->stream_errno == EIO);
+ unchanged = FALSE;
+ goto cleanup;
+ }
+ test_assert(ret == -1 && input->stream_errno == 0);
+
+ data = i_stream_get_data(input, &size);
+ i_assert(size >= prefix_len &&
+ memcmp(data, mail_broken_input_body_prefix, prefix_len) == 0);
+ data += prefix_len;
+ size -= prefix_len;
+
+ i_assert(attachment_data != NULL);
+ unchanged = attachment_data->used <= strlen(body) &&
+ memcmp(attachment_data->data, body, attachment_data->used) == 0 &&
+ strlen(body) - attachment_data->used == size &&
+ memcmp(data, body + attachment_data->used, size) == 0;
+
+cleanup:
+ buffer_free(&attachment_data);
+ if (array_is_created(&attachments))
+ array_free(&attachments);
+
+ i_stream_unref(&input);
+ i_stream_unref(&datainput);
+ i_free(mail_text);
+ return unchanged;
+}
+
+static void test_istream_attachment_extractor(void)
+{
+ unsigned int i;
+
+ test_begin("istream attachment extractor");
+ for (i = 0; i < N_ELEMENTS(mail_broken_input_bodies); i++)
+ test_assert(test_istream_attachment_extractor_one(mail_broken_input_bodies[i], 0));
+ for (i = 0; i < N_ELEMENTS(mail_nonbroken_input_bodies); i++)
+ test_assert(!test_istream_attachment_extractor_one(mail_nonbroken_input_bodies[i], 0));
+ test_end();
+}
+
+static void test_istream_attachment_extractor_error(void)
+{
+ unsigned int i;
+
+ test_begin("istream attachment extractor error");
+ for (int err_type = 1; err_type <= 2; err_type++) {
+ for (i = 0; i < N_ELEMENTS(mail_broken_input_bodies); i++)
+ test_istream_attachment_extractor_one(mail_broken_input_bodies[i], err_type);
+ for (i = 0; i < N_ELEMENTS(mail_nonbroken_input_bodies); i++)
+ test_istream_attachment_extractor_one(mail_nonbroken_input_bodies[i], err_type);
+ }
+ test_end();
+}
+
+static void test_istream_attachment_connector(void)
+{
+ struct istream *input;
+
+ test_begin("istream attachment connector");
+ input = i_stream_create_from_data(mail_input, sizeof(mail_input));
+ test_assert(test_input_stream(input) == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static int test_input_file(const char *path)
+{
+ struct istream *file_input;
+ int ret = 0;
+
+ lib_init();
+
+ file_input = i_stream_create_file(path, 64);
+ if (test_input_stream(file_input) < 0) {
+ fprintf(stderr, "istream-attachment-extractor: mismatch on file %s\n",
+ path);
+ ret = -1;
+ }
+ i_stream_unref(&file_input);
+
+ lib_deinit();
+ return ret;
+}
+
+int main(int argc, char *argv[])
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_attachment,
+ test_istream_attachment_extractor,
+ test_istream_attachment_extractor_error,
+ test_istream_attachment_connector,
+ NULL
+ };
+ if (argc > 1)
+ return test_input_file(argv[1]) < 0 ? 1 : 0;
+ else
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-istream-binary-converter.c b/src/lib-mail/test-istream-binary-converter.c
new file mode 100644
index 0000000..c32dcb0
--- /dev/null
+++ b/src/lib-mail/test-istream-binary-converter.c
@@ -0,0 +1,215 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base64.h"
+#include "buffer.h"
+#include "str.h"
+#include "sha1.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "istream-binary-converter.h"
+#include "test-common.h"
+
+#include <stdio.h>
+
+#define BINARY_TEXT_LONG "we have\ra lot \nof \0binary stuff in here\n" \
+"b adjig sadjg jasidgjiaehga3wht8a3w8ghxjc dsgad hasdghsd gasd ds" \
+"jdsoga sjdga0w3tjhawjgsertniq3n5oqerjqw2r89q23h awhrqh835r8a"
+#define BINARY_TEXT_LONG_BASE64 \
+"d2UgaGF2ZQ1hIGxvdCAKb2YgAGJpbmFyeSBzdHVmZiBpbiBoZXJlCmIgYWRqaWcgc2FkamcgamFz\r\n" \
+"aWRnamlhZWhnYTN3aHQ4YTN3OGdoeGpjIGRzZ2FkIGhhc2RnaHNkIGdhc2QgZHNqZHNvZ2Egc2pk\r\n" \
+"Z2EwdzN0amhhd2pnc2VydG5pcTNuNW9xZXJqcXcycjg5cTIzaCBhd2hycWg4MzVyOGE="
+
+#define BINARY_TEXT_SHORT "eh"
+#define BINARY_TEXT_SHORT_BASE64 "ZWg="
+
+static const char mail_input_mime[] =
+"MIME-Version: 1.0\r\n"
+"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
+"\r\n"
+"mime header\r\n"
+"\r\n--bound\r\n"
+"Content-Transfer-Encoding: binary\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+BINARY_TEXT_LONG
+"\r\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"Content-Transfer-Encoding: binary\r\n"
+"\r\n"
+BINARY_TEXT_SHORT
+"\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+"hello world\r\n"
+"\r\n--bound--\r\n";
+
+static const char mail_output_mime[] =
+"MIME-Version: 1.0\r\n"
+"Content-Type: multipart/alternative;\r\n boundary=\"bound\"\r\n"
+"\r\n"
+"mime header\r\n"
+"\r\n--bound\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+BINARY_TEXT_LONG_BASE64
+"\r\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"\r\n"
+BINARY_TEXT_SHORT_BASE64
+"\n--bound\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+"hello world\r\n"
+"\r\n--bound--\r\n";
+
+static const char mail_input_root_hdr[] =
+"MIME-Version: 1.0\r\n"
+"Content-Transfer-Encoding: binary\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n";
+
+static const char mail_output_root_hdr[] =
+"MIME-Version: 1.0\r\n"
+"Content-Transfer-Encoding: base64\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n";
+
+static const char mail_root_nonbinary[] =
+"MIME-Version: 1.0\r\n"
+"Content-Type: text/plain\r\n"
+"\r\n"
+"hello\n\n";
+
+static void
+test_istream_binary_converter_test(const char *mail_input, unsigned int mail_input_len,
+ const char *mail_output, unsigned int mail_output_len,
+ unsigned int idx)
+{
+ struct istream *datainput, *input;
+ const unsigned char *data;
+ size_t i, size;
+ int ret;
+
+ datainput = test_istream_create_data(mail_input, mail_input_len);
+ test_istream_set_allow_eof(datainput, FALSE);
+ input = i_stream_create_binary_converter(datainput);
+
+ for (i = 1; i <= mail_input_len; i++) {
+ test_istream_set_size(datainput, i);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert_idx(ret == 0, idx);
+ }
+ test_istream_set_allow_eof(datainput, TRUE);
+ while ((ret = i_stream_read(input)) > 0) ;
+ test_assert_idx(ret == -1, idx);
+
+ data = i_stream_get_data(input, &size);
+ test_assert_idx(size == mail_output_len &&
+ memcmp(data, mail_output, size) == 0, idx);
+ i_stream_unref(&input);
+ i_stream_unref(&datainput);
+}
+
+static void test_istream_binary_converter_mime(void)
+{
+ test_begin("istream binary converter in mime parts");
+ test_istream_binary_converter_test(mail_input_mime, sizeof(mail_input_mime)-1,
+ mail_output_mime, sizeof(mail_output_mime)-1, 0);
+ test_end();
+}
+
+static void test_istream_binary_converter_root(void)
+{
+ buffer_t *inbuf = t_buffer_create(512);
+ buffer_t *outbuf = t_buffer_create(512);
+ const char *const suffixes[] = { "\n", "\r\n", "\n\r\n\n\n" };
+ unsigned int i;
+ unsigned int input_hdr_len = sizeof(mail_input_root_hdr)-1;
+
+ test_begin("istream binary converter in root");
+ buffer_append(inbuf, mail_input_root_hdr, input_hdr_len);
+ buffer_append(outbuf, mail_output_root_hdr, sizeof(mail_output_root_hdr)-1);
+ for (i = 0; i < N_ELEMENTS(suffixes); i++) {
+ buffer_set_used_size(inbuf, input_hdr_len);
+ buffer_set_used_size(outbuf, sizeof(mail_output_root_hdr)-1);
+ buffer_append(inbuf, BINARY_TEXT_SHORT, sizeof(BINARY_TEXT_SHORT)-1);
+ buffer_append(inbuf, suffixes[i], strlen(suffixes[i]));
+ base64_encode(CONST_PTR_OFFSET(inbuf->data, input_hdr_len),
+ inbuf->used - input_hdr_len, outbuf);
+ test_istream_binary_converter_test(inbuf->data, inbuf->used,
+ outbuf->data, outbuf->used, i);
+ }
+ test_end();
+}
+
+static void test_istream_binary_converter_root_nonbinary(void)
+{
+ test_begin("istream binary converter in root having non-binary");
+ test_istream_binary_converter_test(mail_root_nonbinary, sizeof(mail_root_nonbinary)-1,
+ mail_root_nonbinary, sizeof(mail_root_nonbinary)-1, 0);
+ test_end();
+}
+
+static int test_input_file(const char *path)
+{
+ struct istream *file_input, *input, *input2;
+ const unsigned char *data;
+ size_t size;
+ struct sha1_ctxt hash;
+ unsigned char hash_file[SHA1_RESULTLEN], hash_converter[SHA1_RESULTLEN];
+ int ret = 0;
+
+ lib_init();
+
+ file_input = i_stream_create_file(path, 64);
+
+ /* get hash when directly reading input */
+ input = i_stream_create_crlf(file_input);
+ sha1_init(&hash);
+ while (i_stream_read_more(input, &data, &size) > 0) {
+ sha1_loop(&hash, data, size);
+ i_stream_skip(input, size);
+ }
+ sha1_result(&hash, hash_file);
+ i_stream_unref(&input);
+
+ /* get hash when going through converter */
+ i_stream_seek(file_input, 0);
+ input = i_stream_create_crlf(file_input);
+ input2 = i_stream_create_binary_converter(input);
+ sha1_init(&hash);
+ while (i_stream_read_more(input2, &data, &size) > 0) {
+ sha1_loop(&hash, data, size);
+ i_stream_skip(input2, size);
+ }
+ sha1_result(&hash, hash_converter);
+ i_stream_unref(&input2);
+ i_stream_unref(&input);
+
+ if (memcmp(hash_file, hash_converter, SHA1_RESULTLEN) != 0) {
+ fprintf(stderr, "istream-binary-converter: mismatch on file %s\n",
+ path);
+ ret = 1;
+ }
+
+ i_stream_unref(&file_input);
+ lib_deinit();
+ return ret;
+}
+
+int main(int argc, char *argv[])
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_binary_converter_mime,
+ test_istream_binary_converter_root,
+ test_istream_binary_converter_root_nonbinary,
+ NULL
+ };
+ if (argc > 1)
+ return test_input_file(argv[1]);
+ else
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-istream-dot.c b/src/lib-mail/test-istream-dot.c
new file mode 100644
index 0000000..b77f364
--- /dev/null
+++ b/src/lib-mail/test-istream-dot.c
@@ -0,0 +1,230 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "istream-dot.h"
+#include "test-common.h"
+
+struct dot_test {
+ const char *input;
+ const char *output;
+ const char *parent_input;
+};
+
+static void test_istream_dot_one(const struct dot_test *test,
+ bool send_last_lf, bool test_bufsize)
+{
+ struct istream *test_input, *input;
+ const unsigned char *data;
+ size_t size;
+ unsigned int i;
+ size_t outsize, input_len, output_len;
+ string_t *str;
+ uoff_t offset;
+ int ret;
+
+ test_input = test_istream_create(test->input);
+ input = i_stream_create_dot(test_input, send_last_lf);
+
+ input_len = strlen(test->input);
+ output_len = strlen(test->output);
+ if (!send_last_lf &&
+ (test->input[input_len-1] == '\n' ||
+ strstr(test->input, "\n.\n") != NULL ||
+ strstr(test->input, "\n.\r\n") != NULL)) {
+ if (output_len > 0 &&
+ test->output[output_len-1] == '\n') {
+ output_len--;
+ if (output_len > 0 &&
+ test->output[output_len-1] == '\r')
+ output_len--;
+ }
+ }
+
+ str = t_str_new(256);
+ if (!test_bufsize) {
+ outsize = 1; i = 0;
+ i_stream_set_max_buffer_size(input, outsize);
+ test_istream_set_size(test_input, 1);
+ while ((ret = i_stream_read(input)) != -1) {
+ switch (ret) {
+ case -2:
+ i_stream_set_max_buffer_size(input, ++outsize);
+ offset = test_input->v_offset;
+ /* seek one byte backwards so stream gets
+ reset */
+ i_stream_seek(test_input, offset - 1);
+ /* go back to original position */
+ test_istream_set_size(test_input, offset);
+ i_stream_skip(test_input, 1);
+ /* and finally allow reading one more byte */
+ test_istream_set_size(test_input, offset + 1);
+ break;
+ case 0:
+ test_istream_set_size(test_input, ++i);
+ break;
+ default:
+ test_assert(ret > 0);
+
+ data = i_stream_get_data(input, &size);
+ str_append_data(str, data, size);
+ i_stream_skip(input, size);
+ }
+ }
+ test_istream_set_size(test_input, input_len);
+ (void)i_stream_read(test_input);
+ } else {
+ test_istream_set_size(test_input, input_len);
+ size = 0;
+ for (i = 1; i < output_len; i++) {
+ i_stream_set_max_buffer_size(input, i);
+ test_assert(i_stream_read(input) == 1);
+ test_assert(i_stream_read(input) == -2);
+ data = i_stream_get_data(input, &size);
+ test_assert(memcmp(data, test->output, size) == 0);
+ }
+ i_stream_set_max_buffer_size(input, i+2);
+ if (size < output_len)
+ test_assert(i_stream_read(input) == 1);
+ test_assert(i_stream_read(input) == -1);
+
+ data = i_stream_get_data(input, &size);
+ if (size > 0)
+ str_append_data(str, data, size);
+ }
+ test_assert(input->stream_errno == 0);
+ test_assert(str_len(str) == output_len);
+ test_assert(memcmp(str_data(str), test->output, output_len) == 0);
+
+ /* read the data after the '.' line and verify it's still there */
+ i_stream_set_max_buffer_size(test_input, SIZE_MAX);
+ (void)i_stream_read(test_input);
+ data = i_stream_get_data(test_input, &size);
+ test_assert(size == strlen(test->parent_input));
+ if (size > 0)
+ test_assert(memcmp(data, test->parent_input, size) == 0);
+
+ i_stream_unref(&test_input);
+ i_stream_unref(&input);
+}
+
+static void test_istream_dot_error(const char *input_str, bool test_bufsize)
+{
+ struct istream *test_input, *input;
+ unsigned int i;
+ size_t outsize, input_len;
+ uoff_t offset;
+ int ret;
+
+ test_input = test_istream_create(input_str);
+ input = i_stream_create_dot(test_input, FALSE);
+
+ input_len = strlen(input_str);
+
+ if (!test_bufsize) {
+ outsize = 1; i = 0;
+ i_stream_set_max_buffer_size(input, outsize);
+ test_istream_set_size(test_input, 1);
+ while ((ret = i_stream_read(input)) != -1) {
+ switch (ret) {
+ case -2:
+ i_stream_set_max_buffer_size(input, ++outsize);
+ offset = test_input->v_offset;
+ /* seek one byte backwards so stream gets
+ reset */
+ i_stream_seek(test_input, offset - 1);
+ /* go back to original position */
+ test_istream_set_size(test_input, offset);
+ i_stream_skip(test_input, 1);
+ /* and finally allow reading one more byte */
+ test_istream_set_size(test_input, offset + 1);
+ break;
+ case 0:
+ test_istream_set_size(test_input, ++i);
+ break;
+ default:
+ test_assert(ret > 0);
+ }
+ }
+ test_istream_set_size(test_input, input_len);
+ (void)i_stream_read(test_input);
+ } else {
+ test_istream_set_size(test_input, input_len);
+ for (i = 1; i <= input_len; i++) {
+ i_stream_set_max_buffer_size(input, i);
+ (void)i_stream_read(input);
+ (void)i_stream_read(input);
+ }
+ i_stream_set_max_buffer_size(input, i+1);
+ (void)i_stream_read(input);
+ }
+ test_assert(input->stream_errno == EPIPE);
+
+ i_stream_unref(&test_input);
+ i_stream_unref(&input);
+}
+
+static void test_istream_dot(void)
+{
+ static struct dot_test tests[] = {
+ { "..foo\n..\n.foo\n.\nfoo", ".foo\n.\nfoo\n", "foo" },
+ { "..foo\r\n..\r\n.foo\r\n.\r\nfoo", ".foo\r\n.\r\nfoo\r\n", "foo" },
+ { "\r.\r\n.\r\n", "\r.\r\n", "" },
+ { "\n\r.\r\r\n.\r\n", "\n\r.\r\r\n", "" },
+ { "\r\n.\rfoo\n.\n", "\r\n\rfoo\n", "" },
+ { "\r\n.\r\n", "\r\n", "" },
+ { "\n.\r\n", "\n", "" },
+ { "\n.\n", "\n", "" },
+ { ".\r\n", "", "" },
+ { ".\n", "", "" }
+ };
+ static const char *error_tests[] = {
+ "",
+ ".",
+ "..",
+ ".\r",
+ ".\rx",
+ "..\r\n",
+ "\r.",
+ "\r.\r",
+ "\r.\rx",
+ "\r.\r\n",
+ "\r.\n",
+ "\r..\n",
+ "\r\n",
+ "\r\n.",
+ "\r\n.\r",
+ "\r\n.\rx",
+ "\r\n.\rx\n",
+ "\r\n..\r\n",
+ "\n",
+ "\n.",
+ "\n.\r",
+ "\n.\rx",
+ "\n..\r\n"
+ };
+ unsigned int i;
+
+ test_begin("dot istream");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_istream_dot_one(&tests[i], TRUE, TRUE);
+ test_istream_dot_one(&tests[i], TRUE, FALSE);
+ test_istream_dot_one(&tests[i], FALSE, TRUE);
+ test_istream_dot_one(&tests[i], FALSE, FALSE);
+ }
+ for (i = 0; i < N_ELEMENTS(error_tests); i++) {
+ test_istream_dot_error(error_tests[i], FALSE);
+ test_istream_dot_error(error_tests[i], TRUE);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_dot,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-istream-header-filter.c b/src/lib-mail/test-istream-header-filter.c
new file mode 100644
index 0000000..f982c02
--- /dev/null
+++ b/src/lib-mail/test-istream-header-filter.c
@@ -0,0 +1,701 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "message-header-parser.h"
+#include "istream-header-filter.h"
+#include "test-common.h"
+
+struct run_ctx {
+ header_filter_callback *callback;
+ unsigned int callback_call_count;
+ bool null_hdr_seen;
+ bool eoh_seen;
+ bool callback_called;
+};
+
+static void run_callback(struct header_filter_istream *input,
+ struct message_header_line *hdr,
+ bool *matched, struct run_ctx *ctx)
+{
+ i_assert(!ctx->null_hdr_seen);
+
+ ctx->callback_call_count++;
+ if (hdr == NULL)
+ ctx->null_hdr_seen = TRUE;
+ else {
+ i_assert(!ctx->eoh_seen);
+ if (hdr->eoh)
+ ctx->eoh_seen = TRUE;
+ }
+ if (ctx->callback != NULL)
+ ctx->callback(input, hdr, matched, NULL);
+ ctx->callback_called = TRUE;
+}
+
+static inline void
+test_istream_run_prep(struct run_ctx *run_ctx,
+ header_filter_callback *callback)
+{
+ i_zero(run_ctx);
+ run_ctx->callback = callback;
+ run_ctx->null_hdr_seen = FALSE;
+ run_ctx->eoh_seen = FALSE;
+ run_ctx->callback_called = FALSE;
+}
+
+static void
+test_istream_run_check(struct run_ctx *run_ctx,
+ struct istream *filter,
+ const char *output,
+ enum header_filter_flags flags,
+ bool first,
+ size_t *size_r)
+{
+ const unsigned char *data;
+ const struct stat *st;
+
+ if (first)
+ test_assert(run_ctx->null_hdr_seen);
+ else
+ test_assert(run_ctx->null_hdr_seen == run_ctx->callback_called);
+
+ if (first && ((flags & HEADER_FILTER_ADD_MISSING_EOH) != 0))
+ test_assert(run_ctx->eoh_seen);
+
+ data = i_stream_get_data(filter, size_r);
+ test_assert(*size_r == strlen(output) &&
+ memcmp(data, output, *size_r) == 0);
+
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == *size_r);
+
+ /* make sure buffer doesn't change when returning -1 */
+ i_stream_skip(filter, 1);
+ test_assert(i_stream_read(filter) == -1);
+ test_assert(memcmp(data, output, *size_r) == 0);
+}
+
+static void
+test_istream_run(struct istream *test_istream,
+ unsigned int input_len, const char *output,
+ enum header_filter_flags flags,
+ header_filter_callback *callback)
+{
+ struct run_ctx run_ctx;
+ struct istream *filter;
+ unsigned int i, orig_callback_call_count;
+ size_t size;
+
+ test_istream_run_prep(&run_ctx, callback);
+
+ filter = i_stream_create_header_filter(test_istream, flags, NULL, 0,
+ run_callback, &run_ctx);
+
+ for (i = 1; i < input_len; i++) {
+ test_istream_set_size(test_istream, i);
+ test_assert(i_stream_read(filter) >= 0);
+ }
+ test_istream_set_size(test_istream, input_len);
+ test_assert(i_stream_read(filter) > 0);
+ test_assert(i_stream_read(filter) == -1);
+
+ test_istream_run_check(&run_ctx, filter, output, flags, TRUE, &size);
+ orig_callback_call_count = run_ctx.callback_call_count;
+
+ /* run again to make sure it's still correct the second time */
+ test_istream_run_prep(&run_ctx, callback);
+
+ i_stream_skip(filter, size);
+ i_stream_seek(filter, 0);
+ while (i_stream_read(filter) > 0) ;
+ test_istream_run_check(&run_ctx, filter, output, flags, FALSE, &size);
+ test_assert(run_ctx.callback_call_count == 0 ||
+ run_ctx.callback_call_count == orig_callback_call_count);
+
+ i_stream_unref(&filter);
+}
+
+static void ATTR_NULL(3)
+filter_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched, void *context ATTR_UNUSED)
+{
+ if (hdr != NULL && (hdr->name_offset == 0 ||
+ strcmp(hdr->name, "X-Drop") == 0)) {
+ /* drop 1) first header, 2) X-Drop header */
+ *matched = TRUE;
+ }
+}
+
+static void test_istream_filter(void)
+{
+ static const char *exclude_headers[] = { "Subject", "To" };
+ const char *input = "From: foo\nFrom: abc\nTo: bar\nSubject: plop\nX-Drop: 1\n\nhello world\n";
+ const char *output = "From: abc\n\nhello world\n";
+ struct istream *istream, *filter, *filter2;
+ unsigned int i;
+ size_t input_len = strlen(input);
+ size_t output_len = strlen(output);
+ const unsigned char *data;
+ const struct stat *st;
+ size_t size;
+
+ test_begin("i_stream_create_header_filter: exclude");
+ istream = test_istream_create(input);
+ filter = i_stream_create_header_filter(istream,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR,
+ exclude_headers,
+ N_ELEMENTS(exclude_headers),
+ filter_callback, NULL);
+ filter2 = i_stream_create_header_filter(filter,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR,
+ exclude_headers,
+ N_ELEMENTS(exclude_headers),
+ *null_header_filter_callback,
+ NULL);
+ i_stream_unref(&filter);
+ filter = filter2;
+
+ for (i = 1; i < input_len; i++) {
+ test_istream_set_size(istream, i);
+ test_assert(i_stream_read(filter) >= 0);
+ }
+ test_istream_set_size(istream, input_len);
+ test_assert(i_stream_read(filter) > 0);
+ test_assert(i_stream_read(filter) == -1);
+
+ data = i_stream_get_data(filter, &size);
+ test_assert(size == output_len && memcmp(data, output, size) == 0);
+
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == size);
+
+ i_stream_skip(filter, size);
+ i_stream_seek(filter, 0);
+ while (i_stream_read(filter) > 0) ;
+ data = i_stream_get_data(filter, &size);
+ test_assert(size == output_len && memcmp(data, output, size) == 0);
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == size);
+
+ i_stream_unref(&filter);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void add_random_text(string_t *dest, unsigned int count)
+{
+ unsigned int i;
+
+ for (i = 0; i < count; i++)
+ str_append_c(dest, i_rand_minmax('a', 'z'));
+}
+
+static void ATTR_NULL(3)
+filter2_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched, bool *null_hdr_seen)
+{
+ if (hdr == NULL)
+ *null_hdr_seen = TRUE;
+ else if (strcmp(hdr->name, "To") == 0)
+ *matched = TRUE;
+}
+
+static void test_istream_filter_large_buffer(void)
+{
+ string_t *input, *output;
+ struct istream *istream, *filter;
+ const struct stat *st;
+ const unsigned char *data;
+ size_t size, prefix_len;
+ const char *p;
+ unsigned int i;
+ bool null_hdr_seen = FALSE;
+
+ test_begin("i_stream_create_header_filter: large buffer");
+
+ input = str_new(default_pool, 1024*128);
+ output = str_new(default_pool, 1024*128);
+ str_append(input, "From: ");
+ add_random_text(input, 1024*31);
+ str_append(input, "\nTo: ");
+ add_random_text(input, 1024*32);
+ str_append(input, "\nSubject: ");
+ add_random_text(input, 1024*34);
+ str_append(input, "\n\nbody\n");
+
+ istream = test_istream_create_data(str_data(input), str_len(input));
+ test_istream_set_max_buffer_size(istream, 8192);
+
+ filter = i_stream_create_header_filter(istream,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR,
+ NULL, 0,
+ filter2_callback,
+ &null_hdr_seen);
+
+ for (i = 0; i < 2; i++) {
+ for (;;) {
+ ssize_t ret = i_stream_read(filter);
+ i_assert(ret != 0);
+ if (ret == -1)
+ break;
+ if (ret == -2) {
+ unsigned char orig_data[128];
+ size_t orig_data_size;
+
+ data = i_stream_get_data(filter, &size);
+ orig_data_size = I_MIN(sizeof(orig_data), size);
+ memcpy(orig_data, data, orig_data_size);
+
+ /* skip only a bit */
+ size = I_MIN(size, 1);
+ str_append_data(output, data, size);
+ i_stream_skip(filter, size);
+
+ /* do another read */
+ ret = i_stream_read(filter);
+ i_assert(ret == -2 || ret > 0);
+ /* make sure the old data pointer is still
+ usable if -2 is returned */
+ if (ret != -2)
+ data = i_stream_get_data(filter, &size);
+ else {
+ test_assert(memcmp(data, orig_data, orig_data_size) == 0);
+ data = i_stream_get_data(filter, &size);
+ }
+ str_append_data(output, data, size);
+ i_stream_skip(filter, size);
+ }
+ }
+ /* callbacks are called only once */
+ test_assert(null_hdr_seen == (i == 0));
+
+ data = i_stream_get_data(filter, &size);
+ test_assert(size <= 8192);
+ str_append_data(output, data, size);
+
+ p = strstr(str_c(input), "To: ");
+ i_assert(p != NULL);
+ prefix_len = p - str_c(input);
+ test_assert(strncmp(str_c(input), str_c(output), prefix_len) == 0);
+
+ p = strchr(p, '\n');
+ i_assert(p != NULL);
+ test_assert(strcmp(p+1, str_c(output) + prefix_len) == 0);
+
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == filter->v_offset + size);
+
+ /* seek back and retry once with caching and different
+ buffer size */
+ i_stream_seek(filter, 0);
+ str_truncate(output, 0);
+ test_istream_set_max_buffer_size(istream, 4096);
+ null_hdr_seen = FALSE;
+ }
+
+ str_free(&input);
+ str_free(&output);
+ i_stream_unref(&filter);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void test_istream_filter_large_buffer2(void)
+{
+ static const char *wanted_headers[] = { "References" };
+ string_t *input, *output;
+ struct istream *istream, *filter;
+ const struct stat *st;
+ const unsigned char *data;
+ size_t size;
+ unsigned int i;
+ int ret;
+
+ test_begin("i_stream_create_header_filter: large buffer2");
+
+ input = str_new(default_pool, 1024*128);
+ output = str_new(default_pool, 1024*128);
+ str_append(input, "References: ");
+ add_random_text(input, 1024*64);
+ str_append(input, "\r\n\r\n");
+
+ istream = test_istream_create_data(str_data(input), str_len(input));
+ test_istream_set_max_buffer_size(istream, 8192);
+
+ filter = i_stream_create_header_filter(istream,
+ HEADER_FILTER_INCLUDE | HEADER_FILTER_HIDE_BODY,
+ wanted_headers, N_ELEMENTS(wanted_headers),
+ *null_header_filter_callback, NULL);
+
+ for (i = 0; i < 2; i++) {
+ while ((ret = i_stream_read_more(filter, &data, &size)) > 0) {
+ str_append_data(output, data, size);
+ i_stream_skip(filter, size);
+ }
+ test_assert(ret == -1);
+ test_assert(filter->stream_errno == 0);
+
+ test_assert(strcmp(str_c(input), str_c(output)) == 0);
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == filter->v_offset + size);
+
+ /* seek back and retry once with caching and different
+ buffer size */
+ i_stream_seek(filter, 0);
+ str_truncate(output, 0);
+ test_istream_set_max_buffer_size(istream, 4096);
+ }
+
+ str_free(&input);
+ str_free(&output);
+ i_stream_unref(&filter);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void
+filter3_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched ATTR_UNUSED, string_t *dest)
+{
+ if (hdr != NULL)
+ message_header_line_write(dest, hdr);
+}
+
+static void test_istream_callbacks(void)
+{
+ string_t *input, *output;
+ const struct stat *st;
+ struct istream *istream, *filter;
+ unsigned int i;
+
+ test_begin("i_stream_create_header_filter: callbacks");
+
+ input = str_new(default_pool, 1024*128);
+ output = str_new(default_pool, 1024*128);
+ str_append(input, "From: first line\n ");
+ add_random_text(input, 1024*31);
+ str_append(input, "\nTo: first line\n\tsecond line\n\t");
+ add_random_text(input, 1024*32);
+ str_append(input, "\n last line\nSubject: ");
+ add_random_text(input, 1024*34);
+ str_append(input, "\n");
+
+ istream = test_istream_create_data(str_data(input), str_len(input));
+ test_istream_set_max_buffer_size(istream, 8192);
+
+ filter = i_stream_create_header_filter(istream,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR,
+ NULL, 0,
+ filter3_callback,
+ output);
+
+ /* callback should be called exactly once for all the header input */
+ for (i = 0; i < 2; i++) {
+ while (i_stream_read(filter) != -1)
+ i_stream_skip(filter, i_stream_get_data_size(filter));
+ }
+
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == str_len(output));
+ test_assert(strcmp(str_c(output), str_c(input)) == 0);
+ str_free(&input);
+ str_free(&output);
+ i_stream_unref(&filter);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void ATTR_NULL(3)
+edit_callback(struct header_filter_istream *input,
+ struct message_header_line *hdr,
+ bool *matched, void *context ATTR_UNUSED)
+{
+ if (hdr == NULL)
+ return;
+ if (hdr->eoh) {
+ /* add a new header */
+ const char *new_hdr = "Added: header\n\n";
+ i_stream_header_filter_add(input, new_hdr, strlen(new_hdr));
+ *matched = TRUE;
+ } else if (strcasecmp(hdr->name, "To") == 0) {
+ /* modify To header */
+ const char *new_to = "To: 123\n";
+ *matched = TRUE;
+ i_stream_header_filter_add(input, new_to, strlen(new_to));
+ }
+}
+
+static void test_istream_edit(void)
+{
+ const char *input = "From: foo\nTo: bar\n\nhello world\n";
+ const char *output = "From: foo\nTo: 123\nAdded: header\n\nhello world\n";
+ struct istream *istream;
+
+ test_begin("i_stream_create_header_filter: edit headers");
+ istream = test_istream_create(input);
+ test_istream_run(istream, strlen(input), output,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR,
+ edit_callback);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void test_istream_end_body_with_lf(void)
+{
+ const char *input = "From: foo\n\nhello world";
+ const char *output = "From: foo\n\nhello world\n";
+ const struct stat *st;
+ struct istream *istream, *filter;
+ unsigned int i;
+ size_t input_len = strlen(input);
+ size_t output_len = strlen(output);
+ const unsigned char *data;
+ string_t *str = t_str_new(64);
+ size_t size;
+
+ test_begin("i_stream_create_header_filter: end_body_with_lf");
+ istream = test_istream_create(input);
+ filter = i_stream_create_header_filter(istream,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_NO_CR |
+ HEADER_FILTER_END_BODY_WITH_LF,
+ NULL, 0,
+ *null_header_filter_callback,
+ NULL);
+
+ for (i = 1; i < input_len; i++) {
+ test_istream_set_size(istream, i);
+ test_assert(i_stream_read(filter) >= 0);
+ }
+ test_istream_set_size(istream, input_len);
+ test_assert(i_stream_read(filter) > 0);
+ test_assert(i_stream_read(filter) > 0);
+ test_assert(i_stream_read(filter) == -1);
+
+ data = i_stream_get_data(filter, &size);
+ test_assert(size == output_len && memcmp(data, output, size) == 0);
+ test_assert(i_stream_stat(filter, TRUE, &st) == 0 &&
+ (uoff_t)st->st_size == filter->v_offset + size);
+
+ i_stream_skip(filter, size);
+ i_stream_seek(filter, 0);
+ for (i = 1; i < input_len; i++) {
+ test_istream_set_size(istream, i);
+ test_assert(i_stream_read(filter) >= 0);
+
+ data = i_stream_get_data(filter, &size);
+ if (size > 0)
+ str_append_data(str, data, size);
+ i_stream_skip(filter, size);
+ }
+ test_istream_set_size(istream, input_len);
+ test_assert(i_stream_read(filter) == 1);
+ test_assert(i_stream_read(filter) == 1);
+ test_assert(i_stream_read(filter) == -1);
+
+ data = i_stream_get_data(filter, &size);
+ str_append_data(str, data, size);
+ test_assert(strcmp(str_c(str), output) == 0);
+
+ i_stream_unref(&filter);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void test_istream_add_missing_eoh(void)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ unsigned int extra;
+ } tests[] = {
+ { "", "\n", 0 },
+ { "From: foo", "From: foo\n\n", 1 },
+ { "From: foo\n", "From: foo\n\n", 1 },
+ { "From: foo\n\n", "From: foo\n\n", 1 },
+ { "From: foo\n\nbar", "From: foo\n\nbar", 0 },
+ { "From: foo\r\n", "From: foo\r\n\r\n", 1 },
+ { "From: foo\r\n\r\n", "From: foo\r\n\r\n", 0 },
+ { "From: foo\r\n\r\nbar", "From: foo\r\n\r\nbar", 0 }
+ };
+ struct istream *istream;
+ unsigned int i;
+
+ test_begin("i_stream_create_header_filter: add missing EOH");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ istream = test_istream_create(tests[i].input);
+ test_istream_run(istream,
+ strlen(tests[i].input) + tests[i].extra,
+ tests[i].output,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_CRLF_PRESERVE |
+ HEADER_FILTER_ADD_MISSING_EOH,
+ *null_header_filter_callback);
+ i_stream_unref(&istream);
+ }
+ test_end();
+}
+
+static void test_istream_add_missing_eoh_and_edit(void)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ } tests[] = {
+ { "From: foo\nTo: bar\n",
+ "From: foo\nTo: 123\nAdded: header\n\n" },
+ { "From: foo\nTo: bar\n\n",
+ "From: foo\nTo: 123\nAdded: header\n\n" },
+ { "From: foo\nTo: bar\n\nbody\n",
+ "From: foo\nTo: 123\nAdded: header\n\nbody\n" },
+ };
+ struct istream *istream;
+ unsigned int i;
+
+ test_begin("i_stream_create_header_filter: add missing EOH and edit headers");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ istream = test_istream_create(tests[i].input);
+ test_istream_run(istream, strlen(tests[i].input), tests[i].output,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_ADD_MISSING_EOH |
+ HEADER_FILTER_NO_CR,
+ edit_callback);
+ i_stream_unref(&istream);
+ }
+ test_end();
+}
+
+static void test_istream_hide_body(void)
+{
+ static const struct {
+ const char *input;
+ const char *output;
+ int extra;
+ } tests[] = {
+ { "From: foo", "From: foo", 0 },
+ { "From: foo\n", "From: foo\n", 0 },
+ { "From: foo\n\n", "From: foo\n\n", 1 },
+ { "From: foo\n\nbar", "From: foo\n\n", -2 },
+ { "From: foo\r\n", "From: foo\r\n", 0 },
+ { "From: foo\r\n\r\n", "From: foo\r\n\r\n", 0 },
+ { "From: foo\r\n\r\nbar", "From: foo\r\n\r\n", -3 }
+ };
+ struct istream *istream;
+ unsigned int i;
+
+ test_begin("i_stream_create_header_filter: hide body");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ istream = test_istream_create(tests[i].input);
+ test_istream_run(istream,
+ (int)strlen(tests[i].input) + tests[i].extra,
+ tests[i].output,
+ HEADER_FILTER_EXCLUDE |
+ HEADER_FILTER_CRLF_PRESERVE |
+ HEADER_FILTER_HIDE_BODY,
+ *null_header_filter_callback);
+ i_stream_unref(&istream);
+ }
+ test_end();
+}
+
+static void ATTR_NULL(3)
+strip_eoh_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched, void *context ATTR_UNUSED)
+{
+ if (hdr != NULL && hdr->eoh)
+ *matched = TRUE;
+}
+
+static void test_istream_strip_eoh(void)
+{
+ const char *input = "From: foo\nTo: bar\n\nhello world\n";
+ const char *output = "From: foo\nTo: bar\nhello world\n";
+ struct istream *istream;
+
+ test_begin("i_stream_create_header_filter: strip_eoh");
+ istream = test_istream_create(input);
+ test_istream_run(istream, strlen(input), output,
+ HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
+ strip_eoh_callback);
+ i_stream_unref(&istream);
+
+ test_end();
+}
+
+static void ATTR_NULL(3)
+missing_eoh_callback(struct header_filter_istream *input ATTR_UNUSED,
+ struct message_header_line *hdr,
+ bool *matched ATTR_UNUSED, void *context ATTR_UNUSED)
+{
+ if (hdr == NULL) {
+ const char *new_hdr = "Subject: added\n\n";
+ i_stream_header_filter_add(input, new_hdr, strlen(new_hdr));
+ }
+}
+
+static void test_istream_missing_eoh_callback(void)
+{
+ const char *input = "From: foo\nTo: bar\n";
+ const char *output = "From: foo\nTo: bar\nSubject: added\n\n";
+ struct istream *istream;
+
+ test_begin("i_stream_create_header_filter: add headers when EOH is missing");
+ istream = test_istream_create(input);
+ test_istream_run(istream, strlen(input) + 1, output,
+ HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
+ missing_eoh_callback);
+ i_stream_unref(&istream);
+ test_end();
+}
+
+static void test_istream_empty_missing_eoh_callback(void)
+{
+ const char *input = "";
+ const char *output = "Subject: added\n\n";
+ struct istream *istream;
+
+ test_begin("i_stream_create_header_filter: add headers when mail is empty");
+ istream = test_istream_create(input);
+ test_istream_run(istream, strlen(input)+1, output,
+ HEADER_FILTER_EXCLUDE | HEADER_FILTER_NO_CR,
+ missing_eoh_callback);
+ i_stream_unref(&istream);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_filter,
+ test_istream_filter_large_buffer,
+ test_istream_filter_large_buffer2,
+ test_istream_callbacks,
+ test_istream_edit,
+ test_istream_add_missing_eoh,
+ test_istream_add_missing_eoh_and_edit,
+ test_istream_end_body_with_lf,
+ test_istream_hide_body,
+ test_istream_strip_eoh,
+ test_istream_missing_eoh_callback,
+ test_istream_empty_missing_eoh_callback,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-istream-qp-decoder.c b/src/lib-mail/test-istream-qp-decoder.c
new file mode 100644
index 0000000..cdf5b22
--- /dev/null
+++ b/src/lib-mail/test-istream-qp-decoder.c
@@ -0,0 +1,197 @@
+/* Copyright (c) 2010-2018 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 char *input;
+ const char *output;
+ int stream_errno;
+ int eof;
+} tests[] = {
+ { "p=C3=A4=C3=A4t=C3=B6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0 , 0 },
+ { "p=c3=a4=c3=a4t=c3=b6s= \n", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 0 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 1 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 2 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 3 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 4 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 5 },
+ { "p=c3=a4= \t \n=c3=\r\n=a4t= \r\n=c3=b6s", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", 0, 7 },
+ { "p=c3", "p\xC3", 0, 2 },
+ { "=0A=0D ", "\n\r", 0, 7 },
+ { "foo_bar", "foo_bar", 0, 0 },
+ { "\n\n", "\r\n\r\n", 0, 0 },
+ { "\r\n\n\n\r\n", "\r\n\r\n\r\n\r\n", 0, 0 },
+ /* Unnecessarily encoded */
+ { "=66=6f=6f=42=61=72", "fooBar", 0, 4 },
+ /* Expected to be encoded but not */
+ { "\xc3\x9c""berm=c3=a4\xc3\x9figer Gebrauch", "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch", 0, 9 },
+ /* Decode control characters */
+ { "=0C=07", "\x0C\x07", 0, 0 },
+ /* Data */
+ { "=DE=AD=BE=EF", "\xDE\xAD\xBE\xEF", 0, 0 },
+ /* Non hex data */
+ { "=FJ=X1", "", EINVAL, 0 },
+ /* No content allowed after Soft Line Break */
+ { "=C3=9C = ","\xc3\x9c ", EPIPE, 0 },
+ /* Boundary delimiter */
+ { "=C3=9C=\r\n-------","\xc3\x9c-------", 0, 0 },
+ { "=----------- =C3=9C","", EINVAL, 0 },
+ { "=___________ =C3=9C","", EINVAL, 0 },
+ { "___________ =C3=9C","___________ \xc3\x9c", 0, 0 },
+ { "=2D=2D=2D=2D=2D=2D =C3=9C","------ \xc3\x9c", 0, 0 },
+ { "=FC=83=BF=BF=BF=BF", "\xFC\x83\xBF\xBF\xBF\xBF", 0, 0 },
+ { "=FE=FE=FF=FF", "\xFE\xFE\xFF\xFF", 0, 0 },
+ { "\xFF=C3=9C\xFE\xFF""foobar", "\xFF\xc3\x9c\xFE\xFF""foobar", 0, 0 },
+
+ { "p=c3=a4\rasdf", "p\xC3\xA4", EINVAL, 0 },
+ { "=___________ \xc3\x9c","", EINVAL, 0 },
+ { "p=c", "p", EPIPE, 0 },
+ { "p=A", "p", EPIPE, 0 },
+ { "p=Ax", "p", EINVAL, 0 },
+ { "___________ \xc3\x9c=C3=9","___________ \xc3\x9c\xC3", EPIPE, 0},
+ { "p=c3=a4=c3=a4t=c3=b6s= ", "p\xC3\xA4\xC3\xA4t\xC3\xB6s", EPIPE, 0 },
+ /* Soft Line Break example from the RFC */
+ {
+ "Now's the time =\r\nfor all folk to come=\r\n to the aid of "
+ "their country.", "Now's the time for all folk to come to the"
+ " aid of their country.", 0, 41
+ },
+};
+
+static bool is_hex(char c) {
+ return ((c >= 48 && c <= 57) || (c >= 65 && c <= 70)
+ || (c >= 97 && c <= 102));
+
+}
+
+static unsigned int
+get_encoding_size_diff(const char *qp_input, unsigned int limit)
+{
+ unsigned int encoded_chars = 0;
+ unsigned int soft_line_breaks = 0;
+ for (unsigned int i = 0; i < limit; i++) {
+ char c = qp_input[i];
+ if (c == '=' && i+2 < limit) {
+ if (qp_input[i+1] == '\r' && qp_input[i+2] == '\n') {
+ soft_line_breaks++;
+ i += 2;
+ limit += 3;
+ } else if (is_hex(qp_input[i+1]) && is_hex(qp_input[i+2])) {
+ encoded_chars++;
+ i += 2;
+ limit += 2;
+ }
+ }
+ }
+ return encoded_chars*2 + soft_line_breaks*3;
+}
+
+static void
+decode_test(const char *qp_input, const char *output, int stream_errno,
+ unsigned int buffer_size, unsigned int eof)
+{
+ size_t qp_input_len = strlen(qp_input);
+ struct istream *input_data, *input_data_limited, *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_decoder(input_data);
+
+ 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_data(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_data(str, data, size);
+ i_stream_skip(input, size);
+ }
+ }
+ test_assert(ret == -1);
+ test_assert(input->stream_errno == stream_errno);
+
+ if (stream_errno == 0) {
+ /* Test seeking on streams where the testcases do not
+ * expect a specific errno already */
+ uoff_t v_off = input->v_offset;
+ /* Seeking backwards */
+ i_stream_seek(input, 0);
+ test_assert(input->v_offset == 0);
+
+ /* Seeking forward */
+ i_stream_seek(input, v_off+1);
+ test_assert(input->stream_errno == ESPIPE);
+ }
+ /* Compare outputs */
+ test_assert_strcmp(str_c(str), output);
+
+ if (eof > 0) {
+ /* Insert early EOF into input_data */
+ i_stream_seek(input_data, 0);
+ str_truncate(str, 0);
+ input_data_limited = i_stream_create_limit(input_data, eof);
+ test_istream_set_allow_eof(input_data_limited, TRUE);
+ i_stream_unref(&input);
+ input = i_stream_create_qp_decoder(input_data_limited);
+ while ((ret = i_stream_read_more(input, &data, &size)) > 0) {
+ str_append_data(str, data, size);
+ i_stream_skip(input, size);
+ }
+ test_assert(ret == -1);
+ /* If there is no error still assume that the result is valid
+ * till artifical eof. */
+ if (input->stream_errno == 0) {
+ unsigned int encoding_margin =
+ get_encoding_size_diff(qp_input, eof);
+
+ /* Cut the expected output at eof of input*/
+ const char *expected_output =
+ t_strdup_printf("%.*s", eof-encoding_margin,
+ output);
+ test_assert_strcmp(str_c(str), expected_output);
+ }
+ test_assert(input->eof);
+ i_stream_unref(&input_data_limited);
+ }
+
+ i_stream_unref(&input);
+ i_stream_unref(&input_data);
+}
+
+static void test_istream_qp_decoder(void)
+{
+ unsigned int i, j;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_begin(t_strdup_printf("istream qp decoder %u", i+1));
+ for (j = 1; j < 10; j++) T_BEGIN {
+ decode_test(tests[i].input, tests[i].output,
+ tests[i].stream_errno, j, tests[i].eof);
+ } T_END;
+ test_end();
+ }
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_istream_qp_decoder,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-istream-qp-encoder.c b/src/lib-mail/test-istream-qp-encoder.c
new file mode 100644
index 0000000..148fe24
--- /dev/null
+++ b/src/lib-mail/test-istream-qp-encoder.c
@@ -0,0 +1,160 @@
+/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */
+
+#include "test-lib.h"
+#include "str.h"
+#include "istream-private.h"
+#include "istream-qp.h"
+
+#define WHITESPACE10 " \t \t \t"
+#define WHITESPACE70 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10
+
+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
+ },
+ /* Test line breaking */
+ { WHITESPACE70"1234567", WHITESPACE70"123=\r\n4567", 0 },
+ { WHITESPACE70" 7", WHITESPACE70" =20=\r\n 7", 0 },
+ { WHITESPACE70""WHITESPACE10"1", WHITESPACE70" =20=\r\n \t \t \t1", 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_data(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_data(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);
+
+ if (stream_errno == 0) {
+ /* Test seeking on streams where the testcases do not
+ * expect a specific errno already. */
+ uoff_t v_off = input->v_offset;
+ /* Seeking backwards */
+ i_stream_seek(input, 0);
+ test_assert(input->v_offset == 0);
+
+ /* Seeking forward */
+ i_stream_seek(input, v_off+1);
+ test_assert(input->stream_errno == ESPIPE);
+ }
+
+ i_stream_unref(&input);
+ /* Test closing stream gives expected results */
+ i_stream_seek(input_data, 0);
+ input = i_stream_create_qp_encoder(input_data, 0);
+ i_stream_close(input);
+ test_assert(input->closed);
+ test_assert(i_stream_read_more(input, &data, &size) == -1);
+
+ 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);
+}
diff --git a/src/lib-mail/test-mail-html2text.c b/src/lib-mail/test-mail-html2text.c
new file mode 100644
index 0000000..73e93f7
--- /dev/null
+++ b/src/lib-mail/test-mail-html2text.c
@@ -0,0 +1,120 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "mail-html2text.h"
+#include "test-common.h"
+
+static const struct {
+ const char *input;
+ const char *output;
+} tests[] = {
+ { "&&aaaaaaaaaa", "" },
+
+ { "a&amp;&lt;&clubs;&gt;b",
+ "a&<\xE2\x99\xA3>b" },
+ { "&", "" },
+ { "&amp", "" },
+
+ { "a<style>stylesheet is ignored</style>b",
+ "a b" },
+ { "a<stylea>b</stylea>c",
+ "a b c" },
+ { "a<!--x <p foo=\"bar\">commented tags ignored also</p> y-->b",
+ "ab" },
+ { "a<script>javascript <p>foo</p> ignored</script>b",
+ "a b" },
+ { "a<scripta>b</scripta>c",
+ "a b c" },
+ { "a<blockquote><blockquote>second level</blockquote>ignored</blockquote>b",
+ "a b" },
+ { "a<![CDATA[<style>]] >b</style>]]>c",
+ "a<style>]] >b</style>c" },
+
+ { "a<foo", "a" },
+ { "a<blockquote", "a" },
+ { "a<blockquote>foo</blockquote", "a " },
+ { "a<", "a" },
+ { "a<![CDATA[b", "ab" },
+ { "a<![CDATA[b]]", "ab" },
+ { "a&#228;", "a\xC3\xA4" },
+ { "a&#xe4;", "a\xC3\xA4" },
+ { "&#8364;", "\xE2\x82\xAC" },
+ { "&#deee;", "" }, // invalid codepoint
+};
+
+static const char *test_blockquote_input[] = {
+ "a<blockquote>b<blockquote><blockquote>c</blockquote>d</blockquote>e</blockquote>f",
+ "a&amp;<blockquote>b&amp;<blockquote>&amp;<blockquote>&amp;c</blockquote>d&amp;</blockquote>&amp;e</blockquote>f&amp;",
+ NULL
+};
+
+static const char *test_blockquote_output[] = {
+ "a\n> b\n> \n> c\n> d\n> e\nf",
+ "a&\n> b&\n> &\n> &c\n> d&\n> &e\nf&",
+ NULL
+};
+
+static void test_mail_html2text(void)
+{
+ string_t *str = t_str_new(128);
+ struct mail_html2text *ht;
+ unsigned int i, j;
+
+ test_begin("mail_html2text()");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ ht = mail_html2text_init(MAIL_HTML2TEXT_FLAG_SKIP_QUOTED);
+ for (j = 0; tests[i].input[j] != '\0'; j++) {
+ unsigned char c = tests[i].input[j];
+ mail_html2text_more(ht, &c, 1, str);
+ }
+ test_assert_idx(strcmp(str_c(str), tests[i].output) == 0, i);
+ mail_html2text_deinit(&ht);
+ str_truncate(str, 0);
+ }
+
+ /* test without skipping quoted */
+ for (unsigned int i = 0; test_blockquote_input[i] != NULL; i++) {
+ str_truncate(str, 0);
+ ht = mail_html2text_init(0);
+ mail_html2text_more(ht, (const void *)test_blockquote_input[i],
+ strlen(test_blockquote_input[i]), str);
+ test_assert_idx(strcmp(str_c(str), test_blockquote_output[i]) == 0, i);
+ mail_html2text_deinit(&ht);
+ }
+
+ test_end();
+}
+
+static void test_mail_html2text_random(void)
+{
+ string_t *str = t_str_new(128);
+ struct mail_html2text *ht;
+
+ test_begin("mail_html2text() random");
+ for (unsigned int i = 0; i < 1000; i++) {
+ char valid_chars[] = { '0', 'a', '<', '>', '&', ';', '\\', '\'', '"', '/' };
+ unsigned char s[2];
+
+ ht = mail_html2text_init(0);
+ for (unsigned int i = 0; i < 100; i++) {
+ s[0] = valid_chars[i_rand_limit(N_ELEMENTS(valid_chars))];
+ s[1] = valid_chars[i_rand_limit(N_ELEMENTS(valid_chars))];
+ mail_html2text_more(ht, s, i_rand_minmax(1, 2), str);
+ }
+ mail_html2text_deinit(&ht);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_html2text,
+ test_mail_html2text_random,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-mail-user-hash.c b/src/lib-mail/test-mail-user-hash.c
new file mode 100644
index 0000000..7e2a496
--- /dev/null
+++ b/src/lib-mail/test-mail-user-hash.c
@@ -0,0 +1,185 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "mail-user-hash.h"
+#include "test-common.h"
+
+#include "md5.h"
+
+static void test_mail_user_hash(void)
+{
+ struct test_case {
+ const char *username;
+ const char *format;
+ unsigned int hash;
+ } test_cases[] = {
+ {
+ .username = "",
+ .format = "",
+ .hash = 3558706393,
+ },
+ {
+ .username = "testuser",
+ .format = "",
+ .hash = 3558706393,
+ },
+ {
+ .username = "",
+ .format = "%u",
+ .hash = 3558706393,
+ },
+ {
+ .username = "@",
+ .format = "%u",
+ .hash = 1368314517,
+ },
+ {
+ .username = "",
+ .format = "%n@%d",
+ .hash = 1368314517,
+ },
+ {
+ .username = "",
+ .format = "%n",
+ .hash = 3558706393,
+ },
+ {
+ .username = "",
+ .format = "%d",
+ .hash = 3558706393,
+ },
+ {
+ .username = "testuser",
+ .format = "%u",
+ .hash = 1570531526,
+ },
+ {
+ .username = "testuser",
+ .format = "%n",
+ .hash = 1570531526,
+ },
+ {
+ .username = "testuser",
+ .format = "%d",
+ .hash = 3558706393,
+ },
+ {
+ .username = "@domain",
+ .format = "%u",
+ .hash = 3749630072,
+ },
+ {
+ .username = "@domain",
+ .format = "%n@%d",
+ .hash = 3749630072,
+ },
+ {
+ .username = "@domain",
+ .format = "%n",
+ .hash = 3558706393,
+ },
+ {
+ .username = "@domain",
+ .format = "%d",
+ .hash = 2908717800,
+ },
+ {
+ .username = "testuser@domain",
+ .format = "%u",
+ .hash = 3813799143,
+ },
+ {
+ .username = "testuser@domain",
+ .format = "%n@%d",
+ .hash = 3813799143,
+ },
+ {
+ .username = "testuser@domain",
+ .format = "%n",
+ .hash = 1570531526,
+ },
+ {
+ .username = "testuser@domain",
+ .format = "%d",
+ .hash = 2908717800,
+ },
+ {
+ .username = "test@user@domain",
+ .format = "%u",
+ .hash = 2029259821,
+ },
+ {
+ .username = "test@user@domain",
+ .format = "%n@%d",
+ .hash = 2029259821,
+ },
+ {
+ .username = "test@user@domain",
+ .format = "%n",
+ .hash = 160394189,
+ },
+ {
+ .username = "test@user@domain",
+ .format = "%d",
+ .hash = 1841230927,
+ }
+ };
+
+ test_begin("mail_user_hash");
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ const struct test_case *tc = &test_cases[i];
+ const char *error = NULL;
+ unsigned int hash;
+ test_assert_idx(mail_user_hash(tc->username, tc->format, &hash,
+ &error), i);
+ test_assert_idx(error == NULL, i);
+ test_assert_idx(hash == tc->hash, i);
+ }
+
+ test_end();
+}
+
+static void test_mail_user_hash_errors(void)
+{
+ test_begin("mail_user_hash_errors");
+
+ struct test_case {
+ const char *username;
+ const char *format;
+ unsigned int hash;
+ const char *error;
+ } test_cases[] = {
+ {
+ .username = "testuser@domain",
+ .format = "%{invalid}",
+ .hash = 1466562296,
+ .error = "Unknown variable '%invalid'",
+ },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ const struct test_case *tc = &test_cases[i];
+ const char *error = NULL;
+ unsigned int hash = 0;
+ test_assert_idx(mail_user_hash(tc->username, tc->format, &hash,
+ &error) == FALSE, i);
+ test_assert_idx(tc->hash == hash, i);
+ test_assert_strcmp_idx(tc->error, error, i);
+ test_assert_idx(tc->hash == hash, i);
+ }
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mail_user_hash,
+ test_mail_user_hash_errors,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-mbox-from.c b/src/lib-mail/test-mbox-from.c
new file mode 100644
index 0000000..2fce324
--- /dev/null
+++ b/src/lib-mail/test-mbox-from.c
@@ -0,0 +1,104 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "utc-offset.h"
+#include "mbox-from.h"
+#include "test-common.h"
+
+#include <time.h>
+
+struct test_mbox_from_parse_output {
+ time_t time;
+ int tz_offset;
+ const char *sender;
+ int ret;
+};
+
+static void test_mbox_from_parse(void)
+{
+ static const char *input[] = {
+ "user@domain Thu Nov 29 23:33:09 1973 +0200",
+ "user@domain Thu Nov 29 19:33:09 1973 -0200",
+ "\"user name\"@domain Fri Jan 2 10:13:52 UTC 1970 +0000",
+ "user Fri Jan 2 10:14 1970 +0000",
+ "user Fri, 2 Jan 1970 10:14:00 +0000",
+ "user Fri, 2 Jan 1970 10:14 +0000",
+ " Fri Jan 2 10:14 1970 +0000",
+ "user Fri, 2 Foo 1970 10:14:00",
+ "Fri Jan 2 10:14 1970 +0000",
+ "user Fri Jan x 10:14 1970 +0000",
+ "user Fri Jan 2 0:14 1970 +0000",
+ "user Fri Jan 2 xx:14 1970 +0000",
+ "user Fri Jan 2 10: 1970 +0000",
+ "user Fri Jan 2 10:xx 1970 +0000",
+ "user Fri Jan 2 10:xx +0000",
+ };
+ static struct test_mbox_from_parse_output output[] = {
+ { 123456789, 2*60, "user@domain", 0 },
+ { 123456789, -2*60, "user@domain", 0 },
+ { 123232, 0, "\"user name\"@domain", 0 },
+ { 123240, 0, "user", 0 },
+ { 123240, 0, "user", 0 },
+ { 123240, 0, "user", 0 },
+ { 123240, 0, "", 0 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ { 0, 0, NULL, -1 },
+ };
+ unsigned int i, j;
+ size_t len;
+ struct tm *tm;
+ char *sender;
+ bool success;
+ time_t t;
+ int tz, ret;
+
+ for (j = 0; j < 2; j++) {
+ for (i = 0; i < N_ELEMENTS(input); i++) {
+ len = strlen(input[i]) - j*6;
+ ret = mbox_from_parse((const unsigned char *)input[i],
+ len, &t, &tz, &sender);
+ success = (ret < 0 && output[i].ret < 0) ||
+ (ret == output[i].ret && t == output[i].time &&
+ tz == output[i].tz_offset &&
+ strcmp(sender, output[i].sender) == 0);
+ i_free(sender);
+ test_out(t_strdup_printf("mbox_from_parse(%d,%d)", j, i), success);
+
+ /* prepare for testing without timezone */
+ if (output[i].ret == 0) {
+ output[i].time += output[i].tz_offset*60;
+ tm = localtime(&output[i].time);
+ output[i].tz_offset = utc_offset(tm, output[i].time);
+ output[i].time -= output[i].tz_offset*60;
+ }
+ }
+ }
+}
+
+static void test_mbox_from_create(void)
+{
+ time_t t = 1234567890;
+ int tz;
+
+ test_begin("mbox_from_create()");
+ tz = utc_offset(localtime(&t), t) * -60;
+ test_assert(strcmp(mbox_from_create("user", t+tz),
+ "From user Fri Feb 13 23:31:30 2009\n") == 0);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_mbox_from_parse,
+ test_mbox_from_create,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-address.c b/src/lib-mail/test-message-address.c
new file mode 100644
index 0000000..e6204bb
--- /dev/null
+++ b/src/lib-mail/test-message-address.c
@@ -0,0 +1,532 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "message-address.h"
+#include "test-common.h"
+
+enum test_message_address {
+ TEST_MESSAGE_ADDRESS_FLAG_SKIP_LIST = BIT(0),
+};
+
+static bool cmp_addr(const struct message_address *a1,
+ const struct message_address *a2)
+{
+ return null_strcmp(a1->name, a2->name) == 0 &&
+ null_strcmp(a1->route, a2->route) == 0 &&
+ null_strcmp(a1->mailbox, a2->mailbox) == 0 &&
+ null_strcmp(a1->domain, a2->domain) == 0 &&
+ a1->invalid_syntax == a2->invalid_syntax;
+}
+
+static const struct message_address *
+test_parse_address(const char *input, bool fill_missing)
+{
+ const enum message_address_parse_flags flags =
+ fill_missing ? MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING : 0;
+ /* duplicate the input (without trailing NUL) so valgrind notices
+ if there's any out-of-bounds access */
+ size_t input_len = strlen(input);
+ unsigned char *input_dup = i_memdup(input, input_len);
+ const struct message_address *addr =
+ message_address_parse(pool_datastack_create(),
+ input_dup, input_len, UINT_MAX, flags);
+ i_free(input_dup);
+ return addr;
+}
+
+static void test_message_address(void)
+{
+ static const struct test {
+ const char *input;
+ const char *wanted_output;
+ const char *wanted_filled_output;
+ struct message_address addr;
+ struct message_address filled_addr;
+ enum test_message_address flags;
+ } tests[] = {
+ /* user@domain -> <user@domain> */
+ { "user@domain", "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+ { "\"user\"@domain", "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+ { "\"user name\"@domain", "<\"user name\"@domain>", NULL,
+ { NULL, NULL, NULL, "user name", "domain", FALSE },
+ { NULL, NULL, NULL, "user name", "domain", FALSE }, 0 },
+ { "\"user@na\\\\me\"@domain", "<\"user@na\\\\me\"@domain>", NULL,
+ { NULL, NULL, NULL, "user@na\\me", "domain", FALSE },
+ { NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, 0 },
+ { "\"user\\\"name\"@domain", "<\"user\\\"name\"@domain>", NULL,
+ { NULL, NULL, NULL, "user\"name", "domain", FALSE },
+ { NULL, NULL, NULL, "user\"name", "domain", FALSE }, 0 },
+ { "\"\"@domain", "<\"\"@domain>", NULL,
+ { NULL, NULL, NULL, "", "domain", FALSE },
+ { NULL, NULL, NULL, "", "domain", FALSE }, 0 },
+ { "user", "<user>", "<user@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "user", "", TRUE },
+ { NULL, NULL, NULL, "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "@domain", "<\"\"@domain>", "<MISSING_MAILBOX@domain>",
+ { NULL, NULL, NULL, "", "domain", TRUE },
+ { NULL, NULL, NULL, "MISSING_MAILBOX", "domain", TRUE }, 0 },
+
+ /* Display Name -> Display Name */
+ { "Display Name", "\"Display Name\"", "\"Display Name\" <MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "", "", TRUE },
+ { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+ { "\"Display Name\"", "\"Display Name\"", "\"Display Name\" <MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "", "", TRUE },
+ { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+ { "Display \"Name\"", "\"Display Name\"", "\"Display Name\" <MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "", "", TRUE },
+ { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+ { "\"Display\" \"Name\"", "\"Display Name\"", "\"Display Name\" <MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "", "", TRUE },
+ { NULL, "Display Name", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+ { "\"\"", "", "<MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, "", NULL, "", "", TRUE },
+ { NULL, "", NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+
+ /* <user@domain> -> <user@domain> */
+ { "<user@domain>", NULL, NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+ { "<\"user\"@domain>", "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+ { "<\"user name\"@domain>", NULL, NULL,
+ { NULL, NULL, NULL, "user name", "domain", FALSE },
+ { NULL, NULL, NULL, "user name", "domain", FALSE }, 0 },
+ { "<\"user@na\\\\me\"@domain>", NULL, NULL,
+ { NULL, NULL, NULL, "user@na\\me", "domain", FALSE },
+ { NULL, NULL, NULL, "user@na\\me", "domain", FALSE }, 0 },
+ { "<\"user\\\"name\"@domain>", NULL, NULL,
+ { NULL, NULL, NULL, "user\"name", "domain", FALSE },
+ { NULL, NULL, NULL, "user\"name", "domain", FALSE }, 0 },
+ { "<\"\"@domain>", NULL, NULL,
+ { NULL, NULL, NULL, "", "domain", FALSE },
+ { NULL, NULL, NULL, "", "domain", FALSE }, 0 },
+ { "<user>", NULL, "<user@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "user", "", TRUE },
+ { NULL, NULL, NULL, "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "<@route>", "<@route:\"\">", "<INVALID_ROUTE:MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, NULL, "@route", "", "", TRUE },
+ { NULL, NULL, "INVALID_ROUTE", "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+
+ /* user@domain (Display Name) -> "Display Name" <user@domain> */
+ { "user@domain (DisplayName)", "DisplayName <user@domain>", NULL,
+ { NULL, "DisplayName", NULL, "user", "domain", FALSE },
+ { NULL, "DisplayName", NULL, "user", "domain", FALSE }, 0 },
+ { "user@domain (Display Name)", "\"Display Name\" <user@domain>", NULL,
+ { NULL, "Display Name", NULL, "user", "domain", FALSE },
+ { NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 },
+ { "user@domain (Display\"Name)", "\"Display\\\"Name\" <user@domain>", NULL,
+ { NULL, "Display\"Name", NULL, "user", "domain", FALSE },
+ { NULL, "Display\"Name", NULL, "user", "domain", FALSE }, 0 },
+ { "user (Display Name)", "\"Display Name\" <user>", "\"Display Name\" <user@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "user", "", TRUE },
+ { NULL, "Display Name", NULL, "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "@domain (Display Name)", "\"Display Name\" <\"\"@domain>", "\"Display Name\" <MISSING_MAILBOX@domain>",
+ { NULL, "Display Name", NULL, "", "domain", TRUE },
+ { NULL, "Display Name", NULL, "MISSING_MAILBOX", "domain", TRUE }, 0 },
+ { "user@domain ()", "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+
+ /* Display Name <user@domain> -> "Display Name" <user@domain> */
+ { "DisplayName <user@domain>", NULL, NULL,
+ { NULL, "DisplayName", NULL, "user", "domain", FALSE },
+ { NULL, "DisplayName", NULL, "user", "domain", FALSE }, 0 },
+ { "Display Name <user@domain>", "\"Display Name\" <user@domain>", NULL,
+ { NULL, "Display Name", NULL, "user", "domain", FALSE },
+ { NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 },
+ { "\"Display Name\" <user@domain>", NULL, NULL,
+ { NULL, "Display Name", NULL, "user", "domain", FALSE },
+ { NULL, "Display Name", NULL, "user", "domain", FALSE }, 0 },
+ { "\"Display\\\"Name\" <user@domain>", NULL, NULL,
+ { NULL, "Display\"Name", NULL, "user", "domain", FALSE },
+ { NULL, "Display\"Name", NULL, "user", "domain", FALSE }, 0 },
+ { "Display Name <user>", "\"Display Name\" <user>", "\"Display Name\" <user@MISSING_DOMAIN>",
+ { NULL, "Display Name", NULL, "user", "", TRUE },
+ { NULL, "Display Name", NULL, "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "\"\" <user@domain>", "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE },
+ { NULL, NULL, NULL, "user", "domain", FALSE }, 0 },
+
+ /* <@route:user@domain> -> <@route:user@domain> */
+ { "<@route:user@domain>", NULL, NULL,
+ { NULL, NULL, "@route", "user", "domain", FALSE },
+ { NULL, NULL, "@route", "user", "domain", FALSE }, 0 },
+ { "<@route,@route2:user@domain>", NULL, NULL,
+ { NULL, NULL, "@route,@route2", "user", "domain", FALSE },
+ { NULL, NULL, "@route,@route2", "user", "domain", FALSE }, 0 },
+ { "<@route@route2:user@domain>", "<@route,@route2:user@domain>", NULL,
+ { NULL, NULL, "@route,@route2", "user", "domain", FALSE },
+ { NULL, NULL, "@route,@route2", "user", "domain", FALSE }, 0 },
+ { "<@route@route2:user>", "<@route,@route2:user>", "<@route,@route2:user@MISSING_DOMAIN>",
+ { NULL, NULL, "@route,@route2", "user", "", TRUE },
+ { NULL, NULL, "@route,@route2", "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "<@route@route2:\"\"@domain>", "<@route,@route2:\"\"@domain>", NULL,
+ { NULL, NULL, "@route,@route2", "", "domain", FALSE },
+ { NULL, NULL, "@route,@route2", "", "domain", FALSE }, 0 },
+
+ /* Display Name <@route:user@domain> ->
+ "Display Name" <@route:user@domain> */
+ { "Display Name <@route:user@domain>", "\"Display Name\" <@route:user@domain>", NULL,
+ { NULL, "Display Name", "@route", "user", "domain", FALSE },
+ { NULL, "Display Name", "@route", "user", "domain", FALSE }, 0 },
+ { "Display Name <@route,@route2:user@domain>", "\"Display Name\" <@route,@route2:user@domain>", NULL,
+ { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE },
+ { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, 0 },
+ { "Display Name <@route@route2:user@domain>", "\"Display Name\" <@route,@route2:user@domain>", NULL,
+ { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE },
+ { NULL, "Display Name", "@route,@route2", "user", "domain", FALSE }, 0 },
+ { "Display Name <@route@route2:user>", "\"Display Name\" <@route,@route2:user>", "\"Display Name\" <@route,@route2:user@MISSING_DOMAIN>",
+ { NULL, "Display Name", "@route,@route2", "user", "", TRUE },
+ { NULL, "Display Name", "@route,@route2", "user", "MISSING_DOMAIN", TRUE }, 0 },
+ { "Display Name <@route@route2:\"\"@domain>", "\"Display Name\" <@route,@route2:\"\"@domain>", NULL,
+ { NULL, "Display Name", "@route,@route2", "", "domain", FALSE },
+ { NULL, "Display Name", "@route,@route2", "", "domain", FALSE }, 0 },
+
+ /* other tests: */
+ { "\"foo: <a@b>;,\" <user@domain>", NULL, NULL,
+ { NULL, "foo: <a@b>;,", NULL, "user", "domain", FALSE },
+ { NULL, "foo: <a@b>;,", NULL, "user", "domain", FALSE }, 0 },
+ { "<>", "", "<MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "", "", TRUE },
+ { NULL, NULL, NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+ { "<@>", "", "<INVALID_ROUTE:MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "", "", TRUE },
+ { NULL, NULL, "INVALID_ROUTE", "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE }, 0 },
+
+ /* Test against a out-of-bounds read bug - keep these two tests
+ together in this same order: */
+ { "aaaa@", "<aaaa>", "<aaaa@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "aaaa", "", TRUE },
+ { NULL, NULL, NULL, "aaaa", "MISSING_DOMAIN", TRUE }, 0 },
+ { "a(aa", "", "<MISSING_MAILBOX@MISSING_DOMAIN>",
+ { NULL, NULL, NULL, "", "", TRUE },
+ { NULL, NULL, NULL, "MISSING_MAILBOX", "MISSING_DOMAIN", TRUE },
+ TEST_MESSAGE_ADDRESS_FLAG_SKIP_LIST },
+ };
+ static struct message_address group_prefix = {
+ NULL, NULL, NULL, "group", NULL, FALSE
+ };
+ static struct message_address group_suffix = {
+ NULL, NULL, NULL, NULL, NULL, FALSE
+ };
+ const struct message_address *addr;
+ string_t *str, *group;
+ const char *wanted_string;
+ unsigned int i;
+
+ test_begin("message address parsing");
+ str = t_str_new(128);
+ group = t_str_new(256);
+
+ for (i = 0; i < N_ELEMENTS(tests)*2; i++) {
+ const struct test *test = &tests[i/2];
+ const struct message_address *test_wanted_addr;
+ bool fill_missing = i%2 != 0;
+
+ test_wanted_addr = !fill_missing ?
+ &test->addr : &test->filled_addr;
+ addr = test_parse_address(test->input, fill_missing);
+ test_assert_idx(addr != NULL && addr->next == NULL &&
+ cmp_addr(addr, test_wanted_addr), i);
+
+ /* test the address alone */
+ str_truncate(str, 0);
+ message_address_write(str, addr);
+ if (fill_missing && test->wanted_filled_output != NULL)
+ wanted_string = test->wanted_filled_output;
+ else if (test->wanted_output != NULL)
+ wanted_string = test->wanted_output;
+ else
+ wanted_string = test->input;
+ test_assert_idx(strcmp(str_c(str), wanted_string) == 0, i);
+
+ if ((test->flags & TEST_MESSAGE_ADDRESS_FLAG_SKIP_LIST) != 0)
+ continue;
+
+ /* test the address as a list of itself */
+ for (unsigned int list_length = 2; list_length <= 5; list_length++) {
+ str_truncate(group, 0);
+ str_append(group, test->input);
+ for (unsigned int j = 1; j < list_length; j++) {
+ if ((j % 2) == 0)
+ str_append(group, ",");
+ else
+ str_append(group, " , \n ");
+ str_append(group, test->input);
+ }
+
+ addr = test_parse_address(str_c(group), fill_missing);
+ for (unsigned int j = 0; j < list_length; j++) {
+ test_assert_idx(addr != NULL &&
+ cmp_addr(addr, test_wanted_addr), i);
+ if (addr != NULL)
+ addr = addr->next;
+ }
+ test_assert_idx(addr == NULL, i);
+ }
+
+ /* test the address as a group of itself */
+ for (unsigned int list_length = 1; list_length <= 5; list_length++) {
+ str_truncate(group, 0);
+ str_printfa(group, "group: %s", test->input);
+ for (unsigned int j = 1; j < list_length; j++) {
+ if ((j % 2) == 0)
+ str_append(group, ",");
+ else
+ str_append(group, " , \n ");
+ str_append(group, test->input);
+ }
+ str_append_c(group, ';');
+
+ addr = test_parse_address(str_c(group), fill_missing);
+ test_assert(addr != NULL && cmp_addr(addr, &group_prefix));
+ addr = addr->next;
+ for (unsigned int j = 0; j < list_length; j++) {
+ test_assert_idx(addr != NULL &&
+ cmp_addr(addr, test_wanted_addr), i);
+ if (addr != NULL)
+ addr = addr->next;
+ }
+ test_assert_idx(addr != NULL && addr->next == NULL &&
+ cmp_addr(addr, &group_suffix), i);
+ }
+ }
+ test_end();
+
+ test_begin("message address parsing with empty group");
+ str_truncate(group, 0);
+ str_append(group, "group:;");
+ addr = test_parse_address(str_c(group), FALSE);
+ str_truncate(str, 0);
+ message_address_write(str, addr);
+ test_assert(addr != NULL && cmp_addr(addr, &group_prefix));
+ addr = addr->next;
+ test_assert(addr != NULL && addr->next == NULL &&
+ cmp_addr(addr, &group_suffix));
+ test_assert(strcmp(str_c(str), "group:;") == 0);
+ test_end();
+
+ test_begin("message address parsing empty string");
+ test_assert(message_address_parse(unsafe_data_stack_pool, &uchar_nul, 0, 10,
+ MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING) == NULL);
+ str_truncate(str, 0);
+ message_address_write(str, NULL);
+ test_assert(str_len(str) == 0);
+ test_end();
+}
+
+static void test_message_address_nuls(void)
+{
+ const unsigned char input[] =
+ "\"user\0nuls\\\0-esc\"@[domain\0nuls\\\0-esc] (comment\0nuls\\\0-esc)";
+ const struct message_address output = {
+ NULL, "comment\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", NULL,
+ "user\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc",
+ "[domain\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc]", FALSE
+ };
+ const struct message_address *addr;
+
+ test_begin("message address parsing with NULs");
+ addr = message_address_parse(pool_datastack_create(),
+ input, sizeof(input)-1, UINT_MAX, 0);
+ test_assert(addr != NULL && cmp_addr(addr, &output));
+ test_end();
+}
+
+static void test_message_address_nuls_display_name(void)
+{
+ const unsigned char input[] =
+ "\"displayname\0nuls\\\0-esc\" <\"user\0nuls\\\0-esc\"@[domain\0nuls\\\0-esc]>";
+ const struct message_address output = {
+ NULL, "displayname\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc", NULL,
+ "user\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc",
+ "[domain\xEF\xBF\xBDnuls\\\xEF\xBF\xBD-esc]", FALSE
+ };
+ const struct message_address *addr;
+
+ test_begin("message address parsing with NULs in display-name");
+ addr = message_address_parse(pool_datastack_create(),
+ input, sizeof(input)-1, UINT_MAX, 0);
+ test_assert(addr != NULL && cmp_addr(addr, &output));
+ test_end();
+}
+
+static void test_message_address_non_strict_dots(void)
+{
+ const char *const inputs[] = {
+ ".@example.com",
+ "..@example.com",
+ "..foo@example.com",
+ "..foo..@example.com",
+ "..foo..bar..@example.com",
+ };
+ const struct message_address *addr;
+ struct message_address output = {
+ NULL, NULL, NULL, "local-part",
+ "example.com", FALSE
+ };
+
+ test_begin("message address parsing with non-strict dots");
+ for (unsigned int i = 0; i < N_ELEMENTS(inputs); i++) {
+ const unsigned char *addr_input =
+ (const unsigned char *)inputs[i];
+ /* invalid with strict-dots flag */
+ addr = message_address_parse(pool_datastack_create(),
+ addr_input, strlen(inputs[i]), UINT_MAX,
+ MESSAGE_ADDRESS_PARSE_FLAG_STRICT_DOTS);
+ test_assert_idx(addr != NULL && addr->invalid_syntax, i);
+
+ /* valid without the strict-dots flag */
+ addr = message_address_parse(pool_datastack_create(),
+ addr_input, strlen(inputs[i]), UINT_MAX, 0);
+ output.mailbox = t_strcut(inputs[i], '@');
+ test_assert_idx(addr != NULL && cmp_addr(addr, &output), i);
+ }
+ test_end();
+}
+
+static int
+test_parse_path(const char *input, const struct message_address **addr_r)
+{
+ struct message_address *addr;
+ char *input_dup;
+ int ret;
+
+ /* duplicate the input (without trailing NUL) so valgrind notices
+ if there's any out-of-bounds access */
+ size_t input_len = strlen(input);
+ if (input_len > 0)
+ input = input_dup = i_memdup(input, input_len);
+ ret = message_address_parse_path(pool_datastack_create(),
+ (const unsigned char *)input, input_len,
+ &addr);
+ if (input_len > 0)
+ i_free(input_dup);
+ *addr_r = addr;
+ return ret;
+}
+
+static void test_message_address_path(void)
+{
+ static const struct test {
+ const char *input;
+ const char *wanted_output;
+ struct message_address addr;
+ } tests[] = {
+ { "<>", NULL,
+ { NULL, NULL, NULL, NULL, NULL, FALSE } },
+ { " < > ", "<>",
+ { NULL, NULL, NULL, NULL, NULL, FALSE } },
+ { "<user@domain>", NULL,
+ { NULL, NULL, NULL, "user", "domain", FALSE } },
+ { " <user@domain> ", "<user@domain>",
+ { NULL, NULL, NULL, "user", "domain", FALSE } },
+ { "user@domain", "<user@domain>",
+ { NULL, NULL, NULL, "user", "domain", FALSE } },
+ { " user@domain ", "<user@domain>",
+ { NULL, NULL, NULL, "user", "domain", FALSE } },
+ { "<\"user\"@domain>", "<user@domain>",
+ { NULL, NULL, NULL, "user", "domain", FALSE } },
+ { "<\"user name\"@domain>", NULL,
+ { NULL, NULL, NULL, "user name", "domain", FALSE } },
+ { "<\"user@na\\\\me\"@domain>", NULL,
+ { NULL, NULL, NULL, "user@na\\me", "domain", FALSE } },
+ { "<\"user\\\"name\"@domain>", NULL,
+ { NULL, NULL, NULL, "user\"name", "domain", FALSE } },
+ { "<\"\"@domain>", NULL,
+ { NULL, NULL, NULL, "", "domain", FALSE } },
+ { "<@source", "<>",
+ { NULL, NULL, NULL, NULL, NULL, TRUE } },
+ };
+ const struct message_address *addr;
+ string_t *str;
+ const char *wanted_string;
+ unsigned int i;
+
+ test_begin("message address path parsing");
+ str = t_str_new(128);
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const struct test *test = &tests[i];
+ const struct message_address *test_wanted_addr;
+ int ret;
+
+ test_wanted_addr = &test->addr;
+ ret = test_parse_path(test->input, &addr);
+ if (addr->invalid_syntax)
+ test_assert_idx(ret == -1, i);
+ else
+ test_assert_idx(ret == 0, i);
+ test_assert_idx(addr != NULL && addr->next == NULL &&
+ cmp_addr(addr, test_wanted_addr), i);
+
+ /* test the address alone */
+ str_truncate(str, 0);
+ message_address_write(str, addr);
+ if (test->wanted_output != NULL)
+ wanted_string = test->wanted_output;
+ else
+ wanted_string = test->input;
+ test_assert_idx(strcmp(str_c(str), wanted_string) == 0, i);
+ }
+ test_end();
+}
+
+static void test_message_address_path_invalid(void)
+{
+ static const char *tests[] = {
+ "",
+ "<",
+ " < ",
+ ">",
+ " > ",
+ "<user@domain",
+ " <user@domain ",
+ "user@domain>",
+ " user@domain> ",
+ "<user>",
+ "<@route@route2:user>",
+ "<@domain>",
+ "@domain",
+ " @domain ",
+ "<user@>",
+ "user@",
+ " user@ ",
+ "<user@domain>bladiebla",
+ "user@domain@"
+ };
+ const struct message_address *addr;
+ unsigned int i;
+
+ test_begin("message address path invalid");
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const char *test = tests[i];
+ int ret;
+
+ ret = test_parse_path(test, &addr);
+ test_assert_idx(ret < 0, i);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_address,
+ test_message_address_nuls,
+ test_message_address_nuls_display_name,
+ test_message_address_non_strict_dots,
+ test_message_address_path,
+ test_message_address_path_invalid,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-date.c b/src/lib-mail/test-message-date.c
new file mode 100644
index 0000000..b522ff2
--- /dev/null
+++ b/src/lib-mail/test-message-date.c
@@ -0,0 +1,64 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "message-date.h"
+#include "test-common.h"
+
+struct test_message_date {
+ const char *input;
+ time_t time;
+ int tz_offset;
+ bool ret;
+};
+
+static void test_message_date_parse(void)
+{
+ static const struct test_message_date tests[] = {
+#ifdef TIME_T_SIGNED
+ { "Thu, 01 Jan 1970 01:59:59 +0200", -1, 2*60, TRUE },
+ { "Fri, 13 Dec 1901 20:45:53 +0000", -2147483647, 0, TRUE },
+#endif
+#if (TIME_T_MAX_BITS > 32 || !defined(TIME_T_SIGNED))
+ { "Sun, 07 Feb 2106 06:28:15 +0000", 4294967295U, 0, TRUE },
+#endif
+ { "Wed, 07 Nov 2007 01:07:20 +0200", 1194390440, 2*60, TRUE },
+ { "Wed, 07 Nov 2007 01:07:20", 1194397640, 0, TRUE },
+ { "Thu, 01 Jan 1970 02:00:00 +0200", 0, 2*60, TRUE },
+ { "Tue, 19 Jan 2038 03:14:07 +0000", 2147483647, 0, TRUE },
+ { "Tue, 19 Jan 2038", 0, 0, FALSE },
+ /* June leap second */
+ { "Tue, 30 Jun 2015 23:59:59 +0300", 1435697999, 3*60, TRUE },
+ { "Tue, 30 Jun 2015 23:59:60 +0300", 1435697999, 3*60, TRUE },
+ { "Wed, 01 Jul 2015 00:00:00 +0300", 1435698000, 3*60, TRUE },
+ /* Invalid leap second */
+ { "Tue, 24 Jan 2017 15:59:60 +0300", 1485262799, 3*60, TRUE },
+ /* December leap second */
+ { "Sat, 31 Dec 2016 23:59:59 +0200", 1483221599, 2*60, TRUE },
+ { "Sat, 31 Dec 2016 23:59:60 +0200", 1483221599, 2*60, TRUE },
+ { "Sun, 01 Jan 2017 00:00:00 +0200", 1483221600, 2*60, TRUE },
+ };
+ unsigned int i;
+ bool success;
+ time_t t;
+ int tz;
+ bool ret;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const struct test_message_date *test = &tests[i];
+ ret = message_date_parse((const unsigned char *)test->input,
+ strlen(test->input), &t, &tz);
+ success = (!ret && !test->ret) ||
+ (ret == test->ret && t == test->time &&
+ tz == test->tz_offset);
+ test_out(t_strdup_printf("message_date_parse(%d)", i), success);
+ }
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_date_parse,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-decoder.c b/src/lib-mail/test-message-decoder.c
new file mode 100644
index 0000000..edf9210
--- /dev/null
+++ b/src/lib-mail/test-message-decoder.c
@@ -0,0 +1,513 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "charset-utf8.h"
+#include "message-parser.h"
+#include "message-header-decode.h"
+#include "message-decoder.h"
+#include "message-part-data.h"
+#include "test-common.h"
+
+void message_header_decode_utf8(const unsigned char *data, size_t size,
+ buffer_t *dest,
+ normalizer_func_t *normalizer ATTR_UNUSED)
+{
+ buffer_append(dest, data, size);
+}
+
+static void test_message_decoder(void)
+{
+ struct message_decoder_context *ctx;
+ struct message_part part;
+ struct message_header_line hdr;
+ struct message_block input, output;
+
+ test_begin("message decoder");
+
+ i_zero(&part);
+ i_zero(&input);
+ memset(&output, 0xff, sizeof(output));
+ input.part = &part;
+
+ ctx = message_decoder_init(NULL, 0);
+
+ i_zero(&hdr);
+ hdr.name = "Content-Transfer-Encoding";
+ hdr.name_len = strlen(hdr.name);
+ hdr.full_value = (const void *)"quoted-printable";
+ hdr.full_value_len = strlen((const char *)hdr.full_value);
+ input.hdr = &hdr;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 0);
+
+ input.hdr = NULL;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+
+ input.hdr = NULL;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+
+ input.data = (const void *)"foo ";
+ input.size = strlen((const char *)input.data);
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 3);
+ test_assert(memcmp(output.data, "foo", 3) == 0);
+
+ input.data = (const void *)"bar";
+ input.size = strlen((const char *)input.data);
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 14);
+ test_assert(memcmp(output.data, " bar", 14) == 0);
+
+ /* partial text - \xC3\xA4 in quoted-printable. we should get a single
+ UTF-8 letter as result */
+ input.data = (const void *)"="; input.size = 1;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 0);
+ input.data = (const void *)"C"; input.size = 1;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 0);
+ input.data = (const void *)"3"; input.size = 1;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 0);
+ input.data = (const void *)"=A"; input.size = 2;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 0);
+ input.data = (const void *)"4"; input.size = 1;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(output.size == 2);
+ test_assert(memcmp(output.data, "\xC3\xA4", 2) == 0);
+
+ message_decoder_deinit(&ctx);
+
+ test_end();
+}
+
+static void test_message_decoder_multipart(void)
+{
+ static const char test_message_input[] =
+ "Content-Type: multipart/mixed; boundary=foo\n"
+ "\n"
+ "--foo\n"
+ "Content-Transfer-Encoding: quoted-printable\n"
+ "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "p=C3=A4iv=C3=A4=C3=A4\n"
+ "\n"
+ "--foo\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "ecO2dMOkIHZhYW4uCg== ignored\n"
+ "--foo\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "?garbage\n"
+ "--foo--\n";
+ const struct message_parser_settings parser_set = { .flags = 0, };
+ struct message_parser_ctx *parser;
+ struct message_decoder_context *decoder;
+ struct message_part *parts;
+ struct message_block input, output;
+ struct istream *istream;
+ string_t *str_out = t_str_new(20);
+ int ret;
+
+ test_begin("message decoder multipart");
+
+ istream = test_istream_create(test_message_input);
+ parser = message_parser_init(pool_datastack_create(), istream, &parser_set);
+ decoder = message_decoder_init(NULL, 0);
+
+ test_istream_set_allow_eof(istream, FALSE);
+ for (size_t i = 0; i < sizeof(test_message_input); i++) {
+ if (i == sizeof(test_message_input)-1)
+ test_istream_set_allow_eof(istream, TRUE);
+ test_istream_set_size(istream, i);
+ while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+ if (message_decoder_decode_next_block(decoder, &input, &output) &&
+ output.hdr == NULL && output.size > 0)
+ str_append_data(str_out, output.data, output.size);
+ }
+ if (i == sizeof(test_message_input)-1)
+ test_assert(ret == -1);
+ else
+ test_assert(ret == 0);
+ }
+ /* NOTE: qp-decoder decoder changes \n into \r\n */
+ test_assert_strcmp(str_c(str_out), "p\xC3\xA4iv\xC3\xA4\xC3\xA4\r\ny\xC3\xB6t\xC3\xA4 vaan.\n");
+
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+ test_assert(istream->stream_errno == 0);
+ i_stream_unref(&istream);
+ test_end();
+}
+
+static void test_message_decoder_current_content_type(void)
+{
+ struct message_decoder_context *ctx;
+ struct message_part part, part2, part3;
+ struct message_header_line hdr;
+ struct message_block input, output;
+
+ test_begin("message_decoder_current_content_type()");
+
+ i_zero(&part);
+ part2 = part3 = part;
+
+ i_zero(&input);
+ memset(&output, 0xff, sizeof(output));
+ input.part = &part;
+
+ ctx = message_decoder_init(NULL, 0);
+ test_assert(message_decoder_current_content_type(ctx) == NULL);
+
+ /* multipart/mixed */
+ i_zero(&hdr);
+ hdr.name = "Content-Type";
+ hdr.name_len = strlen(hdr.name);
+ hdr.full_value = (const void *)"multipart/mixed; boundary=x";
+ hdr.full_value_len = strlen((const char *)hdr.full_value);
+ input.hdr = &hdr;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+
+ input.hdr = NULL;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(strcmp(message_decoder_current_content_type(ctx), "multipart/mixed") == 0);
+
+ /* child 1 */
+ input.part = &part2;
+ hdr.full_value = (const void *)"text/plain";
+ hdr.full_value_len = strlen((const char *)hdr.full_value);
+ input.hdr = &hdr;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+
+ input.hdr = NULL;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(strcmp(message_decoder_current_content_type(ctx), "text/plain") == 0);
+
+ /* child 2 */
+ input.part = &part3;
+ hdr.full_value = (const void *)"application/pdf";
+ hdr.full_value_len = strlen((const char *)hdr.full_value);
+ input.hdr = &hdr;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+
+ input.hdr = NULL;
+ test_assert(message_decoder_decode_next_block(ctx, &input, &output));
+ test_assert(strcmp(message_decoder_current_content_type(ctx), "application/pdf") == 0);
+
+ /* reset */
+ message_decoder_decode_reset(ctx);
+ test_assert(message_decoder_current_content_type(ctx) == NULL);
+
+ message_decoder_deinit(&ctx);
+
+ test_end();
+}
+
+static void test_message_decoder_content_transfer_encoding(void)
+{
+ static const unsigned char test_message_input[] =
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: 1.0\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 7bit\n"
+"Content-Type: text/plain; charset=us-ascii\n\n"
+"Move black king to queen's bishop\n\n"
+"--1\n"
+"Content-Transfer-Encoding:\t\t\t\tbinary\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 8bit\t\t\t\r\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: quoted-printable \r\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move =E2=99=9A to =E2=99=9B's =E2=99=9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: base64\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"TW92ZSDimZogdG8g4pmbJ3Mg4pmdCg==\n\n"
+"--1--\n";
+
+ static const char test_message_output[] =
+"Move black king to queen's bishop\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\r\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n";
+
+ test_begin("message decoder content transfer encoding");
+
+ const struct message_parser_settings parser_set = { .flags = 0, };
+ struct message_parser_ctx *parser;
+ struct message_decoder_context *decoder;
+ struct message_part *parts, *part;
+ struct message_block input, output;
+ struct istream *istream;
+ string_t *str_out = t_str_new(20);
+ int ret;
+
+ pool_t pool = pool_alloconly_create("message parser", 10240);
+ istream = test_istream_create_data(test_message_input,
+ sizeof(test_message_input)-1);
+ parser = message_parser_init(pool, istream, &parser_set);
+ decoder = message_decoder_init(NULL, 0);
+
+ while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+ message_part_data_parse_from_header(pool, input.part, input.hdr);
+ if (message_decoder_decode_next_block(decoder, &input, &output) &&
+ output.hdr == NULL && output.size > 0)
+ str_append_data(str_out, output.data, output.size);
+ }
+
+ test_assert(ret == -1);
+ test_assert_strcmp(test_message_output, str_c(str_out));
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+ test_assert(istream->stream_errno == 0);
+
+ /* validate parts */
+
+ part = parts;
+ test_assert(part->children_count == 5);
+ part = part->children;
+ test_assert_strcmp(part->data->content_type, "text");
+ test_assert_strcmp(part->data->content_subtype, "plain");
+ test_assert_strcmp(part->data->content_transfer_encoding, "7bit");
+ test_assert_strcmp(part->data->content_type, "text");
+
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "binary");
+ test_assert_strcmp(part->data->content_type, "text");
+ test_assert_strcmp(part->data->content_subtype, "plain");
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "8bit");
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "quoted-printable");
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "base64");
+ i_stream_unref(&istream);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_decoder_invalid_content_transfer_encoding(void)
+{
+ static const unsigned char test_message_input[] =
+ /* all of the child parts have invalid content transfer encoding */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: 1.0\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 6bit\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move black king to queen's bishop\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 7bits\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 8 bit\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 7-bit\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding: 8-bit\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move \xE2\x99\x9A to \xE2\x99\x9B's \xE2\x99\x9D\n\n"
+"--1\n"
+"Content-Transfer-Encoding:\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"Move =E2=99=9A to =E2=99=9B's =E2=99=9D\n\n"
+"--1--\n";
+
+ const char *test_message_output = "";
+
+ test_begin("message decoder content transfer invalid encoding");
+
+ const struct message_parser_settings parser_set = { .flags = 0 };
+ struct message_parser_ctx *parser;
+ struct message_decoder_context *decoder;
+ struct message_part *parts, *part;
+ struct message_block input, output;
+ struct istream *istream;
+ string_t *str_out = t_str_new(20);
+ int ret;
+
+ pool_t pool = pool_alloconly_create("message parser", 10240);
+ istream = test_istream_create_data(test_message_input,
+ sizeof(test_message_input)-1);
+ parser = message_parser_init(pool, istream, &parser_set);
+ decoder = message_decoder_init(NULL, 0);
+
+ while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+ message_part_data_parse_from_header(pool, input.part, input.hdr);
+ if (input.hdr != NULL &&
+ strcasecmp(input.hdr->name, "content-transfer-encoding") == 0) {
+ enum message_cte cte = message_decoder_parse_cte(input.hdr);
+ test_assert(cte == MESSAGE_CTE_UNKNOWN);
+ }
+ if (message_decoder_decode_next_block(decoder, &input, &output) &&
+ output.hdr == NULL && output.size > 0)
+ str_append_data(str_out, output.data, output.size);
+ }
+
+ test_assert(ret == -1);
+ test_assert_strcmp(test_message_output, str_c(str_out));
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+ test_assert(istream->stream_errno == 0);
+
+ part = parts;
+ test_assert(part->children_count == 6);
+ part = part->children;
+ test_assert_strcmp(part->data->content_type, "text");
+ test_assert_strcmp(part->data->content_subtype, "plain");
+ test_assert_strcmp(part->data->content_transfer_encoding, "6bit");
+ test_assert_strcmp(part->data->content_type, "text");
+
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "7bits");
+ test_assert_strcmp(part->data->content_type, "text");
+ test_assert_strcmp(part->data->content_subtype, "plain");
+ part = part->next;
+ test_assert(part->data->content_transfer_encoding == NULL);
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "7-bit");
+ part = part->next;
+ test_assert_strcmp(part->data->content_transfer_encoding, "8-bit");
+ part = part->next;
+ test_assert(part->next == NULL);
+ i_stream_unref(&istream);
+ pool_unref(&pool);
+
+#define X10(a) a a a a a a a a a a
+
+#undef TEST_CASE
+#define TEST_CASE(value, result) \
+ { \
+ .hdr = { \
+ .name = "Content-Transfer-Encoding", \
+ .name_len = 25, \
+ .full_value = (const unsigned char*)value, \
+ .full_value_len = sizeof(value)-1, \
+ }, \
+ .cte = result, \
+ }
+
+ const struct {
+ const struct message_header_line hdr;
+ enum message_cte cte;
+ } test_case[] = {
+ TEST_CASE("(binary comment) base64", MESSAGE_CTE_BASE64),
+ TEST_CASE("(\"binary\" ( (comment) test) ) base64", MESSAGE_CTE_BASE64),
+ TEST_CASE("base64 binary", MESSAGE_CTE_UNKNOWN),
+ TEST_CASE("base64\0binary", MESSAGE_CTE_UNKNOWN),
+ TEST_CASE("\0binary", MESSAGE_CTE_UNKNOWN),
+ TEST_CASE("( " X10(X10(X10(X10("a")))) " ) base64", MESSAGE_CTE_BASE64),
+ TEST_CASE("( " X10(X10(X10(X10("a")))) " ) base64 ( " X10(X10(X10(X10("a")))) ")", MESSAGE_CTE_BASE64),
+ TEST_CASE("( base64", MESSAGE_CTE_UNKNOWN),
+ TEST_CASE("base64 (", MESSAGE_CTE_BASE64),
+ TEST_CASE(X10(X10(X10(X10(" ")))) " base64", MESSAGE_CTE_BASE64),
+ TEST_CASE("base64 ; logging-type=\"foobar\"", MESSAGE_CTE_BASE64),
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS(test_case); i++) {
+ test_assert_idx(message_decoder_parse_cte(&test_case[i].hdr) == test_case[i].cte, i);
+ }
+
+ test_end();
+}
+
+static void test_message_decoder_charset(void)
+{
+ /* ensure we decode correctly */
+ static const unsigned char test_message_input[] =
+ /* none of these should work */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: 1.0\n\n"
+"--1\n"
+"Content-Transfer-Encoding: binary\n"
+"Content-Type: text/plain; charset=utf-16le\n\n"
+"\x54\x00\x65\x00\x73\x00\x74\x00\x20\x00\x6d\x00\x65\x00\x73\x00\x73\x00\x61\x00\x67\x00\x65\x00\n\x00\n"
+"--1\n"
+"Content-Transfer-Encoding: base64\n"
+"Content-Type: text/plain; charset=utf-16be\n\n"
+"AFQAZQBzAHQAIABtAGUAcwBzAGEAZwBlAAo=\n\n"
+"--1\n"
+"Content-Transfer-Encoding: base64\n"
+"Content-Type: text/plain; charset=utf-16le\n\n"
+"VABlAHMAdAAgAG0AZQBzAHMAYQBnAGUACgA=\n\n"
+"--1\n"
+"Content-Transfer-Encoding: base64\n"
+"Content-Type: text/plain; charset=EUC-JP\n\n"
+"odjApLOmv824osDruMCh2Q==\n\n"
+"--1\n"
+"Content-Transfer-Encoding: binary\n"
+"Content-Type: text/plain; charset=UTF-8\n\n"
+"\xad\xad\xad\xad\xad\xad\n"
+"--1--\n";
+
+ static const char *test_message_output =
+"Test message\nTest message\nTest message\n"
+"\xe3\x80\x8e\xe4\xb8\x96\xe7\x95\x8c\xe4\xba\xba"
+"\xe6\xa8\xa9\xe5\xae\xa3\xe8\xa8\x80\xe3\x80\x8f"
+UNICODE_REPLACEMENT_CHAR_UTF8;
+
+ test_begin("message decoder charset");
+
+ const struct message_parser_settings parser_set = { .flags = 0, };
+ struct message_parser_ctx *parser;
+ struct message_decoder_context *decoder;
+ struct message_part *parts;
+ struct message_block input, output;
+ struct istream *istream;
+ string_t *str_out = t_str_new(20);
+ int ret;
+
+ pool_t pool = pool_alloconly_create("message parser", 10240);
+ istream = test_istream_create_data(test_message_input,
+ sizeof(test_message_input)-1);
+ parser = message_parser_init(pool, istream, &parser_set);
+ decoder = message_decoder_init(NULL, 0);
+
+ while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+ message_part_data_parse_from_header(pool, input.part, input.hdr);
+ if (message_decoder_decode_next_block(decoder, &input, &output) &&
+ output.hdr == NULL && output.size > 0)
+ str_append_data(str_out, output.data, output.size);
+ }
+
+ test_assert(ret == -1);
+ test_assert_strcmp(test_message_output, str_c(str_out));
+ message_decoder_deinit(&decoder);
+ message_parser_deinit(&parser, &parts);
+ test_assert(istream->stream_errno == 0);
+
+ i_stream_unref(&istream);
+ pool_unref(&pool);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_decoder,
+ test_message_decoder_multipart,
+ test_message_decoder_current_content_type,
+ test_message_decoder_content_transfer_encoding,
+ test_message_decoder_invalid_content_transfer_encoding,
+ test_message_decoder_charset,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-header-decode.c b/src/lib-mail/test-message-header-decode.c
new file mode 100644
index 0000000..0221d1a
--- /dev/null
+++ b/src/lib-mail/test-message-header-decode.c
@@ -0,0 +1,216 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "randgen.h"
+#include "charset-utf8.h"
+#include "message-header-encode.h"
+#include "message-header-decode.h"
+#include "test-common.h"
+
+static void test_message_header_decode(void)
+{
+ static const char *data[] = {
+ " \t=?utf-8?q?=c3=a4?= =?utf-8?q?=c3=a4?= b \t\r\n ", "\xC3\xA4\xC3\xA4 b \t\r\n ",
+ "a =?utf-8?q?=c3=a4?= b", "a \xC3\xA4 b",
+ "a =?utf-8?q?=c3=a4?= b", "a \xC3\xA4 b",
+ "a =?utf-8?q?=c3=a4?=\t\t\r\n =?utf-8?q?=c3=a4?= b", "a \xC3\xA4\xC3\xA4 b",
+ "a =?utf-8?q?=c3=a4?= x =?utf-8?q?=c3=a4?= b", "a \xC3\xA4 x \xC3\xA4 b",
+ "a =?utf-8?b?w6TDpCDDpA==?= b", "a \xC3\xA4\xC3\xA4 \xC3\xA4 b",
+ "=?utf-8?b?w6Qgw6Q=?=", "\xC3\xA4 \xC3\xA4",
+ "a =?utf-8?b?////?= b", "a "UNICODE_REPLACEMENT_CHAR_UTF8" b",
+ "a =?utf-16le?b?UADkAGkAdgDkAOQA?= b", "a P\xC3\xA4iv\xC3\xA4\xC3\xA4 b",
+ "a =?utf-9?b?UMOkaXbDpMOk?= b", "a P\xC3\xA4iv\xC3\xA4\xC3\xA4 b",
+
+ };
+ string_t *dest;
+ unsigned int i;
+
+ test_begin("message header decode");
+
+ dest = t_str_new(256);
+ for (i = 0; i < N_ELEMENTS(data); i += 2) {
+ str_truncate(dest, 0);
+ message_header_decode_utf8((const unsigned char *)data[i],
+ strlen(data[i]), dest, NULL);
+ test_assert_strcmp_idx(str_c(dest), data[i+1], i / 2);
+ }
+ test_end();
+}
+
+static void test_message_header_decode_read_overflow(void)
+{
+ const unsigned char input[] = "=?utf-8?Q?=EF?=";
+ string_t *dest = t_str_new(32);
+
+ test_begin("message header decode read overflow");
+ message_header_decode_utf8(input, sizeof(input)-2, dest, NULL);
+ test_end();
+}
+
+static void check_encoded(string_t *encoded, unsigned int test_idx)
+{
+ const unsigned char *enc = str_data(encoded), *p, *pend;
+ size_t enc_len = str_len(encoded), cur_line_len = 0;
+
+ p = enc;
+ pend = enc + enc_len;
+ while (p < pend) {
+ if (*p == '\r') {
+ p++;
+ continue;
+ }
+ if (*p == '\n') {
+ test_assert_idx(cur_line_len <= 76, test_idx);
+ cur_line_len = 0;
+ p++;
+ continue;
+ }
+ cur_line_len++;
+ test_assert_idx((*p >= 0x20 && *p <= 0x7e) || *p == '\t',
+ test_idx);
+ p++;
+ }
+
+ test_assert_idx(cur_line_len <= 76, test_idx);
+}
+
+static void
+check_encode_decode_result(const unsigned char *inbuf, size_t inbuf_len,
+ string_t *out, unsigned int test_idx)
+{
+ static const unsigned char *rep_char =
+ (const unsigned char *)UNICODE_REPLACEMENT_CHAR_UTF8;
+ static const ptrdiff_t rep_char_len =
+ UNICODE_REPLACEMENT_CHAR_UTF8_LEN;
+ const unsigned char *outbuf = str_data(out);
+ size_t outbuf_len = str_len(out);
+ const unsigned char *pin, *pinend, *pout, *poutend;
+ bool invalid_char = FALSE;
+
+ if (test_has_failed())
+ return;
+
+ pin = inbuf;
+ pinend = inbuf + inbuf_len;
+ pout = outbuf;
+ poutend = outbuf + outbuf_len;
+
+ while (pin < pinend) {
+ unichar_t ch;
+ int nch;
+
+ nch = uni_utf8_get_char_n(pin, pinend - pin, &ch);
+ if (nch <= 0) {
+ /* Invalid character; check proper substitution of
+ replacement character in encoded/decoded output. */
+ pin++;
+ if (!invalid_char) {
+ /* Only one character is substituted for a run
+ of bad stuff. */
+ test_assert_idx(
+ (poutend - pout) >= rep_char_len &&
+ memcmp(pout, rep_char,
+ rep_char_len) == 0, test_idx);
+ pout += rep_char_len;
+ }
+ invalid_char = TRUE;
+ } else {
+ /* Valid character; check matching character bytes. */
+ invalid_char = FALSE;
+ test_assert_idx((pinend - pin) >= nch &&
+ (poutend - pout) >= nch &&
+ memcmp(pin, pout, nch) == 0, test_idx);
+ pin += nch;
+ pout += nch;
+ }
+
+ if (test_has_failed())
+ return;
+ }
+
+ /* Both buffers must have reached the end now. */
+ test_assert_idx(pin == pinend && pout == poutend, test_idx);
+}
+
+static void test_message_header_decode_encode_random(void)
+{
+ string_t *encoded, *decoded;
+ unsigned char buf[1024];
+ unsigned int i, j, buflen;
+
+ test_begin("message header encode & decode randomly (7 bit)");
+
+ encoded = t_str_new(256);
+ decoded = t_str_new(256);
+ for (i = 0; i < 1000; i++) {
+ /* fill only with 7bit data so we don't have to worry about
+ the data being valid UTF-8 */
+ buflen = i_rand_limit(sizeof(buf));
+ for (j = 0; j < buflen; j++)
+ buf[j] = i_rand_limit(128);
+
+ str_truncate(encoded, 0);
+ str_truncate(decoded, 0);
+
+ /* test Q */
+ message_header_encode_q(buf, buflen, encoded, 0);
+ check_encoded(encoded, i);
+ message_header_decode_utf8(encoded->data, encoded->used,
+ decoded, NULL);
+ test_assert_idx(decoded->used == buflen &&
+ memcmp(decoded->data, buf, buflen) == 0, i);
+
+ /* test B */
+ str_truncate(encoded, 0);
+ str_truncate(decoded, 0);
+
+ message_header_encode_b(buf, buflen, encoded, 0);
+ check_encoded(encoded, i);
+ message_header_decode_utf8(encoded->data, encoded->used,
+ decoded, NULL);
+ test_assert_idx(decoded->used == buflen &&
+ memcmp(decoded->data, buf, buflen) == 0, i);
+ }
+ test_end();
+
+ test_begin("message header encode & decode randomly (8 bit)");
+
+ for (i = 0; i < 1000; i++) {
+ buflen = i_rand_limit(sizeof(buf));
+ random_fill(buf, buflen);
+
+ str_truncate(encoded, 0);
+ str_truncate(decoded, 0);
+
+ /* test Q */
+ message_header_encode_q(buf, buflen, encoded, 0);
+ check_encoded(encoded, i);
+ message_header_decode_utf8(encoded->data, encoded->used,
+ decoded, NULL);
+ check_encode_decode_result(buf, buflen, decoded, i);
+
+ /* test B */
+ str_truncate(encoded, 0);
+ str_truncate(decoded, 0);
+
+ message_header_encode_b(buf, buflen, encoded, 0);
+ check_encoded(encoded, i);
+ message_header_decode_utf8(encoded->data, encoded->used,
+ decoded, NULL);
+ check_encode_decode_result(buf, buflen, decoded, i);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_header_decode,
+ test_message_header_decode_read_overflow,
+ test_message_header_decode_encode_random,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-header-encode.c b/src/lib-mail/test-message-header-encode.c
new file mode 100644
index 0000000..b1c5645
--- /dev/null
+++ b/src/lib-mail/test-message-header-encode.c
@@ -0,0 +1,295 @@
+/* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "base64.h"
+#include "buffer.h"
+#include "str.h"
+#include "message-header-encode.h"
+#include "test-common.h"
+
+static bool verify_q(const char *str, unsigned int i, bool starts_with_a)
+{
+ unsigned int line_start = i, char_count = 0;
+
+ if (str_begins(str+i, "\n\t")) {
+ i += 2;
+ line_start = i - 1;
+ }
+
+ for (;;) {
+ if (!str_begins(str+i, "=?utf-8?q?"))
+ return FALSE;
+ i += 10;
+
+ if (starts_with_a) {
+ if (str[i] != 'a')
+ return FALSE;
+ starts_with_a = FALSE;
+ i++;
+ }
+ while (!str_begins(str+i, "?=")) {
+ if (!str_begins(str+i, "=C3=A4"))
+ return FALSE;
+ i += 6;
+ char_count++;
+ }
+ i += 2;
+ if (i - line_start > 76)
+ return FALSE;
+
+ if (str[i] == '\0')
+ break;
+ if (!str_begins(str+i, "\n\t"))
+ return FALSE;
+ i += 2;
+ line_start = i - 1;
+ }
+ return char_count == 40;
+}
+
+static void test_message_header_encode_q(void)
+{
+ string_t *input = t_str_new(100);
+ string_t *str = t_str_new(512);
+ unsigned int i, j, skip;
+
+ test_begin("message header encode q");
+
+ str_append_c(input, 'a');
+ for (i = 0; i < 40; i++)
+ str_append(input, "\xC3\xA4");
+ for (i = 0; i < 80; i++) {
+ for (skip = 0; skip < 2; skip++) {
+ str_truncate(str, 0);
+ for (j = 1; j < i; j++)
+ str_append_c(str, 'X');
+ if (i != 0)
+ str_append_c(str, ' ');
+
+ message_header_encode_q(str_data(input) + skip,
+ str_len(input) - skip, str,
+ i == 0 ? 0 : i+1);
+ test_assert(verify_q(str_c(str), i, skip == 0));
+ }
+ }
+ test_end();
+}
+
+static bool verify_b(const char *str, unsigned int i, bool starts_with_a)
+{
+ unsigned int line_start = i, start, j, char_count = 0;
+ char bufdata[1000];
+ buffer_t buf;
+
+ buffer_create_from_data(&buf, bufdata, sizeof(bufdata));
+ if (str_begins(str+i, "\n\t")) {
+ i += 2;
+ line_start = i - 1;
+ }
+
+ for (;;) {
+ if (!str_begins(str+i, "=?utf-8?b?"))
+ return FALSE;
+ i += 10;
+
+ start = i;
+ for (; str[i] != '?'; i++) {
+ if (str[i] == '\0')
+ return FALSE;
+ }
+ buffer_set_used_size(&buf, 0);
+ if (base64_decode(str+start, i-start, NULL, &buf) < 0)
+ return FALSE;
+ i++;
+
+ if (!starts_with_a)
+ j = 0;
+ else {
+ if (bufdata[0] != 'a')
+ return FALSE;
+ starts_with_a = FALSE;
+ j = 1;
+ }
+ for (; j < buf.used; j += 2) {
+ if (bufdata[j] != '\xc3' || bufdata[j+1] != '\xa4')
+ return FALSE;
+ char_count++;
+ }
+ if (j != buf.used)
+ return FALSE;
+
+ if (str[i++] != '=')
+ return FALSE;
+
+ if (i - line_start > 76)
+ return FALSE;
+
+ if (str[i] == '\0')
+ break;
+ if (!str_begins(str+i, "\n\t"))
+ return FALSE;
+ i += 2;
+ line_start = i - 1;
+ }
+ return char_count == 40;
+}
+
+static void test_message_header_encode_b(void)
+{
+ string_t *input = t_str_new(100);
+ string_t *str = t_str_new(512);
+ unsigned int i, j, skip;
+
+ test_begin("message header encode b");
+
+ str_append_c(input, 'a');
+ for (i = 0; i < 40; i++)
+ str_append(input, "\xC3\xA4");
+ for (i = 0; i < 80; i++) {
+ for (skip = 0; skip < 2; skip++) {
+ str_truncate(str, 0);
+ for (j = 1; j < i; j++)
+ str_append_c(str, 'X');
+ if (i != 0)
+ str_append_c(str, ' ');
+
+ message_header_encode_b(str_data(input) + skip,
+ str_len(input) - skip, str,
+ i == 0 ? 0 : i+1);
+ test_assert(verify_b(str_c(str), i, skip == 0));
+ }
+ }
+ test_end();
+}
+
+static void test_message_header_encode(void)
+{
+ const char *data[] = {
+ "a b", "a b",
+ "a bc\xC3\xA4""de f", "a =?utf-8?q?bc=C3=A4de?= f",
+ "a \xC3\xA4\xC3\xA4 \xC3\xA4 b", "a =?utf-8?b?w6TDpCDDpA==?= b",
+ "\xC3\xA4 a \xC3\xA4", "=?utf-8?q?=C3=A4_a_=C3=A4?=",
+ "\xC3\xA4\xC3\xA4 a \xC3\xA4", "=?utf-8?b?w6TDpCBhIMOk?=",
+ "=", "=",
+ "?", "?",
+ "a=?", "a=?",
+ "=?", "=?utf-8?q?=3D=3F?=",
+ "=?x", "=?utf-8?q?=3D=3Fx?=",
+ "a\n=?", "a\n\t=?utf-8?q?=3D=3F?=",
+ "a\t=?", "a\t=?utf-8?q?=3D=3F?=",
+ "a =?", "a =?utf-8?q?=3D=3F?=",
+ "foo\001bar", "=?utf-8?q?foo=01bar?=",
+ "\x01\x02\x03\x04\x05\x06\x07\x08", "=?utf-8?b?AQIDBAUGBwg=?=",
+#define TEXT30 "123456789012345678901234567890"
+ TEXT30 " \xc3\xa4 " TEXT30 "\xc3\xa4 stuff",
+ TEXT30 " =?utf-8?q?=C3=A4_12345678901234567890123456?=\n"
+ "\t=?utf-8?q?7890=C3=A4?= stuff",
+
+ "a\r\n b", "a\r\n b",
+ "a\r\n\tb", "a\r\n\tb",
+ "a\r\nb", "a\r\n\tb",
+ "a\n b", "a\n b",
+ "a\n b", "a\n b",
+ "a\nb", "a\n\tb",
+ "a\r\n", "a",
+ "a\n", "a",
+ "foo\n \001bar", "foo\n =?utf-8?q?=01bar?=",
+ "foo\001\n bar", "=?utf-8?q?foo=01?=\n bar",
+ "\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4"
+ "\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4\xC3\xA4",
+ "=?utf-8?b?w6TDpMOkw6TDpMOkw6TDpMOkw6TDpMOkw6TDpA==?=",
+ /* Bad UTF-8 */
+ "foofoo-\x80\x80\x80\x80\x80\x80\x80-barbar",
+ "=?utf-8?q?foofoo-=EF=BF=BD-barbar?=",
+ "foobarfoobar-\x80\x80\x80\x80\x80\x80\x80",
+ "=?utf-8?q?foobarfoobar-=EF=BF=BD?=",
+ "\x80\x80\x80\x80\x80\x80\x80-foobarfoobar",
+ "=?utf-8?q?=EF=BF=BD-foobarfoobar?=",
+ "foofoo-\x80\x80\x80\x80\x80\x80\x80-barbarbarbar-"
+ "\x81\x82\x83\x84\x85\x86\x87-bazbaz",
+ "=?utf-8?q?foofoo-=EF=BF=BD-barbarbarbar-"
+ "=EF=BF=BD-bazbaz?=",
+ "foobarfoobarfoobar-\x80\x80\x80\x80\x80\x80\x80-"
+ "\x81\x82\x83\x84\x85\x86\x87-bazbaz",
+ "=?utf-8?q?foobarfoobarfoobar-=EF=BF=BD-"
+ "=EF=BF=BD-bazbaz?=",
+ "\x80\x80\x80\x80\x80\x80\x80-foobarfoobarfoobar-"
+ "\x81\x82\x83\x84\x85\x86\x87-bazbaz",
+ "=?utf-8?q?=EF=BF=BD-foobarfoobarfoobar-"
+ "=EF=BF=BD-bazbaz?=",
+ "foofoo-\xC3-barbar",
+ "=?utf-8?q?foofoo-=EF=BF=BD-barbar?=",
+ "foobarfoobar-\xC3",
+ "=?utf-8?q?foobarfoobar-=EF=BF=BD?=",
+ "\xC3-foobarfoobar",
+ "=?utf-8?q?=EF=BF=BD-foobarfoobar?=",
+ "f-\x80\x80\x80\x80\x80\x80\x80-b", "=?utf-8?b?Zi3vv70tYg==?=",
+ "fb-\x80\x80\x80\x80\x80\x80\x80", "=?utf-8?b?ZmIt77+9?=",
+ "\x80\x80\x80\x80\x80\x80\x80-fb", "=?utf-8?b?77+9LWZi?=",
+ "ff-\x80\x80\x80\x80\x80\x80\x80-bb-"
+ "\x81\x82\x83\x84\x85\x86\x87-zz",
+ "=?utf-8?b?ZmYt77+9LWJiLe+/vS16eg==?=",
+ "fbfb-\x80\x80\x80\x80\x80\x80\x80-"
+ "\x81\x82\x83\x84\x85\x86\x87-zz",
+ "=?utf-8?b?ZmJmYi3vv70t77+9LXp6?=",
+ "\x80\x80\x80\x80\x80\x80\x80-ff-"
+ "\x81\x82\x83\x84\x85\x86\x87-zz",
+ "=?utf-8?b?77+9LWZmLe+/vS16eg==?=",
+ "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80",
+ "=?utf-8?b?77+9?=",
+ "-\xC3-\xC3-\xC3-\xC3-\xC3-\xC3-",
+ "=?utf-8?q?-=EF=BF=BD-=EF=BF=BD-"
+ "=EF=BF=BD-=EF=BF=BD-=EF=BF=BD-=EF=BF=BD-?=",
+ "\xC3--\xC3-\xC3-\xC3-\xC3--\xC3",
+ "=?utf-8?q?=EF=BF=BD--=EF=BF=BD-"
+ "=EF=BF=BD-=EF=BF=BD-=EF=BF=BD--=EF=BF=BD?=",
+ "-\xC3\xC3-\xC3\xC3-\xC3\xC3-\xC3\xC3-\xC3\xC3-\xC3\xC3-",
+ "=?utf-8?b?Le+/vS3vv70t77+9Le+/vS3vv70t77+9LQ==?=",
+ "-\xC3\xC3\xC3-\xC3\xC3\xC3-\xC3\xC3\xC3-"
+ "\xC3\xC3\xC3-\xC3\xC3\xC3-\xC3\xC3\xC3-",
+ "=?utf-8?b?Le+/vS3vv70t77+9Le+/vS3vv70t77+9LQ==?=",
+ "\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3\xC3",
+ "=?utf-8?b?77+9?=",
+ "-\xC3\xA4\xC3\xC3-\xC3\xC3\xA4\xC3-\xC3\xC3\xC3\xA4-"
+ "\xC3\xC3\xC3\xA4-\xC3\xC3\xA4\xC3-\xC3\xA4\xC3\xC3-",
+ "=?utf-8?b?LcOk77+9Le+/vcOk77+9Le+/"
+ "vcOkLe+/vcOkLe+/vcOk77+9LcOk77+9LQ==?=",
+ };
+ string_t *str = t_str_new(128);
+ unsigned int i;
+
+ test_begin("message header encode");
+ for (i = 0; i < N_ELEMENTS(data); i += 2) {
+ str_truncate(str, 0);
+ message_header_encode(data[i], str);
+ test_assert_strcmp(str_c(str), data[i+1]);
+ }
+ test_end();
+}
+
+static void test_message_header_encode_data(void)
+{
+ string_t *str = t_str_new(128);
+ static unsigned char nuls[10] = { 0, };
+
+ test_begin("message header encode data");
+ message_header_encode_data(nuls, 1, str);
+ test_assert_strcmp(str_c(str), "=?utf-8?q?=00?=");
+
+ str_truncate(str, 0);
+ message_header_encode_data(nuls, sizeof(nuls), str);
+ test_assert_strcmp(str_c(str), "=?utf-8?b?AAAAAAAAAAAAAA==?=");
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_header_encode_q,
+ test_message_header_encode_b,
+ test_message_header_encode,
+ test_message_header_encode_data,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-header-hash.c b/src/lib-mail/test-message-header-hash.c
new file mode 100644
index 0000000..7dd6fd8
--- /dev/null
+++ b/src/lib-mail/test-message-header-hash.c
@@ -0,0 +1,111 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "test-common.h"
+#include "md5.h"
+#include "message-header-hash.h"
+
+static const char test_input_with_nuls[] = {
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
+ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
+ "\x20!?x??yz\x7f\x80\x90\xff-plop\xff"
+};
+
+static const struct {
+ const char *input;
+ unsigned int version;
+ const char *output;
+} tests[] = {
+ { "???hi???", 1, "???hi???" },
+
+ { test_input_with_nuls, 2, "?\t\n? !?x?yz?-plop?" },
+ { "?hi?", 2, "?hi?" },
+ { "\x01hi\x01", 2, "?hi?" },
+ { "???hi???", 2, "?hi?" },
+ { "\x01?hi??\x01", 2, "?hi?" },
+ { "?\t?hi?\t?", 2, "?\t?hi?\t?" },
+ { "\n\nhi\n\n", 2, "\n\nhi\n\n" },
+ { "", 2, "" },
+ { " ", 2, " " },
+ { " ", 2, " " },
+ { "? ? ? hi \x01\x02 \x03 ", 2, "? ? ? hi ? ? " },
+
+ { test_input_with_nuls, 3, "?\t\n?!?x?yz?-plop?" },
+ { "\n\nhi\n\n", 3, "\n\nhi\n\n" },
+ { "", 3, "" },
+ { " ", 3, "" },
+ { " ", 3, "" },
+ { " ? ", 3, "?" },
+ { "? ? ? hi \x01\x02 \x03 ", 3, "???hi??" },
+ { " \t \t", 3, "\t\t" },
+
+ { test_input_with_nuls, 4, "?\n?!?x?yz?-plop?" },
+ { "\n\nhi\n\n", 4, "\n\nhi\n\n" },
+ { "", 4, "" },
+ { " ", 4, "" },
+ { " \t \t", 4, "" },
+ { "foo\t\t", 4, "foo" },
+};
+
+static void test_message_header_hash_more(void)
+{
+ struct message_header_hash_context ctx;
+ struct md5_context md5_ctx;
+ unsigned char md5_input[MD5_RESULTLEN], md5_output[MD5_RESULTLEN];
+
+ test_begin("message_header_hash_more");
+ for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+ const unsigned char *test_input =
+ (const unsigned char *)tests[i].input;
+ size_t input_len = tests[i].input == test_input_with_nuls ?
+ sizeof(test_input_with_nuls)-1 : strlen(tests[i].input);
+ md5_init(&md5_ctx);
+ i_zero(&ctx);
+ message_header_hash_more(&ctx, &hash_method_md5, &md5_ctx,
+ tests[i].version, test_input,
+ input_len);
+ md5_final(&md5_ctx, md5_input);
+
+ md5_init(&md5_ctx);
+ md5_update(&md5_ctx, tests[i].output, strlen(tests[i].output));
+ md5_final(&md5_ctx, md5_output);
+
+ test_assert_idx(memcmp(md5_input, md5_output, MD5_RESULTLEN) == 0, i);
+
+ /* single byte at a time */
+ md5_init(&md5_ctx);
+ i_zero(&ctx);
+ for (unsigned int j = 0; j < input_len; j++) {
+ message_header_hash_more(&ctx, &hash_method_md5,
+ &md5_ctx, tests[i].version,
+ test_input+j, 1);
+ }
+ md5_final(&md5_ctx, md5_input);
+ test_assert_idx(memcmp(md5_input, md5_output, MD5_RESULTLEN) == 0, i);
+
+ /* random number of chars at a time */
+ md5_init(&md5_ctx);
+ i_zero(&ctx);
+ for (unsigned int j = 0; j < input_len; ) {
+ const unsigned char *input_part =
+ (const unsigned char *)tests[i].input + j;
+ unsigned int len = i_rand_minmax(1, input_len - j);
+ message_header_hash_more(&ctx, &hash_method_md5,
+ &md5_ctx, tests[i].version,
+ input_part, len);
+ j += len;
+ }
+ md5_final(&md5_ctx, md5_input);
+ test_assert_idx(memcmp(md5_input, md5_output, MD5_RESULTLEN) == 0, i);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_header_hash_more,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-header-parser.c b/src/lib-mail/test-message-header-parser.c
new file mode 100644
index 0000000..700d341
--- /dev/null
+++ b/src/lib-mail/test-message-header-parser.c
@@ -0,0 +1,479 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "unichar.h"
+#include "istream.h"
+#include "message-size.h"
+#include "message-header-parser.h"
+#include "test-common.h"
+
+#define TEST1_MSG_BODY_LEN 5
+static const char *test1_msg =
+ "h1: v1\n"
+ "h2:\n"
+ " v2\r\n"
+ "h3: \r\n"
+ "\tv3\n"
+ "\tw3\r\n"
+ "h4: \r\n"
+ "\n"
+ " body";
+
+static void
+test_message_header_parser_one(struct message_header_parser_ctx *parser,
+ enum message_header_parser_flags hdr_flags)
+{
+ struct message_header_line *hdr;
+ bool use_full_value;
+
+ use_full_value = hdr_flags != 0;
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 0);
+ if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP) == 0)
+ test_assert(hdr->full_value_offset == 4);
+ else
+ test_assert(hdr->full_value_offset == 5);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h1") == 0);
+ if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP) == 0) {
+ test_assert(hdr->middle_len == 2 && memcmp(hdr->middle, ": ", 2) == 0);
+ test_assert(hdr->value_len == 3 && memcmp(hdr->value, " v1", 3) == 0);
+ } else {
+ test_assert(hdr->middle_len == 3 && memcmp(hdr->middle, ": ", 3) == 0);
+ test_assert(hdr->value_len == 2 && memcmp(hdr->value, "v1", 2) == 0);
+ }
+ test_assert(!hdr->continues && !hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && !hdr->crlf_newline);
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 8 && hdr->full_value_offset == 11);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h2") == 0);
+ test_assert(hdr->middle_len == 1 && memcmp(hdr->middle, ":", 1) == 0);
+ test_assert(hdr->value_len == 0);
+ test_assert(hdr->continues && !hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && !hdr->crlf_newline);
+ if (use_full_value) hdr->use_full_value = TRUE;
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 8 && hdr->full_value_offset == 11);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h2") == 0);
+ test_assert(hdr->middle_len == 1 && memcmp(hdr->middle, ":", 1) == 0);
+ test_assert(hdr->value_len == 3 && memcmp(hdr->value, " v2", 3) == 0);
+ test_assert(!hdr->continues && hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && hdr->crlf_newline);
+ if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0) {
+ test_assert(hdr->full_value_len == 3 &&
+ memcmp(hdr->full_value, " v2", 3) == 0);
+ } else if (use_full_value) {
+ test_assert(hdr->full_value_len == 4 &&
+ memcmp(hdr->full_value, "\n v2", 4) == 0);
+ }
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 17 && hdr->full_value_offset == 21);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h3") == 0);
+ test_assert(hdr->middle_len == 2 && memcmp(hdr->middle, ": ", 2) == 0);
+ test_assert(hdr->value_len == 0);
+ test_assert(hdr->continues && !hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && hdr->crlf_newline);
+ if (use_full_value) hdr->use_full_value = TRUE;
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 17 && hdr->full_value_offset == 21);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h3") == 0);
+ test_assert(hdr->middle_len == 2 && memcmp(hdr->middle, ": ", 2) == 0);
+ test_assert(hdr->value_len == 3 && memcmp(hdr->value, "\tv3", 3) == 0);
+ test_assert(hdr->continues && hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && !hdr->crlf_newline);
+ if (use_full_value) hdr->use_full_value = TRUE;
+ if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0) {
+ test_assert(hdr->full_value_len == 3 &&
+ memcmp(hdr->full_value, " v3", 3) == 0);
+ } else if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_DROP_CR) != 0) {
+ test_assert(hdr->full_value_len == 4 &&
+ memcmp(hdr->full_value, "\n\tv3", 4) == 0);
+ } else if (use_full_value) {
+ test_assert(hdr->full_value_len == 5 &&
+ memcmp(hdr->full_value, "\r\n\tv3", 5) == 0);
+ }
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 17 && hdr->full_value_offset == 21);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h3") == 0);
+ test_assert(hdr->middle_len == 2 && memcmp(hdr->middle, ": ", 2) == 0);
+ test_assert(hdr->value_len == 3 && memcmp(hdr->value, "\tw3", 3) == 0);
+ test_assert(!hdr->continues && hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && hdr->crlf_newline);
+ if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE) != 0) {
+ test_assert(hdr->full_value_len == 6 &&
+ memcmp(hdr->full_value, " v3 w3", 6) == 0);
+ } else if ((hdr_flags & MESSAGE_HEADER_PARSER_FLAG_DROP_CR) != 0) {
+ test_assert(hdr->full_value_len == 8 &&
+ memcmp(hdr->full_value, "\n\tv3\n\tw3", 8) == 0);
+ } else if (use_full_value) {
+ test_assert(hdr->full_value_len == 9 &&
+ memcmp(hdr->full_value, "\r\n\tv3\n\tw3", 9) == 0);
+ }
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 32 && hdr->full_value_offset == 36);
+ test_assert(hdr->name_len == 2 && strcmp(hdr->name, "h4") == 0);
+ test_assert(hdr->middle_len == 2 && memcmp(hdr->middle, ": ", 2) == 0);
+ test_assert(hdr->value_len == 0 && memcmp(hdr->value, "", 0) == 0);
+ test_assert(!hdr->continues && !hdr->continued && !hdr->eoh &&
+ !hdr->no_newline && hdr->crlf_newline);
+ test_assert(hdr->full_value_len == 0 && hdr->full_value != NULL);
+
+ test_assert(message_parse_header_next(parser, &hdr) > 0);
+ test_assert(hdr->name_offset == 38 && hdr->full_value_offset == 38);
+ test_assert(hdr->name_len == 0 && hdr->middle_len == 0 && hdr->value_len == 0);
+ test_assert(!hdr->continues && !hdr->continued && hdr->eoh &&
+ !hdr->no_newline && !hdr->crlf_newline);
+
+ test_assert(message_parse_header_next(parser, &hdr) < 0);
+}
+
+static void test_message_header_parser(void)
+{
+ static enum message_header_parser_flags max_hdr_flags =
+ MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP |
+ MESSAGE_HEADER_PARSER_FLAG_DROP_CR |
+ MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE;
+ enum message_header_parser_flags hdr_flags;
+ struct message_header_parser_ctx *parser;
+ struct message_size hdr_size, hdr_size2;
+ struct istream *input;
+ bool has_nuls;
+
+ test_begin("message header parser");
+ input = test_istream_create(test1_msg);
+
+ for (hdr_flags = 0; hdr_flags <= max_hdr_flags; hdr_flags++) {
+ i_stream_seek(input, 0);
+ parser = message_parse_header_init(input, &hdr_size, hdr_flags);
+ test_message_header_parser_one(parser, hdr_flags);
+ message_parse_header_deinit(&parser);
+ i_stream_seek(input, 0);
+ message_get_header_size(input, &hdr_size2, &has_nuls);
+ }
+
+ test_assert(!has_nuls);
+ test_assert(hdr_size.physical_size == hdr_size2.physical_size);
+ test_assert(hdr_size.virtual_size == hdr_size2.virtual_size);
+ test_assert(hdr_size.lines == hdr_size2.lines);
+ test_assert(hdr_size.physical_size == strlen(test1_msg)-TEST1_MSG_BODY_LEN);
+ test_assert(hdr_size.virtual_size == strlen(test1_msg) - TEST1_MSG_BODY_LEN + 4);
+
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void hdr_write(string_t *str, struct message_header_line *hdr)
+{
+ if (!hdr->continued) {
+ str_append(str, hdr->name);
+ if (hdr->middle_len > 0)
+ str_append_data(str, hdr->middle, hdr->middle_len);
+ }
+ str_append_data(str, hdr->value, hdr->value_len);
+ if (!hdr->no_newline) {
+ if (hdr->crlf_newline)
+ str_append_c(str, '\r');
+ str_append_c(str, '\n');
+ }
+}
+
+static void test_message_header_parser_partial(void)
+{
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+ unsigned int i, max = (strlen(test1_msg)-TEST1_MSG_BODY_LEN)*2;
+ string_t *str;
+ int ret;
+
+ test_begin("message header parser partial");
+ input = test_istream_create(test1_msg);
+ test_istream_set_allow_eof(input, FALSE);
+
+ str = t_str_new(max);
+ parser = message_parse_header_init(input, NULL, 0);
+ for (i = 0; i <= max; i++) {
+ test_istream_set_size(input, i/2);
+ while ((ret = message_parse_header_next(parser, &hdr)) > 0)
+ hdr_write(str, hdr);
+ test_assert((ret == 0 && i < max) ||
+ (ret < 0 && i == max));
+ }
+ message_parse_header_deinit(&parser);
+
+ str_append(str, " body");
+ test_assert(strcmp(str_c(str), test1_msg) == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void
+test_message_header_parser_long_lines_str(const char *str,
+ unsigned int buffer_size,
+ struct message_size *size_r,
+ struct message_size *size_2_r)
+{
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+ unsigned int i;
+ size_t len = strlen(str);
+ bool has_nuls;
+
+ input = test_istream_create(str);
+ test_istream_set_max_buffer_size(input, buffer_size);
+
+ parser = message_parse_header_init(input, size_r, 0);
+ for (i = 1; i <= len; i++) {
+ test_istream_set_size(input, i);
+ while (message_parse_header_next(parser, &hdr) > 0) ;
+ }
+ message_parse_header_deinit(&parser);
+ i_stream_seek(input, 0);
+ /* Buffer must be +1 for message_get_header_size as it's using
+ i_stream_read_bytes which does not work with lower buffersize
+ because it returns -2 (input buffer full) if 2 bytes are wanted. */
+ test_istream_set_max_buffer_size(input, buffer_size+1);
+ message_get_header_size(input, size_2_r, &has_nuls);
+ i_stream_unref(&input);
+}
+
+#define NAME10 "1234567890"
+#define NAME100 NAME10 NAME10 NAME10 NAME10 NAME10 \
+ NAME10 NAME10 NAME10 NAME10 NAME10
+#define NAME1000 NAME100 NAME100 NAME100 NAME100 NAME100 \
+ NAME100 NAME100 NAME100 NAME100 NAME100
+
+static void test_message_header_parser_long_lines(void)
+{
+ static const char *lf_str = NAME10": 345\n\n";
+ static const char *crlf_str = NAME10": 345\r\n\r\n";
+ static const char *lf_str_vl = NAME1000": Is a long header name\n\n";
+ static const char *crlf_str_vl = NAME1000": Is a long header name\r\n\r\n";
+ static const char *lf_str_ol = NAME1000 \
+ NAME100 ": Is a overlong header name\n\n";
+ static const char *crlf_str_ol = NAME1000 \
+ NAME100 ": Is a overlong header name\r\n\r\n";
+
+ struct message_size hdr_size, hdr_size2;
+ size_t i, len;
+
+ test_begin("message header parser long lines");
+ len = strlen(lf_str);
+ for (i = 2; i < len; i++) {
+ test_message_header_parser_long_lines_str(lf_str, i, &hdr_size, &hdr_size2);
+ test_assert(hdr_size.physical_size == len);
+ test_assert(hdr_size.virtual_size == len + 2);
+ test_assert(hdr_size.virtual_size == hdr_size2.virtual_size);
+ test_assert(hdr_size.physical_size == hdr_size2.physical_size);
+ }
+ len = strlen(crlf_str);
+ for (i = 3; i < len; i++) {
+ test_message_header_parser_long_lines_str(crlf_str, i, &hdr_size, &hdr_size2);
+ test_assert(hdr_size.physical_size == len);
+ test_assert(hdr_size.virtual_size == len);
+ test_assert(hdr_size.virtual_size == hdr_size2.virtual_size);
+ test_assert(hdr_size.physical_size == hdr_size2.physical_size);
+ }
+
+ /* increment these faster, otherwise the test is very slow */
+ len = strlen(lf_str_vl);
+ for (i = 3; i < len; i *= 2) {
+ test_message_header_parser_long_lines_str(lf_str_vl, i, &hdr_size, &hdr_size2);
+ test_assert(hdr_size.physical_size == len);
+ test_assert(hdr_size.virtual_size == len + 2);
+ test_assert(hdr_size.virtual_size == hdr_size2.virtual_size);
+ test_assert(hdr_size.physical_size == hdr_size2.physical_size);
+ }
+ len = strlen(crlf_str_vl);
+ for (i = 3; i < len; i *= 2) {
+ test_message_header_parser_long_lines_str(crlf_str_vl, i, &hdr_size, &hdr_size2);
+ test_assert(hdr_size.physical_size == len);
+ test_assert(hdr_size.virtual_size == len);
+ test_assert(hdr_size.virtual_size == hdr_size2.virtual_size);
+ test_assert(hdr_size.physical_size == hdr_size2.physical_size);
+ }
+
+ /* test that parsing overlength lines work so that name & middle are
+ empty. */
+
+ struct message_header_line *hdr;
+ struct message_header_parser_ctx *ctx;
+ struct istream *input;
+
+ input = test_istream_create(lf_str_ol);
+ ctx = message_parse_header_init(input, NULL, 0);
+
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ *hdr->name == '\0' && hdr->middle == uchar_empty_ptr &&
+ hdr->name_len == 0 && hdr->middle_len == 0 &&
+ hdr->value != NULL && hdr->value_len > 0);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->eoh);
+ message_parse_header_deinit(&ctx);
+ i_stream_unref(&input);
+
+ input = test_istream_create(crlf_str_ol);
+ ctx = message_parse_header_init(input, NULL, 0);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ *hdr->name == '\0' && hdr->middle == uchar_empty_ptr &&
+ hdr->name_len == 0 && hdr->middle_len == 0 &&
+ hdr->value != NULL && hdr->value_len > 0);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->eoh);
+ message_parse_header_deinit(&ctx);
+ i_stream_unref(&input);
+
+ /* test offset parsing */
+ static const char *data = "h1" NAME1000 NAME100 \
+ ": value1\r\n" \
+ "h2" NAME1000 NAME100 \
+ ": value2\r\n" \
+ "h3" NAME1000 NAME100 \
+ ": value3\r\n\r\n";
+ input = test_istream_create(data);
+ ctx = message_parse_header_init(input, NULL, 0);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->full_value[0] == 'h' &&
+ hdr->full_value[1] == '1' &&
+ hdr->full_value_offset == 0);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->full_value[0] == 'h' &&
+ hdr->full_value[1] == '2' &&
+ hdr->full_value_offset == 1112);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->full_value[0] == 'h' &&
+ hdr->full_value[1] == '3' &&
+ hdr->full_value_offset == 2224);
+ test_assert(message_parse_header_next(ctx, &hdr) > 0 &&
+ hdr->eoh);
+
+ message_parse_header_deinit(&ctx);
+ i_stream_unref(&input);
+
+ test_end();
+}
+
+static void test_message_header_parser_extra_cr_in_eoh(void)
+{
+ static const char *str = "a:b\n\r\r\n";
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+
+ test_begin("message header parser extra CR in EOH");
+
+ input = test_istream_create(str);
+ parser = message_parse_header_init(input, NULL, 0);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ strcmp(hdr->name, "a") == 0);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ *hdr->value == '\r' && hdr->value_len == 1 &&
+ hdr->full_value_offset == 4 &&
+ hdr->middle_len == 0 &&
+ hdr->name_len == 0 && !hdr->eoh);
+ test_assert(message_parse_header_next(parser, &hdr) < 0);
+ message_parse_header_deinit(&parser);
+ test_assert(input->stream_errno == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void test_message_header_parser_no_eoh(void)
+{
+ static const char *str = "a:b\n";
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+
+ test_begin("message header parser no EOH");
+
+ input = test_istream_create(str);
+ parser = message_parse_header_init(input, NULL, 0);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ strcmp(hdr->name, "a") == 0);
+ test_assert_strcmp(message_header_strdup(pool_datastack_create(),
+ hdr->value, hdr->value_len),
+ "b");
+ test_assert(message_parse_header_next(parser, &hdr) < 0);
+ message_parse_header_deinit(&parser);
+ test_assert(input->stream_errno == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void test_message_header_parser_nul(void)
+{
+ static const unsigned char str[] = "a :\0\0b\n";
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+
+ test_begin("message header parser NUL");
+
+ input = test_istream_create_data(str, sizeof(str)-1);
+ parser = message_parse_header_init(input, NULL, 0);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ strcmp(hdr->name, "a") == 0);
+ test_assert(hdr->value_len >= 3 && memcmp("\0\0b", hdr->value, 3) == 0);
+ test_assert_strcmp(message_header_strdup(pool_datastack_create(),
+ hdr->value, hdr->value_len),
+ UNICODE_REPLACEMENT_CHAR_UTF8 UNICODE_REPLACEMENT_CHAR_UTF8"b");
+ test_assert(message_parse_header_next(parser, &hdr) < 0);
+ message_parse_header_deinit(&parser);
+ test_assert(input->stream_errno == 0);
+ i_stream_unref(&input);
+ test_end();
+}
+
+static void test_message_header_parser_extra_crlf_in_name(void)
+{
+ static const unsigned char str[] = "X-Header\r\n Name: Header Value\n\n";
+ struct message_header_parser_ctx *parser;
+ struct message_header_line *hdr;
+ struct istream *input;
+ test_begin("message header parser CRLF in header name");
+
+ input = test_istream_create_data(str, sizeof(str)-1);
+ parser = message_parse_header_init(input, NULL, 0);
+ hdr = NULL;
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ *hdr->name == '\0' && hdr->middle == uchar_empty_ptr &&
+ hdr->name_len == 0 && hdr->middle_len == 0 &&
+ hdr->value != NULL && hdr->value_len > 0);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ *hdr->name == '\0' && hdr->middle == uchar_empty_ptr &&
+ hdr->name_len == 0 && hdr->middle_len == 0 &&
+ hdr->value != NULL && hdr->value_len > 0 &&
+ hdr->continued);
+ test_assert(message_parse_header_next(parser, &hdr) > 0 &&
+ hdr->eoh);
+
+ message_parse_header_deinit(&parser);
+ i_stream_unref(&input);
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_header_parser,
+ test_message_header_parser_partial,
+ test_message_header_parser_long_lines,
+ test_message_header_parser_extra_cr_in_eoh,
+ test_message_header_parser_no_eoh,
+ test_message_header_parser_nul,
+ test_message_header_parser_extra_crlf_in_name,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-id.c b/src/lib-mail/test-message-id.c
new file mode 100644
index 0000000..d5abf1f
--- /dev/null
+++ b/src/lib-mail/test-message-id.c
@@ -0,0 +1,46 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "message-id.h"
+#include "test-common.h"
+
+static void test_message_id_get_next(void)
+{
+ const char *input[] = {
+ "<foo@bar>",
+ "<foo@bar>,skipped,<foo2@bar2>",
+ "(c) < (c) foo (c) @ (c) bar (c) > (c)",
+ "<\"foo 2\"@bar>"
+ };
+ const char *output[] = {
+ "foo@bar", NULL,
+ "foo@bar", "foo2@bar2", NULL,
+ "foo@bar", NULL,
+ "foo 2@bar", NULL
+ };
+ const char *msgid, *next_msgid;
+ unsigned int i, j;
+
+ test_begin("message id parser");
+ for (i = 0, j = 0; i < N_ELEMENTS(input); i++) {
+ msgid = input[i];
+ while ((next_msgid = message_id_get_next(&msgid)) != NULL) {
+ if (output[j] == NULL)
+ break;
+ test_assert(strcmp(output[j], next_msgid) == 0);
+ j++;
+ }
+ test_assert(output[j++] == NULL && next_msgid == NULL);
+ }
+ test_assert(j == N_ELEMENTS(output));
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_id_get_next,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-parser.c b/src/lib-mail/test-message-parser.c
new file mode 100644
index 0000000..663bfe8
--- /dev/null
+++ b/src/lib-mail/test-message-parser.c
@@ -0,0 +1,1398 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "message-parser.h"
+#include "message-part-data.h"
+#include "message-size.h"
+#include "test-common.h"
+
+static const char test_msg[] =
+"Return-Path: <test@example.org>\n"
+"Subject: Hello world\n"
+"From: Test User <test@example.org>\n"
+"To: Another User <test2@example.org>\n"
+"Message-Id: <1.2.3.4@example>\n"
+"Mime-Version: 1.0\n"
+"Date: Sun, 23 May 2007 04:58:08 +0300\n"
+"Content-Type: multipart/signed; micalg=pgp-sha1;\n"
+" protocol=\"application/pgp-signature\";\n"
+" boundary=\"=-GNQXLhuj24Pl1aCkk4/d\"\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"Content-Type: text/plain\n"
+"Content-Transfer-Encoding: quoted-printable\n"
+"\n"
+"There was a day=20\n"
+"a happy=20day\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"Content-Type: application/pgp-signature; name=signature.asc\n"
+"\n"
+"-----BEGIN PGP SIGNATURE-----\n"
+"Version: GnuPG v1.2.4 (GNU/Linux)\n"
+"\n"
+"invalid\n"
+"-----END PGP SIGNATURE-----\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d--\n"
+"\n"
+"\n";
+#define TEST_MSG_LEN (sizeof(test_msg)-1)
+
+static const struct message_parser_settings set_empty = { .flags = 0 };
+
+static int message_parse_stream(pool_t pool, struct istream *input,
+ const struct message_parser_settings *set,
+ bool parse_data, struct message_part **parts_r)
+{
+ int ret;
+ struct message_parser_ctx *parser;
+ struct message_block block;
+
+ i_zero(&block);
+ parser = message_parser_init(pool, input, set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0)
+ if (parse_data)
+ message_part_data_parse_from_header(pool, block.part,
+ block.hdr);
+ message_parser_deinit(&parser, parts_r);
+ test_assert(input->stream_errno == 0);
+ return ret;
+}
+
+static void test_parsed_parts(struct istream *input, struct message_part *parts)
+{
+ const struct message_parser_settings parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK,
+ };
+ struct message_parser_ctx *parser;
+ struct message_block block;
+ struct message_part *parts2;
+ uoff_t i, input_size;
+ const char *error;
+
+ i_stream_seek(input, 0);
+ if (i_stream_get_size(input, TRUE, &input_size) < 0)
+ i_unreached();
+
+ parser = message_parser_init_from_parts(parts, input, &parser_set);
+ for (i = 1; i <= input_size*2+1; i++) {
+ test_istream_set_size(input, i/2);
+ if (i > TEST_MSG_LEN*2)
+ test_istream_set_allow_eof(input, TRUE);
+ while (message_parser_parse_next_block(parser, &block) > 0) ;
+ }
+ test_assert(message_parser_deinit_from_parts(&parser, &parts2, &error) == 0);
+ test_assert(message_part_is_equal(parts, parts2));
+}
+
+static void test_message_parser_small_blocks(void)
+{
+ struct message_parser_ctx *parser;
+ struct istream *input;
+ struct message_part *parts, *parts2;
+ struct message_block block;
+ unsigned int i, end_of_headers_idx;
+ string_t *output;
+ pool_t pool;
+ const char *error;
+ int ret;
+
+ test_begin("message parser in small blocks");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(test_msg);
+ output = t_str_new(128);
+
+ /* full parsing */
+ const struct message_parser_settings full_parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_INCLUDE_MULTIPART_BLOCKS |
+ MESSAGE_PARSER_FLAG_INCLUDE_BOUNDARIES,
+ };
+ parser = message_parser_init(pool, input, &full_parser_set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0) {
+ if (block.hdr != NULL)
+ message_header_line_write(output, block.hdr);
+ else if (block.size > 0)
+ str_append_data(output, block.data, block.size);
+ }
+
+ test_assert(ret < 0);
+ message_parser_deinit(&parser, &parts);
+ test_assert(input->stream_errno == 0);
+ test_assert(strcmp(test_msg, str_c(output)) == 0);
+
+ /* parsing in small blocks */
+ i_stream_seek(input, 0);
+ test_istream_set_allow_eof(input, FALSE);
+
+ parser = message_parser_init(pool, input, &set_empty);
+ for (i = 1; i <= TEST_MSG_LEN*2+1; i++) {
+ test_istream_set_size(input, i/2);
+ if (i > TEST_MSG_LEN*2)
+ test_istream_set_allow_eof(input, TRUE);
+ while ((ret = message_parser_parse_next_block(parser,
+ &block)) > 0) ;
+ test_assert((ret == 0 && i <= TEST_MSG_LEN*2) ||
+ (ret < 0 && i > TEST_MSG_LEN*2));
+ }
+ message_parser_deinit(&parser, &parts2);
+ test_assert(input->stream_errno == 0);
+ test_assert(message_part_is_equal(parts, parts2));
+
+ /* parsing in small blocks from preparsed parts */
+ i_stream_seek(input, 0);
+ test_istream_set_allow_eof(input, FALSE);
+
+ end_of_headers_idx = (strstr(test_msg, "\n-----") - test_msg);
+ const struct message_parser_settings preparsed_parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK,
+ };
+ parser = message_parser_init_from_parts(parts, input,
+ &preparsed_parser_set);
+ for (i = 1; i <= TEST_MSG_LEN*2+1; i++) {
+ test_istream_set_size(input, i/2);
+ if (i > TEST_MSG_LEN*2)
+ test_istream_set_allow_eof(input, TRUE);
+ while ((ret = message_parser_parse_next_block(parser,
+ &block)) > 0) ;
+ test_assert((ret == 0 && i/2 <= end_of_headers_idx) ||
+ (ret < 0 && i/2 > end_of_headers_idx));
+ }
+ test_assert(message_parser_deinit_from_parts(&parser, &parts2, &error) == 0);
+ test_assert(message_part_is_equal(parts, parts2));
+
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_stop_early(void)
+{
+ struct istream *input, *input2;
+ struct message_part *parts;
+ unsigned int i;
+ pool_t pool;
+
+ test_begin("message parser in stop early");
+ pool = pool_alloconly_create("message parser", 524288);
+ input = test_istream_create(test_msg);
+
+ test_istream_set_allow_eof(input, FALSE);
+ for (i = 1; i <= TEST_MSG_LEN+1; i++) {
+ i_stream_seek(input, 0);
+ test_istream_set_size(input, i);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) == 0);
+
+ /* test preparsed - first re-parse everything with a stream
+ that sees EOF at this position */
+ input2 = i_stream_create_from_data(test_msg, i);
+ test_assert(message_parse_stream(pool, input2, &set_empty, FALSE, &parts) == -1);
+
+ /* now parse from the parts */
+ i_stream_seek(input2, 0);
+ test_assert(message_parse_stream(pool, input2, &set_empty, FALSE, &parts) == -1);
+
+ i_stream_unref(&input2);
+ }
+
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_get_sizes(struct istream *input,
+ struct message_size *body_size_r,
+ struct message_size *header_size_r,
+ bool expect_has_nuls)
+{
+ bool has_nuls;
+ i_zero(body_size_r);
+ i_zero(header_size_r);
+
+ message_get_header_size(input, header_size_r, &has_nuls);
+ test_assert(has_nuls == expect_has_nuls);
+ message_get_body_size(input, body_size_r, &has_nuls);
+ test_assert(has_nuls == expect_has_nuls);
+}
+
+static void test_message_parser_assert_sizes(const struct message_part *part,
+ const struct message_size *body_size,
+ const struct message_size *header_size)
+{
+ test_assert(part->header_size.lines == header_size->lines);
+ test_assert(part->header_size.physical_size == header_size->physical_size);
+ test_assert(part->header_size.virtual_size == header_size->virtual_size);
+ test_assert(part->body_size.lines == body_size->lines);
+ test_assert(part->body_size.physical_size == body_size->physical_size);
+ test_assert(part->body_size.virtual_size == body_size->virtual_size);
+}
+
+static void test_message_parser_truncated_mime_headers(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\":foo\"\n"
+"\n"
+"--:foo\n"
+"--:foo\n"
+"Content-Type: text/plain\n"
+"--:foo\n"
+"Content-Type: text/plain\r\n"
+"--:foo\n"
+"Content-Type: text/html\n"
+"--:foo--\n";
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser truncated mime headers");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ test_assert((parts->flags & MESSAGE_PART_FLAG_MULTIPART) != 0);
+ test_assert(parts->children_count == 4);
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 48);
+ test_assert(parts->header_size.virtual_size == 48+2);
+ test_assert(parts->body_size.lines == 8);
+ test_assert(parts->body_size.physical_size == 112);
+ test_assert(parts->body_size.virtual_size == 112+7);
+ test_message_parser_assert_sizes(parts, &body_size, &header_size);
+
+ test_assert(parts->children->physical_pos == 55);
+ test_assert(parts->children->header_size.physical_size == 0);
+ test_assert(parts->children->body_size.physical_size == 0);
+ test_assert(parts->children->body_size.lines == 0);
+ test_assert(parts->children->next->physical_pos == 62);
+ test_assert(parts->children->next->header_size.physical_size == 24);
+ test_assert(parts->children->next->header_size.virtual_size == 24);
+ test_assert(parts->children->next->header_size.lines == 0);
+ test_assert(parts->children->next->next->physical_pos == 94);
+ test_assert(parts->children->next->next->header_size.physical_size == 24);
+ test_assert(parts->children->next->next->header_size.virtual_size == 24);
+ test_assert(parts->children->next->next->header_size.lines == 0);
+ test_assert(parts->children->next->next->next->physical_pos == 127);
+ test_assert(parts->children->next->next->next->header_size.physical_size == 23);
+ test_assert(parts->children->next->next->next->header_size.virtual_size == 23);
+ test_assert(parts->children->next->next->next->header_size.lines == 0);
+ for (part = parts->children; part != NULL; part = part->next) {
+ test_assert(part->children_count == 0);
+ test_assert(part->body_size.physical_size == 0);
+ test_assert(part->body_size.virtual_size == 0);
+ }
+ test_assert(parts->children->next->next->next->next == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_truncated_mime_headers2(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"ab\"\n"
+"\n"
+"--ab\n"
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--ab\n"
+"Content-Type: text/plain\n"
+"\n"
+"--a\n\n";
+ struct istream *input;
+ struct message_part *parts;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser truncated mime headers 2");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children_count == 2);
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 46);
+ test_assert(parts->header_size.virtual_size == 46+2);
+ test_assert(parts->body_size.lines == 8);
+ test_assert(parts->body_size.physical_size == 86);
+ test_assert(parts->body_size.virtual_size == 86+8);
+ test_message_parser_assert_sizes(parts, &body_size, &header_size);
+
+ test_assert(parts->children->children_count == 0);
+ test_assert(parts->children->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->physical_pos == 51);
+ test_assert(parts->children->header_size.lines == 1);
+ test_assert(parts->children->header_size.physical_size == 44);
+ test_assert(parts->children->header_size.virtual_size == 44+1);
+ test_assert(parts->children->body_size.lines == 0);
+ test_assert(parts->children->body_size.physical_size == 0);
+ test_assert(parts->children->children == NULL);
+
+ test_assert(parts->children->next->children_count == 0);
+ test_assert(parts->children->next->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->next->physical_pos == 101);
+ test_assert(parts->children->next->header_size.lines == 2);
+ test_assert(parts->children->next->header_size.physical_size == 26);
+ test_assert(parts->children->next->header_size.virtual_size == 26+2);
+ test_assert(parts->children->next->body_size.lines == 2);
+ test_assert(parts->children->next->body_size.physical_size == 5);
+ test_assert(parts->children->next->body_size.virtual_size == 5+2);
+ test_assert(parts->children->next->children == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_truncated_mime_headers3(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"ab\"\n";
+ struct istream *input;
+ struct message_part *parts;
+ pool_t pool;
+
+ test_begin("message parser truncated mime headers 3");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ test_assert(parts->children_count == 0);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 1);
+ test_assert(parts->header_size.physical_size == 45);
+ test_assert(parts->header_size.virtual_size == 45+1);
+ test_assert(parts->body_size.lines == 0);
+ test_assert(parts->body_size.physical_size == 0);
+
+ test_assert(parts->children == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_empty_multipart(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"ab\"\n"
+"\n"
+"body\n";
+ struct istream *input;
+ struct message_part *parts;
+ pool_t pool;
+
+ test_begin("message parser empty multipart");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ test_assert(parts->children_count == 0);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 46);
+ test_assert(parts->header_size.virtual_size == 46+2);
+ test_assert(parts->body_size.lines == 1);
+ test_assert(parts->body_size.physical_size == 5);
+ test_assert(parts->body_size.virtual_size == 5+1);
+
+ test_assert(parts->children == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_duplicate_mime_boundary(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--a\n"
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--a\n"
+"Content-Type: text/plain\n"
+"\n"
+"body\n";
+ struct istream *input;
+ struct message_part *parts;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser duplicate mime boundary");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ test_assert(parts->children_count == 2);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 45);
+ test_assert(parts->header_size.virtual_size == 45+2);
+ test_assert(parts->body_size.lines == 7);
+ test_assert(parts->body_size.physical_size == 84);
+ test_assert(parts->body_size.virtual_size == 84+7);
+ test_message_parser_assert_sizes(parts, &body_size, &header_size);
+
+ test_assert(parts->children->children_count == 1);
+ test_assert(parts->children->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->physical_pos == 49);
+ test_assert(parts->children->header_size.lines == 2);
+ test_assert(parts->children->header_size.physical_size == 45);
+ test_assert(parts->children->header_size.virtual_size == 45+2);
+ test_assert(parts->children->body_size.lines == 4);
+ test_assert(parts->children->body_size.physical_size == 35);
+ test_assert(parts->children->body_size.virtual_size == 35+4);
+ test_assert(parts->children->children->children_count == 0);
+ test_assert(parts->children->children->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->children->physical_pos == 98);
+ test_assert(parts->children->children->header_size.lines == 2);
+ test_assert(parts->children->children->header_size.physical_size == 26);
+ test_assert(parts->children->children->header_size.virtual_size == 26+2);
+ test_assert(parts->children->children->body_size.lines == 1);
+ test_assert(parts->children->children->body_size.physical_size == 5);
+ test_assert(parts->children->children->body_size.virtual_size == 5+1);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_garbage_suffix_mime_boundary(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--ab\n"
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--ac\n"
+"Content-Type: text/plain\n"
+"\n"
+"body\n";
+ struct istream *input;
+ struct message_part *parts;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser garbage suffix mime boundary");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ test_assert(parts->children_count == 2);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 45);
+ test_assert(parts->header_size.virtual_size == 45+2);
+ test_assert(parts->body_size.lines == 7);
+ test_assert(parts->body_size.physical_size == 86);
+ test_assert(parts->body_size.virtual_size == 86+7);
+ test_message_parser_assert_sizes(parts, &body_size, &header_size);
+
+ test_assert(parts->children->children_count == 1);
+ test_assert(parts->children->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->physical_pos == 50);
+ test_assert(parts->children->header_size.lines == 2);
+ test_assert(parts->children->header_size.physical_size == 45);
+ test_assert(parts->children->header_size.virtual_size == 45+2);
+ test_assert(parts->children->body_size.lines == 4);
+ test_assert(parts->children->body_size.physical_size == 36);
+ test_assert(parts->children->body_size.virtual_size == 36+4);
+ test_assert(parts->children->children->children_count == 0);
+ test_assert(parts->children->children->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->children->physical_pos == 100);
+ test_assert(parts->children->children->header_size.lines == 2);
+ test_assert(parts->children->children->header_size.physical_size == 26);
+ test_assert(parts->children->children->header_size.virtual_size == 26+2);
+ test_assert(parts->children->children->body_size.lines == 1);
+ test_assert(parts->children->children->body_size.physical_size == 5);
+ test_assert(parts->children->children->body_size.virtual_size == 5+1);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_trailing_dashes(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"a--\"\n"
+"\n"
+"--a--\n"
+"Content-Type: multipart/mixed; boundary=\"a----\"\n"
+"\n"
+"--a----\n"
+"Content-Type: text/plain\n"
+"\n"
+"body\n"
+"--a------\n"
+"Content-Type: text/html\n"
+"\n"
+"body2\n"
+"--a----";
+ struct istream *input;
+ struct message_part *parts;
+ pool_t pool;
+
+ test_begin("message parser trailing dashes");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ test_assert(parts->children_count == 2);
+ test_assert(parts->children->next == NULL);
+ test_assert(parts->children->children_count == 1);
+ test_assert(parts->children->children->next == NULL);
+ test_assert(parts->children->children->children_count == 0);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_continuing_mime_boundary(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--a\n"
+"Content-Type: multipart/mixed; boundary=\"ab\"\n"
+"\n"
+"--ab\n"
+"Content-Type: text/plain\n"
+"\n"
+"body\n";
+ struct istream *input;
+ struct message_part *parts;
+ pool_t pool;
+
+ test_begin("message parser continuing mime boundary");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+
+ test_assert(parts->children_count == 2);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 45);
+ test_assert(parts->header_size.virtual_size == 45+2);
+ test_assert(parts->body_size.lines == 7);
+ test_assert(parts->body_size.physical_size == 86);
+ test_assert(parts->body_size.virtual_size == 86+7);
+ test_assert(parts->children->children_count == 1);
+ test_assert(parts->children->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->physical_pos == 49);
+ test_assert(parts->children->header_size.lines == 2);
+ test_assert(parts->children->header_size.physical_size == 46);
+ test_assert(parts->children->header_size.virtual_size == 46+2);
+ test_assert(parts->children->body_size.lines == 4);
+ test_assert(parts->children->body_size.physical_size == 36);
+ test_assert(parts->children->body_size.virtual_size == 36+4);
+ test_assert(parts->children->children->children_count == 0);
+ test_assert(parts->children->children->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->children->physical_pos == 100);
+ test_assert(parts->children->children->header_size.lines == 2);
+ test_assert(parts->children->children->header_size.physical_size == 26);
+ test_assert(parts->children->children->header_size.virtual_size == 26+2);
+ test_assert(parts->children->children->body_size.lines == 1);
+ test_assert(parts->children->children->body_size.physical_size == 5);
+ test_assert(parts->children->children->body_size.virtual_size == 5+1);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_continuing_truncated_mime_boundary(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--a\n"
+"Content-Type: multipart/mixed; boundary=\"ab\"\n"
+"MIME-Version: 1.0\n"
+"--ab\n"
+"Content-Type: text/plain\n"
+"\n"
+"--ab--\n"
+"--a--\n\n";
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser continuing truncated mime boundary");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ part = parts;
+ test_assert(part->children_count == 3);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 9);
+ test_assert(part->body_size.physical_size == 112);
+ test_assert(part->body_size.virtual_size == 112+9);
+ test_message_parser_assert_sizes(part, &body_size, &header_size);
+
+ part = parts->children;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->physical_pos == 49);
+ test_assert(part->header_size.lines == 1);
+ test_assert(part->header_size.physical_size == 45+17);
+ test_assert(part->header_size.virtual_size == 45+17+1);
+ test_assert(part->body_size.lines == 0);
+ test_assert(part->body_size.physical_size == 0);
+ test_assert(part->children == NULL);
+
+ /* this will not be a child, since the header was truncated. I guess
+ we could make it, but it would complicate the message-parser even
+ more. */
+ part = parts->children->next;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->physical_pos == 117);
+ test_assert(part->header_size.lines == 1);
+ test_assert(part->header_size.physical_size == 25);
+ test_assert(part->header_size.virtual_size == 25+1);
+ test_assert(part->body_size.lines == 0);
+ test_assert(part->body_size.physical_size == 0);
+ test_assert(part->children == NULL);
+
+ part = parts->children->next->next;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 0);
+ test_assert(part->header_size.physical_size == 0);
+ test_assert(part->body_size.lines == 0);
+ test_assert(part->body_size.physical_size == 0);
+ test_assert(part->children == NULL);
+ test_assert(part->next == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_continuing_mime_boundary_reverse(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"ab\"\n"
+"\n"
+"--ab\n"
+"Content-Type: multipart/mixed; boundary=\"a\"\n"
+"\n"
+"--a\n"
+"Content-Type: text/plain\n"
+"\n"
+"body\n"
+"--ab\n"
+"Content-Type: text/html\n"
+"\n"
+"body2\n";
+ struct istream *input;
+ struct message_part *parts;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser continuing mime boundary reverse");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ test_assert(parts->children_count == 3);
+ test_assert(parts->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->header_size.lines == 2);
+ test_assert(parts->header_size.physical_size == 46);
+ test_assert(parts->header_size.virtual_size == 46+2);
+ test_assert(parts->body_size.lines == 11);
+ test_assert(parts->body_size.physical_size == 121);
+ test_assert(parts->body_size.virtual_size == 121+11);
+ test_message_parser_assert_sizes(parts, &body_size, &header_size);
+
+ test_assert(parts->children->children_count == 1);
+ test_assert(parts->children->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->physical_pos == 51);
+ test_assert(parts->children->header_size.lines == 2);
+ test_assert(parts->children->header_size.physical_size == 45);
+ test_assert(parts->children->header_size.virtual_size == 45+2);
+ test_assert(parts->children->body_size.lines == 3);
+ test_assert(parts->children->body_size.physical_size == 34);
+ test_assert(parts->children->body_size.virtual_size == 34+3);
+ test_assert(parts->children->children->children_count == 0);
+ test_assert(parts->children->children->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->children->physical_pos == 100);
+ test_assert(parts->children->children->header_size.lines == 2);
+ test_assert(parts->children->children->header_size.physical_size == 26);
+ test_assert(parts->children->children->header_size.virtual_size == 26+2);
+ test_assert(parts->children->children->body_size.lines == 0);
+ test_assert(parts->children->children->body_size.physical_size == 4);
+ test_assert(parts->children->children->body_size.virtual_size == 4);
+ test_assert(parts->children->next->children_count == 0);
+ test_assert(parts->children->next->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(parts->children->next->physical_pos == 136);
+ test_assert(parts->children->next->header_size.lines == 2);
+ test_assert(parts->children->next->header_size.physical_size == 25);
+ test_assert(parts->children->next->header_size.virtual_size == 25+2);
+ test_assert(parts->children->next->body_size.lines == 1);
+ test_assert(parts->children->next->body_size.physical_size == 6);
+ test_assert(parts->children->next->body_size.virtual_size == 6+1);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_no_eoh(void)
+{
+ static const char input_msg[] = "a:b\n";
+ struct message_parser_ctx *parser;
+ struct istream *input;
+ struct message_part *parts;
+ struct message_block block;
+ pool_t pool;
+
+ test_begin("message parser no EOH");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ parser = message_parser_init(pool, input, &set_empty);
+ test_assert(message_parser_parse_next_block(parser, &block) > 0 &&
+ block.hdr != NULL && strcmp(block.hdr->name, "a") == 0 &&
+ block.hdr->value_len == 1 && block.hdr->value[0] == 'b');
+ test_assert(message_parser_parse_next_block(parser, &block) > 0 &&
+ block.hdr == NULL && block.size == 0);
+ test_assert(message_parser_parse_next_block(parser, &block) < 0);
+ message_parser_deinit(&parser, &parts);
+ test_assert(input->stream_errno == 0);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_long_mime_boundary(void)
+{
+ /* Close the boundaries in wrong reverse order. But because all
+ boundaries are actually truncated to the same size (..890) it
+ works the same as if all of them were duplicate boundaries. */
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"1234567890123456789012345678901234567890123456789012345678901234567890123456789012\"\n"
+"\n"
+"--1234567890123456789012345678901234567890123456789012345678901234567890123456789012\n"
+"Content-Type: multipart/mixed; boundary=\"123456789012345678901234567890123456789012345678901234567890123456789012345678901\"\n"
+"\n"
+"--123456789012345678901234567890123456789012345678901234567890123456789012345678901\n"
+"Content-Type: multipart/mixed; boundary=\"12345678901234567890123456789012345678901234567890123456789012345678901234567890\"\n"
+"\n"
+"--12345678901234567890123456789012345678901234567890123456789012345678901234567890\n"
+"Content-Type: text/plain\n"
+"\n"
+"1\n"
+"--1234567890123456789012345678901234567890123456789012345678901234567890123456789012\n"
+"Content-Type: text/plain\n"
+"\n"
+"22\n"
+"--123456789012345678901234567890123456789012345678901234567890123456789012345678901\n"
+"Content-Type: text/plain\n"
+"\n"
+"333\n"
+"--12345678901234567890123456789012345678901234567890123456789012345678901234567890\n"
+"Content-Type: text/plain\n"
+"\n"
+"4444\n";
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser long mime boundary");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &set_empty, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ part = parts;
+ test_assert(part->children_count == 6);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 126);
+ test_assert(part->header_size.virtual_size == 126+2);
+ test_assert(part->body_size.lines == 22);
+ test_assert(part->body_size.physical_size == 871);
+ test_assert(part->body_size.virtual_size == 871+22);
+ test_message_parser_assert_sizes(part, &body_size, &header_size);
+
+ part = parts->children;
+ test_assert(part->children_count == 5);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 125);
+ test_assert(part->header_size.virtual_size == 125+2);
+ test_assert(part->body_size.lines == 19);
+ test_assert(part->body_size.physical_size == 661);
+ test_assert(part->body_size.virtual_size == 661+19);
+
+ part = parts->children->children;
+ test_assert(part->children_count == 4);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 124);
+ test_assert(part->header_size.virtual_size == 124+2);
+ test_assert(part->body_size.lines == 16);
+ test_assert(part->body_size.physical_size == 453);
+ test_assert(part->body_size.virtual_size == 453+16);
+
+ part = parts->children->children->children;
+ for (unsigned int i = 1; i <= 3; i++, part = part->next) {
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 26);
+ test_assert(part->header_size.virtual_size == 26+2);
+ test_assert(part->body_size.lines == 0);
+ test_assert(part->body_size.physical_size == i);
+ test_assert(part->body_size.virtual_size == i);
+ }
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_part_nested_limit(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"\n"
+"--1\n"
+"Content-Type: multipart/mixed; boundary=\"2\"\n"
+"\n"
+"--2\n"
+"Content-Type: text/plain\n"
+"\n"
+"1\n"
+"--2\n"
+"Content-Type: text/plain\n"
+"\n"
+"22\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"333\n";
+ const struct message_parser_settings parser_set = {
+ .max_nested_mime_parts = 2,
+ };
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser mime part nested limit");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &parser_set, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ part = parts;
+ test_assert(part->children_count == 2);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 15);
+ test_assert(part->body_size.physical_size == 148);
+ test_assert(part->body_size.virtual_size == 148+15);
+ test_message_parser_assert_sizes(part, &body_size, &header_size);
+
+ part = parts->children;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_IS_MIME |
+ MESSAGE_PART_FLAG_OVERFLOW));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 7);
+ test_assert(part->body_size.physical_size == 64);
+ test_assert(part->body_size.virtual_size == 64+7);
+
+ part = parts->children->next;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 26);
+ test_assert(part->header_size.virtual_size == 26+2);
+ test_assert(part->body_size.lines == 1);
+ test_assert(part->body_size.physical_size == 4);
+ test_assert(part->body_size.virtual_size == 4+1);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_part_nested_limit_rfc822(void)
+{
+static const char input_msg[] =
+"Content-Type: message/rfc822\n"
+"\n"
+"Content-Type: message/rfc822\n"
+"\n"
+"Content-Type: text/plain\n"
+"\n"
+"1\n";
+ const struct message_parser_settings parser_set = {
+ .max_nested_mime_parts = 2,
+ };
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser mime part nested limit rfc822");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &parser_set, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ part = parts;
+ test_assert(part->children_count == 1);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MESSAGE_RFC822 | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 30);
+ test_assert(part->header_size.virtual_size == 30+2);
+ test_assert(part->body_size.lines == 5);
+ test_assert(part->body_size.physical_size == 58);
+ test_assert(part->body_size.virtual_size == 58+5);
+ test_message_parser_assert_sizes(part, &body_size, &header_size);
+
+ part = parts->children;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_IS_MIME |
+ MESSAGE_PART_FLAG_OVERFLOW));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 30);
+ test_assert(part->header_size.virtual_size == 30+2);
+ test_assert(part->body_size.lines == 3);
+ test_assert(part->body_size.physical_size == 28);
+ test_assert(part->body_size.virtual_size == 28+3);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_part_limit(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"\n"
+"--1\n"
+"Content-Type: multipart/mixed; boundary=\"2\"\n"
+"\n"
+"--2\n"
+"Content-Type: text/plain\n"
+"\n"
+"1\n"
+"--2\n"
+"Content-Type: text/plain\n"
+"\n"
+"22\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"333\n";
+ const struct message_parser_settings parser_set = {
+ .max_total_mime_parts = 4,
+ };
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_size body_size, header_size;
+ pool_t pool;
+
+ test_begin("message parser mime part limit");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &parser_set, FALSE, &parts) < 0);
+
+ i_stream_seek(input, 0);
+ test_message_parser_get_sizes(input, &body_size, &header_size, FALSE);
+
+ part = parts;
+ test_assert(part->children_count == 3);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 15);
+ test_assert(part->body_size.physical_size == 148);
+ test_assert(part->body_size.virtual_size == 148+15);
+ test_message_parser_assert_sizes(part, &body_size, &header_size);
+
+ part = parts->children;
+ test_assert(part->children_count == 2);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 12);
+ test_assert(part->body_size.physical_size == 99);
+ test_assert(part->body_size.virtual_size == 99+12);
+
+ part = parts->children->children;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 26);
+ test_assert(part->header_size.virtual_size == 26+2);
+ test_assert(part->body_size.lines == 0);
+ test_assert(part->body_size.physical_size == 1);
+ test_assert(part->body_size.virtual_size == 1);
+
+ part = parts->children->children->next;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_TEXT |
+ MESSAGE_PART_FLAG_IS_MIME |
+ MESSAGE_PART_FLAG_OVERFLOW));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 26);
+ test_assert(part->header_size.virtual_size == 26+2);
+ test_assert(part->body_size.lines == 5);
+ test_assert(part->body_size.physical_size == 37);
+ test_assert(part->body_size.virtual_size == 37+5);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_part_limit_rfc822(void)
+{
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"\n"
+"--1\n"
+"Content-Type: multipart/mixed; boundary=\"2\"\n"
+"\n"
+"--2\n"
+"Content-Type: message/rfc822\n"
+"\n"
+"Content-Type: text/plain\n"
+"\n"
+"1\n"
+"--2\n"
+"Content-Type: message/rfc822\n"
+"\n"
+"Content-Type: text/plain\n"
+"\n"
+"22\n"
+"--1\n"
+"Content-Type: message/rfc822\n"
+"\n"
+"Content-Type: text/plain\n"
+"\n"
+"333\n";
+ const struct message_parser_settings parser_set = {
+ .max_total_mime_parts = 3,
+ };
+ struct message_parser_ctx *parser;
+ struct istream *input;
+ struct message_part *parts, *part;
+ struct message_block block;
+ pool_t pool;
+ int ret;
+
+ test_begin("message parser mime part limit rfc822");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ parser = message_parser_init(pool, input, &parser_set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0) ;
+ test_assert(ret < 0);
+ message_parser_deinit(&parser, &parts);
+
+ part = parts;
+ test_assert(part->children_count == 2);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 21);
+ test_assert(part->body_size.physical_size == 238);
+ test_assert(part->body_size.virtual_size == 238+21);
+
+ part = parts->children;
+ test_assert(part->children_count == 1);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 45+2);
+ test_assert(part->body_size.lines == 18);
+ test_assert(part->body_size.physical_size == 189);
+ test_assert(part->body_size.virtual_size == 189+18);
+
+ part = parts->children->children;
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == (MESSAGE_PART_FLAG_IS_MIME |
+ MESSAGE_PART_FLAG_OVERFLOW));
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 30);
+ test_assert(part->header_size.virtual_size == 30+2);
+ test_assert(part->body_size.lines == 15);
+ test_assert(part->body_size.physical_size == 155);
+ test_assert(part->body_size.virtual_size == 155+15);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_version(void)
+{
+ test_begin("message parser mime version");
+
+ /* Check that MIME version is accepted. */
+static const char *const input_msgs[] = {
+ /* valid mime header */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: 1.0\n\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"hello, world\n"
+"--1\n",
+ /* future mime header */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: 2.0\n\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"hello, world\n"
+"--1\n",
+ /* invalid value in mime header */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version: abc\n\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"hello, world\n"
+"--1\n",
+ /* missing value in mime header */
+"Content-Type: multipart/mixed; boundary=\"1\"\n"
+"MIME-Version:\n\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"hello, world\n"
+"--1\n"
+ };
+
+ const struct message_parser_settings parser_set = {
+ .max_total_mime_parts = 2,
+ .flags = MESSAGE_PARSER_FLAG_MIME_VERSION_STRICT,
+ };
+ struct istream *input;
+ struct message_part *parts, *part;
+ pool_t pool;
+
+ for (size_t i = 0; i < N_ELEMENTS(input_msgs); i++) {
+ ssize_t variance = (ssize_t)strlen(input_msgs[i]) - (ssize_t)strlen(input_msgs[0]);
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msgs[i]);
+
+ test_assert(message_parse_stream(pool, input, &parser_set, TRUE, &parts) < 0);
+ part = parts;
+
+ test_assert_idx(part->children_count == 1, i);
+ test_assert_idx(part->flags == (MESSAGE_PART_FLAG_MULTIPART | MESSAGE_PART_FLAG_IS_MIME), i);
+ test_assert_idx(part->header_size.lines == 3, i);
+ test_assert_idx(part->header_size.physical_size == (size_t)(63 + variance), i);
+ test_assert_idx(part->header_size.virtual_size == (size_t)(66 + variance), i);
+ test_assert_idx(part->body_size.lines == 5, i);
+ test_assert_idx(part->body_size.physical_size == 47, i);
+ test_assert_idx(part->body_size.virtual_size == 52, i);
+ test_assert_strcmp_idx(part->data->content_type, "multipart", i);
+ test_assert_strcmp_idx(part->data->content_subtype, "mixed", i);
+ part = part->children;
+
+ test_assert_idx(part->children_count == 0, i);
+ test_assert_idx(part->flags == (MESSAGE_PART_FLAG_TEXT |
+ MESSAGE_PART_FLAG_IS_MIME |
+ MESSAGE_PART_FLAG_OVERFLOW), i);
+ test_assert_idx(part->header_size.lines == 2, i);
+ test_assert_idx(part->header_size.physical_size == 26, i);
+ test_assert_idx(part->header_size.virtual_size == 28, i);
+ test_assert_idx(part->body_size.lines == 2, i);
+ test_assert_idx(part->body_size.physical_size == 17, i);
+ test_assert_strcmp_idx(part->data->content_type, "text", i);
+ test_assert_strcmp_idx(part->data->content_subtype, "plain", i);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ };
+
+ /* test for +10MB header */
+ const size_t test_hdr_size = 10*1024*1024UL;
+ const size_t test_msg_size = test_hdr_size + 1024UL;
+ /* add space for parser */
+ pool = pool_alloconly_create("10mb header", test_msg_size + 10240UL);
+ string_t *buffer = str_new(pool, test_msg_size + 1);
+
+ str_append(buffer, "MIME-Version: ");
+
+ /* @UNSAFE */
+ char *tmp = buffer_append_space_unsafe(buffer, test_hdr_size);
+ memset(tmp, 'a', test_hdr_size);
+
+ str_append_c(buffer, '\n');
+ str_append(buffer, "Content-Type: multipart/mixed; boundary=1\n\n--1--");
+
+ input = test_istream_create_data(buffer->data, buffer->used);
+ test_assert(message_parse_stream(pool, input, &parser_set, TRUE, &parts) < 0);
+
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+static void test_message_parser_mime_version_missing(void)
+{
+ test_begin("message parser mime version missing");
+
+static const char input_msg[] =
+"Content-Type: multipart/mixed; boundary=\"1\"\n\n"
+"--1\n"
+"Content-Type: text/plain\n"
+"\n"
+"hello, world\n"
+"--1\n";
+
+ const struct message_parser_settings parser_set = {
+ .max_total_mime_parts = 2,
+ .flags = MESSAGE_PARSER_FLAG_MIME_VERSION_STRICT,
+ };
+ struct istream *input;
+ struct message_part *parts, *part;
+ pool_t pool;
+
+ pool = pool_alloconly_create("message parser", 10240);
+ input = test_istream_create(input_msg);
+
+ test_assert(message_parse_stream(pool, input, &parser_set, TRUE, &parts) < 0);
+ part = parts;
+
+ /* non-MIME message should end up as plain text mail */
+
+ test_assert(part->children_count == 0);
+ test_assert(part->flags == MESSAGE_PART_FLAG_TEXT);
+ test_assert(part->header_size.lines == 2);
+ test_assert(part->header_size.physical_size == 45);
+ test_assert(part->header_size.virtual_size == 47);
+ test_assert(part->body_size.lines == 5);
+ test_assert(part->body_size.physical_size == 47);
+ test_assert(part->body_size.virtual_size == 52);
+ test_assert(part->children == NULL);
+ test_assert(part->data->content_type == NULL);
+ test_assert(part->data->content_subtype == NULL);
+
+ test_parsed_parts(input, parts);
+ i_stream_unref(&input);
+ pool_unref(&pool);
+
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_parser_small_blocks,
+ test_message_parser_stop_early,
+ test_message_parser_truncated_mime_headers,
+ test_message_parser_truncated_mime_headers2,
+ test_message_parser_truncated_mime_headers3,
+ test_message_parser_empty_multipart,
+ test_message_parser_duplicate_mime_boundary,
+ test_message_parser_garbage_suffix_mime_boundary,
+ test_message_parser_trailing_dashes,
+ test_message_parser_continuing_mime_boundary,
+ test_message_parser_continuing_truncated_mime_boundary,
+ test_message_parser_continuing_mime_boundary_reverse,
+ test_message_parser_long_mime_boundary,
+ test_message_parser_no_eoh,
+ test_message_parser_mime_part_nested_limit,
+ test_message_parser_mime_part_nested_limit_rfc822,
+ test_message_parser_mime_part_limit,
+ test_message_parser_mime_part_limit_rfc822,
+ test_message_parser_mime_version,
+ test_message_parser_mime_version_missing,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-part-serialize.c b/src/lib-mail/test-message-part-serialize.c
new file mode 100644
index 0000000..2e03140
--- /dev/null
+++ b/src/lib-mail/test-message-part-serialize.c
@@ -0,0 +1,266 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "message-parser.h"
+#include "message-part.h"
+#include "message-part-serialize.h"
+#include "test-common.h"
+
+static const struct message_parser_settings set_empty = { .flags = 0 };
+
+static int message_parse_stream(pool_t pool, struct istream *input,
+ const struct message_parser_settings *set,
+ struct message_part **parts_r)
+{
+ int ret;
+ struct message_parser_ctx *parser;
+ struct message_block block;
+
+ i_zero(&block);
+ parser = message_parser_init(pool, input, set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0) ;
+ message_parser_deinit(&parser, parts_r);
+ test_assert(input->stream_errno == 0);
+ return ret;
+}
+
+static void test_parsed_parts(struct istream *input, struct message_part *parts)
+{
+ const struct message_parser_settings parser_set = {
+ .flags = MESSAGE_PARSER_FLAG_SKIP_BODY_BLOCK,
+ };
+ struct message_parser_ctx *parser;
+ struct message_block block;
+ struct message_part *parts2;
+ const char *error;
+
+ i_stream_seek(input, 0);
+
+ parser = message_parser_init_from_parts(parts, input, &parser_set);
+ while (message_parser_parse_next_block(parser, &block) > 0) ;
+ test_assert(message_parser_deinit_from_parts(&parser, &parts2, &error) == 0);
+ test_assert(message_part_is_equal(parts, parts2));
+}
+
+#define TEST_CASE_DATA(x) \
+ { .value = (const unsigned char*)((x)), .value_len = sizeof((x))-1 }
+
+static void test_message_serialize_deserialize(void)
+{
+static struct test_case {
+ struct test_case_data {
+ const unsigned char *value;
+ size_t value_len;
+ } input;
+ int expect_ret;
+} test_cases[] = {
+ {
+ .input = TEST_CASE_DATA("hello, world"),
+ .expect_ret = -1,
+ },
+ {
+ .input = TEST_CASE_DATA(
+"Subject: Hide and seek\n"
+"MIME-Version: 1.0\n"
+"Content-Type: multipart/mixed; boundary=1\n"
+"\n--1\n"
+"Content-Type: multipart/signed; protocol=\"signature/plain\"; migalc=\"pen+paper\"; boundary=2\n"
+"X-Signature-Type: penmanship\n"
+"\n--2\n"
+"Content-Type: multipart/alternative; boundary=3\n"
+"\n--3\n"
+"Content-Type: text/html; charset=us-ascii\n\n"
+"<html><head><title>Search me</title></head><body><p>Don't find me here</p></body></html>\n"
+"\n--3\n"
+"Content-Type: text/plain\n"
+"Content-Transfer-Encoding: binary\n"
+"\n"
+"Search me, and Find me here"
+"\n--3--\n"
+"\n--2\n"
+"Content-Type: signature/plain; charset=us-ascii\n"
+"\n"
+"Signed by undersigned"
+"\n--2--\n"
+"\n--1--"),
+ .expect_ret = -1,
+ },
+ {
+ .input = TEST_CASE_DATA(
+"From: Moderator-Address <moderator>\n" \
+"Content-Type: multipart/digest; boundary=1;\n" \
+"\n\n--1\n" \
+"From: someone-else <someone@else>\n" \
+"Subject: my opinion\n" \
+"\n" \
+"This is my opinion" \
+"\n--1\n\n" \
+"From: another one <another@one>\n" \
+"Subject: i disagree\n" \
+"\n" \
+"Not agreeing one bit!" \
+"\n--1\n\n" \
+"From: attachment <attachment@user>\n" \
+"Subject: funny hat\n" \
+"Content-Type: multipart/mixed; boundary=2\n" \
+"\n--2\n" \
+"Content-Type: text/plain\n"
+"Content-Transfer-Encoding: binary\n"
+"\n" \
+"Lovely attachment for you" \
+"\n--2\n" \
+"Content-Type: application/octet-stream; disposition=attachment; name=\"test.txt\"\n" \
+"Content-Transfer-Encoding: binary\n" \
+"\n" \
+"Foobar" \
+"\n--2--" \
+"\n--1--"),
+ .expect_ret = -1,
+ },
+};
+ test_begin("message part serialize deserialize");
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) {
+ const struct test_case *tc = &test_cases[i];
+ struct message_part *parts;
+ const char *error;
+ pool_t pool = pool_alloconly_create("message parser", 10240);
+ struct istream *is =
+ test_istream_create_data(tc->input.value, tc->input.value_len);
+ test_assert(message_parse_stream(pool, is, &set_empty, &parts) ==
+ tc->expect_ret);
+ buffer_t *dest = buffer_create_dynamic(pool, 256);
+ message_part_serialize(parts, dest);
+ parts = message_part_deserialize(pool, dest->data, dest->used,
+ &error);
+ test_assert(parts != NULL);
+ if (parts != NULL)
+ test_parsed_parts(is, parts);
+ else
+ i_error("message_part_deserialize: %s", error);
+ i_stream_unref(&is);
+ pool_unref(&pool);
+ }
+ test_end();
+}
+
+#define TEST_CASE(data, size, expect_error) \
+ test_assert(message_part_deserialize(pool, (data), (size), &error) == NULL); \
+ test_assert_strcmp(error, (expect_error))
+
+static void test_message_deserialize_errors(void)
+{
+ test_begin("message part deserialize errors");
+ const char *error = NULL;
+ struct message_part part, child1, child2;
+ pool_t pool = pool_datastack_create();
+ buffer_t *dest = buffer_create_dynamic(pool, 256);
+
+ /* empty part */
+ TEST_CASE("", 0, "Not enough data");
+
+ /* truncated part */
+ TEST_CASE("\x08\x00\x00", 3, "Not enough data");
+
+ /* bad sizes */
+ i_zero(&part);
+ part.flags = MESSAGE_PART_FLAG_TEXT;
+ part.header_size.virtual_size = 0;
+ part.header_size.physical_size = 100;
+ message_part_serialize(&part, dest);
+ TEST_CASE(dest->data, dest->used, "header_size.virtual_size too small");
+ buffer_set_used_size(dest, 0);
+
+ i_zero(&part);
+ part.flags = MESSAGE_PART_FLAG_TEXT;
+ part.body_size.virtual_size = 0;
+ part.body_size.physical_size = 100;
+ message_part_serialize(&part, dest);
+ TEST_CASE(dest->data, dest->used, "body_size.virtual_size too small");
+ buffer_set_used_size(dest, 0);
+
+ i_zero(&part);
+ part.flags = MESSAGE_PART_FLAG_MESSAGE_RFC822;
+ message_part_serialize(&part, dest);
+ TEST_CASE(dest->data, dest->used, "message/rfc822 part has no children");
+ buffer_set_used_size(dest, 0);
+
+ i_zero(&part);
+ i_zero(&child1);
+ i_zero(&child2);
+ part.flags = MESSAGE_PART_FLAG_MESSAGE_RFC822;
+ part.children_count = 2;
+ child1.flags = MESSAGE_PART_FLAG_TEXT;
+ child1.parent = &part;
+ part.children = &child1;
+ child2.flags = MESSAGE_PART_FLAG_TEXT;
+ part.children->next = &child2;
+ child2.parent = &part;
+ message_part_serialize(&part, dest);
+ TEST_CASE(dest->data, dest->used, "message/rfc822 part has multiple children");
+ buffer_set_used_size(dest, 0);
+
+ i_zero(&part);
+ i_zero(&child1);
+ part.flags = MESSAGE_PART_FLAG_MULTIPART|MESSAGE_PART_FLAG_IS_MIME;
+ part.children_count = 1;
+ child1.flags = MESSAGE_PART_FLAG_TEXT;
+ child1.parent = &part;
+ part.children = &child1;
+ message_part_serialize(&part, dest);
+ for (size_t i = 0; i < dest->used - 1; i++)
+ TEST_CASE(dest->data, i, "Not enough data");
+ buffer_append_c(dest, '\x00');
+ TEST_CASE(dest->data, dest->used, "Too much data");
+
+ test_end();
+}
+
+static enum fatal_test_state test_message_deserialize_fatals(unsigned int stage)
+{
+ const char *error = NULL;
+ struct message_part part, child1, child2;
+
+ pool_t pool = pool_datastack_create();
+ buffer_t *dest = buffer_create_dynamic(pool, 256);
+
+ switch(stage) {
+ case 0:
+ test_expect_fatal_string("part->children == NULL");
+ test_begin("message deserialize fatals");
+ i_zero(&part);
+ i_zero(&child1);
+ i_zero(&child2);
+ part.flags = MESSAGE_PART_FLAG_MULTIPART|MESSAGE_PART_FLAG_IS_MIME;
+ part.children_count = 1;
+ child1.flags = MESSAGE_PART_FLAG_TEXT;
+ child1.parent = &part;
+ part.children = &child1;
+ child2.parent = &child1;
+ child1.children_count = 1;
+ child1.children = &child2;
+
+ message_part_serialize(&part, dest);
+ TEST_CASE(dest->data, dest->used, "message/rfc822 part has multiple children");
+ buffer_set_used_size(dest, 0);
+ return FATAL_TEST_FAILURE;
+ };
+
+ test_end();
+ return FATAL_TEST_FINISHED;
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_serialize_deserialize,
+ test_message_deserialize_errors,
+ NULL
+ };
+ static enum fatal_test_state (*const fatal_functions[])(unsigned int) = {
+ test_message_deserialize_fatals,
+ NULL
+ };
+ return test_run_with_fatals(test_functions, fatal_functions);
+}
diff --git a/src/lib-mail/test-message-part.c b/src/lib-mail/test-message-part.c
new file mode 100644
index 0000000..49cfd65
--- /dev/null
+++ b/src/lib-mail/test-message-part.c
@@ -0,0 +1,117 @@
+/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "message-parser.h"
+#include "test-common.h"
+
+static const char test_msg[] =
+"From user@domain Fri Feb 22 17:06:23 2008\n"
+"From: user@domain.org\n"
+"Date: Sat, 24 Mar 2007 23:00:00 +0200\n"
+"Mime-Version: 1.0\n"
+"Content-Type: multipart/mixed; boundary=\"foo bar\"\n"
+"\n"
+"Root MIME prologue\n"
+"\n"
+"--foo bar\n"
+"Content-Type: text/x-myown; charset=us-ascii\n"
+"\n"
+"hello\n"
+"\n"
+"--foo bar\n"
+"Content-Type: message/rfc822\n"
+"\n"
+"From: sub@domain.org\n"
+"Date: Sun, 12 Aug 2012 12:34:56 +0300\n"
+"Subject: submsg\n"
+"Content-Type: multipart/alternative; boundary=\"sub1\"\n"
+"\n"
+"Sub MIME prologue\n"
+"--sub1\n"
+"Content-Type: text/html\n"
+"\n"
+"<p>Hello world</p>\n"
+"\n"
+"--sub1\n"
+"Content-Type: multipart/alternative; boundary=\"sub2\"\n"
+"\n"
+"--sub2\n"
+"Content-Type: multipart/alternative; boundary=\"sub3\"\n"
+"\n"
+"--sub3\n"
+"\n"
+"sub3 text\n"
+"--sub3\n"
+"\n"
+"sub3 text2\n"
+"--sub3--\n"
+"\n"
+"sub2 text\n"
+"--sub2\n"
+"\n"
+"sub2 text2\n"
+"--sub1--\n"
+"Sub MIME epilogue\n"
+"\n"
+"--foo bar\n"
+"Content-Type: text/plain\n"
+"\n"
+"Another part\n"
+"--foo bar--\n"
+"Root MIME epilogue\n"
+"\n";
+#define TEST_MSG_LEN (sizeof(test_msg)-1)
+
+static void test_message_part_idx(void)
+{
+ const struct message_parser_settings set = { .flags = 0 };
+ struct message_parser_ctx *parser;
+ struct istream *input;
+ struct message_part *parts, *part, *prev_part;
+ struct message_block block;
+ unsigned int i, prev_idx = 0, part_idx;
+ pool_t pool;
+ int ret;
+
+ test_begin("message part indexes");
+ pool = pool_alloconly_create("message parser", 10240);
+ input = i_stream_create_from_data(test_msg, TEST_MSG_LEN);
+
+ parser = message_parser_init(pool, input, &set);
+ while ((ret = message_parser_parse_next_block(parser, &block)) > 0) {
+ part_idx = message_part_to_idx(block.part);
+ test_assert(part_idx >= prev_idx);
+ prev_idx = part_idx;
+ }
+ test_assert(ret < 0);
+ message_parser_deinit(&parser, &parts);
+ test_assert(input->stream_errno == 0);
+
+ part = message_part_by_idx(parts, 0);
+ test_assert(part == parts);
+ test_assert(message_part_by_idx(parts, 1) == parts->children);
+
+ for (i = 1; i < 11; i++) {
+ prev_part = part;
+ part = message_part_by_idx(parts, i);
+ test_assert(part != NULL);
+ test_assert(part != NULL && message_part_to_idx(part) == i);
+ test_assert(part != NULL && prev_part != NULL &&
+ prev_part->physical_pos < part->physical_pos);
+ }
+ test_assert(message_part_by_idx(parts, i) == NULL);
+
+ i_stream_unref(&input);
+ pool_unref(&pool);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_part_idx,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-search.c b/src/lib-mail/test-message-search.c
new file mode 100644
index 0000000..d137a58
--- /dev/null
+++ b/src/lib-mail/test-message-search.c
@@ -0,0 +1,521 @@
+/* Copyright (c) 2016-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "str.h"
+#include "unichar.h"
+#include "message-parser.h"
+#include "message-search.h"
+#include "test-common.h"
+
+struct test_case_data {
+ const unsigned char *value;
+ size_t value_len;
+};
+
+#define TEST_CASE_DATA(x) \
+ { .value = (const unsigned char*)((x)), .value_len = sizeof((x))-1 }
+#define TEST_CASE_DATA_EMPTY \
+ { .value = NULL, .value_len = 0 }
+#define TEST_CASE_PLAIN_PREAMBLE \
+"Content-Type: text/plain\n" \
+"Content-Transfer-Encoding: binary\n"
+
+struct test_case {
+ struct test_case_data input;
+ const char *search;
+ struct test_case_data output;
+ bool expect_found;
+ bool expect_body;
+ bool expect_header;
+ const char *hdr_name;
+};
+
+static void compare_search_result(const struct test_case *tc,
+ const struct message_block *block,
+ size_t i)
+{
+ if (block->hdr != NULL) {
+ /* found header */
+ test_assert_idx(tc->expect_header == TRUE, i);
+ test_assert_strcmp_idx(tc->hdr_name, block->hdr->name, i);
+ test_assert_idx(block->hdr->full_value != NULL &&
+ tc->output.value != NULL &&
+ tc->output.value_len <= block->hdr->full_value_len &&
+ memcmp(tc->output.value, block->hdr->full_value,
+ tc->output.value_len) == 0, i);
+ } else if (block->data != NULL) {
+ /* found body */
+ test_assert_idx(tc->expect_body == TRUE, i);
+ test_assert_idx(block->data != NULL &&
+ tc->output.value != NULL &&
+ tc->output.value_len <= block->size &&
+ memcmp(tc->output.value, block->data,
+ tc->output.value_len) == 0, i);
+ } else {
+ test_assert_idx(tc->expect_header == FALSE, i);
+ test_assert_idx(tc->expect_body == FALSE, i);
+ }
+}
+
+#define SIGNED_MIME_CORPUS \
+"Subject: Hide and seek\n" \
+"MIME-Version: 1.0\n" \
+"Content-Type: multipart/mixed; boundary=1\n" \
+"\n--1\n" \
+"Content-Type: multipart/signed; protocol=\"signature/plain\"; migalc=\"pen+paper\"; boundary=2\n" \
+"X-Signature-Type: penmanship\n" \
+"\n--2\n" \
+"Content-Type: multipart/alternative; boundary=3\n" \
+"\n--3\n" \
+"Content-Type: text/html; charset=us-ascii\n\n" \
+"<html><head><title>Search me</title></head><body><p>Don't find me here</p></body></html>\n" \
+"\n--3\n" \
+TEST_CASE_PLAIN_PREAMBLE \
+"\n" \
+"Search me, and Find me here" \
+"\n--3--\n" \
+"\n--2\n" \
+"Content-Type: signature/plain; charset=us-ascii\n" \
+"\n" \
+"Signed by undersigned" \
+"\n--2--\n" \
+"\n--1--"
+
+#define PARTIAL_MESSAGE_CORPUS \
+"X-Weird-Header-1: Bar\n" \
+"X-Weird-Header-2: Hello\n" \
+"Message-ID: <c6cceebc-1dcf-11eb-be8c-f7ca132cbfea@example.org>\n" \
+"Content-Type: text/plain; charset=\"us-ascii\"\n" \
+"Content-Transfer-Encoding: base64\n" \
+"\n" \
+"dGhpcyBpcyB0aGUgZmlyc3QgcGFydCBvZiB0aGUgbWVzc2FnZQo="
+
+#define PARTIAL_MIME_CORPUS \
+"Subject: In parts\n" \
+"MIME-Version: 1.0\n" \
+"Content-Type: multipart/mixed; boundary=1\n" \
+"\n--1\n" \
+TEST_CASE_PLAIN_PREAMBLE \
+"\n" \
+"Hi, this is the fancy thing I was talking about!" \
+"\n--1\n" \
+"Content-Type: Message/Partial; number=1; total=5; id=\"heks68ewe@example.org\"\n" \
+"\n" \
+PARTIAL_MESSAGE_CORPUS \
+"\n--1--\n"
+
+#define UT8_CORPUS_CONTENT \
+"\xe4\xba\xba\xe6\xa8\xa9\xe3\x81\xae\xe7\x84\xa1\xe8\xa6\x96\xe5\x8f\x8a"
+
+#define UTF8_CORPUS \
+"Subject: =?UTF-8?B?44GT44KT44Gr44Gh44Gv?=\n" \
+"MIME-Version: 1.0\n" \
+"Content-Type: multipart/mixed; boundary=1;\n" \
+" comment=\"\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf\xe5\xa2\x83\xe7\x95\x8c\xe3" \
+ "\x81\xae\xe3\x81\x82\xe3\x82\x8b\xe3\x83\xa1\xe3\x83\x83\xe3\x82" \
+ "\xbb\xe3\x83\xbc\xe3\x82\xb8\xe3\x81\xa7\xe3\x81\x99\"\n" \
+"\n--1\n" \
+TEST_CASE_PLAIN_PREAMBLE \
+"Content-Language: ja\n" \
+"\n" \
+UT8_CORPUS_CONTENT \
+"\n--1--"
+
+#define MULTIPART_DIGEST_CORPUS \
+"From: Moderator-Address <moderator>\n" \
+"Content-Type: multipart/digest; boundary=1;\n" \
+"\n\n--1\n" \
+"From: someone-else <someone@else>\n" \
+"Subject: my opinion\n" \
+"\n" \
+"This is my opinion" \
+"\n--1\n\n" \
+"From: another one <another@one>\n" \
+"Subject: i disagree\n" \
+"\n" \
+"Not agreeing one bit!" \
+"\n--1\n\n" \
+"From: attachment <attachment@user>\n" \
+"Subject: funny hat\n" \
+"Content-Type: multipart/mixed; boundary=2\n" \
+"\n--2\n" \
+TEST_CASE_PLAIN_PREAMBLE \
+"\n" \
+"Lovely attachment for you" \
+"\n--2\n" \
+"Content-Type: application/octet-stream; disposition=attachment; name=\"test.txt\"\n" \
+"Content-Transfer-Encoding: binary\n" \
+"\n" \
+"Foobar" \
+"\n--2--" \
+"\n--1--"
+
+static void test_message_search(void)
+{
+ const struct test_case test_cases[] = {
+ { /* basic test */
+ .input = TEST_CASE_DATA(
+"MIME-Version: 1.0\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hello, world"),
+ .search = "Hello",
+ .output = TEST_CASE_DATA("Hello, world"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* look for something that's not found */
+ .input = TEST_CASE_DATA(
+"MIME-Version: 1.0\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hallo, world"),
+ .search = "Hello",
+ .output = TEST_CASE_DATA_EMPTY,
+ .expect_found = FALSE,
+ },
+ { /* header value search */
+ .input = TEST_CASE_DATA(
+"Subject: Hello, World\n"
+"MIME-Version: 1.0\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hallo, world"),
+ .search = "Hello",
+ .output = TEST_CASE_DATA("Hello, World"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Subject",
+ },
+ { /* header value wrapped in base64 */
+ .input = TEST_CASE_DATA(
+"Subject: =?UTF-8?B?SGVsbG8sIFdvcmxk?=\n"
+"MIME-Version: 1.0\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hallo, world"),
+ .search = "Hello",
+ .output = TEST_CASE_DATA("Hello, World"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Subject",
+ },
+ { /* hidden inside one multipart */
+ .input = TEST_CASE_DATA(
+"Subject: Hide and seek\n"
+"MIME-Version: 1.0\n"
+"CONTENT-TYPE: MULTIPART/MIXED; BOUNDARY=\"A\"\n\n"
+"--A\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hallo, world"
+"\n--A\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hullo, world"
+"\n--A\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Hello, world"
+"\n--A--\n"
+),
+ .search = "Hello",
+ .output = TEST_CASE_DATA("Hello, world"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* same with emoji boundary */
+ .input = TEST_CASE_DATA(
+"Subject: Hide and seek\n"
+"MIME-Version: 1.0\n"
+"CONTENT-TYPE: MULTIPART/MIXED; BOUNDARY=\"\xF0\x9F\x98\x82\"; COMMENT=\"Boundary is U+1F602\"\n\n"
+"--\xF0\x9F\x98\x82\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Face with Tears of Joy"
+"\n--\xF0\x9F\x98\x82\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Emoji"
+"\n--\xF0\x9F\x98\x82--\n"
+),
+ .search = "Emoji",
+ .output = TEST_CASE_DATA("Emoji"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* Nested body search */
+ .input = TEST_CASE_DATA(SIGNED_MIME_CORPUS),
+ .search = "Find me here",
+ .output = TEST_CASE_DATA("Search me, and Find me here"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* Nested body search (won't look into signature/plain) */
+ .input = TEST_CASE_DATA(SIGNED_MIME_CORPUS),
+ .search = "undersigned",
+ .output = TEST_CASE_DATA_EMPTY,
+ .expect_found = FALSE,
+ },
+ { /* Nested mime part header search */
+ .input = TEST_CASE_DATA(SIGNED_MIME_CORPUS),
+ .search = "penmanship",
+ .output = TEST_CASE_DATA("penmanship"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "X-Signature-Type",
+ },
+ { /* Nested mime part header parameter search */
+ .input = TEST_CASE_DATA(SIGNED_MIME_CORPUS),
+ .search = "pen+paper",
+ .output = TEST_CASE_DATA("multipart/signed; protocol=\"signature/plain\"; migalc=\"pen+paper\"; boundary=2"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Content-Type",
+ },
+ { /* Partial message - must not parse the content */
+ .input = TEST_CASE_DATA(PARTIAL_MIME_CORPUS),
+ .search = "Bar",
+ .output = TEST_CASE_DATA(PARTIAL_MESSAGE_CORPUS),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* Partial message - must not parse the content */
+ .input = TEST_CASE_DATA(PARTIAL_MIME_CORPUS),
+ .search = "fancy thing",
+ .output = TEST_CASE_DATA("Hi, this is the fancy thing I was talking about!"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* UTF-8 searches */
+ .input = TEST_CASE_DATA(UTF8_CORPUS),
+ .search = "\xe4\xba\xba\xe6\xa8\xa9",
+ .output = TEST_CASE_DATA(UT8_CORPUS_CONTENT),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* UTF-8 search header */
+ .input = TEST_CASE_DATA(UTF8_CORPUS),
+ .search = "\xe3\x81\x93\xe3\x82\x93",
+ .output = TEST_CASE_DATA("\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Subject",
+ },
+ { /* UTF-8 searches content-type parameter */
+ .input = TEST_CASE_DATA(UTF8_CORPUS),
+ .search = "\xe3\x81\xa7\xe3\x81\x99",
+ .output = TEST_CASE_DATA(
+"multipart/mixed; boundary=1;\n comment=\"\xe3\x81\x93\xe3\x82\x8c\xe3\x81\xaf"
+"\xe5\xa2\x83\xe7\x95\x8c\xe3\x81\xae\xe3\x81\x82\xe3\x82\x8b\xe3\x83\xa1\xe3"
+"\x83\x83\xe3\x82\xbb\xe3\x83\xbc\xe3\x82\xb8\xe3\x81\xa7\xe3\x81\x99\""),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Content-Type",
+ },
+ {
+ /* Invalid UTF-8 boundary (should not matter) */
+ .input = TEST_CASE_DATA(
+"Content-Type: multipart/mixed; boundary=\"\xff\xff\xff\xff\"\n"
+"\n--\xff\xff\xff\xff\n"
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Can you find me?"
+"\n--\xff\xff\xff\xff--"),
+ .search = "Can you find me?",
+ .output = TEST_CASE_DATA("Can you find me?"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ {
+ /* Invalid UTF-8 in subject (should work) */
+ .input = TEST_CASE_DATA(
+"Subject: =?UTF-8?B?Um90dGVuIP////8gdGV4dA==?="
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Such horror"),
+ .search = "Rotten",
+ .output = TEST_CASE_DATA("Rotten "UNICODE_REPLACEMENT_CHAR_UTF8" text"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Subject",
+ },
+ {
+ /* Invalid UTF-8 in body (should work) */
+ .input = TEST_CASE_DATA(
+"Subject: =?UTF-8?B?Um90dGVuIP////8gdGV4dA==?="
+TEST_CASE_PLAIN_PREAMBLE
+"\n"
+"Such horror \xff\xff\xff\xff"),
+ .search = "Such horror",
+ .output = TEST_CASE_DATA("Such horror "UNICODE_REPLACEMENT_CHAR_UTF8),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ {
+ /* UTF-8 in content-type parameter */
+ .input = TEST_CASE_DATA(
+"Content-Type: multipart/mixed; boundary=1; \xF0\x9F\x98\xAD=\"\xF0\x9F\xA5\xBA U+1F62D=U+1F97A\"\n"
+"\n--1--\n"),
+ .search = "U+1F62D",
+ .output = TEST_CASE_DATA("multipart/mixed; boundary=1; \xF0\x9F\x98\xAD=\"\xF0\x9F\xA5\xBA U+1F62D=U+1F97A\""),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Content-Type",
+ },
+ {
+ /* Broken UTF-8 in content-type parameter */
+ .input = TEST_CASE_DATA(
+"Content-Type: multipart/mixed; boundary=1;"
+" \xFF\xFF\xFF\xFF=\"\xF0\x9F\xA5\xBA U+1F62D=U+1F97A\"\n"
+"\n--1--\n"),
+ .search = "U+1F62D",
+ .output = TEST_CASE_DATA("multipart/mixed; boundary=1; "UNICODE_REPLACEMENT_CHAR_UTF8"=\"\xF0\x9F\xA5\xBA U+1F62D=U+1F97A\""),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Content-Type",
+ },
+ { /* Multipart digest */
+ .input = TEST_CASE_DATA(MULTIPART_DIGEST_CORPUS),
+ .search = "Not agreeing",
+ .output = TEST_CASE_DATA("Not agreeing one bit!"),
+ .expect_found = TRUE,
+ .expect_body = TRUE,
+ },
+ { /* Multipart digest header */
+ .input = TEST_CASE_DATA(MULTIPART_DIGEST_CORPUS),
+ .search = "someone-else",
+ .output = TEST_CASE_DATA("someone-else <someone@else>"),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "From",
+ },
+ { /* Multipart digest header parameter */
+ .input = TEST_CASE_DATA(MULTIPART_DIGEST_CORPUS),
+ .search = "test.txt",
+ .output = TEST_CASE_DATA("application/octet-stream; disposition=attachment; name=\"test.txt\""),
+ .expect_found = TRUE,
+ .expect_body = FALSE,
+ .expect_header = TRUE,
+ .hdr_name = "Content-Type",
+ },
+};
+
+ test_begin("message search");
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ struct message_search_context *sctx;
+ struct message_block raw_block, decoded_block;
+ struct message_part *parts;
+ const char *error;
+ bool found = FALSE;
+ const struct test_case *tc = &test_cases[i];
+ struct message_parser_settings set = {
+ .hdr_flags = MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP,
+ };
+ pool_t pool = pool_alloconly_create("message parser", 10240);
+ struct istream *is =
+ test_istream_create_data(tc->input.value, tc->input.value_len);
+ struct message_parser_ctx *pctx =
+ message_parser_init(pool, is, &set);
+ int ret;
+ sctx = message_search_init(tc->search, NULL, tc->expect_header ?
+ 0 : MESSAGE_SEARCH_FLAG_SKIP_HEADERS);
+ while ((ret = message_parser_parse_next_block(pctx, &raw_block)) > 0) {
+ if (message_search_more_get_decoded(sctx, &raw_block,
+ &decoded_block)) {
+ found = TRUE;
+ compare_search_result(tc, &decoded_block, i);
+ }
+ }
+ test_assert(ret == -1);
+ test_assert_idx(tc->expect_found == found, i);
+ message_parser_deinit(&pctx, &parts);
+ test_assert(is->stream_errno == 0);
+ i_stream_seek(is, 0);
+ if ((ret = message_search_msg(sctx, is, parts, &error)) < 0) {
+ i_error("Search error: %s", error);
+ } else {
+ test_assert_idx(tc->expect_found == (ret == 1), i);
+ }
+ /* and once more */
+ i_stream_seek(is, 0);
+ if ((ret = message_search_msg(sctx, is, NULL, &error)) < 0) {
+ i_error("Search error: %s", error);
+ } else {
+ test_assert_idx(tc->expect_found == (ret == 1), i);
+ }
+ message_search_deinit(&sctx);
+ test_assert(is->stream_errno == 0);
+ i_stream_unref(&is);
+ pool_unref(&pool);
+ } T_END;
+
+ test_end();
+
+}
+
+static void test_message_search_more_get_decoded(void)
+{
+ const char input[] = "p\xC3\xB6\xC3\xB6";
+ const unsigned char text_plain[] = "text/plain; charset=utf-8";
+ struct message_search_context *ctx1, *ctx2;
+ struct message_block raw_block, decoded_block;
+ struct message_header_line hdr;
+ struct message_part part;
+ unsigned int i;
+
+ test_begin("message_search_more_get_decoded()");
+
+ ctx1 = message_search_init("p\xC3\xA4\xC3\xA4", NULL, 0);
+ ctx2 = message_search_init("p\xC3\xB6\xC3\xB6", NULL, 0);
+
+ i_zero(&raw_block);
+ raw_block.part = &part;
+
+ /* feed the Content-Type header */
+ i_zero(&hdr);
+ hdr.name = "Content-Type"; hdr.name_len = strlen(hdr.name);
+ hdr.value = hdr.full_value = text_plain;
+ hdr.value_len = hdr.full_value_len = sizeof(text_plain)-1;
+ raw_block.hdr = &hdr;
+ test_assert(!message_search_more_get_decoded(ctx1, &raw_block, &decoded_block));
+ test_assert(!message_search_more_decoded(ctx2, &decoded_block));
+
+ /* EOH */
+ raw_block.hdr = NULL;
+ test_assert(!message_search_more_get_decoded(ctx1, &raw_block, &decoded_block));
+ test_assert(!message_search_more_decoded(ctx2, &decoded_block));
+
+ /* body */
+ raw_block.size = 1;
+ for (i = 0; input[i] != '\0'; i++) {
+ raw_block.data = (const void *)&input[i];
+ test_assert(!message_search_more_get_decoded(ctx1, &raw_block, &decoded_block));
+ test_assert(message_search_more_decoded(ctx2, &decoded_block) == (input[i+1] == '\0'));
+ }
+ message_search_deinit(&ctx1);
+ message_search_deinit(&ctx2);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_search,
+ test_message_search_more_get_decoded,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-size.c b/src/lib-mail/test-message-size.c
new file mode 100644
index 0000000..4342c27
--- /dev/null
+++ b/src/lib-mail/test-message-size.c
@@ -0,0 +1,159 @@
+/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "istream.h"
+#include "message-size.h"
+#include "test-common.h"
+
+static const char test_msg[] =
+"Return-Path: <test@example.org>\n"
+"Subject: Hello world\n"
+"From: Test User <test@example.org>\n"
+"To: Another User <test2@example.org>\n"
+"Message-Id: <1.2.3.4@example>\n"
+"Mime-Version: 1.0\n"
+"Date: Sun, 23 May 2007 04:58:08 +0300\n"
+"Content-Type: multipart/signed; micalg=pgp-sha1;\n"
+" protocol=\"application/pgp-signature\";\n"
+" boundary=\"=-GNQXLhuj24Pl1aCkk4/d\"\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"Content-Type: text/plain\n"
+"Content-Transfer-Encoding: quoted-printable\n"
+"\n"
+"There was a day=20\n"
+"a happy=20day\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"Content-Type: application/pgp-signature; name=signature.asc\n"
+"\n"
+"-----BEGIN PGP SIGNATURE-----\n"
+"Version: GnuPG v1.2.4 (GNU/Linux)\n"
+"\n"
+"invalid\n"
+"-----END PGP SIGNATURE-----\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d--\n"
+"\n"
+"\n";
+
+static const char test_msg_with_nuls[] =
+"Return-Path: <test@example.org>\n"
+"Subject: Hello world\n"
+"From: Test User <test@example.org>\n"
+"To: Another User <test2@example.org>\n"
+"Message-Id: <1.2.3.4@example>\n"
+"Mime-Version: 1.0\0\n"
+"Date: Sun, 23 May 2007 04:58:08 +0300\n"
+"Content-Type: multipart/signed; micalg=pgp-sha1;\n"
+" protocol=\"application/pgp-signature\";\n"
+" boundary=\"=-GNQXLhuj24Pl1aCkk4/d\"\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"\n"
+"Content-Type: text/plain\n"
+"Content-Transfer-Encoding: quoted-printable\n"
+"\n"
+"There was\0 a day=20\n"
+"a happy=20day\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d\n"
+"Content-Type: application/pgp-signature; name=signature.asc\n"
+"\n"
+"-----BEGIN PGP SIGNATURE-----\n"
+"Version: GnuPG v1.2.4 (GNU/Linux)\n"
+"\n"
+"inva\0lid\n"
+"-----END PGP SIGNATURE-----\n"
+"\n"
+"--=-GNQXLhuj24Pl1aCkk4/d--\n"
+"\n"
+"\n";
+
+struct test_case {
+ const char *test_name;
+ const char *message;
+ bool has_nuls;
+ unsigned int body_newlines;
+ unsigned int header_newlines;
+ unsigned int message_len;
+ unsigned int header_len;
+};
+static const struct test_case test_cases[] = {
+ {
+ .test_name = "message size",
+ .message = test_msg,
+ .has_nuls = FALSE,
+ .body_newlines = 19,
+ .header_newlines = 11,
+ .message_len = sizeof(test_msg)-1,
+ .header_len = 335,
+ },
+ {
+ .test_name = "message size with nuls",
+ .message = test_msg_with_nuls,
+ .has_nuls = TRUE,
+ .body_newlines = 20,
+ .header_newlines = 11,
+ .message_len = sizeof(test_msg_with_nuls)-1,
+ .header_len = 336,
+ },
+};
+
+static void test_message_size(void)
+{
+ struct istream *input;
+ struct message_size body_size, header_size;
+ bool has_nuls;
+ bool last_cr;
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(test_cases); i++) {
+ test_begin(test_cases[i].test_name);
+ input = i_stream_create_from_data(test_cases[i].message,
+ test_cases[i].message_len);
+
+ /* Read physical_size */
+ message_get_header_size(input, &header_size, &has_nuls);
+ test_assert_idx(has_nuls == test_cases[i].has_nuls, i);
+ test_assert_idx(input->v_offset == test_cases[i].header_len, i);
+ message_get_body_size(input, &body_size, &has_nuls);
+ test_assert_idx(has_nuls == test_cases[i].has_nuls, i);
+ test_assert_idx(input->v_offset - body_size.physical_size ==
+ test_cases[i].header_len, i);
+ test_assert_idx(body_size.physical_size + header_size.physical_size ==
+ test_cases[i].message_len, i);
+
+ /* Test last_cr handling */
+ i_stream_seek(input, 0);
+ message_skip_virtual(input, 0, &last_cr);
+ test_assert_idx(!last_cr, i);
+ message_skip_virtual(input, header_size.virtual_size-1, &last_cr);
+ test_assert_idx(last_cr, i);
+ message_skip_virtual(input, 2, &last_cr);
+ test_assert_idx(!last_cr, i);
+
+ /* Skipped header size so read body again */
+ message_get_body_size(input, &body_size, &has_nuls);
+ test_assert_idx(has_nuls == test_cases[i].has_nuls, i);
+ test_assert_idx(input->v_offset - body_size.physical_size ==
+ test_cases[i].header_len, i);
+ test_assert_idx(body_size.physical_size + test_cases[i].body_newlines ==
+ body_size.virtual_size, i);
+ test_assert_idx(body_size.virtual_size + header_size.virtual_size -
+ test_cases[i].body_newlines - test_cases[i].header_newlines ==
+ test_cases[i].message_len, i);
+
+ i_stream_unref(&input);
+ test_end();
+ }
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_size,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-message-snippet.c b/src/lib-mail/test-message-snippet.c
new file mode 100644
index 0000000..bf1a0b3
--- /dev/null
+++ b/src/lib-mail/test-message-snippet.c
@@ -0,0 +1,329 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "istream.h"
+#include "unichar.h"
+#include "message-snippet.h"
+#include "test-common.h"
+
+static const struct {
+ const char *input;
+ unsigned int max_snippet_chars;
+ const char *output;
+} tests[] = {
+ { "Content-Type: text/plain\n"
+ "\n"
+ "1234567890 234567890",
+ 12,
+ "1234567890 2" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ "line1\n>quote2\nline2\n",
+ 100,
+ "line1 line2" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ "line1\n>quote2\n> quote3\n > line4\n\n \t\t \nline5\n \t ",
+ 100,
+ "line1 > line4 line5" },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "hyv\xC3\xA4\xC3\xA4 p\xC3\xA4iv\xC3\xA4\xC3\xA4",
+ 11,
+ "hyv\xC3\xA4\xC3\xA4 p\xC3\xA4iv\xC3\xA4" },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "Content-Transfer-Encoding: quoted-printable\n"
+ "\n"
+ "hyv=C3=A4=C3=A4 p=C3=A4iv=C3=A4=C3=A4",
+ 11,
+ "hyv\xC3\xA4\xC3\xA4 p\xC3\xA4iv\xC3\xA4" },
+
+ { "Content-Transfer-Encoding: quoted-printable\n"
+ "Content-Type: text/html;\n"
+ " charset=utf-8\n"
+ "\n"
+ "<html><head><meta http-equiv=3D\"Content-Type\" content=3D\"text/html =\n"
+ "charset=3Dutf-8\"></head><body style=3D\"word-wrap: break-word; =\n"
+ "-webkit-nbsp-mode: space; -webkit-line-break: after-white-space;\" =\n"
+ "class=3D\"\">Hi,<div class=3D\"\"><br class=3D\"\"></div><div class=3D\"\">How =\n"
+ "is it going? <blockquote>quoted text is ignored</blockquote>\n"
+ "&gt; -foo\n"
+ "</div><br =class=3D\"\"></body></html>=\n",
+ 100,
+ "Hi, How is it going?" },
+
+ { "Content-Transfer-Encoding: quoted-printable\n"
+ "Content-Type: application/xhtml+xml;\n"
+ " charset=utf-8\n"
+ "\n"
+ "<html><head><meta http-equiv=3D\"Content-Type\" content=3D\"text/html =\n"
+ "charset=3Dutf-8\"></head><body style=3D\"word-wrap: break-word; =\n"
+ "-webkit-nbsp-mode: space; -webkit-line-break: after-white-space;\" =\n"
+ "class=3D\"\">Hi,<div class=3D\"\"><br class=3D\"\"></div><div class=3D\"\">How =\n"
+ "is it going? <blockquote>quoted text is ignored</blockquote>\n"
+ "&gt; -foo\n"
+ "</div><br =class=3D\"\"></body></html>=\n",
+ 100,
+ "Hi, How is it going?" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ ">quote1\n>quote2\n",
+ 100,
+ ">quote1 quote2" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ ">quote1\n>quote2\nbottom\nposter\n",
+ 100,
+ "bottom poster" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ "top\nposter\n>quote1\n>quote2\n",
+ 100,
+ "top poster" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ ">quoted long text",
+ 7,
+ ">quoted" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ ">quoted long text",
+ 8,
+ ">quoted" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ "whitespace and more",
+ 10,
+ "whitespace" },
+ { "Content-Type: text/plain\n"
+ "\n"
+ "whitespace and more",
+ 11,
+ "whitespace" },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "Invalid utf8 \x80\xff\n",
+ 100,
+ "Invalid utf8 "UNICODE_REPLACEMENT_CHAR_UTF8 },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "Incomplete utf8 \xC3",
+ 100,
+ "Incomplete utf8" },
+ { "Content-Transfer-Encoding: quoted-printable\n"
+ "Content-Type: text/html;\n"
+ " charset=utf-8\n"
+ "\n"
+ "<html><head><meta http-equiv=3D\"Content-Type\" content=3D\"text/html =\n"
+ "charset=3Dutf-8\"></head><body style=3D\"word-wrap: break-word; =\n"
+ "-webkit-nbsp-mode: space; -webkit-line-break: after-white-space;\" =\n"
+ "class=3D\"\"><div><blockquote>quoted text is included</blockquote>\n"
+ "</div><br =class=3D\"\"></body></html>=\n",
+ 100,
+ ">quoted text is included" },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ "I think\n",
+ 100,
+ "I think"
+ },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ " Lorem Ipsum\n",
+ 100,
+ "Lorem Ipsum"
+ },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ " I think\n",
+ 100,
+ "I think"
+ },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ " A cat\n",
+ 100,
+ "A cat"
+ },
+ { "Content-Type: text/plain; charset=utf-8\n"
+ "\n"
+ " \n",
+ 100,
+ ""
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><p>part one</p></body></head>\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><p>part two</p></body></head>\n"
+ "\n--a--\n",
+ 100,
+ "part one"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/alternative; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><p>part one</p></body></head>\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/plain; charset=utf-8\n\n"
+ "part two\n"
+ "\n--a--\n",
+ 100,
+ "part one"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><div><p></p><!-- comment --></body></head>\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><p>part two</p></body></head>\n"
+ "\n--a--\n",
+ 100,
+ "part two"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/alternative; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/plain; charset=utf-8\n\n"
+ "> original text\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/plain; charset=utf-8\n\n"
+ "part two\n"
+ "\n--a--\n",
+ 100,
+ ">original text"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/alternative; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/plain; charset=utf-8\n\n"
+ "top poster\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/plain; charset=utf-8\n\n"
+ "> original text\n"
+ "\n--a--\n",
+ 100,
+ "top poster"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><div><p></p><!-- comment --></body></head>\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><blockquote><!-- another --></blockquote>\n"
+ "</body></head>\n"
+ "\n--a--\n",
+ 100,
+ ""
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "</body></head>\n"
+ "\n--a--\n",
+ 100,
+ ""
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Type: application/octet-stream\n\n"
+ "U2hvdWxkIG5vdCBiZSBpbiBzbmlwcGV0\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: 7bit\n"
+ "Content-Type: text/html; charset=utf-8\n\n"
+ "<html><head></head><body><p>Should be in snippet</p></body></html>\n"
+ "\n--a--\n",
+ 100,
+ "Should be in snippet"
+ },
+ { "MIME-Version: 1.0\n"
+ "Content-Type: multipart/mixed; boundary=a\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Type: application/octet-stream\n\n"
+ "U2hvdWxkIG5vdCBiZSBpbiBzbmlwcGV0\n"
+ "\n--a\n"
+ "Content-Transfer-Encoding: base64\n"
+ "Content-Type: TeXT/html; charset=utf-8\n\n"
+ "PGh0bWw+PGhlYWQ+PC9oZWFkPjxib2R5PjxwPlNob3VsZCBiZSBpbiBzbmlwcGV0PC9wPjwvYm9k\n"
+ "eT48L2h0bWw+\n"
+ "\n--a--\n",
+ 100,
+ "Should be in snippet"
+ },
+};
+
+static void test_message_snippet(void)
+{
+ string_t *str = t_str_new(128);
+ struct istream *input;
+ unsigned int i;
+
+ test_begin("message snippet");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ str_truncate(str, 0);
+ input = test_istream_create(tests[i].input);
+ /* Limit the input max buffer size so the parsing uses multiple
+ blocks. 45 = large enough to be able to read the Content-*
+ headers. */
+ test_istream_set_max_buffer_size(input,
+ I_MIN(45, strlen(tests[i].input)));
+ test_assert_idx(message_snippet_generate(input, tests[i].max_snippet_chars, str) == 0, i);
+ test_assert_strcmp_idx(tests[i].output, str_c(str), i);
+ i_stream_destroy(&input);
+ }
+ test_end();
+}
+
+static void test_message_snippet_nuls(void)
+{
+ const char input_text[] = "\nfoo\0bar";
+ string_t *str = t_str_new(128);
+ struct istream *input;
+
+ test_begin("message snippet with NULs");
+
+ input = i_stream_create_from_data(input_text, sizeof(input_text)-1);
+ test_assert(message_snippet_generate(input, 5, str) == 0);
+ test_assert_strcmp(str_c(str), "fooba");
+ i_stream_destroy(&input);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_message_snippet,
+ test_message_snippet_nuls,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-ostream-dot.c b/src/lib-mail/test-ostream-dot.c
new file mode 100644
index 0000000..9ddb563
--- /dev/null
+++ b/src/lib-mail/test-ostream-dot.c
@@ -0,0 +1,103 @@
+/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "istream.h"
+#include "ostream.h"
+#include "ostream-dot.h"
+#include "test-common.h"
+
+struct dot_test {
+ const char *input;
+ const char *output;
+};
+
+static void test_ostream_dot_one(const struct dot_test *test)
+{
+ struct istream *test_input;
+ struct ostream *output, *test_output;
+ buffer_t *output_data;
+ const unsigned char *data;
+ size_t size;
+ ssize_t ret;
+
+ test_input = test_istream_create(test->input);
+ output_data = t_buffer_create(1024);
+ test_output = o_stream_create_buffer(output_data);
+
+ output = o_stream_create_dot(test_output, FALSE);
+
+ while ((ret = i_stream_read(test_input)) > 0 || ret == -2) {
+ data = i_stream_get_data(test_input, &size);
+ ret = o_stream_send(output, data, size);
+ test_assert(ret >= 0);
+ if (ret <= 0)
+ break;
+ i_stream_skip(test_input, ret);
+ }
+
+ test_assert(test_input->eof);
+
+ test_assert(o_stream_finish(output) > 0);
+ test_assert(output->offset == strlen(test->input));
+ test_assert(test_output->offset == strlen(test->output));
+ o_stream_unref(&output);
+ o_stream_unref(&test_output);
+
+ test_assert(strcmp(str_c(output_data), test->output) == 0);
+
+ i_stream_unref(&test_input);
+}
+
+static void test_ostream_dot(void)
+{
+ static struct dot_test tests[] = {
+ { "foo\r\n.\r\n", "foo\r\n..\r\n.\r\n" },
+ { "foo\n.\n", "foo\r\n..\r\n.\r\n" },
+ { ".foo\r\n.\r\nfoo\r\n", "..foo\r\n..\r\nfoo\r\n.\r\n" },
+ { ".foo\n.\nfoo\n", "..foo\r\n..\r\nfoo\r\n.\r\n" },
+ { "\r\n", "\r\n.\r\n" },
+ { "\n", "\r\n.\r\n" },
+ { "", "\r\n.\r\n" },
+ };
+ unsigned int i;
+
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ test_begin(t_strdup_printf("dot ostream[%d]:", i));
+ test_ostream_dot_one(&tests[i]);
+ test_end();
+ }
+}
+
+static void test_ostream_dot_parent_almost_full(void)
+{
+ buffer_t *output_data;
+ struct ostream *test_output, *output;
+ ssize_t ret;
+
+ test_begin("dot ostream parent almost full");
+ output_data = t_buffer_create(1024);
+ test_output = test_ostream_create_nonblocking(output_data, 1);
+ test_ostream_set_max_output_size(test_output, 1);
+
+ output = o_stream_create_dot(test_output, FALSE);
+ ret = o_stream_send(output, "a", 1);
+ test_assert(ret == 0);
+ ret = o_stream_send(output, "bc", 2);
+ test_assert(ret == 0);
+ o_stream_unref(&output);
+
+ o_stream_unref(&test_output);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_ostream_dot,
+ test_ostream_dot_parent_almost_full,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-qp-decoder.c b/src/lib-mail/test-qp-decoder.c
new file mode 100644
index 0000000..9b4d827
--- /dev/null
+++ b/src/lib-mail/test-qp-decoder.c
@@ -0,0 +1,188 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "str.h"
+#include "qp-decoder.h"
+#include "test-common.h"
+
+struct test_quoted_printable_decode_data {
+ const char *input;
+ const char *output;
+ size_t error_pos;
+ int ret;
+};
+
+static void test_qp_decoder(void)
+{
+#define WHITESPACE10 " \t \t \t"
+#define WHITESPACE70 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10
+ static struct test_quoted_printable_decode_data tests[] = {
+ { "foo \r\nbar=\n", "foo\r\nbar", 0, 0 },
+ { "foo\t=\nbar", "foo\tbar", 0, 0 },
+ { "foo = \n=01", "foo \001", 0, 0 },
+ { "foo =\t\r\nbar", "foo bar", 0, 0 },
+ { "foo =\r\n=01", "foo \001", 0, 0 },
+ { "foo \nbar=\r\n", "foo\r\nbar", 0, 0 },
+ { "=0A=0D ", "\n\r", 0, 0 },
+ { "foo_bar", "foo_bar", 0, 0 },
+ { "\n\n", "\r\n\r\n", 0, 0 },
+ { "\r\n\n\n\r\n", "\r\n\r\n\r\n\r\n", 0, 0 },
+
+ { "foo=", "foo=", 4, -1 },
+ { "foo= =66", "foo= f", 5, -1 },
+ { "foo= \t", "foo= \t", 6, -1 },
+ { "foo= \r", "foo= \r", 6, -1 },
+ { "foo= \r bar", "foo= \r bar", 6, -1 },
+ { "foo=A", "foo=A", 5, -1 },
+ { "foo=Ax", "foo=Ax", 5, -1 },
+ { "foo=Ax=xy", "foo=Ax=xy", 5, -1 },
+
+ /* above 76 whitespaces is invalid and gets truncated
+ (at 77th whitespace because of the current implementation) */
+ { WHITESPACE70" 7\n", WHITESPACE70" 7\r\n", 0, 0 },
+ { WHITESPACE70" 8\n", WHITESPACE70" 8\r\n", 77, -1 },
+ { WHITESPACE70" 9\n", WHITESPACE70" 9\r\n", 78, -1 },
+ { WHITESPACE70" 0\n", WHITESPACE70" 0\r\n", 79, -1 },
+ /* Expect extra whitespace to be truncated */
+ { WHITESPACE70" 7\n"WHITESPACE10"", WHITESPACE70" 7\r\n", 0, 0 },
+ { WHITESPACE70" 7=\r\n"WHITESPACE10, WHITESPACE70" 7", 0, 0 },
+ /* Unnecessarily encoded */
+ { "=66=6f=6f=42=61=72", "fooBar", 0, 0 },
+ /* Expected to be encoded but not */
+ { "\xc3\x9c""berm=c3=a4\xc3\x9figer Gebrauch", "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch", 0, 0 },
+ /* Decode control characters */
+ { "=0C=07", "\x0C\x07", 0, 0 },
+ /* Data */
+ { "=DE=AD=BE=EF", "\xDE\xAD\xBE\xEF", 0, 0 },
+ /* Non hex data */
+ { "=FJ=X1", "=FJ=X1", 2, -1 },
+ /* No content allowed after Soft Line Break */
+ { "=C3=9C = ","\xc3\x9c"" = ", 9, -1 },
+ /* Boundary delimiter */
+ { "=C3=9C=\r\n-------","\xc3\x9c""-------", 0, 0 },
+ { "=----------- =C3=9C","=----------- \xc3\x9c""", 1, -1 },
+ { "=___________ =C3=9C","=___________ \xc3\x9c""", 1, -1 },
+ { "___________ =C3=9C","___________ \xc3\x9c""", 0, 0 },
+ { "=2D=2D=2D=2D=2D=2D =C3=9C","------ \xc3\x9c""", 0, 0 },
+ { "=FC=83=BF=BF=BF=BF", "\xFC\x83\xBF\xBF\xBF\xBF", 0, 0 },
+ { "=FE=FE=FF=FF", "\xFE\xFE\xFF\xFF", 0, 0 },
+ { "\xFF=C3=9C\xFE\xFF""foobar", "\xFF\xc3\x9c""\xFE\xFF""foobar", 0, 0 },
+ /* Unnecessarily encoded and trailing whitespace */
+ {
+ "=66=6f=6f=42=61=72 ",
+ "fooBar", 0, 0
+ },
+ /* Indicate error if encoded line is longer then 76 */
+ {
+ WHITESPACE70" =C3=9C\n",
+ WHITESPACE70" \xc3\x9c""\r\n", 77, -1
+ },
+ /* Soft Line Break example from the RFC */
+ {
+ "Now's the time =\r\nfor all folk to come=\r\n to the"
+ " aid of their country.",
+ "Now's the time for all folk to come to the aid of "
+ "their country.", 0, 0
+ },
+ {
+ "=C3=9Cberm=C3=A4=C3=9Figer Gebrauch",
+ "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch", 0, 0
+ },
+ /* Softlinebreak without following content */
+ {
+ "=C3=9Cberm=C3=A4=C3=9Figer Gebrauch=",
+ "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch=", 36, -1
+ },
+ /* Lowercase formally illegal but allowed for robustness */
+ {
+ "=c3=9cberm=c3=a4=c3=9figer Gebrauch",
+ "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch", 0, 0
+ },
+ /* Control characters in input */
+ {
+ "=c3=9c=10berm=c3=a4=c3=9figer Geb=0Frauch",
+ "\xc3\x9c\x10""berm\xc3\xa4\xc3\x9figer Geb\x0Frauch", 0, 0
+ },
+ /* Trailing whitespace */
+ {
+ "Trailing Whitesp=C3=A4ce =\r\n ",
+ "Trailing Whitesp\xc3\xa4""ce ", 0 ,0
+ },
+ {
+ "Trailing Whitesp=C3=A4ce ",
+ "Trailing Whitesp\xc3\xa4""ce", 0 ,0
+ },
+ {
+ "=54=65=73=74=20=6D=65=73=73=61=67=65",
+ "Test message", 0 , 0
+ },
+ {
+ "=E3=81=93=E3=82=8C=E3=81=AF=E5=A2\r\n=83=E7=95=8C=E3"
+ "=81=AE=E3=81=82=E3=82=8B=E3=83=A1=E3=83=83=E3=82=BB="
+ "E3=83=BC=E3=82=B8=E3=81=A7=E3=81=99",
+ "\xE3\x81\x93\xE3\x82\x8C\xE3\x81\xAF\xE5\xA2\r\n\x83"
+ "\xE7\x95\x8C\xE3\x81\xAE\xE3\x81\x82\xE3\x82\x8B\xE3"
+ "\x83\xA1\xE3\x83\x83\xE3\x82\xBB\xE3\x83\xBC\xE3\x82"
+ "\xB8\xE3\x81\xA7\xE3\x81\x99", 0, 0
+ },
+ {
+ "=E3=81\xc3\xf1=93=E3=82=8\xff""C=E3=81=AF=E5=A2",
+ "\xE3\x81\xc3\xf1\x93\xE3\x82=8\xff""C\xE3\x81\xAF\xE5\xA2",
+ 19, -1
+ },
+ {
+ "\x77Hello\x76=20 \x20 =E3=81\xc3\xf1=93=E3=82",
+ "wHellov \xE3\x81\xc3\xf1\x93\xE3\x82",
+ 0, 0
+ },
+ };
+ string_t *str;
+ unsigned int i, j;
+
+ test_begin("qp-decoder");
+ str = t_str_new(128);
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ const char *input = tests[i].input;
+ struct qp_decoder *qp = qp_decoder_init(str);
+ size_t error_pos;
+ const char *error;
+ int ret;
+
+ /* try all at once */
+ ret = qp_decoder_more(qp, (const void *)input, strlen(input),
+ &error_pos, &error);
+ if (qp_decoder_finish(qp, &error) < 0 && ret == 0) {
+ error_pos = strlen(input);
+ ret = -1;
+ }
+ test_assert_idx(ret == tests[i].ret, i);
+ test_assert_idx(ret == 0 || error_pos == tests[i].error_pos, i);
+ test_assert_strcmp_idx(str_c(str), tests[i].output, i);
+
+ /* try in small pieces */
+ str_truncate(str, 0);
+ ret = 0;
+ for (j = 0; input[j] != '\0'; j++) {
+ unsigned char c = (unsigned char)input[j];
+ if (qp_decoder_more(qp, &c, 1, &error_pos, &error) < 0)
+ ret = -1;
+ }
+ if (qp_decoder_finish(qp, &error) < 0)
+ ret = -1;
+ test_assert_idx(ret == tests[i].ret, i);
+ test_assert_strcmp_idx(str_c(str), tests[i].output, i);
+
+ qp_decoder_deinit(&qp);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_qp_decoder,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-qp-encoder.c b/src/lib-mail/test-qp-encoder.c
new file mode 100644
index 0000000..bb31b1e
--- /dev/null
+++ b/src/lib-mail/test-qp-encoder.c
@@ -0,0 +1,206 @@
+/* Copyright (c) 2017-2018 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;
+ size_t max_line_len;
+};
+
+static void test_qp_encoder(void)
+{
+#define WHITESPACE10 " \t \t \t"
+#define WHITESPACE70 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10 WHITESPACE10
+ static struct test_quoted_printable_encode_data tests[] = {
+ { "", 0, "", 20 },
+ { "a", 1, "a", 20 },
+ { "a b \r c d", 9, "a b =0D c d", 20 },
+ { "a b c d\r", 8, "a b c d=0D", 20 },
+ { "a b \n c d", 9, "a b \r\n c d", 20 },
+ {
+ "test wrap at max 20 characters tab\ttoo", 38,
+ "test wrap at max=20=\r\n20 characters tab=09=\r\ntoo",
+ 20
+ },
+ { "Invalid UTF-8 sequence in \x99", 27, "Invalid UTF-8 sequ=\r\nence in =99", 20 },
+ { "keep CRLF\r\non two lines", 23, "keep CRLF\r\non two lines", 20 },
+ /* Trailing whitespace should be followed by encoded char. */
+ { "Keep trailing whitesp\xC3\xA4""ce ", 26, "Keep trailing whit=\r\nesp=C3=A4ce =", 20 },
+ { "Keep trailing whitesp\xC3\xA4""ce\t", 26, "Keep trailing whitesp=C3=A4ce\t=", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce ", 26, "Keep trailing whitesp=C3=A4ce =", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce ", 28, "Keep trailing whitesp=C3=A4ce =", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce \t ", 28, "Keep trailing whitesp=C3=A4ce \t =", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce ", 29, "Keep trailing whitesp=C3=A4ce =", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce ", 30, "Keep trailing whitesp=C3=A4ce =", 67 },
+ { "Keep trailing whitesp\xC3\xA4""ce ", 31, "Keep trailing whitesp=C3=A4ce =", 67 },
+ /* Test line breaking */
+ { WHITESPACE70"1234567", 77, WHITESPACE70"1234=\r\n567", 76 },
+ { WHITESPACE70" 7", 77, WHITESPACE70" =20=\r\n 7", 76 },
+ { WHITESPACE70""WHITESPACE10"1", 81, WHITESPACE70" =20=\r\n\t \t \t1", 76 },
+ /* Control characters */
+ { "\x0C\x07", 2, "=0C=07", 20},
+ /* Data */
+ { "\xDE\xAD\xBE\xEF""deadbeef", 12 ,"=DE=AD=BE=EFdeadbe=\r\nef", 20 },
+ { "\xDE""de""\xAD""ad""\xBE""be""\xEF""ef", 12 ,"=DEde=ADad=BEbe=EF=\r\nef", 20 },
+ /* boundary delimiter */
+ { "___________ \xc3\x9c", 14, "___________ =C3=9C", 20 },
+ { "----------- \xc3\x9c", 14, "----------- =C3=9C", 20 },
+ { "=---------- \xc3\x9c", 14, "=3D---------- =C3=\r\n=9C", 20 },
+ { "=__________ \xc3\x9c", 14, "=3D__________ =C3=\r\n=9C", 20 },
+ /* mixed inputs */
+ { "\xed\xae\x80\xed\xbf\xbf", 6, "=ED=AE=80=ED=BF=BF", 20 },
+ { "f\x6f\x6f""bar\xae\x80\xed\xbf\xbf", 11, "foobar=AE=80=ED=BF=\r\n=BF", 20 },
+ {
+ "\xc3\x9c""ber\x6d\xc3\xa4\xc3\x9f\x69\x67\x0a\xe0\x80\x80 \xf0\x9d\x84\x9e", 21,
+ "=C3=9Cberm=C3=A4=C3=9Fig\r\n=E0=80=80 =F0=9D=84=9E",
+ 76
+ },
+ {
+ "\xc3\x9c""ber\x6d\xc3\xa4\xc3\x9f\x69\x67\x0a\xe0\x80\x80 \xf0\x9d\x84\x9e", 21,
+ "=C3=9Cberm=C3=A4=\r\n=C3=9Fig\r\n=E0=80=80 =F0=9D=\r\n=84=9E",
+ 20
+ },
+ {
+ "\xc3\x9c""ber\x6dä\xc3\x9fi\x0a\xe0g\x80\x80 \xf0\x9d\x84\x9e", 21,
+ "=C3=9Cberm=C3=A4=C3=9Fi\r\n=E0g=80=80 =F0=9D=84=9E",
+ 76
+ },
+ {
+ "\xc3\x9c""ber\x6dä\xc3\xff\x9fi\x0a\xe0g\x80\x80\xfe\xf0\x9d\x84\x9e", 22,
+ "=C3=9Cberm=C3=A4=C3=FF=9Fi\r\n=E0g=80=80=FE=F0=9D=84=9E",
+ 76
+ },
+ };
+ 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, tests[i].max_line_len, 0);
+
+ /* try all at once */
+ qp_encoder_more(qp, input, tests[i].input_len);
+ qp_encoder_finish(qp);
+
+ test_assert_strcmp_idx(str_c(str), tests[i].output, 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_strcmp_idx(str_c(str), tests[i].output, 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" ,20 },
+ {
+ "\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*",
+ 20
+ },
+ };
+
+ 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, tests[i].max_line_len, 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?=", 75 },
+ { "J'esuis de paris caf\xc3\xa9", 22, "=?utf-8?Q?J'esuis_de_paris_caf=C3=A9?=", 75 },
+ { "hello_world", 11, "=?utf-8?Q?hello=5Fworld?=", 75 },
+ {
+ "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?=",
+ 75
+ },
+ };
+
+ 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, tests[i].max_line_len, 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);
+}
diff --git a/src/lib-mail/test-quoted-printable.c b/src/lib-mail/test-quoted-printable.c
new file mode 100644
index 0000000..3cce59e
--- /dev/null
+++ b/src/lib-mail/test-quoted-printable.c
@@ -0,0 +1,52 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "str.h"
+#include "quoted-printable.h"
+#include "test-common.h"
+
+static void test_quoted_printable_q_decode(void)
+{
+ const char *data[] = {
+ "=0A=0D ", "\n\r ",
+ "__foo__bar__", " foo bar ",
+ "foo=", "foo=",
+ "foo=A", "foo=A",
+ "foo=Ax", "foo=Ax",
+ "foo=Ax=xy", "foo=Ax=xy",
+ "=C3=9Cberm=C3=A4=C3=9Figer Gebrauch", "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch",
+ /* Lowercase formally illegal but allowed for robustness */
+ "=c3=9cberm=c3=a4=c3=9figer Gebrauch", "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch",
+ /* Unnecessarily encoded */
+ "=66=6f=6f=42=61=72", "fooBar",
+ /* Expected to be encoded but not */
+ "\xc3\x9c""berm=c3=a4\xc3\x9figer Gebrauch", "\xc3\x9c""berm\xc3\xa4\xc3\x9figer Gebrauch",
+ /* Decode control characters */
+ "=0C=07", "\x0C\x07",
+ "=DE=AD=BE=EF", "\xDE\xAD\xBE\xEF",
+ /* Non-Hex data */
+ "=FJ=X1", "=FJ=X1",
+ };
+ buffer_t *buf;
+ unsigned int i;
+
+ test_begin("quoted printable q decode");
+ buf = t_buffer_create(128);
+ for (i = 0; i < N_ELEMENTS(data); i += 2) {
+ quoted_printable_q_decode((const void *)data[i], strlen(data[i]),
+ buf);
+ test_assert_strcmp_idx(data[i+1], str_c(buf), i/2);
+ buffer_set_used_size(buf, 0);
+ }
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_quoted_printable_q_decode,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-rfc2231-parser.c b/src/lib-mail/test-rfc2231-parser.c
new file mode 100644
index 0000000..f54910a
--- /dev/null
+++ b/src/lib-mail/test-rfc2231-parser.c
@@ -0,0 +1,51 @@
+/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "rfc822-parser.h"
+#include "rfc2231-parser.h"
+#include "test-common.h"
+
+static void test_rfc2231_parser(void)
+{
+ const unsigned char input[] =
+ "; key4*=us-ascii''foo"
+ "; key*2=ba%"
+ "; key2*0=a"
+ "; key3*0*=us-ascii'en'xyz"
+ "; key*0=\"f\0oo\""
+ "; key2*1*=b%25"
+ "; key3*1=plop%"
+ "; key*1=baz";
+ const char *output[] = {
+ "key",
+ "f\xEF\xBF\xBDoobazba%",
+ "key2*",
+ "''ab%25",
+ "key3*",
+ "us-ascii'en'xyzplop%25",
+ "key4*",
+ "us-ascii''foo",
+ NULL
+ };
+ struct rfc822_parser_context parser;
+ const char *const *result;
+ unsigned int i;
+
+ test_begin("rfc2231 parser");
+ rfc822_parser_init(&parser, input, sizeof(input)-1, NULL);
+ test_assert(rfc2231_parse(&parser, &result) == 0);
+ for (i = 0; output[i] != NULL && result[i] != NULL; i++)
+ test_assert_idx(strcmp(output[i], result[i]) == 0, i);
+ rfc822_parser_deinit(&parser);
+ test_assert(output[i] == NULL && result[i] == NULL);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_rfc2231_parser,
+ NULL
+ };
+ return test_run(test_functions);
+}
diff --git a/src/lib-mail/test-rfc822-parser.c b/src/lib-mail/test-rfc822-parser.c
new file mode 100644
index 0000000..a0e7ad0
--- /dev/null
+++ b/src/lib-mail/test-rfc822-parser.c
@@ -0,0 +1,445 @@
+/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "str.h"
+#include "rfc822-parser.h"
+#include "test-common.h"
+
+static void test_rfc822_parse_comment(void)
+{
+ static const struct {
+ const char *input, *output;
+ int ret;
+ } tests[] = {
+ { "(", "", -1 },
+ { "(()", "", -1 },
+
+ { "()", "", 0 },
+ { "(())", "()", 0 },
+ { "(foo ( bar ) baz)", "foo ( bar ) baz", 0 },
+ { "(foo\t\tbar)", "foo\t\tbar", 0 },
+ { "(foo\\(bar)", "foo(bar", 0 },
+ { "(foo\\\\bar)", "foo\\bar", 0 },
+ { "(foo\\\\\\\\)", "foo\\\\", 0 },
+ { "(foo\\)bar)", "foo)bar", 0 },
+ { "(foo\"flop\"\"bar)", "foo\"flop\"\"bar", 0 },
+
+ { "(foo\n bar)", "foo bar", 0 },
+ { "(foo\n\t\t bar)", "foo\t\t bar", 0 },
+ { "(foo\\\n bar)", "foo\\ bar", 0 },
+ { "(foo\\\r\n bar)", "foo\\ bar", 0 },
+ };
+ struct rfc822_parser_context parser, parser2;
+ string_t *str = t_str_new(64);
+ unsigned int i = 0;
+
+ test_begin("rfc822 parse comment");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ rfc822_parser_init(&parser, (const void *)tests[i].input,
+ strlen(tests[i].input), str);
+ rfc822_parser_init(&parser2, (const void *)tests[i].input,
+ strlen(tests[i].input), NULL);
+ test_assert_idx(rfc822_skip_comment(&parser) == tests[i].ret, i);
+ test_assert_idx(rfc822_skip_comment(&parser2) == tests[i].ret, i);
+ test_assert_idx(tests[i].ret < 0 ||
+ strcmp(tests[i].output, str_c(str)) == 0, i);
+ rfc822_parser_deinit(&parser);
+ rfc822_parser_deinit(&parser2);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+static void test_rfc822_parse_comment_nuls(void)
+{
+ const unsigned char input[] = "(\000a\000\000b\\\000c(\000d)\000)";
+ const char output[] = "!a!!b\\!c(!d)!";
+ struct rfc822_parser_context parser;
+ string_t *str = t_str_new(64);
+
+ test_begin("rfc822 parse comment with NULs");
+
+ rfc822_parser_init(&parser, input, sizeof(input)-1, str);
+ test_assert(rfc822_skip_comment(&parser) == 0);
+ /* should be same as input, except the outer () removed */
+ test_assert(str_len(str) == sizeof(input)-1-2 &&
+ memcmp(input+1, str_data(str), str_len(str)) == 0);
+ rfc822_parser_deinit(&parser);
+
+ str_truncate(str, 0);
+ rfc822_parser_init(&parser, input, sizeof(input)-1, str);
+ parser.nul_replacement_str = "!";
+ test_assert(rfc822_skip_comment(&parser) == 0);
+ test_assert(strcmp(str_c(str), output) == 0);
+ rfc822_parser_deinit(&parser);
+
+ test_end();
+}
+
+static void test_rfc822_parse_quoted_string(void)
+{
+ static const struct {
+ const char *input, *output;
+ int ret;
+ } tests[] = {
+ { "\"", "", -1 },
+ { "\"\"", "", 0 },
+ { "\"foo\"", "foo", 0 },
+ { "\"\"foo", "", 1 },
+ { "\"\"\"", "", 1 },
+ { "\"\\\"\"", "\"", 0 },
+ { "\"\\\\\"", "\\", 0 },
+ { "\"\\\\foo\\\\foo\\\\\"", "\\foo\\foo\\", 0 },
+ { "\"foo\n bar\"", "foo bar", 0 },
+ { "\"foo\n\t\t bar\"", "foo\t\t bar", 0 },
+ { "\"foo\\\n bar\"", "foo\\ bar", 0 },
+ { "\"foo\\\r\n bar\"", "foo\\ bar", 0 },
+ };
+ struct rfc822_parser_context parser;
+ string_t *str = t_str_new(64);
+ unsigned int i = 0;
+
+ test_begin("rfc822 parse quoted string");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ rfc822_parser_init(&parser, (const void *)tests[i].input,
+ strlen(tests[i].input), NULL);
+ test_assert_idx(rfc822_parse_quoted_string(&parser, str) == tests[i].ret, i);
+ test_assert_idx(tests[i].ret < 0 ||
+ strcmp(tests[i].output, str_c(str)) == 0, i);
+ rfc822_parser_deinit(&parser);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+static void test_rfc822_parse_dot_atom(void)
+{
+ static const struct {
+ const char *input, *output;
+ int ret;
+ } tests[] = {
+ { "foo", "foo", 0 },
+ { "foo.bar", "foo.bar", 0 },
+ { "foo.bar.baz", "foo.bar.baz", 0 },
+ { "foo . \tbar (comments) . (...) baz\t ", "foo.bar.baz", 0 },
+
+ { ".", "", -1 },
+ { "..", "", -1 },
+ { ".foo", "", -1 },
+ { "foo.", "foo.", -1 },
+ { "foo..bar", "foo.", -1 },
+ { "foo. .bar", "foo.", -1 },
+ { "foo.(middle).bar", "foo.", -1 },
+ { "foo. ", "foo.", -1 },
+ { "foo.\t", "foo.", -1 },
+ { "foo.(ending)", "foo.", -1 },
+ };
+ struct rfc822_parser_context parser;
+ string_t *str = t_str_new(64);
+ string_t *input2 = t_str_new(64);
+ unsigned int i = 0;
+
+ test_begin("rfc822 parse dot-atom");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ rfc822_parser_init(&parser, (const void *)tests[i].input,
+ strlen(tests[i].input), NULL);
+ test_assert_idx(rfc822_parse_dot_atom(&parser, str) == tests[i].ret, i);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ rfc822_parser_deinit(&parser);
+ str_truncate(str, 0);
+
+ /* same input but with "," appended should return 1 on success,
+ and -1 still on error. */
+ int expected_ret = tests[i].ret == -1 ? -1 : 1;
+ str_append(input2, tests[i].input);
+ str_append_c(input2, ',');
+ rfc822_parser_init(&parser, str_data(input2),
+ str_len(input2), NULL);
+ test_assert_idx(rfc822_parse_dot_atom(&parser, str) == expected_ret, i);
+ test_assert_idx(strcmp(tests[i].output, str_c(str)) == 0, i);
+ rfc822_parser_deinit(&parser);
+
+ str_truncate(str, 0);
+ str_truncate(input2, 0);
+ }
+ test_end();
+}
+
+static void test_rfc822_parse_domain_literal(void)
+{
+ static const struct {
+ const char *input, *output;
+ int ret;
+ } tests[] = {
+ { "@[", "", -1 },
+ { "@[foo", "", -1 },
+ { "@[foo[]", "", -1 },
+ { "@[foo[]]", "", -1 },
+ { "@[]", "[]", 0 },
+ { "@[foo bar]", "[foo bar]", 0 },
+ { "@[foo\n bar]", "[foo bar]", 0 },
+ { "@[foo\n\t\t bar]", "[foo\t\t bar]", 0 },
+ { "@[foo\\\n bar]", "[foo\\ bar]", 0 },
+ };
+ struct rfc822_parser_context parser;
+ string_t *str = t_str_new(64);
+ unsigned int i = 0;
+
+ test_begin("rfc822 parse domain literal");
+ for (i = 0; i < N_ELEMENTS(tests); i++) {
+ rfc822_parser_init(&parser, (const void *)tests[i].input,
+ strlen(tests[i].input), NULL);
+ test_assert_idx(rfc822_parse_domain(&parser, str) == tests[i].ret, i);
+ test_assert_idx(tests[i].ret < 0 ||
+ strcmp(tests[i].output, str_c(str)) == 0, i);
+ rfc822_parser_deinit(&parser);
+ str_truncate(str, 0);
+ }
+ test_end();
+}
+
+#undef TEST_STRING
+#define TEST_STRING(a) .input = (const unsigned char*)a, .input_len = sizeof(a)-1
+
+static void test_rfc822_parse_content_type(void)
+{
+ const struct {
+ const unsigned char *input;
+ size_t input_len;
+ int ret;
+ const char *output;
+ } test_cases[] = {
+ { TEST_STRING(""), -1, "" },
+ { TEST_STRING(";charset=us-ascii"), -1, "" },
+ { TEST_STRING(" ;charset=us-ascii"), -1, "" },
+ { TEST_STRING("/"), -1, "" },
+ { TEST_STRING("/;charset=us-ascii"), -1, "" },
+ { TEST_STRING("/ ;charset=us-ascii"), -1, "" },
+ { TEST_STRING("text/"), -1, "" },
+ { TEST_STRING("text/;charset=us-ascii"), -1, "" },
+ { TEST_STRING("text/ ;charset=us-ascii"), -1, "" },
+ { TEST_STRING("/plain"), -1, "" },
+ { TEST_STRING("/plain;charset=us-ascii"), -1, "" },
+ { TEST_STRING("/plain ;charset=us-ascii"), -1, "" },
+ { TEST_STRING("text/plain"), 0, "text/plain" },
+ { TEST_STRING("text/plain;charset=us-ascii"), 1, "text/plain" },
+ { TEST_STRING("text/plain ;charset=us-ascii"), 1, "text/plain" },
+ { TEST_STRING("text/plain/format"), -1, "" },
+ { TEST_STRING("text/plain/format;charset=us-ascii"), -1, "" },
+ { TEST_STRING("text/plain/format ;charset=us-ascii"), -1, "" },
+ { TEST_STRING("\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e"),
+ 0, "\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e" },
+ { TEST_STRING("\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e;charset=utf-8"),
+ 1, "\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e" },
+ { TEST_STRING("\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e ;charset=utf-8"),
+ 1, "\xe5\x90\xab\xe9\x87\x8f/\xe7\xa8\xae\xe9\xa1\x9e" },
+ { TEST_STRING("application/ld+json"), 0, "application/ld+json" },
+ { TEST_STRING("application/ld+json;charset=us-ascii"),
+ 1, "application/ld+json" },
+ { TEST_STRING("application/ld+json ;charset=us-ascii"),
+ 1, "application/ld+json" },
+ { TEST_STRING("application/x-magic-cap-package-1.0"),
+ 0, "application/x-magic-cap-package-1.0" },
+ { TEST_STRING("application/x-magic-cap-package-1.0;charset=us-ascii"),
+ 1, "application/x-magic-cap-package-1.0" },
+ { TEST_STRING("application/x-magic-cap-package-1.0 ;charset=us-ascii"),
+ 1, "application/x-magic-cap-package-1.0" },
+ { TEST_STRING("application/pro_eng"), 0, "application/pro_eng" },
+ { TEST_STRING("application/pro_eng;charset=us-ascii"),
+ 1, "application/pro_eng" },
+ { TEST_STRING("application/pro_eng ;charset=us-ascii"),
+ 1, "application/pro_eng" },
+ { TEST_STRING("application/wordperfect6.1"),
+ 0, "application/wordperfect6.1" },
+ { TEST_STRING("application/wordperfect6.1;charset=us-ascii"),
+ 1, "application/wordperfect6.1" },
+ { TEST_STRING("application/wordperfect6.1 ;charset=us-ascii"),
+ 1, "application/wordperfect6.1" },
+ { TEST_STRING("application/vnd.openxmlformats-officedocument.wordprocessingml.template"),
+ 0, "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
+ { TEST_STRING("application/vnd.openxmlformats-officedocument.wordprocessingml.template;charset=us-ascii"),
+ 1, "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
+ { TEST_STRING("application/vnd.openxmlformats-officedocument.wordprocessingml.template ;charset=us-asii"),
+ 1, "application/vnd.openxmlformats-officedocument.wordprocessingml.template" },
+ { TEST_STRING("(hello) text (plain) / (world) plain (eod)"),
+ 0, "text/plain" },
+ { TEST_STRING("(hello) text (plain) / (world) plain (eod);charset=us-ascii"),
+ 1, "text/plain" },
+ { TEST_STRING("(hello) text (plain) / (world) plain (eod); charset=us-ascii"),
+ 1, "text/plain" },
+ { TEST_STRING("message/rfc822\r\n"), 0, "message/rfc822" },
+ { TEST_STRING(" \t\r message/rfc822 \t\r\n"),
+ 0, "message/rfc822" },
+ { TEST_STRING(" \t\r message/rfc822 \t ;charset=us-ascii\r\n"),
+ 1, "message/rfc822" },
+ { TEST_STRING(" \t\r message/rfc822 \t ; charset=us-ascii\r\n"),
+ 1, "message/rfc822" },
+ { TEST_STRING("test\0/ty\0pe"), -1, "" },
+ };
+
+ for (size_t i = 0; i < N_ELEMENTS(test_cases); i++) T_BEGIN {
+ string_t *value = t_str_new(64);
+ struct rfc822_parser_context parser;
+
+ rfc822_parser_init(&parser, test_cases[i].input,
+ test_cases[i].input_len, NULL);
+ test_assert_idx(rfc822_parse_content_type(&parser, value) ==
+ test_cases[i].ret, i);
+ test_assert_strcmp_idx(test_cases[i].output, str_c(value), i);
+ rfc822_parser_deinit(&parser);
+ } T_END;
+}
+
+static void test_rfc822_parse_content_param(void)
+{
+ const char *input =
+ "; key1=value1#$!%&'*+-.^_`{|}~"
+ "; key2=\" \\\"(),/:;<=>?@[\\\\]\"";
+ const struct {
+ const char *key, *value;
+ } output[] = {
+ { "key1", "value1#$!%&'*+-.^_`{|}~" },
+ { "key2", " \"(),/:;<=>?@[\\]" }
+ };
+ struct rfc822_parser_context parser;
+ const char *key;
+ string_t *value = t_str_new(64);
+ unsigned int i = 0;
+ int ret;
+
+ test_begin("rfc822 parse content param");
+ rfc822_parser_init(&parser, (const void *)input, strlen(input), NULL);
+ while ((ret = rfc822_parse_content_param(&parser, &key, value)) > 0 &&
+ i < N_ELEMENTS(output)) {
+ test_assert_idx(strcmp(output[i].key, key) == 0, i);
+ test_assert_idx(strcmp(output[i].value, str_c(value)) == 0, i);
+ i++;
+ }
+ rfc822_parser_deinit(&parser);
+ test_assert(ret == 0);
+ test_assert(i == N_ELEMENTS(output));
+ test_end();
+}
+
+struct param {
+ const char *key, *value;
+};
+
+static void parse_content_type_param(const void *input, size_t input_len,
+ const char *content_type,
+ const struct param *params, size_t param_count,
+ bool expect_content_type, int expect_ret,
+ int idx)
+{
+ struct rfc822_parser_context parser;
+ const char *key;
+ string_t *value = t_str_new(64);
+ unsigned int i = 0;
+ int ret;
+
+ i_assert(params != NULL || param_count == 0);
+
+ rfc822_parser_init(&parser, input, input_len, NULL);
+ ret = rfc822_parse_content_type(&parser, value);
+ test_assert_idx((expect_content_type && ret == 1) ||
+ (!expect_content_type && ret == -1), idx);
+ test_assert_strcmp_idx(content_type, str_c(value), idx);
+
+ /* parse content type first */
+ while ((ret = rfc822_parse_content_param(&parser, &key, value)) > 0) {
+ if (i < param_count) {
+ test_assert_strcmp_idx(params[i].key, key, idx);
+ test_assert_strcmp_idx(params[i].value, str_c(value), idx);
+ }
+ i++;
+ }
+
+ test_assert_idx(expect_ret == ret, idx);
+ test_assert_idx(i == param_count, idx);
+ rfc822_parser_deinit(&parser);
+}
+
+#undef TEST_STRING
+#define TEST_STRING(a) (a), sizeof((a))-1
+
+#define X10(a) a a a a a a a a a a
+
+static void test_rfc822_parse_content_type_param(void)
+{
+ const char *input =
+ "(hello) text/plain ; (should we skip;comments=\"yes\")"
+ " param=value"
+ " ; param2=value2 (with comments (with comment) with;comment=\"yes\") "
+ " ; param3=\"value3 (with no comment ; or=value)\""
+ " ; param4=\"\xe7\xa8\xae\xe9\xa1\x9e\""
+ " ; \xe5\x90\xab\xe9\x87\x8f=\"\xe7\xa8\xae\xe9\xa1\x9e\""
+ " ; "X10(X10(X10("a")))"="X10(X10(X10("a")))
+ " ; "X10(X10(X10(X10("a"))))"="X10(X10(X10(X10("a"))))
+ " ; (comment) param7 (comment2) = (comment3) value7 (comment4) "
+ ;
+
+ const struct param output[] = {
+ { "param", "value" },
+ { "param2", "value2" },
+ { "param3", "value3 (with no comment ; or=value)" },
+ { "param4", "\xe7\xa8\xae\xe9\xa1\x9e" },
+ { "\xe5\x90\xab\xe9\x87\x8f", "\xe7\xa8\xae\xe9\xa1\x9e" },
+ { X10(X10(X10("a"))), X10(X10(X10("a"))) },
+ { X10(X10(X10(X10("a")))), X10(X10(X10(X10("a")))) },
+ { "param7", "value7" },
+ };
+ const struct param output5[] = {
+ { "charset", "" },
+ };
+
+ test_begin("rfc822 parse content type with params");
+
+ int idx = 0;
+ parse_content_type_param(input, strlen(input), "text/plain",
+ output, N_ELEMENTS(output), TRUE, 0, idx++);
+ parse_content_type_param(TEST_STRING("text/"), "", NULL, 0, FALSE, 0, idx++);
+ parse_content_type_param(
+ TEST_STRING("text/\0plain ;"), "", NULL, 0, FALSE, -1, idx++);
+ parse_content_type_param(
+ TEST_STRING("text/plain\0;charset=us-ascii"), "", NULL, 0, FALSE, -1, idx++);
+ parse_content_type_param(
+ TEST_STRING("text/plain;charset\0=us-ascii"), "text/plain", NULL, 0, TRUE, -1, idx++);
+ parse_content_type_param(
+ TEST_STRING("text/plain;charset="), "text/plain",
+ output5, N_ELEMENTS(output5), TRUE, 0, idx++);
+ parse_content_type_param(
+ TEST_STRING("text/plain ; ; charset=us-ascii"), "text/plain", NULL, 0, TRUE, -1, idx++);
+ /* build a large one */
+ ARRAY(struct param) output2;
+ t_array_init(&output2, 1000);
+ string_t *large = t_str_new(10000);
+ str_append(large, "text/plain");
+ for (unsigned int i = 0; i < 1000; i++) {
+ str_printfa(large, " ; param%u=\"value%u\"", i, i);
+ struct param *param = array_append_space(&output2);
+ param->key = t_strdup_printf("param%u", i);
+ param->value = t_strdup_printf("value%u", i);
+ }
+ parse_content_type_param(large->data, large->used,
+ "text/plain",
+ array_idx(&output2, 0), array_count(&output2),
+ TRUE, 0, idx++);
+ test_end();
+}
+
+int main(void)
+{
+ static void (*const test_functions[])(void) = {
+ test_rfc822_parse_comment,
+ test_rfc822_parse_comment_nuls,
+ test_rfc822_parse_quoted_string,
+ test_rfc822_parse_dot_atom,
+ test_rfc822_parse_domain_literal,
+ test_rfc822_parse_content_type,
+ test_rfc822_parse_content_param,
+ test_rfc822_parse_content_type_param,
+ NULL
+ };
+ return test_run(test_functions);
+}