diff options
Diffstat (limited to 'src/lib-mail')
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..c19f328 --- /dev/null +++ b/src/lib-mail/Makefile.in @@ -0,0 +1,1629 @@ +# Makefile.in generated by automake 1.16.1 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994-2018 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 (​) 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(×tamp); + + /* 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(×tamp); + 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&<♣>b", + "a&<\xE2\x99\xA3>b" }, + { "&", "" }, + { "&", "" }, + + { "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ä", "a\xC3\xA4" }, + { "aä", "a\xC3\xA4" }, + { "€", "\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&<blockquote>b&<blockquote>&<blockquote>&c</blockquote>d&</blockquote>&e</blockquote>f&", + 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 = ∂ + + 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 = ∂ + + 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.children = &child1; + child2.flags = MESSAGE_PART_FLAG_TEXT; + part.children->next = &child2; + child2.parent = ∂ + 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.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.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 = ∂ + + /* 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" + "> -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" + "> -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); +} |