diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:51:24 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:51:24 +0000 |
commit | f7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch) | |
tree | a3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/doveadm/dsync | |
parent | Initial commit. (diff) | |
download | dovecot-upstream.tar.xz dovecot-upstream.zip |
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
36 files changed, 16782 insertions, 0 deletions
diff --git a/src/doveadm/dsync/Makefile.am b/src/doveadm/dsync/Makefile.am new file mode 100644 index 0000000..20cbfc7 --- /dev/null +++ b/src/doveadm/dsync/Makefile.am @@ -0,0 +1,76 @@ +pkglib_LTLIBRARIES = libdovecot-dsync.la +noinst_LTLIBRARIES = libdsync.la + +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-test \ + -I$(top_srcdir)/src/lib-settings \ + -I$(top_srcdir)/src/lib-ssl-iostream \ + -I$(top_srcdir)/src/lib-master \ + -I$(top_srcdir)/src/lib-mail \ + -I$(top_srcdir)/src/lib-imap \ + -I$(top_srcdir)/src/lib-index \ + -I$(top_srcdir)/src/lib-storage + +libdsync_la_SOURCES = \ + dsync-brain.c \ + dsync-brain-mailbox.c \ + dsync-brain-mailbox-tree.c \ + dsync-brain-mailbox-tree-sync.c \ + dsync-brain-mails.c \ + dsync-deserializer.c \ + dsync-mail.c \ + dsync-mailbox.c \ + dsync-mailbox-import.c \ + dsync-mailbox-export.c \ + dsync-mailbox-state.c \ + dsync-mailbox-tree.c \ + dsync-mailbox-tree-fill.c \ + dsync-mailbox-tree-sync.c \ + dsync-serializer.c \ + dsync-ibc.c \ + dsync-ibc-stream.c \ + dsync-ibc-pipe.c \ + dsync-transaction-log-scan.c + +libdovecot_dsync_la_SOURCES = +libdovecot_dsync_la_LIBADD = libdsync.la ../../lib-storage/libdovecot-storage.la ../../lib-dovecot/libdovecot.la +libdovecot_dsync_la_DEPENDENCIES = libdsync.la +libdovecot_dsync_la_LDFLAGS = -export-dynamic + +pkginc_libdir = $(pkgincludedir) +pkginc_lib_HEADERS = \ + dsync-brain.h \ + dsync-ibc.h + +noinst_HEADERS = \ + dsync-brain-private.h \ + dsync-mail.h \ + dsync-mailbox.h \ + dsync-mailbox-import.h \ + dsync-mailbox-export.h \ + dsync-mailbox-state.h \ + dsync-mailbox-tree.h \ + dsync-mailbox-tree-private.h \ + dsync-serializer.h \ + dsync-deserializer.h \ + dsync-ibc-private.h \ + dsync-transaction-log-scan.h + +test_programs = \ + test-dsync-mailbox-tree-sync + +noinst_PROGRAMS = $(test_programs) + +test_libs = \ + ../../lib-test/libtest.la \ + ../../lib/liblib.la + +test_dsync_mailbox_tree_sync_SOURCES = test-dsync-mailbox-tree-sync.c +test_dsync_mailbox_tree_sync_LDADD = dsync-mailbox-tree-sync.lo dsync-mailbox-tree.lo $(test_libs) +test_dsync_mailbox_tree_sync_DEPENDENCIES = $(pkglib_LTLIBRARIES) $(test_libs) + +check-local: + for bin in $(test_programs); do \ + if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \ + done diff --git a/src/doveadm/dsync/Makefile.in b/src/doveadm/dsync/Makefile.in new file mode 100644 index 0000000..ba39895 --- /dev/null +++ b/src/doveadm/dsync/Makefile.in @@ -0,0 +1,1019 @@ +# 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@ +noinst_PROGRAMS = $(am__EXEEXT_1) +subdir = src/doveadm/dsync +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 = +am__EXEEXT_1 = test-dsync-mailbox-tree-sync$(EXEEXT) +PROGRAMS = $(noinst_PROGRAMS) +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)$(pkglibdir)" \ + "$(DESTDIR)$(pkginc_libdir)" +LTLIBRARIES = $(noinst_LTLIBRARIES) $(pkglib_LTLIBRARIES) +am_libdovecot_dsync_la_OBJECTS = +libdovecot_dsync_la_OBJECTS = $(am_libdovecot_dsync_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 = +libdovecot_dsync_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(libdovecot_dsync_la_LDFLAGS) \ + $(LDFLAGS) -o $@ +libdsync_la_LIBADD = +am_libdsync_la_OBJECTS = dsync-brain.lo dsync-brain-mailbox.lo \ + dsync-brain-mailbox-tree.lo dsync-brain-mailbox-tree-sync.lo \ + dsync-brain-mails.lo dsync-deserializer.lo dsync-mail.lo \ + dsync-mailbox.lo dsync-mailbox-import.lo \ + dsync-mailbox-export.lo dsync-mailbox-state.lo \ + dsync-mailbox-tree.lo dsync-mailbox-tree-fill.lo \ + dsync-mailbox-tree-sync.lo dsync-serializer.lo dsync-ibc.lo \ + dsync-ibc-stream.lo dsync-ibc-pipe.lo \ + dsync-transaction-log-scan.lo +libdsync_la_OBJECTS = $(am_libdsync_la_OBJECTS) +am_test_dsync_mailbox_tree_sync_OBJECTS = \ + test-dsync-mailbox-tree-sync.$(OBJEXT) +test_dsync_mailbox_tree_sync_OBJECTS = \ + $(am_test_dsync_mailbox_tree_sync_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)/dsync-brain-mailbox-tree-sync.Plo \ + ./$(DEPDIR)/dsync-brain-mailbox-tree.Plo \ + ./$(DEPDIR)/dsync-brain-mailbox.Plo \ + ./$(DEPDIR)/dsync-brain-mails.Plo ./$(DEPDIR)/dsync-brain.Plo \ + ./$(DEPDIR)/dsync-deserializer.Plo \ + ./$(DEPDIR)/dsync-ibc-pipe.Plo \ + ./$(DEPDIR)/dsync-ibc-stream.Plo ./$(DEPDIR)/dsync-ibc.Plo \ + ./$(DEPDIR)/dsync-mail.Plo \ + ./$(DEPDIR)/dsync-mailbox-export.Plo \ + ./$(DEPDIR)/dsync-mailbox-import.Plo \ + ./$(DEPDIR)/dsync-mailbox-state.Plo \ + ./$(DEPDIR)/dsync-mailbox-tree-fill.Plo \ + ./$(DEPDIR)/dsync-mailbox-tree-sync.Plo \ + ./$(DEPDIR)/dsync-mailbox-tree.Plo \ + ./$(DEPDIR)/dsync-mailbox.Plo ./$(DEPDIR)/dsync-serializer.Plo \ + ./$(DEPDIR)/dsync-transaction-log-scan.Plo \ + ./$(DEPDIR)/test-dsync-mailbox-tree-sync.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 = +SOURCES = $(libdovecot_dsync_la_SOURCES) $(libdsync_la_SOURCES) \ + $(test_dsync_mailbox_tree_sync_SOURCES) +DIST_SOURCES = $(libdovecot_dsync_la_SOURCES) $(libdsync_la_SOURCES) \ + $(test_dsync_mailbox_tree_sync_SOURCES) +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +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@ +pkglib_LTLIBRARIES = libdovecot-dsync.la +noinst_LTLIBRARIES = libdsync.la +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-test \ + -I$(top_srcdir)/src/lib-settings \ + -I$(top_srcdir)/src/lib-ssl-iostream \ + -I$(top_srcdir)/src/lib-master \ + -I$(top_srcdir)/src/lib-mail \ + -I$(top_srcdir)/src/lib-imap \ + -I$(top_srcdir)/src/lib-index \ + -I$(top_srcdir)/src/lib-storage + +libdsync_la_SOURCES = \ + dsync-brain.c \ + dsync-brain-mailbox.c \ + dsync-brain-mailbox-tree.c \ + dsync-brain-mailbox-tree-sync.c \ + dsync-brain-mails.c \ + dsync-deserializer.c \ + dsync-mail.c \ + dsync-mailbox.c \ + dsync-mailbox-import.c \ + dsync-mailbox-export.c \ + dsync-mailbox-state.c \ + dsync-mailbox-tree.c \ + dsync-mailbox-tree-fill.c \ + dsync-mailbox-tree-sync.c \ + dsync-serializer.c \ + dsync-ibc.c \ + dsync-ibc-stream.c \ + dsync-ibc-pipe.c \ + dsync-transaction-log-scan.c + +libdovecot_dsync_la_SOURCES = +libdovecot_dsync_la_LIBADD = libdsync.la ../../lib-storage/libdovecot-storage.la ../../lib-dovecot/libdovecot.la +libdovecot_dsync_la_DEPENDENCIES = libdsync.la +libdovecot_dsync_la_LDFLAGS = -export-dynamic +pkginc_libdir = $(pkgincludedir) +pkginc_lib_HEADERS = \ + dsync-brain.h \ + dsync-ibc.h + +noinst_HEADERS = \ + dsync-brain-private.h \ + dsync-mail.h \ + dsync-mailbox.h \ + dsync-mailbox-import.h \ + dsync-mailbox-export.h \ + dsync-mailbox-state.h \ + dsync-mailbox-tree.h \ + dsync-mailbox-tree-private.h \ + dsync-serializer.h \ + dsync-deserializer.h \ + dsync-ibc-private.h \ + dsync-transaction-log-scan.h + +test_programs = \ + test-dsync-mailbox-tree-sync + +test_libs = \ + ../../lib-test/libtest.la \ + ../../lib/liblib.la + +test_dsync_mailbox_tree_sync_SOURCES = test-dsync-mailbox-tree-sync.c +test_dsync_mailbox_tree_sync_LDADD = dsync-mailbox-tree-sync.lo dsync-mailbox-tree.lo $(test_libs) +test_dsync_mailbox_tree_sync_DEPENDENCIES = $(pkglib_LTLIBRARIES) $(test_libs) +all: all-am + +.SUFFIXES: +.SUFFIXES: .c .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/doveadm/dsync/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/doveadm/dsync/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}; \ + } + +install-pkglibLTLIBRARIES: $(pkglib_LTLIBRARIES) + @$(NORMAL_INSTALL) + @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \ + list2=; for p in $$list; do \ + if test -f $$p; then \ + list2="$$list2 $$p"; \ + else :; fi; \ + done; \ + test -z "$$list2" || { \ + echo " $(MKDIR_P) '$(DESTDIR)$(pkglibdir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(pkglibdir)" || exit 1; \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(pkglibdir)'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(pkglibdir)"; \ + } + +uninstall-pkglibLTLIBRARIES: + @$(NORMAL_UNINSTALL) + @list='$(pkglib_LTLIBRARIES)'; test -n "$(pkglibdir)" || list=; \ + for p in $$list; do \ + $(am__strip_dir) \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(pkglibdir)/$$f'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(pkglibdir)/$$f"; \ + done + +clean-pkglibLTLIBRARIES: + -test -z "$(pkglib_LTLIBRARIES)" || rm -f $(pkglib_LTLIBRARIES) + @list='$(pkglib_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}; \ + } + +libdovecot-dsync.la: $(libdovecot_dsync_la_OBJECTS) $(libdovecot_dsync_la_DEPENDENCIES) $(EXTRA_libdovecot_dsync_la_DEPENDENCIES) + $(AM_V_CCLD)$(libdovecot_dsync_la_LINK) -rpath $(pkglibdir) $(libdovecot_dsync_la_OBJECTS) $(libdovecot_dsync_la_LIBADD) $(LIBS) + +libdsync.la: $(libdsync_la_OBJECTS) $(libdsync_la_DEPENDENCIES) $(EXTRA_libdsync_la_DEPENDENCIES) + $(AM_V_CCLD)$(LINK) $(libdsync_la_OBJECTS) $(libdsync_la_LIBADD) $(LIBS) + +test-dsync-mailbox-tree-sync$(EXEEXT): $(test_dsync_mailbox_tree_sync_OBJECTS) $(test_dsync_mailbox_tree_sync_DEPENDENCIES) $(EXTRA_test_dsync_mailbox_tree_sync_DEPENDENCIES) + @rm -f test-dsync-mailbox-tree-sync$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(test_dsync_mailbox_tree_sync_OBJECTS) $(test_dsync_mailbox_tree_sync_LDADD) $(LIBS) + +mostlyclean-compile: + -rm -f *.$(OBJEXT) + +distclean-compile: + -rm -f *.tab.c + +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-brain-mailbox-tree-sync.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-brain-mailbox-tree.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-brain-mailbox.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-brain-mails.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-brain.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-deserializer.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-ibc-pipe.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-ibc-stream.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-ibc.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mail.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-export.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-import.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-state.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-tree-fill.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-tree-sync.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox-tree.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-mailbox.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-serializer.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/dsync-transaction-log-scan.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-dsync-mailbox-tree-sync.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 $@ $< + +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)$(pkglibdir)" "$(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 clean-pkglibLTLIBRARIES mostlyclean-am + +distclean: distclean-am + -rm -f ./$(DEPDIR)/dsync-brain-mailbox-tree-sync.Plo + -rm -f ./$(DEPDIR)/dsync-brain-mailbox-tree.Plo + -rm -f ./$(DEPDIR)/dsync-brain-mailbox.Plo + -rm -f ./$(DEPDIR)/dsync-brain-mails.Plo + -rm -f ./$(DEPDIR)/dsync-brain.Plo + -rm -f ./$(DEPDIR)/dsync-deserializer.Plo + -rm -f ./$(DEPDIR)/dsync-ibc-pipe.Plo + -rm -f ./$(DEPDIR)/dsync-ibc-stream.Plo + -rm -f ./$(DEPDIR)/dsync-ibc.Plo + -rm -f ./$(DEPDIR)/dsync-mail.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-export.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-import.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-state.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-tree-fill.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-tree-sync.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-tree.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox.Plo + -rm -f ./$(DEPDIR)/dsync-serializer.Plo + -rm -f ./$(DEPDIR)/dsync-transaction-log-scan.Plo + -rm -f ./$(DEPDIR)/test-dsync-mailbox-tree-sync.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-pkglibLTLIBRARIES + +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)/dsync-brain-mailbox-tree-sync.Plo + -rm -f ./$(DEPDIR)/dsync-brain-mailbox-tree.Plo + -rm -f ./$(DEPDIR)/dsync-brain-mailbox.Plo + -rm -f ./$(DEPDIR)/dsync-brain-mails.Plo + -rm -f ./$(DEPDIR)/dsync-brain.Plo + -rm -f ./$(DEPDIR)/dsync-deserializer.Plo + -rm -f ./$(DEPDIR)/dsync-ibc-pipe.Plo + -rm -f ./$(DEPDIR)/dsync-ibc-stream.Plo + -rm -f ./$(DEPDIR)/dsync-ibc.Plo + -rm -f ./$(DEPDIR)/dsync-mail.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-export.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-import.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-state.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-tree-fill.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-tree-sync.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox-tree.Plo + -rm -f ./$(DEPDIR)/dsync-mailbox.Plo + -rm -f ./$(DEPDIR)/dsync-serializer.Plo + -rm -f ./$(DEPDIR)/dsync-transaction-log-scan.Plo + -rm -f ./$(DEPDIR)/test-dsync-mailbox-tree-sync.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 uninstall-pkglibLTLIBRARIES + +.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 \ + clean-pkglibLTLIBRARIES 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-pkglibLTLIBRARIES 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 uninstall-pkglibLTLIBRARIES + +.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/doveadm/dsync/dsync-brain-mailbox-tree-sync.c b/src/doveadm/dsync/dsync-brain-mailbox-tree-sync.c new file mode 100644 index 0000000..2030e04 --- /dev/null +++ b/src/doveadm/dsync/dsync-brain-mailbox-tree-sync.c @@ -0,0 +1,229 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "mail-namespace.h" +#include "mail-storage.h" +#include "dsync-mailbox-tree.h" +#include "dsync-brain-private.h" + +static int +sync_create_box(struct dsync_brain *brain, struct mailbox *box, + const guid_128_t mailbox_guid, uint32_t uid_validity, + enum mail_error *error_r) +{ + struct mailbox_metadata metadata; + struct mailbox_update update; + enum mail_error error; + const char *errstr; + int ret; + + i_zero(&update); + memcpy(update.mailbox_guid, mailbox_guid, sizeof(update.mailbox_guid)); + update.uid_validity = uid_validity; + + if (mailbox_create(box, &update, FALSE) < 0) { + errstr = mailbox_get_last_internal_error(box, &error); + if (error != MAIL_ERROR_EXISTS) { + i_error("Can't create mailbox %s: %s", + mailbox_get_vname(box), errstr); + *error_r = error; + return -1; + } + } + if (brain->no_mail_sync) { + /* trust that create worked, we can't actually open it + and verify. */ + return 0; + } + /* sync the mailbox so we can look up its latest status */ + if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) { + i_error("Can't sync mailbox %s: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, error_r)); + return -1; + } + + /* verify that the GUID is what we wanted. if it's not, it probably + means that the mailbox had already been created. then we'll use the + GUID that is higher. + + mismatching UIDVALIDITY is handled later, because we choose it by + checking which mailbox has more messages */ + if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) { + i_error("Can't get mailbox GUID %s: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, error_r)); + return -1; + } + + ret = memcmp(mailbox_guid, metadata.guid, sizeof(metadata.guid)); + + /* if THEIR guid is bigger than OUR guid, and we are not doing + backup in either direction, OR GUID did not match and we are + receiving backup, try change the mailbox GUID. + */ + + if ((ret > 0 && !brain->backup_recv && + !brain->backup_send) || (ret != 0 && brain->backup_recv)) { + if (brain->debug) { + i_debug("brain %c: Changing mailbox %s GUID %s -> %s", + brain->master_brain ? 'M' : 'S', + mailbox_get_vname(box), + guid_128_to_string(metadata.guid), + guid_128_to_string(mailbox_guid)); + } + i_zero(&update); + memcpy(update.mailbox_guid, mailbox_guid, + sizeof(update.mailbox_guid)); + if (mailbox_update(box, &update) < 0) { + i_error("Can't update mailbox GUID %s: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, error_r)); + return -1; + } + /* verify that the update worked */ + if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, + &metadata) < 0) { + i_error("Can't get mailbox GUID %s: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, error_r)); + return -1; + } + if (memcmp(mailbox_guid, metadata.guid, + sizeof(metadata.guid)) != 0) { + i_error("Backend didn't update mailbox %s GUID", + mailbox_get_vname(box)); + *error_r = MAIL_ERROR_TEMP; + return -1; + } + } else if (ret < 0) { + if (brain->debug) { + i_debug("brain %c: Other brain should change mailbox " + "%s GUID %s -> %s", + brain->master_brain ? 'M' : 'S', + mailbox_get_vname(box), + guid_128_to_string(mailbox_guid), + guid_128_to_string(metadata.guid)); + } + } + return 0; +} + +int dsync_brain_mailbox_tree_sync_change(struct dsync_brain *brain, + const struct dsync_mailbox_tree_sync_change *change, + enum mail_error *error_r) +{ + struct mailbox *box = NULL, *destbox; + const char *errstr, *func_name = NULL, *storage_name; + enum mail_error error; + int ret = -1; + + if (brain->backup_send) { + i_assert(brain->no_backup_overwrite); + return 0; + } + + switch (change->type) { + case DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_BOX: + /* make sure we're deleting the correct mailbox */ + ret = dsync_brain_mailbox_alloc(brain, change->mailbox_guid, + &box, &errstr, error_r); + if (ret < 0) { + i_error("Mailbox sync: Couldn't allocate mailbox %s GUID %s: %s", + change->full_name, + guid_128_to_string(change->mailbox_guid), errstr); + return -1; + } + if (ret == 0) { + dsync_brain_set_changes_during_sync(brain, t_strdup_printf( + "Mailbox %s GUID %s deletion conflict: %s", + change->full_name, + guid_128_to_string(change->mailbox_guid), errstr)); + return 0; + } + break; + case DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_DIR: + storage_name = mailbox_list_get_storage_name(change->ns->list, + change->full_name); + if (mailbox_list_delete_dir(change->ns->list, storage_name) == 0) + return 0; + + errstr = mailbox_list_get_last_internal_error(change->ns->list, &error); + if (error == MAIL_ERROR_NOTFOUND || + error == MAIL_ERROR_EXISTS) { + dsync_brain_set_changes_during_sync(brain, t_strdup_printf( + "Mailbox %s mailbox_list_delete_dir conflict: %s", + change->full_name, errstr)); + return 0; + } else { + i_error("Mailbox sync: mailbox_list_delete_dir failed: %s", + errstr); + *error_r = error; + return -1; + } + default: + box = mailbox_alloc(change->ns->list, change->full_name, 0); + break; + } + mailbox_skip_create_name_restrictions(box, TRUE); + switch (change->type) { + case DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_BOX: + ret = sync_create_box(brain, box, change->mailbox_guid, + change->uid_validity, error_r); + mailbox_free(&box); + return ret; + case DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_DIR: + ret = mailbox_create(box, NULL, TRUE); + if (ret < 0 && + mailbox_get_last_mail_error(box) == MAIL_ERROR_EXISTS) { + /* it doesn't matter if somebody else created this + directory or we automatically did while creating its + child mailbox. it's there now anyway and we don't + gain anything by treating this failure any + differently from success. */ + ret = 0; + } + func_name = "mailbox_create"; + break; + case DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_BOX: + ret = mailbox_delete(box); + func_name = "mailbox_delete"; + break; + case DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_DIR: + i_unreached(); + case DSYNC_MAILBOX_TREE_SYNC_TYPE_RENAME: + destbox = mailbox_alloc(change->ns->list, + change->rename_dest_name, 0); + mailbox_skip_create_name_restrictions(destbox, TRUE); + ret = mailbox_rename(box, destbox); + func_name = "mailbox_rename"; + mailbox_free(&destbox); + break; + case DSYNC_MAILBOX_TREE_SYNC_TYPE_SUBSCRIBE: + ret = mailbox_set_subscribed(box, TRUE); + func_name = "mailbox_set_subscribed"; + break; + case DSYNC_MAILBOX_TREE_SYNC_TYPE_UNSUBSCRIBE: + ret = mailbox_set_subscribed(box, FALSE); + func_name = "mailbox_set_subscribed"; + break; + } + if (ret < 0) { + errstr = mailbox_get_last_internal_error(box, &error); + if (error == MAIL_ERROR_EXISTS || + error == MAIL_ERROR_NOTFOUND) { + /* mailbox was already created or was already deleted. + let the next sync figure out what to do */ + dsync_brain_set_changes_during_sync(brain, t_strdup_printf( + "Mailbox %s %s conflict: %s", + mailbox_get_vname(box), func_name, errstr)); + ret = 0; + } else { + i_error("Mailbox %s sync: %s failed: %s", + mailbox_get_vname(box), func_name, errstr); + *error_r = error; + } + } + mailbox_free(&box); + return ret; +} diff --git a/src/doveadm/dsync/dsync-brain-mailbox-tree.c b/src/doveadm/dsync/dsync-brain-mailbox-tree.c new file mode 100644 index 0000000..44b76bf --- /dev/null +++ b/src/doveadm/dsync/dsync-brain-mailbox-tree.c @@ -0,0 +1,624 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "mail-namespace.h" +#include "mailbox-list-private.h" +#include "dsync-ibc.h" +#include "dsync-mailbox-tree.h" +#include "dsync-brain-private.h" + +#include <ctype.h> + +static void dsync_brain_check_namespaces(struct dsync_brain *brain) +{ + struct mail_namespace *ns, *first_ns = NULL; + char sep, escape_char; + + i_assert(brain->hierarchy_sep == '\0'); + i_assert(brain->escape_char == '\0'); + + for (ns = brain->user->namespaces; ns != NULL; ns = ns->next) { + if (!dsync_brain_want_namespace(brain, ns)) + continue; + + sep = mail_namespace_get_sep(ns); + escape_char = mailbox_list_get_settings(ns->list)->vname_escape_char; + if (first_ns == NULL) { + brain->hierarchy_sep = sep; + brain->escape_char = escape_char; + first_ns = ns; + } else if (brain->hierarchy_sep != sep) { + i_fatal("Synced namespaces have conflicting separators " + "('%c' for prefix=\"%s\", '%c' for prefix=\"%s\")", + brain->hierarchy_sep, first_ns->prefix, + sep, ns->prefix); + } else if (brain->escape_char != escape_char) { + i_fatal("Synced namespaces have conflicting escape chars " + "('%c' for prefix=\"%s\", '%c' for prefix=\"%s\")", + brain->escape_char, first_ns->prefix, + escape_char, ns->prefix); + } + } + if (brain->hierarchy_sep != '\0') + return; + + i_fatal("All your namespaces have a location setting. " + "Only namespaces with empty location settings are converted. " + "(One namespace should default to mail_location setting)"); +} + +void dsync_brain_mailbox_trees_init(struct dsync_brain *brain) +{ + struct mail_namespace *ns; + + dsync_brain_check_namespaces(brain); + + brain->local_mailbox_tree = + dsync_mailbox_tree_init(brain->hierarchy_sep, + brain->escape_char, brain->alt_char); + /* we'll convert remote mailbox names to use our own separator */ + brain->remote_mailbox_tree = + dsync_mailbox_tree_init(brain->hierarchy_sep, + brain->escape_char, brain->alt_char); + + /* fill the local mailbox tree */ + for (ns = brain->user->namespaces; ns != NULL; ns = ns->next) { + if (!dsync_brain_want_namespace(brain, ns)) + continue; + if (brain->debug) + i_debug("brain %c: Namespace %s has location %s", + brain->master_brain ? 'M' : 'S', + ns->prefix, ns->set->location); + if (dsync_mailbox_tree_fill(brain->local_mailbox_tree, ns, + brain->sync_box, + brain->sync_box_guid, + brain->exclude_mailboxes, + &brain->mail_error) < 0) { + brain->failed = TRUE; + break; + } + } + + brain->local_tree_iter = + dsync_mailbox_tree_iter_init(brain->local_mailbox_tree); +} + +static const char *const * +dsync_brain_mailbox_to_parts(struct dsync_brain *brain, const char *name) +{ + char sep[] = { brain->hierarchy_sep, '\0' }; + char **parts = p_strsplit(unsafe_data_stack_pool, name, sep); + for (unsigned int i = 0; parts[i] != NULL; i++) { + mailbox_list_name_unescape((const char **)&parts[i], + brain->escape_char); + } + return (const char *const *)parts; +} + +void dsync_brain_send_mailbox_tree(struct dsync_brain *brain) +{ + struct dsync_mailbox_node *node; + enum dsync_ibc_send_ret ret; + const char *full_name; + + while (dsync_mailbox_tree_iter_next(brain->local_tree_iter, + &full_name, &node)) { + if (node->ns == NULL) { + /* This node was created when adding a namespace prefix + to the tree that has multiple hierarchical names, + but the parent names don't belong to any synced + namespace. For example when syncing "-n Shared/user/" + so "Shared/" is skipped. Or if there is e.g. + "Public/files/" namespace prefix, but no "Public/" + namespace at all. */ + continue; + } + + T_BEGIN { + const char *const *parts; + + if (brain->debug) { + i_debug("brain %c: Local mailbox tree: %s %s", + brain->master_brain ? 'M' : 'S', full_name, + dsync_mailbox_node_to_string(node)); + } + + parts = dsync_brain_mailbox_to_parts(brain, full_name); + ret = dsync_ibc_send_mailbox_tree_node(brain->ibc, + parts, node); + } T_END; + if (ret == DSYNC_IBC_SEND_RET_FULL) + return; + } + dsync_mailbox_tree_iter_deinit(&brain->local_tree_iter); + dsync_ibc_send_end_of_list(brain->ibc, DSYNC_IBC_EOL_MAILBOX_TREE); + + brain->state = DSYNC_STATE_SEND_MAILBOX_TREE_DELETES; +} + +void dsync_brain_send_mailbox_tree_deletes(struct dsync_brain *brain) +{ + const struct dsync_mailbox_delete *deletes; + unsigned int count; + + deletes = dsync_mailbox_tree_get_deletes(brain->local_mailbox_tree, + &count); + dsync_ibc_send_mailbox_deletes(brain->ibc, deletes, count, + brain->hierarchy_sep, + brain->escape_char); + + brain->state = DSYNC_STATE_RECV_MAILBOX_TREE; +} + +static bool +dsync_namespace_match_parts(struct mail_namespace *ns, + const char *const *name_parts) +{ + const char *part, *prefix = ns->prefix; + size_t part_len; + char ns_sep = mail_namespace_get_sep(ns); + + if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 && + strcmp(name_parts[0], "INBOX") == 0 && name_parts[1] == NULL) + return TRUE; + + for (; *name_parts != NULL && *prefix != '\0'; name_parts++) { + part = *name_parts; + part_len = strlen(part); + + if (!str_begins(prefix, part)) + return FALSE; + if (prefix[part_len] != ns_sep) + return FALSE; + prefix += part_len + 1; + } + if (*name_parts != NULL) { + /* namespace prefix found with a mailbox */ + return TRUE; + } + if (*prefix == '\0') { + /* namespace prefix itself matched */ + return TRUE; + } + return FALSE; +} + +static struct mail_namespace * +dsync_find_namespace(struct dsync_brain *brain, const char *const *name_parts) +{ + struct mail_namespace *ns, *best_ns = NULL; + + for (ns = brain->user->namespaces; ns != NULL; ns = ns->next) { + if (!dsync_brain_want_namespace(brain, ns)) + continue; + + if (ns->prefix_len == 0) { + /* prefix="" is the fallback namespace */ + if (best_ns == NULL) + best_ns = ns; + } else if (dsync_namespace_match_parts(ns, name_parts)) { + if (best_ns == NULL || + best_ns->prefix_len < ns->prefix_len) + best_ns = ns; + } + } + return best_ns; +} + +static bool +dsync_is_valid_name(struct mail_namespace *ns, const char *vname) +{ + struct mailbox *box; + bool ret; + + box = mailbox_alloc(ns->list, vname, MAILBOX_FLAG_READONLY); + ret = mailbox_verify_create_name(box) == 0; + mailbox_free(&box); + return ret; +} + +static bool +dsync_is_valid_name_until(struct mail_namespace *ns, string_t *vname_full, + unsigned int end_pos) +{ + const char *vname; + if (end_pos == str_len(vname_full)) + vname = str_c(vname_full); + else + vname = t_strndup(str_c(vname_full), end_pos); + return dsync_is_valid_name(ns, vname); +} + +static bool +dsync_fix_mailbox_name_until(struct mail_namespace *ns, string_t *vname_full, + char alt_char, unsigned int start_pos, + unsigned int *_end_pos) +{ + unsigned int end_pos = *_end_pos; + unsigned int i; + + if (dsync_is_valid_name_until(ns, vname_full, end_pos)) + return TRUE; + + /* 1) change any real separators to alt separators (this + wouldn't be necessary with listescape, but don't bother + detecting it) */ + char list_sep = mailbox_list_get_hierarchy_sep(ns->list); + char ns_sep = mail_namespace_get_sep(ns); + if (list_sep != ns_sep) { + char *v = str_c_modifiable(vname_full); + for (i = start_pos; i < end_pos; i++) { + if (v[i] == list_sep) + v[i] = alt_char; + } + if (dsync_is_valid_name_until(ns, vname_full, end_pos)) + return TRUE; + } + + /* 2) '/' characters aren't valid without listescape */ + if (ns_sep != '/' && list_sep != '/') { + char *v = str_c_modifiable(vname_full); + for (i = start_pos; i < end_pos; i++) { + if (v[i] == '/') + v[i] = alt_char; + } + if (dsync_is_valid_name_until(ns, vname_full, end_pos)) + return TRUE; + } + + /* 3) probably some reserved name (e.g. dbox-Mails or ..) */ + str_insert(vname_full, start_pos, "_"); end_pos++; *_end_pos += 1; + if (dsync_is_valid_name_until(ns, vname_full, end_pos)) + return TRUE; + + return FALSE; +} + +static void +dsync_fix_mailbox_name(struct mail_namespace *ns, string_t *vname_str, + char alt_char) +{ + const char *old_vname; + char *vname; + char ns_sep = mail_namespace_get_sep(ns); + guid_128_t guid; + unsigned int i, start_pos; + + vname = str_c_modifiable(vname_str); + if (strncmp(vname, ns->prefix, ns->prefix_len) == 0) + start_pos = ns->prefix_len; + else + start_pos = 0; + + /* replace control chars */ + for (i = start_pos; vname[i] != '\0'; i++) { + if ((unsigned char)vname[i] < ' ') + vname[i] = alt_char; + } + /* make it valid UTF8 */ + if (!uni_utf8_str_is_valid(vname)) { + old_vname = t_strdup(vname + start_pos); + str_truncate(vname_str, start_pos); + if (uni_utf8_get_valid_data((const void *)old_vname, + strlen(old_vname), vname_str)) + i_unreached(); + vname = str_c_modifiable(vname_str); + } + if (dsync_is_valid_name(ns, vname)) + return; + + /* Check/fix each hierarchical name separately */ + const char *p; + do { + i_assert(start_pos <= str_len(vname_str)); + p = strchr(str_c(vname_str) + start_pos, ns_sep); + unsigned int end_pos; + if (p == NULL) + end_pos = str_len(vname_str); + else + end_pos = p - str_c(vname_str); + + if (!dsync_fix_mailbox_name_until(ns, vname_str, alt_char, + start_pos, &end_pos)) { + /* Couldn't fix it. Name is too long? Just give up and + generate a unique name. */ + guid_128_generate(guid); + str_truncate(vname_str, 0); + str_append(vname_str, ns->prefix); + str_append(vname_str, guid_128_to_string(guid)); + i_assert(dsync_is_valid_name(ns, str_c(vname_str))); + break; + } + start_pos = end_pos + 1; + } while (p != NULL); +} + +static int +dsync_get_mailbox_name(struct dsync_brain *brain, const char *const *name_parts, + const char **name_r, struct mail_namespace **ns_r) +{ + struct mail_namespace *ns; + const char *p; + string_t *vname; + char ns_sep; + + i_assert(*name_parts != NULL); + + ns = dsync_find_namespace(brain, name_parts); + if (ns == NULL) + return -1; + ns_sep = mail_namespace_get_sep(ns); + + /* build the mailbox name */ + char escape_chars[] = { + brain->escape_char, + ns_sep, + '\0' + }; + struct dsync_mailbox_list *dlist = DSYNC_LIST_CONTEXT(ns->list); + if (dlist != NULL && !dlist->have_orig_escape_char) { + /* The escape character was added only for dsync internally. + Normally there is no escape character configured. Change + the mailbox names so that it doesn't rely on it. */ + escape_chars[0] = '\0'; + } + vname = t_str_new(128); + for (; *name_parts != NULL; name_parts++) { + if (escape_chars[0] != '\0') { + mailbox_list_name_escape(*name_parts, escape_chars, + vname); + } else { + for (p = *name_parts; *p != '\0'; p++) { + if (*p != ns_sep) + str_append_c(vname, *p); + else + str_append_c(vname, brain->alt_char); + } + } + str_append_c(vname, ns_sep); + } + str_truncate(vname, str_len(vname)-1); + + dsync_fix_mailbox_name(ns, vname, brain->alt_char); + *name_r = str_c(vname); + *ns_r = ns; + return 0; +} + +static void dsync_brain_mailbox_trees_sync(struct dsync_brain *brain) +{ + struct dsync_mailbox_tree_sync_ctx *ctx; + const struct dsync_mailbox_tree_sync_change *change; + enum dsync_mailbox_trees_sync_type sync_type; + enum dsync_mailbox_trees_sync_flags sync_flags = + (brain->debug ? DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG : 0) | + (brain->master_brain ? DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN : 0); + int ret; + + if (brain->no_backup_overwrite) + sync_type = DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY; + else if (brain->backup_send) + sync_type = DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL; + else if (brain->backup_recv) + sync_type = DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE; + else + sync_type = DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY; + + ctx = dsync_mailbox_trees_sync_init(brain->local_mailbox_tree, + brain->remote_mailbox_tree, + sync_type, sync_flags); + while ((change = dsync_mailbox_trees_sync_next(ctx)) != NULL) { + T_BEGIN { + ret = dsync_brain_mailbox_tree_sync_change( + brain, change, &brain->mail_error); + } T_END; + if (ret < 0) { + brain->failed = TRUE; + break; + } + } + if (dsync_mailbox_trees_sync_deinit(&ctx) < 0) + brain->failed = TRUE; +} + +static int +dsync_brain_recv_mailbox_tree_add(struct dsync_brain *brain, + const char *const *parts, + const struct dsync_mailbox_node *remote_node, + const char *sep) +{ + struct dsync_mailbox_node *node; + struct mail_namespace *ns; + const char *name; + + if (dsync_get_mailbox_name(brain, parts, &name, &ns) < 0) + return -1; + if (brain->debug) { + i_debug("brain %c: Remote mailbox tree: %s %s", + brain->master_brain ? 'M' : 'S', + t_strarray_join(parts, sep), + dsync_mailbox_node_to_string(remote_node)); + } + node = dsync_mailbox_tree_get(brain->remote_mailbox_tree, name); + node->ns = ns; + dsync_mailbox_node_copy_data(node, remote_node); + return 0; +} + +bool dsync_brain_recv_mailbox_tree(struct dsync_brain *brain) +{ + const struct dsync_mailbox_node *remote_node; + struct dsync_mailbox_node *dup_node1, *dup_node2; + const char *const *parts; + enum dsync_ibc_recv_ret ret; + int ret2; + char sep[2]; + bool changed = FALSE; + + sep[0] = brain->hierarchy_sep; sep[1] = '\0'; + while ((ret = dsync_ibc_recv_mailbox_tree_node(brain->ibc, &parts, + &remote_node)) > 0) { + T_BEGIN { + ret2 = dsync_brain_recv_mailbox_tree_add( + brain, parts, remote_node, sep); + } T_END; + if (ret2 < 0) { + i_error("Couldn't find namespace for mailbox %s", + t_strarray_join(parts, sep)); + brain->failed = TRUE; + return TRUE; + } + } + if (ret != DSYNC_IBC_RECV_RET_FINISHED) + return changed; + + if (dsync_mailbox_tree_build_guid_hash(brain->remote_mailbox_tree, + &dup_node1, &dup_node2) < 0) { + i_error("Remote sent duplicate mailbox GUID %s for mailboxes %s and %s", + guid_128_to_string(dup_node1->mailbox_guid), + dsync_mailbox_node_get_full_name(brain->remote_mailbox_tree, + dup_node1), + dsync_mailbox_node_get_full_name(brain->remote_mailbox_tree, + dup_node2)); + brain->failed = TRUE; + } + + brain->state = DSYNC_STATE_RECV_MAILBOX_TREE_DELETES; + return TRUE; +} + +static void +dsync_brain_mailbox_tree_add_delete(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_tree *other_tree, + const struct dsync_mailbox_delete *other_del, + const struct dsync_mailbox_node **node_r, + const char **status_r) +{ + const struct dsync_mailbox_node *node; + struct dsync_mailbox_node *other_node, *old_node; + const char *name; + + /* see if we can find the deletion based on mailbox tree that should + still have the mailbox */ + node = *node_r = dsync_mailbox_tree_find_delete(tree, other_del); + if (node == NULL) { + *status_r = "not found"; + return; + } + + switch (other_del->type) { + case DSYNC_MAILBOX_DELETE_TYPE_MAILBOX: + /* mailbox is always deleted */ + break; + case DSYNC_MAILBOX_DELETE_TYPE_DIR: + if (other_del->timestamp <= node->last_renamed_or_created) { + /* we don't want to delete this directory, we already + have a newer timestamp for it */ + *status_r = "keep directory, we have a newer timestamp"; + return; + } + break; + case DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE: + if (other_del->timestamp <= node->last_subscription_change) { + /* we don't want to unsubscribe, since we already have + a newer subscription timestamp */ + *status_r = "keep subscription, we have a newer timestamp"; + return; + } + break; + } + + /* make a node for it in the other mailbox tree */ + name = dsync_mailbox_node_get_full_name(tree, node); + other_node = dsync_mailbox_tree_get(other_tree, name); + + if (other_node->existence == DSYNC_MAILBOX_NODE_EXISTS && + (!guid_128_is_empty(other_node->mailbox_guid) || + other_del->type != DSYNC_MAILBOX_DELETE_TYPE_MAILBOX)) { + /* other side has already created a new mailbox or + directory with this name, we can't delete it */ + *status_r = "name has already been recreated"; + return; + } + + /* ok, mark the other node deleted */ + if (other_del->type == DSYNC_MAILBOX_DELETE_TYPE_MAILBOX) { + memcpy(other_node->mailbox_guid, node->mailbox_guid, + sizeof(other_node->mailbox_guid)); + } + if (other_node->ns != node->ns && other_node->ns != NULL) { + /* namespace mismatch for this node. this shouldn't happen + normally, but especially during some misconfigurations it's + possible that one side has created mailboxes that conflict + with another namespace's prefix. since we're here because + one of the mailboxes was deleted, we'll just ignore this. */ + *status_r = "namespace mismatch"; + return; + } + other_node->ns = node->ns; + if (other_del->type != DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE) { + other_node->existence = DSYNC_MAILBOX_NODE_DELETED; + *status_r = "marked as deleted"; + } else { + other_node->last_subscription_change = other_del->timestamp; + other_node->subscribed = FALSE; + *status_r = "marked as unsubscribed"; + } + + if (dsync_mailbox_tree_guid_hash_add(other_tree, other_node, + &old_node) < 0) + i_unreached(); +} + +bool dsync_brain_recv_mailbox_tree_deletes(struct dsync_brain *brain) +{ + const struct dsync_mailbox_node *node; + const char *status; + const struct dsync_mailbox_delete *deletes; + unsigned int i, count; + char sep, escape_char; + + if (dsync_ibc_recv_mailbox_deletes(brain->ibc, &deletes, &count, + &sep, &escape_char) == 0) + return FALSE; + + /* apply remote's mailbox deletions based on our local tree */ + dsync_mailbox_tree_set_remote_chars(brain->local_mailbox_tree, sep, + escape_char); + for (i = 0; i < count; i++) { + dsync_brain_mailbox_tree_add_delete(brain->local_mailbox_tree, + brain->remote_mailbox_tree, + &deletes[i], &node, &status); + if (brain->debug) { + const char *node_name = node == NULL ? "" : + dsync_mailbox_node_get_full_name(brain->local_mailbox_tree, node); + i_debug("brain %c: Remote mailbox tree deletion: guid=%s type=%s timestamp=%ld name=%s local update=%s", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(deletes[i].guid), + dsync_mailbox_delete_type_to_string(deletes[i].type), + deletes[i].timestamp, node_name, status); + } + } + + /* apply local mailbox deletions based on remote tree */ + deletes = dsync_mailbox_tree_get_deletes(brain->local_mailbox_tree, + &count); + dsync_mailbox_tree_set_remote_chars(brain->remote_mailbox_tree, + brain->hierarchy_sep, + brain->escape_char); + for (i = 0; i < count; i++) { + dsync_brain_mailbox_tree_add_delete(brain->remote_mailbox_tree, + brain->local_mailbox_tree, + &deletes[i], &node, &status); + } + + dsync_brain_mailbox_trees_sync(brain); + brain->state = brain->master_brain ? + DSYNC_STATE_MASTER_SEND_MAILBOX : + DSYNC_STATE_SLAVE_RECV_MAILBOX; + i_assert(brain->local_tree_iter == NULL); + brain->local_tree_iter = + dsync_mailbox_tree_iter_init(brain->local_mailbox_tree); + return TRUE; +} diff --git a/src/doveadm/dsync/dsync-brain-mailbox.c b/src/doveadm/dsync/dsync-brain-mailbox.c new file mode 100644 index 0000000..759018f --- /dev/null +++ b/src/doveadm/dsync/dsync-brain-mailbox.c @@ -0,0 +1,923 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "mail-cache-private.h" +#include "mail-namespace.h" +#include "mail-storage-private.h" +#include "dsync-ibc.h" +#include "dsync-mailbox-tree.h" +#include "dsync-mailbox-import.h" +#include "dsync-mailbox-export.h" +#include "dsync-transaction-log-scan.h" +#include "dsync-brain-private.h" + +static int +ns_mailbox_try_alloc(struct dsync_brain *brain, struct mail_namespace *ns, + const guid_128_t guid, struct mailbox **box_r, + const char **errstr_r, enum mail_error *error_r) +{ + enum mailbox_flags flags = 0; + struct mailbox *box; + enum mailbox_existence existence; + int ret; + + if (brain->backup_send) { + /* make sure mailbox isn't modified */ + flags |= MAILBOX_FLAG_READONLY; + } + + box = mailbox_alloc_guid(ns->list, guid, flags); + ret = mailbox_exists(box, FALSE, &existence); + if (ret < 0) { + *errstr_r = mailbox_get_last_internal_error(box, error_r); + mailbox_free(&box); + return -1; + } + if (existence != MAILBOX_EXISTENCE_SELECT) { + mailbox_free(&box); + *errstr_r = existence == MAILBOX_EXISTENCE_NONE ? + "Mailbox was already deleted" : + "Mailbox is no longer selectable"; + return 0; + } + *box_r = box; + return 1; +} + +int dsync_brain_mailbox_alloc(struct dsync_brain *brain, const guid_128_t guid, + struct mailbox **box_r, const char **errstr_r, + enum mail_error *error_r) +{ + struct mail_namespace *ns; + int ret; + + *box_r = NULL; + + for (ns = brain->user->namespaces; ns != NULL; ns = ns->next) { + if (!dsync_brain_want_namespace(brain, ns)) + continue; + if ((ret = ns_mailbox_try_alloc(brain, ns, guid, box_r, + errstr_r, error_r)) != 0) + return ret; + } + return 0; +} + +static void +dsync_mailbox_cache_field_dup(ARRAY_TYPE(mailbox_cache_field) *dest, + const ARRAY_TYPE(mailbox_cache_field) *src, + pool_t pool) +{ + const struct mailbox_cache_field *src_field; + struct mailbox_cache_field *dest_field; + + p_array_init(dest, pool, array_count(src)); + array_foreach(src, src_field) { + dest_field = array_append_space(dest); + dest_field->name = p_strdup(pool, src_field->name); + dest_field->decision = src_field->decision; + dest_field->last_used = src_field->last_used; + } +} + + +static const struct dsync_mailbox_state * +dsync_mailbox_state_find(struct dsync_brain *brain, + const guid_128_t mailbox_guid) +{ + const uint8_t *guid_p; + + guid_p = mailbox_guid; + return hash_table_lookup(brain->mailbox_states, guid_p); +} + +static void +dsync_mailbox_state_remove(struct dsync_brain *brain, + const guid_128_t mailbox_guid) +{ + const uint8_t *guid_p; + + guid_p = mailbox_guid; + if (hash_table_lookup(brain->mailbox_states, guid_p) != NULL) + hash_table_remove(brain->mailbox_states, guid_p); +} + +void dsync_brain_sync_init_box_states(struct dsync_brain *brain) +{ + if (brain->backup_send) { + /* we have an exporter, but no importer. */ + brain->box_send_state = DSYNC_BOX_STATE_ATTRIBUTES; + brain->box_recv_state = brain->mail_requests ? + DSYNC_BOX_STATE_MAIL_REQUESTS : + DSYNC_BOX_STATE_RECV_LAST_COMMON; + } else if (brain->backup_recv) { + /* we have an importer, but no exporter */ + brain->box_send_state = brain->mail_requests ? + DSYNC_BOX_STATE_MAIL_REQUESTS : + DSYNC_BOX_STATE_DONE; + brain->box_recv_state = DSYNC_BOX_STATE_ATTRIBUTES; + } else { + brain->box_send_state = DSYNC_BOX_STATE_ATTRIBUTES; + brain->box_recv_state = DSYNC_BOX_STATE_ATTRIBUTES; + } +} + +static void +dsync_brain_sync_mailbox_init(struct dsync_brain *brain, + struct mailbox *box, + struct file_lock *lock, + const struct dsync_mailbox *local_dsync_box, + bool wait_for_remote_box) +{ + const struct dsync_mailbox_state *state; + + i_assert(brain->box_importer == NULL); + i_assert(brain->box_exporter == NULL); + i_assert(box->synced); + + brain->box = box; + brain->box_lock = lock; + brain->pre_box_state = brain->state; + if (wait_for_remote_box) { + brain->box_send_state = DSYNC_BOX_STATE_MAILBOX; + brain->box_recv_state = DSYNC_BOX_STATE_MAILBOX; + } else { + dsync_brain_sync_init_box_states(brain); + } + brain->local_dsync_box = *local_dsync_box; + if (brain->dsync_box_pool != NULL) + p_clear(brain->dsync_box_pool); + else { + brain->dsync_box_pool = + pool_alloconly_create(MEMPOOL_GROWING"dsync brain box pool", 2048); + } + dsync_mailbox_cache_field_dup(&brain->local_dsync_box.cache_fields, + &local_dsync_box->cache_fields, + brain->dsync_box_pool); + i_zero(&brain->remote_dsync_box); + + state = dsync_mailbox_state_find(brain, local_dsync_box->mailbox_guid); + if (state != NULL) + brain->mailbox_state = *state; + else { + i_zero(&brain->mailbox_state); + memcpy(brain->mailbox_state.mailbox_guid, + local_dsync_box->mailbox_guid, + sizeof(brain->mailbox_state.mailbox_guid)); + brain->mailbox_state.last_uidvalidity = + local_dsync_box->uid_validity; + } +} + +static void +dsync_brain_sync_mailbox_init_remote(struct dsync_brain *brain, + const struct dsync_mailbox *remote_dsync_box) +{ + enum dsync_mailbox_import_flags import_flags = 0; + const struct dsync_mailbox_state *state; + uint32_t last_common_uid; + uint64_t last_common_modseq, last_common_pvt_modseq; + + i_assert(brain->box_importer == NULL); + i_assert(brain->log_scan != NULL); + + i_assert(memcmp(brain->local_dsync_box.mailbox_guid, + remote_dsync_box->mailbox_guid, + sizeof(remote_dsync_box->mailbox_guid)) == 0); + + brain->remote_dsync_box = *remote_dsync_box; + dsync_mailbox_cache_field_dup(&brain->remote_dsync_box.cache_fields, + &remote_dsync_box->cache_fields, + brain->dsync_box_pool); + + state = dsync_mailbox_state_find(brain, remote_dsync_box->mailbox_guid); + if (state != NULL) { + last_common_uid = state->last_common_uid; + last_common_modseq = state->last_common_modseq; + last_common_pvt_modseq = state->last_common_pvt_modseq; + } else { + last_common_uid = 0; + last_common_modseq = 0; + last_common_pvt_modseq = 0; + } + + if (brain->mail_requests) + import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_WANT_MAIL_REQUESTS; + if (brain->master_brain) + import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_MASTER_BRAIN; + if (brain->backup_recv && !brain->no_backup_overwrite) + import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_REVERT_LOCAL_CHANGES; + if (brain->debug) + import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_DEBUG; + if (brain->local_dsync_box.have_save_guids && + (remote_dsync_box->have_save_guids || + (brain->backup_recv && remote_dsync_box->have_guids))) + import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_MAILS_HAVE_GUIDS; + if (brain->local_dsync_box.have_only_guid128 || + remote_dsync_box->have_only_guid128) + import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_MAILS_USE_GUID128; + if (brain->no_notify) + import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_NO_NOTIFY; + if (brain->empty_hdr_workaround) + import_flags |= DSYNC_MAILBOX_IMPORT_FLAG_EMPTY_HDR_WORKAROUND; + + brain->box_importer = brain->backup_send ? NULL : + dsync_mailbox_import_init(brain->box, brain->virtual_all_box, + brain->log_scan, + last_common_uid, last_common_modseq, + last_common_pvt_modseq, + remote_dsync_box->uid_next, + remote_dsync_box->first_recent_uid, + remote_dsync_box->highest_modseq, + remote_dsync_box->highest_pvt_modseq, + brain->sync_since_timestamp, + brain->sync_until_timestamp, + brain->sync_max_size, + brain->sync_flag, + brain->import_commit_msgs_interval, + import_flags, brain->hdr_hash_version, + brain->hashed_headers); +} + +int dsync_brain_sync_mailbox_open(struct dsync_brain *brain, + const struct dsync_mailbox *remote_dsync_box) +{ + struct mailbox_status status; + enum dsync_mailbox_exporter_flags exporter_flags = 0; + uint32_t last_common_uid, highest_wanted_uid; + uint64_t last_common_modseq, last_common_pvt_modseq; + const char *desync_reason = ""; + bool pvt_too_old; + int ret; + + i_assert(brain->log_scan == NULL); + i_assert(brain->box_exporter == NULL); + + last_common_uid = brain->mailbox_state.last_common_uid; + last_common_modseq = brain->mailbox_state.last_common_modseq; + last_common_pvt_modseq = brain->mailbox_state.last_common_pvt_modseq; + highest_wanted_uid = last_common_uid == 0 ? + (uint32_t)-1 : last_common_uid; + ret = dsync_transaction_log_scan_init(brain->box->view, + brain->box->view_pvt, + highest_wanted_uid, + last_common_modseq, + last_common_pvt_modseq, + &brain->log_scan, &pvt_too_old); + if (ret < 0) { + i_error("Failed to read transaction log for mailbox %s", + mailbox_get_vname(brain->box)); + brain->failed = TRUE; + return -1; + } + + mailbox_get_open_status(brain->box, STATUS_UIDNEXT | + STATUS_HIGHESTMODSEQ | + STATUS_HIGHESTPVTMODSEQ, &status); + if (status.nonpermanent_modseqs) + status.highest_modseq = 0; + if (ret == 0) { + if (pvt_too_old) { + desync_reason = t_strdup_printf( + "Private modseq %"PRIu64" no longer in transaction log " + "(highest=%"PRIu64", last_common_uid=%u, nextuid=%u)", + last_common_pvt_modseq, + status.highest_pvt_modseq, last_common_uid, + status.uidnext); + } else { + desync_reason = t_strdup_printf( + "Modseq %"PRIu64" no longer in transaction log " + "(highest=%"PRIu64", last_common_uid=%u, nextuid=%u)", + last_common_modseq, + status.highest_modseq, last_common_uid, + status.uidnext); + } + } + + if (last_common_uid != 0) { + /* if last_common_* is higher than our current ones it means + that the incremental sync state is stale and we need to do + a full resync */ + if (status.uidnext < last_common_uid) { + desync_reason = t_strdup_printf("uidnext %u < %u", + status.uidnext, last_common_uid); + ret = 0; + } else if (status.highest_modseq < last_common_modseq) { + desync_reason = t_strdup_printf("highest_modseq %"PRIu64" < %"PRIu64, + status.highest_modseq, last_common_modseq); + ret = 0; + } else if (status.highest_pvt_modseq < last_common_pvt_modseq) { + desync_reason = t_strdup_printf("highest_pvt_modseq %"PRIu64" < %"PRIu64, + status.highest_pvt_modseq, last_common_pvt_modseq); + ret = 0; + } + } + if (ret == 0) { + i_warning("Failed to do incremental sync for mailbox %s, " + "retry with a full sync (%s)", + mailbox_get_vname(brain->box), desync_reason); + dsync_brain_set_changes_during_sync(brain, t_strdup_printf( + "Incremental sync failed: %s", desync_reason)); + brain->require_full_resync = TRUE; + return 0; + } + + if (!brain->mail_requests) + exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_AUTO_EXPORT_MAILS; + if (remote_dsync_box->have_save_guids && + (brain->local_dsync_box.have_save_guids || + (brain->backup_send && brain->local_dsync_box.have_guids))) + exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_MAILS_HAVE_GUIDS; + if (brain->no_mail_prefetch) + exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_MINIMAL_DMAIL_FILL; + if (brain->sync_since_timestamp > 0 || + brain->sync_until_timestamp > 0) + exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_TIMESTAMPS; + if (brain->sync_max_size > 0) + exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_VSIZES; + if (remote_dsync_box->messages_count == 0) { + /* remote mailbox is empty - we don't really need to export + header hashes since they're not going to match anything + anyway. */ + exporter_flags |= DSYNC_MAILBOX_EXPORTER_FLAG_NO_HDR_HASHES; + } + + brain->box_exporter = brain->backup_recv ? NULL : + dsync_mailbox_export_init(brain->box, brain->log_scan, + last_common_uid, + exporter_flags, + brain->hdr_hash_version, + brain->hashed_headers); + dsync_brain_sync_mailbox_init_remote(brain, remote_dsync_box); + return 1; +} + +void dsync_brain_sync_mailbox_deinit(struct dsync_brain *brain) +{ + enum mail_error error; + + i_assert(brain->box != NULL); + + array_push_back(&brain->remote_mailbox_states, &brain->mailbox_state); + if (brain->box_exporter != NULL) { + const char *errstr; + + i_assert(brain->failed || brain->require_full_resync || + brain->sync_type == DSYNC_BRAIN_SYNC_TYPE_CHANGED); + if (dsync_mailbox_export_deinit(&brain->box_exporter, + &errstr, &error) < 0) + i_error("Mailbox export failed: %s", errstr); + } + if (brain->box_importer != NULL) { + uint32_t last_common_uid, last_messages_count; + uint64_t last_common_modseq, last_common_pvt_modseq; + const char *changes_during_sync; + bool require_full_resync; + + i_assert(brain->failed); + (void)dsync_mailbox_import_deinit(&brain->box_importer, + FALSE, + &last_common_uid, + &last_common_modseq, + &last_common_pvt_modseq, + &last_messages_count, + &changes_during_sync, + &require_full_resync, + &brain->mail_error); + if (require_full_resync) + brain->require_full_resync = TRUE; + } + if (brain->log_scan != NULL) + dsync_transaction_log_scan_deinit(&brain->log_scan); + file_lock_free(&brain->box_lock); + mailbox_free(&brain->box); + + brain->state = brain->pre_box_state; +} + +static int dsync_box_get(struct mailbox *box, struct dsync_mailbox *dsync_box_r, + enum mail_error *error_r) +{ + const enum mailbox_status_items status_items = + STATUS_UIDVALIDITY | STATUS_UIDNEXT | STATUS_MESSAGES | + STATUS_FIRST_RECENT_UID | STATUS_HIGHESTMODSEQ | + STATUS_HIGHESTPVTMODSEQ; + const enum mailbox_metadata_items metadata_items = + MAILBOX_METADATA_CACHE_FIELDS | MAILBOX_METADATA_GUID; + struct mailbox_status status; + struct mailbox_metadata metadata; + const char *errstr; + enum mail_error error; + + /* get metadata first, since it may autocreate the mailbox */ + if (mailbox_get_metadata(box, metadata_items, &metadata) < 0 || + mailbox_get_status(box, status_items, &status) < 0) { + errstr = mailbox_get_last_internal_error(box, &error); + if (error == MAIL_ERROR_NOTFOUND || + error == MAIL_ERROR_NOTPOSSIBLE) { + /* Mailbox isn't selectable, try the next one. We + should have already caught \Noselect mailboxes, but + check them anyway here. The NOTPOSSIBLE check is + mainly for invalid mbox files. */ + return 0; + } + i_error("Failed to access mailbox %s: %s", + mailbox_get_vname(box), errstr); + *error_r = error; + return -1; + } + if (status.nonpermanent_modseqs) + status.highest_modseq = 0; + + i_assert(status.uidvalidity != 0 || status.messages == 0); + + i_zero(dsync_box_r); + memcpy(dsync_box_r->mailbox_guid, metadata.guid, + sizeof(dsync_box_r->mailbox_guid)); + dsync_box_r->uid_validity = status.uidvalidity; + dsync_box_r->uid_next = status.uidnext; + dsync_box_r->messages_count = status.messages; + dsync_box_r->first_recent_uid = status.first_recent_uid; + dsync_box_r->highest_modseq = status.highest_modseq; + dsync_box_r->highest_pvt_modseq = status.highest_pvt_modseq; + dsync_mailbox_cache_field_dup(&dsync_box_r->cache_fields, + metadata.cache_fields, + pool_datastack_create()); + dsync_box_r->have_guids = status.have_guids; + dsync_box_r->have_save_guids = status.have_save_guids; + dsync_box_r->have_only_guid128 = status.have_only_guid128; + return 1; +} + +static bool +dsync_brain_has_mailbox_state_changed(struct dsync_brain *brain, + const struct dsync_mailbox *dsync_box) +{ + const struct dsync_mailbox_state *state; + + if (brain->sync_type != DSYNC_BRAIN_SYNC_TYPE_STATE) + return TRUE; + + state = dsync_mailbox_state_find(brain, dsync_box->mailbox_guid); + return state == NULL || + state->last_uidvalidity != dsync_box->uid_validity || + state->last_common_uid+1 != dsync_box->uid_next || + state->last_common_modseq != dsync_box->highest_modseq || + state->last_common_pvt_modseq != dsync_box->highest_pvt_modseq || + state->last_messages_count != dsync_box->messages_count; +} + +static int +dsync_brain_try_next_mailbox(struct dsync_brain *brain, struct mailbox **box_r, + struct file_lock **lock_r, + struct dsync_mailbox *dsync_box_r) +{ + enum mailbox_flags flags = 0; + struct dsync_mailbox dsync_box; + struct mailbox *box; + struct file_lock *lock = NULL; + struct dsync_mailbox_node *node; + const char *vname = NULL; + enum mail_error error; + bool synced = FALSE; + int ret; + + *box_r = NULL; + + while (dsync_mailbox_tree_iter_next(brain->local_tree_iter, &vname, &node)) { + if (node->existence == DSYNC_MAILBOX_NODE_EXISTS && + !guid_128_is_empty(node->mailbox_guid)) + break; + vname = NULL; + } + if (vname == NULL) { + /* no more mailboxes */ + dsync_mailbox_tree_iter_deinit(&brain->local_tree_iter); + return -1; + } + + if (brain->backup_send) { + /* make sure mailbox isn't modified */ + flags |= MAILBOX_FLAG_READONLY; + } + box = mailbox_alloc(node->ns->list, vname, flags); + for (;;) { + if ((ret = dsync_box_get(box, &dsync_box, &error)) <= 0) { + if (ret < 0) { + brain->mail_error = error; + brain->failed = TRUE; + } + mailbox_free(&box); + file_lock_free(&lock); + return ret; + } + + /* if mailbox's last_common_* state equals the current state, + we can skip the mailbox */ + if (!dsync_brain_has_mailbox_state_changed(brain, &dsync_box)) { + if (brain->debug) { + i_debug("brain %c: Skipping mailbox %s with unchanged state " + "uidvalidity=%u uidnext=%u highestmodseq=%"PRIu64" highestpvtmodseq=%"PRIu64" messages=%u", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(dsync_box.mailbox_guid), + dsync_box.uid_validity, + dsync_box.uid_next, + dsync_box.highest_modseq, + dsync_box.highest_pvt_modseq, + dsync_box.messages_count); + } + mailbox_free(&box); + file_lock_free(&lock); + return 0; + } + if (synced) { + /* ok, the mailbox really changed */ + break; + } + + /* mailbox appears to have changed. do a full sync here and get the + state again. Lock before syncing. */ + if (dsync_mailbox_lock(brain, box, &lock) < 0) { + brain->failed = TRUE; + mailbox_free(&box); + return -1; + } + if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) { + i_error("Can't sync mailbox %s: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &brain->mail_error)); + brain->failed = TRUE; + mailbox_free(&box); + file_lock_free(&lock); + return -1; + } + synced = TRUE; + } + + *box_r = box; + *lock_r = lock; + *dsync_box_r = dsync_box; + return 1; +} + +static bool +dsync_brain_next_mailbox(struct dsync_brain *brain, struct mailbox **box_r, + struct file_lock **lock_r, + struct dsync_mailbox *dsync_box_r) +{ + int ret; + + if (brain->no_mail_sync) + return FALSE; + + while ((ret = dsync_brain_try_next_mailbox(brain, box_r, lock_r, dsync_box_r)) == 0) + ; + return ret > 0; +} + +void dsync_brain_master_send_mailbox(struct dsync_brain *brain) +{ + struct dsync_mailbox dsync_box; + struct mailbox *box; + struct file_lock *lock; + + i_assert(brain->master_brain); + i_assert(brain->box == NULL); + + if (!dsync_brain_next_mailbox(brain, &box, &lock, &dsync_box)) { + brain->state = DSYNC_STATE_FINISH; + dsync_ibc_send_end_of_list(brain->ibc, DSYNC_IBC_EOL_MAILBOX); + return; + } + + /* start exporting this mailbox (wait for remote to start importing) */ + dsync_ibc_send_mailbox(brain->ibc, &dsync_box); + dsync_brain_sync_mailbox_init(brain, box, lock, &dsync_box, TRUE); + brain->state = DSYNC_STATE_SYNC_MAILS; +} + +bool dsync_boxes_need_sync(struct dsync_brain *brain, + const struct dsync_mailbox *box1, + const struct dsync_mailbox *box2, + const char **reason_r) +{ + if (brain->no_mail_sync) + return FALSE; + if (brain->sync_type != DSYNC_BRAIN_SYNC_TYPE_CHANGED) { + *reason_r = "Full sync"; + return TRUE; + } + if (box1->uid_validity != box2->uid_validity) + *reason_r = t_strdup_printf("UIDVALIDITY changed: %u -> %u", + box1->uid_validity, box2->uid_validity); + else if (box1->uid_next != box2->uid_next) + *reason_r = t_strdup_printf("UIDNEXT changed: %u -> %u", + box1->uid_next, box2->uid_next); + else if (box1->messages_count != box2->messages_count) + *reason_r = t_strdup_printf("Message count changed: %u -> %u", + box1->messages_count, box2->messages_count); + else if (box1->highest_modseq != box2->highest_modseq) { + *reason_r = t_strdup_printf("HIGHESTMODSEQ changed %" + PRIu64" -> %"PRIu64, + box1->highest_modseq, + box2->highest_modseq); + if (box1->highest_modseq == 0 || + box2->highest_modseq == 0) { + *reason_r = t_strdup_printf( + "%s (Permanent MODSEQs aren't supported)", + *reason_r); + } + } else if (box1->highest_pvt_modseq != box2->highest_pvt_modseq) + *reason_r = t_strdup_printf("Private HIGHESTMODSEQ changed %" + PRIu64" -> %"PRIu64, + box1->highest_pvt_modseq, + box2->highest_pvt_modseq); + else if (box1->first_recent_uid != box2->first_recent_uid) + *reason_r = t_strdup_printf("First RECENT UID changed: %u -> %u", + box1->first_recent_uid, box2->first_recent_uid); + else + return FALSE; + return TRUE; +} + +static int +mailbox_cache_field_name_cmp(const struct mailbox_cache_field *f1, + const struct mailbox_cache_field *f2) +{ + return strcmp(f1->name, f2->name); +} + +static void +dsync_cache_fields_update(const struct dsync_mailbox *local_box, + const struct dsync_mailbox *remote_box, + struct mailbox *box, + struct mailbox_update *update) +{ + ARRAY_TYPE(mailbox_cache_field) local_sorted, remote_sorted, changes; + const struct mailbox_cache_field *local_fields, *remote_fields; + unsigned int li, ri, local_count, remote_count; + time_t drop_older_timestamp; + int ret; + + if (array_count(&remote_box->cache_fields) == 0) { + /* remote has no cached fields. there's nothing to update. */ + return; + } + + t_array_init(&local_sorted, array_count(&local_box->cache_fields)); + t_array_init(&remote_sorted, array_count(&remote_box->cache_fields)); + array_append_array(&local_sorted, &local_box->cache_fields); + array_append_array(&remote_sorted, &remote_box->cache_fields); + array_sort(&local_sorted, mailbox_cache_field_name_cmp); + array_sort(&remote_sorted, mailbox_cache_field_name_cmp); + + if (array_count(&local_sorted) == 0) { + /* local has no cached fields. set them to same as remote. */ + array_append_zero(&remote_sorted); + update->cache_updates = array_front(&remote_sorted); + return; + } + + /* figure out what to change */ + local_fields = array_get(&local_sorted, &local_count); + remote_fields = array_get(&remote_sorted, &remote_count); + t_array_init(&changes, local_count + remote_count); + drop_older_timestamp = ioloop_time - + box->index->optimization_set.cache.unaccessed_field_drop_secs; + + for (li = ri = 0; li < local_count || ri < remote_count; ) { + ret = li == local_count ? 1 : + ri == remote_count ? -1 : + strcmp(local_fields[li].name, remote_fields[ri].name); + if (ret == 0) { + /* field exists in both local and remote */ + const struct mailbox_cache_field *lf = &local_fields[li]; + const struct mailbox_cache_field *rf = &remote_fields[ri]; + + if (lf->last_used > rf->last_used || + (lf->last_used == rf->last_used && + lf->decision > rf->decision)) { + /* use local decision and timestamp */ + } else { + array_push_back(&changes, rf); + } + li++; ri++; + } else if (ret < 0) { + /* remote field doesn't exist */ + li++; + } else { + /* local field doesn't exist */ + if (remote_fields[ri].last_used < drop_older_timestamp) { + /* field hasn't be used for a long time, remote + will probably drop this soon as well */ + } else { + array_push_back(&changes, &remote_fields[ri]); + } + ri++; + } + } + i_assert(li == local_count && ri == remote_count); + if (array_count(&changes) > 0) { + array_append_zero(&changes); + update->cache_updates = array_front(&changes); + } +} + +bool dsync_brain_mailbox_update_pre(struct dsync_brain *brain, + struct mailbox *box, + const struct dsync_mailbox *local_box, + const struct dsync_mailbox *remote_box, + const char **reason_r) +{ + struct mailbox_update update; + const struct dsync_mailbox_state *state; + bool ret = TRUE; + + *reason_r = NULL; + i_zero(&update); + + if (local_box->uid_validity != remote_box->uid_validity) { + /* Keep the UIDVALIDITY for the mailbox that has more + messages. If they equal, use the higher UIDVALIDITY. */ + if (remote_box->messages_count > local_box->messages_count || + (remote_box->messages_count == local_box->messages_count && + remote_box->uid_validity > local_box->uid_validity)) + update.uid_validity = remote_box->uid_validity; + + state = dsync_mailbox_state_find(brain, local_box->mailbox_guid); + if (state != NULL && state->last_common_uid > 0) { + /* we can't continue syncing this mailbox in this + session, because the other side already started + sending mailbox changes, but not for all mails. */ + dsync_mailbox_state_remove(brain, local_box->mailbox_guid); + *reason_r = "UIDVALIDITY changed during a stateful sync, need to restart"; + brain->failed = TRUE; + ret = FALSE; + } + } + + dsync_cache_fields_update(local_box, remote_box, box, &update); + + if (update.uid_validity == 0 && + update.cache_updates == NULL) { + /* no changes */ + return ret; + } + + if (mailbox_update(box, &update) < 0) { + i_error("Couldn't update mailbox %s metadata: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &brain->mail_error)); + brain->failed = TRUE; + } + return ret; +} + +static void +dsync_brain_slave_send_mailbox_lost(struct dsync_brain *brain, + const struct dsync_mailbox *dsync_box, + bool ignore) +{ + struct dsync_mailbox delete_box; + + if (brain->debug) { + i_debug("brain %c: We don't have mailbox %s", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(dsync_box->mailbox_guid)); + } + i_zero(&delete_box); + memcpy(delete_box.mailbox_guid, dsync_box->mailbox_guid, + sizeof(delete_box.mailbox_guid)); + t_array_init(&delete_box.cache_fields, 0); + if (ignore) + delete_box.mailbox_ignore = TRUE; + else + delete_box.mailbox_lost = TRUE; + dsync_ibc_send_mailbox(brain->ibc, &delete_box); +} + +bool dsync_brain_slave_recv_mailbox(struct dsync_brain *brain) +{ + const struct dsync_mailbox *dsync_box; + struct dsync_mailbox local_dsync_box; + struct mailbox *box; + struct file_lock *lock; + const char *errstr, *resync_reason, *reason; + enum mail_error error; + int ret; + bool resync; + + i_assert(!brain->master_brain); + i_assert(brain->box == NULL); + + if ((ret = dsync_ibc_recv_mailbox(brain->ibc, &dsync_box)) == 0) + return FALSE; + if (ret < 0) { + brain->state = DSYNC_STATE_FINISH; + return TRUE; + } + + if (dsync_brain_mailbox_alloc(brain, dsync_box->mailbox_guid, + &box, &errstr, &error) < 0) { + i_error("Couldn't allocate mailbox GUID %s: %s", + guid_128_to_string(dsync_box->mailbox_guid), errstr); + brain->mail_error = error; + brain->failed = TRUE; + return TRUE; + } + if (box == NULL) { + /* mailbox was probably deleted/renamed during sync */ + if (brain->backup_send && brain->no_backup_overwrite) { + if (brain->debug) { + i_debug("brain %c: Ignore nonexistent " + "mailbox GUID %s with -1 sync", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(dsync_box->mailbox_guid)); + } + dsync_brain_slave_send_mailbox_lost(brain, dsync_box, TRUE); + return TRUE; + } + //FIXME: verify this from log, and if not log an error. + dsync_brain_set_changes_during_sync(brain, t_strdup_printf( + "Mailbox GUID %s was lost", + guid_128_to_string(dsync_box->mailbox_guid))); + dsync_brain_slave_send_mailbox_lost(brain, dsync_box, FALSE); + return TRUE; + } + /* Lock before syncing */ + if (dsync_mailbox_lock(brain, box, &lock) < 0) { + mailbox_free(&box); + brain->failed = TRUE; + return TRUE; + } + if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) { + i_error("Can't sync mailbox %s: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &brain->mail_error)); + file_lock_free(&lock); + mailbox_free(&box); + brain->failed = TRUE; + return TRUE; + } + + if ((ret = dsync_box_get(box, &local_dsync_box, &error)) <= 0) { + file_lock_free(&lock); + mailbox_free(&box); + if (ret < 0) { + brain->mail_error = error; + brain->failed = TRUE; + return TRUE; + } + /* another process just deleted this mailbox? */ + if (brain->debug) { + i_debug("brain %c: Skipping lost mailbox %s", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(dsync_box->mailbox_guid)); + } + dsync_brain_slave_send_mailbox_lost(brain, dsync_box, FALSE); + return TRUE; + } + i_assert(local_dsync_box.uid_validity != 0); + i_assert(memcmp(dsync_box->mailbox_guid, local_dsync_box.mailbox_guid, + sizeof(dsync_box->mailbox_guid)) == 0); + + resync = !dsync_brain_mailbox_update_pre(brain, box, &local_dsync_box, + dsync_box, &resync_reason); + + if (!dsync_boxes_need_sync(brain, &local_dsync_box, dsync_box, &reason)) { + /* no fields appear to have changed, skip this mailbox */ + if (brain->debug) { + i_debug("brain %c: Skipping unchanged mailbox %s", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(dsync_box->mailbox_guid)); + } + dsync_ibc_send_mailbox(brain->ibc, &local_dsync_box); + file_lock_free(&lock); + mailbox_free(&box); + return TRUE; + } + if (brain->debug) { + i_debug("brain %c: Syncing mailbox %s: %s", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(dsync_box->mailbox_guid), reason); + } + + /* start export/import */ + dsync_brain_sync_mailbox_init(brain, box, lock, &local_dsync_box, FALSE); + if ((ret = dsync_brain_sync_mailbox_open(brain, dsync_box)) < 0) + return TRUE; + if (resync) + dsync_brain_set_changes_during_sync(brain, resync_reason); + if (ret == 0 || resync) { + brain->require_full_resync = TRUE; + dsync_brain_sync_mailbox_deinit(brain); + dsync_brain_slave_send_mailbox_lost(brain, dsync_box, FALSE); + return TRUE; + } + + dsync_ibc_send_mailbox(brain->ibc, &local_dsync_box); + brain->state = DSYNC_STATE_SYNC_MAILS; + return TRUE; +} diff --git a/src/doveadm/dsync/dsync-brain-mails.c b/src/doveadm/dsync/dsync-brain-mails.c new file mode 100644 index 0000000..3feea20 --- /dev/null +++ b/src/doveadm/dsync/dsync-brain-mails.c @@ -0,0 +1,438 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "istream.h" +#include "dsync-ibc.h" +#include "dsync-mail.h" +#include "dsync-mailbox-import.h" +#include "dsync-mailbox-export.h" +#include "dsync-brain-private.h" + +const char *dsync_box_state_names[DSYNC_BOX_STATE_DONE+1] = { + "mailbox", + "changes", + "attributes", + "mail_requests", + "mails", + "recv_last_common", + "done" +}; + +static bool dsync_brain_master_sync_recv_mailbox(struct dsync_brain *brain) +{ + const struct dsync_mailbox *dsync_box; + const char *resync_reason, *reason; + enum dsync_ibc_recv_ret ret; + bool resync; + + i_assert(brain->master_brain); + + if ((ret = dsync_ibc_recv_mailbox(brain->ibc, &dsync_box)) == 0) + return FALSE; + if (ret == DSYNC_IBC_RECV_RET_FINISHED) { + i_error("Remote sent end-of-list instead of a mailbox"); + brain->failed = TRUE; + return TRUE; + } + if (memcmp(dsync_box->mailbox_guid, brain->local_dsync_box.mailbox_guid, + sizeof(dsync_box->mailbox_guid)) != 0) { + i_error("Remote sent mailbox with a wrong GUID"); + brain->failed = TRUE; + return TRUE; + } + + if (dsync_box->mailbox_ignore) { + /* ignore this box */ + if (brain->debug) + i_debug("brain %c: Ignoring missing remote box GUID %s", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(dsync_box->mailbox_guid)); + dsync_brain_sync_mailbox_deinit(brain); + return TRUE; + } + if (dsync_box->mailbox_lost) { + /* remote lost the mailbox. it's probably already deleted, but + verify it on next sync just to be sure */ + dsync_brain_set_changes_during_sync(brain, t_strdup_printf( + "Remote lost mailbox GUID %s (maybe it was just deleted?)", + guid_128_to_string(dsync_box->mailbox_guid))); + brain->require_full_resync = TRUE; + dsync_brain_sync_mailbox_deinit(brain); + return TRUE; + } + resync = !dsync_brain_mailbox_update_pre(brain, brain->box, + &brain->local_dsync_box, + dsync_box, &resync_reason); + + if (!dsync_boxes_need_sync(brain, &brain->local_dsync_box, dsync_box, + &reason)) { + /* no fields appear to have changed, skip this mailbox */ + dsync_brain_sync_mailbox_deinit(brain); + return TRUE; + } + if (brain->debug) { + i_debug("brain %c: Syncing mailbox %s: %s", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(dsync_box->mailbox_guid), reason); + } + if ((ret = dsync_brain_sync_mailbox_open(brain, dsync_box)) < 0) + return TRUE; + if (resync) + dsync_brain_set_changes_during_sync(brain, resync_reason); + if (ret == 0 || resync) { + brain->require_full_resync = TRUE; + brain->failed = TRUE; + dsync_brain_sync_mailbox_deinit(brain); + return TRUE; + } + dsync_brain_sync_init_box_states(brain); + return TRUE; +} + +static bool dsync_brain_recv_mailbox_attribute(struct dsync_brain *brain) +{ + const struct dsync_mailbox_attribute *attr; + struct istream *input; + enum dsync_ibc_recv_ret ret; + + if ((ret = dsync_ibc_recv_mailbox_attribute(brain->ibc, &attr)) == 0) + return FALSE; + if (ret == DSYNC_IBC_RECV_RET_FINISHED) { + brain->box_recv_state = DSYNC_BOX_STATE_CHANGES; + return TRUE; + } + if (dsync_mailbox_import_attribute(brain->box_importer, attr) < 0) + brain->failed = TRUE; + input = attr->value_stream; + i_stream_unref(&input); + return TRUE; +} + +static void dsync_brain_send_end_of_list(struct dsync_brain *brain, + enum dsync_ibc_eol_type type) +{ + i_assert(!brain->failed); + dsync_ibc_send_end_of_list(brain->ibc, type); +} + +static int dsync_brain_export_deinit(struct dsync_brain *brain) +{ + const char *errstr; + enum mail_error error; + + if (dsync_mailbox_export_deinit(&brain->box_exporter, + &errstr, &error) < 0) { + i_error("Exporting mailbox %s failed: %s", + mailbox_get_vname(brain->box), errstr); + brain->mail_error = error; + brain->failed = TRUE; + return -1; + } + return 0; +} + +static void dsync_brain_send_mailbox_attribute(struct dsync_brain *brain) +{ + const struct dsync_mailbox_attribute *attr; + int ret; + + while ((ret = dsync_mailbox_export_next_attr(brain->box_exporter, &attr)) > 0) { + if (dsync_ibc_send_mailbox_attribute(brain->ibc, attr) == 0) + return; + } + if (ret < 0) { + if (dsync_brain_export_deinit(brain) == 0) + i_unreached(); + return; + } + dsync_brain_send_end_of_list(brain, DSYNC_IBC_EOL_MAILBOX_ATTRIBUTE); + brain->box_send_state = DSYNC_BOX_STATE_CHANGES; +} + +static bool dsync_brain_recv_mail_change(struct dsync_brain *brain) +{ + const struct dsync_mail_change *change; + enum dsync_ibc_recv_ret ret; + + if ((ret = dsync_ibc_recv_change(brain->ibc, &change)) == 0) + return FALSE; + if (ret == DSYNC_IBC_RECV_RET_FINISHED) { + if (dsync_mailbox_import_changes_finish(brain->box_importer) < 0) + brain->failed = TRUE; + if (brain->mail_requests && brain->box_exporter != NULL) + brain->box_recv_state = DSYNC_BOX_STATE_MAIL_REQUESTS; + else + brain->box_recv_state = DSYNC_BOX_STATE_MAILS; + return TRUE; + } + if (dsync_mailbox_import_change(brain->box_importer, change) < 0) + brain->failed = TRUE; + return TRUE; +} + +static void dsync_brain_send_mail_change(struct dsync_brain *brain) +{ + const struct dsync_mail_change *change; + int ret; + + while ((ret = dsync_mailbox_export_next(brain->box_exporter, &change)) > 0) { + if (dsync_ibc_send_change(brain->ibc, change) == 0) + return; + } + if (ret < 0) { + if (dsync_brain_export_deinit(brain) == 0) + i_unreached(); + return; + } + dsync_brain_send_end_of_list(brain, DSYNC_IBC_EOL_MAIL_CHANGES); + if (brain->mail_requests && brain->box_importer != NULL) + brain->box_send_state = DSYNC_BOX_STATE_MAIL_REQUESTS; + else + brain->box_send_state = DSYNC_BOX_STATE_MAILS; +} + +static bool dsync_brain_recv_mail_request(struct dsync_brain *brain) +{ + const struct dsync_mail_request *request; + enum dsync_ibc_recv_ret ret; + + i_assert(brain->mail_requests); + i_assert(brain->box_exporter != NULL); + + if ((ret = dsync_ibc_recv_mail_request(brain->ibc, &request)) == 0) + return FALSE; + if (ret == DSYNC_IBC_RECV_RET_FINISHED) { + brain->box_recv_state = brain->box_importer != NULL ? + DSYNC_BOX_STATE_MAILS : + DSYNC_BOX_STATE_RECV_LAST_COMMON; + return TRUE; + } + dsync_mailbox_export_want_mail(brain->box_exporter, request); + return TRUE; +} + +static bool dsync_brain_send_mail_request(struct dsync_brain *brain) +{ + const struct dsync_mail_request *request; + + i_assert(brain->mail_requests); + + while ((request = dsync_mailbox_import_next_request(brain->box_importer)) != NULL) { + if (dsync_ibc_send_mail_request(brain->ibc, request) == 0) + return TRUE; + } + if (brain->box_recv_state < DSYNC_BOX_STATE_MAIL_REQUESTS) + return FALSE; + + dsync_brain_send_end_of_list(brain, DSYNC_IBC_EOL_MAIL_REQUESTS); + if (brain->box_exporter != NULL) + brain->box_send_state = DSYNC_BOX_STATE_MAILS; + else { + i_assert(brain->box_recv_state != DSYNC_BOX_STATE_DONE); + brain->box_send_state = DSYNC_BOX_STATE_DONE; + } + return TRUE; +} + +static void dsync_brain_sync_half_finished(struct dsync_brain *brain) +{ + struct dsync_mailbox_state state; + const char *changes_during_sync; + bool require_full_resync; + + if (brain->box_recv_state < DSYNC_BOX_STATE_RECV_LAST_COMMON || + brain->box_send_state < DSYNC_BOX_STATE_RECV_LAST_COMMON) + return; + + /* finished with this mailbox */ + i_zero(&state); + memcpy(state.mailbox_guid, brain->local_dsync_box.mailbox_guid, + sizeof(state.mailbox_guid)); + state.last_uidvalidity = brain->local_dsync_box.uid_validity; + if (brain->box_importer == NULL) { + /* this mailbox didn't exist on remote */ + state.last_common_uid = brain->local_dsync_box.uid_next-1; + state.last_common_modseq = + brain->local_dsync_box.highest_modseq; + state.last_common_pvt_modseq = + brain->local_dsync_box.highest_pvt_modseq; + state.last_messages_count = + brain->local_dsync_box.messages_count; + } else { + if (dsync_mailbox_import_deinit(&brain->box_importer, + !brain->failed, + &state.last_common_uid, + &state.last_common_modseq, + &state.last_common_pvt_modseq, + &state.last_messages_count, + &changes_during_sync, + &require_full_resync, + &brain->mail_error) < 0) { + if (require_full_resync) { + /* don't treat this as brain failure or the + state won't be sent to the other brain. + this also means we'll continue syncing the + following mailboxes. */ + brain->require_full_resync = TRUE; + } else { + brain->failed = TRUE; + } + } + if (changes_during_sync != NULL) { + state.changes_during_sync = TRUE; + dsync_brain_set_changes_during_sync(brain, changes_during_sync); + } + } + if (brain->require_full_resync) { + state.last_uidvalidity = 0; + state.changes_during_sync = TRUE; + } + brain->mailbox_state = state; + dsync_ibc_send_mailbox_state(brain->ibc, &state); +} + +static bool dsync_brain_recv_mail(struct dsync_brain *brain) +{ + struct dsync_mail *mail; + enum dsync_ibc_recv_ret ret; + + if ((ret = dsync_ibc_recv_mail(brain->ibc, &mail)) == 0) + return FALSE; + if (ret == DSYNC_IBC_RECV_RET_FINISHED) { + brain->box_recv_state = DSYNC_BOX_STATE_RECV_LAST_COMMON; + if (brain->box_exporter != NULL && + brain->box_send_state >= DSYNC_BOX_STATE_RECV_LAST_COMMON) { + if (dsync_brain_export_deinit(brain) < 0) + return TRUE; + } + dsync_brain_sync_half_finished(brain); + return TRUE; + } + if (brain->debug) { + i_debug("brain %c: import mail uid %u guid %s", + brain->master_brain ? 'M' : 'S', mail->uid, mail->guid); + } + if (dsync_mailbox_import_mail(brain->box_importer, mail) < 0) + brain->failed = TRUE; + i_stream_unref(&mail->input); + return TRUE; +} + +static bool dsync_brain_send_mail(struct dsync_brain *brain) +{ + const struct dsync_mail *mail; + + if (brain->mail_requests && + brain->box_recv_state < DSYNC_BOX_STATE_MAILS) { + /* wait for mail requests to finish. we could already start + exporting, but then we're going to do quite a lot of + separate searches. especially with pipe backend we'd do + a separate search for each mail. */ + return FALSE; + } + + while (dsync_mailbox_export_next_mail(brain->box_exporter, &mail) > 0) { + if (dsync_ibc_send_mail(brain->ibc, mail) == 0) + return TRUE; + } + + if (dsync_brain_export_deinit(brain) < 0) + return TRUE; + + brain->box_send_state = DSYNC_BOX_STATE_DONE; + dsync_brain_send_end_of_list(brain, DSYNC_IBC_EOL_MAILS); + + dsync_brain_sync_half_finished(brain); + return TRUE; +} + +static bool dsync_brain_recv_last_common(struct dsync_brain *brain) +{ + enum dsync_ibc_recv_ret ret; + struct dsync_mailbox_state state; + + if ((ret = dsync_ibc_recv_mailbox_state(brain->ibc, &state)) == 0) + return FALSE; + if (ret == DSYNC_IBC_RECV_RET_FINISHED) { + i_error("Remote sent end-of-list instead of a mailbox state"); + brain->failed = TRUE; + return TRUE; + } + i_assert(brain->box_send_state == DSYNC_BOX_STATE_DONE); + i_assert(memcmp(state.mailbox_guid, brain->local_dsync_box.mailbox_guid, + sizeof(state.mailbox_guid)) == 0); + + /* normally the last_common_* values should be the same in local and + remote, but during unexpected changes they may differ. use the + values that are lower as the final state. */ + if (brain->mailbox_state.last_common_uid > state.last_common_uid) + brain->mailbox_state.last_common_uid = state.last_common_uid; + if (brain->mailbox_state.last_common_modseq > state.last_common_modseq) + brain->mailbox_state.last_common_modseq = state.last_common_modseq; + if (brain->mailbox_state.last_common_pvt_modseq > state.last_common_pvt_modseq) + brain->mailbox_state.last_common_pvt_modseq = state.last_common_pvt_modseq; + if (state.changes_during_sync) + brain->changes_during_remote_sync = TRUE; + + dsync_brain_sync_mailbox_deinit(brain); + return TRUE; +} + +bool dsync_brain_sync_mails(struct dsync_brain *brain) +{ + bool changed = FALSE; + + i_assert(brain->box != NULL); + + switch (brain->box_recv_state) { + case DSYNC_BOX_STATE_MAILBOX: + changed = dsync_brain_master_sync_recv_mailbox(brain); + break; + case DSYNC_BOX_STATE_ATTRIBUTES: + changed = dsync_brain_recv_mailbox_attribute(brain); + break; + case DSYNC_BOX_STATE_CHANGES: + changed = dsync_brain_recv_mail_change(brain); + break; + case DSYNC_BOX_STATE_MAIL_REQUESTS: + changed = dsync_brain_recv_mail_request(brain); + break; + case DSYNC_BOX_STATE_MAILS: + changed = dsync_brain_recv_mail(brain); + break; + case DSYNC_BOX_STATE_RECV_LAST_COMMON: + changed = dsync_brain_recv_last_common(brain); + break; + case DSYNC_BOX_STATE_DONE: + break; + } + + if (!dsync_ibc_is_send_queue_full(brain->ibc) && !brain->failed) { + switch (brain->box_send_state) { + case DSYNC_BOX_STATE_MAILBOX: + /* wait for mailbox to be received first */ + break; + case DSYNC_BOX_STATE_ATTRIBUTES: + dsync_brain_send_mailbox_attribute(brain); + changed = TRUE; + break; + case DSYNC_BOX_STATE_CHANGES: + dsync_brain_send_mail_change(brain); + changed = TRUE; + break; + case DSYNC_BOX_STATE_MAIL_REQUESTS: + if (dsync_brain_send_mail_request(brain)) + changed = TRUE; + break; + case DSYNC_BOX_STATE_MAILS: + if (dsync_brain_send_mail(brain)) + changed = TRUE; + break; + case DSYNC_BOX_STATE_RECV_LAST_COMMON: + i_unreached(); + case DSYNC_BOX_STATE_DONE: + break; + } + } + return changed; +} diff --git a/src/doveadm/dsync/dsync-brain-private.h b/src/doveadm/dsync/dsync-brain-private.h new file mode 100644 index 0000000..9849703 --- /dev/null +++ b/src/doveadm/dsync/dsync-brain-private.h @@ -0,0 +1,163 @@ +#ifndef DSYNC_BRAIN_PRIVATE_H +#define DSYNC_BRAIN_PRIVATE_H + +#include "hash.h" +#include "dsync-brain.h" +#include "dsync-mailbox.h" +#include "dsync-mailbox-state.h" + +#define DSYNC_LOCK_FILENAME ".dovecot-sync.lock" +#define DSYNC_MAILBOX_LOCK_FILENAME ".dovecot-box-sync.lock" +#define DSYNC_MAILBOX_DEFAULT_LOCK_TIMEOUT_SECS 30 + +struct dsync_mailbox_tree_sync_change; + +enum dsync_state { + DSYNC_STATE_MASTER_RECV_HANDSHAKE, + DSYNC_STATE_SLAVE_RECV_HANDSHAKE, + /* if sync_type=STATE, the master brain knows the saved "last common + mailbox state". this state is sent to the slave. */ + DSYNC_STATE_MASTER_SEND_LAST_COMMON, + DSYNC_STATE_SLAVE_RECV_LAST_COMMON, + + /* both sides send their mailbox trees */ + DSYNC_STATE_SEND_MAILBOX_TREE, + DSYNC_STATE_SEND_MAILBOX_TREE_DELETES, + DSYNC_STATE_RECV_MAILBOX_TREE, + DSYNC_STATE_RECV_MAILBOX_TREE_DELETES, + + /* master decides in which order mailboxes are synced (it knows the + slave's mailboxes by looking at the received mailbox tree) */ + DSYNC_STATE_MASTER_SEND_MAILBOX, + DSYNC_STATE_SLAVE_RECV_MAILBOX, + /* once mailbox is selected, the mails inside it are synced. + after the mails are synced, another mailbox is synced. */ + DSYNC_STATE_SYNC_MAILS, + + DSYNC_STATE_FINISH, + DSYNC_STATE_DONE +}; + +enum dsync_box_state { + DSYNC_BOX_STATE_MAILBOX, + DSYNC_BOX_STATE_CHANGES, + DSYNC_BOX_STATE_ATTRIBUTES, + DSYNC_BOX_STATE_MAIL_REQUESTS, + DSYNC_BOX_STATE_MAILS, + DSYNC_BOX_STATE_RECV_LAST_COMMON, + DSYNC_BOX_STATE_DONE +}; + +struct dsync_brain { + pool_t pool; + struct mail_user *user; + struct dsync_ibc *ibc; + const char *process_title_prefix; + ARRAY(struct mail_namespace *) sync_namespaces; + const char *sync_box; + struct mailbox *virtual_all_box; + guid_128_t sync_box_guid; + const char *const *exclude_mailboxes; + enum dsync_brain_sync_type sync_type; + time_t sync_since_timestamp; + time_t sync_until_timestamp; + uoff_t sync_max_size; + const char *sync_flag; + char alt_char; + unsigned int import_commit_msgs_interval; + unsigned int hdr_hash_version; + + unsigned int lock_timeout; + int lock_fd; + const char *lock_path; + struct file_lock *lock; + + char hierarchy_sep, escape_char; + struct dsync_mailbox_tree *local_mailbox_tree; + struct dsync_mailbox_tree *remote_mailbox_tree; + struct dsync_mailbox_tree_iter *local_tree_iter; + + enum dsync_state state, pre_box_state; + enum dsync_box_state box_recv_state; + enum dsync_box_state box_send_state; + unsigned int proctitle_update_counter; + + struct dsync_transaction_log_scan *log_scan; + struct dsync_mailbox_importer *box_importer; + struct dsync_mailbox_exporter *box_exporter; + + struct mailbox *box; + struct file_lock *box_lock; + unsigned int mailbox_lock_timeout_secs; + struct dsync_mailbox local_dsync_box, remote_dsync_box; + pool_t dsync_box_pool; + /* list of mailbox states + for master brain: given to brain at init and + for slave brain: received from DSYNC_STATE_SLAVE_RECV_LAST_COMMON */ + HASH_TABLE_TYPE(dsync_mailbox_state) mailbox_states; + /* DSYNC_STATE_MASTER_SEND_LAST_COMMON: current send position */ + struct hash_iterate_context *mailbox_states_iter; + /* state of the mailbox we're currently syncing, changed at + init and deinit */ + struct dsync_mailbox_state mailbox_state; + /* new states for synced mailboxes */ + ARRAY_TYPE(dsync_mailbox_state) remote_mailbox_states; + + const char *changes_during_sync; + enum mail_error mail_error; + + const char *const *hashed_headers; + + bool master_brain:1; + bool mail_requests:1; + bool backup_send:1; + bool backup_recv:1; + bool purge:1; + bool debug:1; + bool sync_visible_namespaces:1; + bool no_mail_sync:1; + bool no_backup_overwrite:1; + bool no_mail_prefetch:1; + bool changes_during_remote_sync:1; + bool require_full_resync:1; + bool verbose_proctitle:1; + bool no_notify:1; + bool failed:1; + bool empty_hdr_workaround:1; +}; + +extern const char *dsync_box_state_names[DSYNC_BOX_STATE_DONE+1]; + +void dsync_brain_mailbox_trees_init(struct dsync_brain *brain); +void dsync_brain_send_mailbox_tree(struct dsync_brain *brain); +void dsync_brain_send_mailbox_tree_deletes(struct dsync_brain *brain); +bool dsync_brain_recv_mailbox_tree(struct dsync_brain *brain); +bool dsync_brain_recv_mailbox_tree_deletes(struct dsync_brain *brain); +int dsync_brain_mailbox_tree_sync_change(struct dsync_brain *brain, + const struct dsync_mailbox_tree_sync_change *change, + enum mail_error *error_r); + +void dsync_brain_sync_mailbox_deinit(struct dsync_brain *brain); +int dsync_brain_mailbox_alloc(struct dsync_brain *brain, const guid_128_t guid, + struct mailbox **box_r, const char **errstr_r, + enum mail_error *error_r); +bool dsync_brain_mailbox_update_pre(struct dsync_brain *brain, + struct mailbox *box, + const struct dsync_mailbox *local_box, + const struct dsync_mailbox *remote_box, + const char **reason_r); +bool dsync_boxes_need_sync(struct dsync_brain *brain, + const struct dsync_mailbox *box1, + const struct dsync_mailbox *box2, + const char **reason_r); +void dsync_brain_sync_init_box_states(struct dsync_brain *brain); +void dsync_brain_set_changes_during_sync(struct dsync_brain *brain, + const char *reason); + +void dsync_brain_master_send_mailbox(struct dsync_brain *brain); +bool dsync_brain_slave_recv_mailbox(struct dsync_brain *brain); +int dsync_brain_sync_mailbox_open(struct dsync_brain *brain, + const struct dsync_mailbox *remote_dsync_box); +bool dsync_brain_sync_mails(struct dsync_brain *brain); + +#endif diff --git a/src/doveadm/dsync/dsync-brain.c b/src/doveadm/dsync/dsync-brain.c new file mode 100644 index 0000000..8ff7247 --- /dev/null +++ b/src/doveadm/dsync/dsync-brain.c @@ -0,0 +1,901 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "hostpid.h" +#include "str.h" +#include "file-create-locked.h" +#include "process-title.h" +#include "settings-parser.h" +#include "master-service.h" +#include "master-service-settings.h" +#include "mail-namespace.h" +#include "dsync-mailbox-tree.h" +#include "dsync-ibc.h" +#include "dsync-brain-private.h" +#include "dsync-mailbox-import.h" +#include "dsync-mailbox-export.h" + +#include <sys/stat.h> + +enum dsync_brain_title { + DSYNC_BRAIN_TITLE_NONE = 0, + DSYNC_BRAIN_TITLE_LOCKING, +}; + +static const char *dsync_state_names[] = { + "master_recv_handshake", + "slave_recv_handshake", + "master_send_last_common", + "slave_recv_last_common", + "send_mailbox_tree", + "send_mailbox_tree_deletes", + "recv_mailbox_tree", + "recv_mailbox_tree_deletes", + "master_send_mailbox", + "slave_recv_mailbox", + "sync_mails", + "finish", + "done" +}; + +struct dsync_mailbox_list_module dsync_mailbox_list_module = + MODULE_CONTEXT_INIT(&mailbox_list_module_register); + +static void dsync_brain_mailbox_states_dump(struct dsync_brain *brain); + +static const char * +dsync_brain_get_proctitle_full(struct dsync_brain *brain, + enum dsync_brain_title title) +{ + string_t *str = t_str_new(128); + const char *import_title, *export_title; + + str_append_c(str, '['); + if (brain->process_title_prefix != NULL) + str_append(str, brain->process_title_prefix); + str_append(str, brain->user->username); + if (brain->box == NULL) { + str_append_c(str, ' '); + str_append(str, dsync_state_names[brain->state]); + } else { + str_append_c(str, ' '); + str_append(str, mailbox_get_vname(brain->box)); + import_title = brain->box_importer == NULL ? "" : + dsync_mailbox_import_get_proctitle(brain->box_importer); + export_title = brain->box_exporter == NULL ? "" : + dsync_mailbox_export_get_proctitle(brain->box_exporter); + if (import_title[0] == '\0' && export_title[0] == '\0') { + str_printfa(str, " send:%s recv:%s", + dsync_box_state_names[brain->box_send_state], + dsync_box_state_names[brain->box_recv_state]); + } else { + if (import_title[0] != '\0') { + str_append(str, " import:"); + str_append(str, import_title); + } + if (export_title[0] != '\0') { + str_append(str, " export:"); + str_append(str, export_title); + } + } + } + switch (title) { + case DSYNC_BRAIN_TITLE_NONE: + break; + case DSYNC_BRAIN_TITLE_LOCKING: + str_append(str, " locking "DSYNC_LOCK_FILENAME); + break; + } + str_append_c(str, ']'); + return str_c(str); +} + +static const char *dsync_brain_get_proctitle(struct dsync_brain *brain) +{ + return dsync_brain_get_proctitle_full(brain, DSYNC_BRAIN_TITLE_NONE); +} + +static void dsync_brain_run_io(void *context) +{ + struct dsync_brain *brain = context; + bool changed, try_pending; + + if (dsync_ibc_has_failed(brain->ibc)) { + io_loop_stop(current_ioloop); + brain->failed = TRUE; + return; + } + + try_pending = TRUE; + do { + if (!dsync_brain_run(brain, &changed)) { + io_loop_stop(current_ioloop); + break; + } + if (changed) + try_pending = TRUE; + else if (try_pending) { + if (dsync_ibc_has_pending_data(brain->ibc)) + changed = TRUE; + try_pending = FALSE; + } + } while (changed); +} + +static struct dsync_brain * +dsync_brain_common_init(struct mail_user *user, struct dsync_ibc *ibc) +{ + struct dsync_brain *brain; + const struct master_service_settings *service_set; + pool_t pool; + + service_set = master_service_settings_get(master_service); + mail_user_ref(user); + + pool = pool_alloconly_create("dsync brain", 10240); + brain = p_new(pool, struct dsync_brain, 1); + brain->pool = pool; + brain->user = user; + brain->ibc = ibc; + brain->sync_type = DSYNC_BRAIN_SYNC_TYPE_UNKNOWN; + brain->lock_fd = -1; + brain->verbose_proctitle = service_set->verbose_proctitle; + hash_table_create(&brain->mailbox_states, pool, 0, + guid_128_hash, guid_128_cmp); + p_array_init(&brain->remote_mailbox_states, pool, 64); + return brain; +} + +static void +dsync_brain_set_flags(struct dsync_brain *brain, enum dsync_brain_flags flags) +{ + brain->mail_requests = + (flags & DSYNC_BRAIN_FLAG_SEND_MAIL_REQUESTS) != 0; + brain->backup_send = (flags & DSYNC_BRAIN_FLAG_BACKUP_SEND) != 0; + brain->backup_recv = (flags & DSYNC_BRAIN_FLAG_BACKUP_RECV) != 0; + brain->debug = (flags & DSYNC_BRAIN_FLAG_DEBUG) != 0; + brain->sync_visible_namespaces = + (flags & DSYNC_BRAIN_FLAG_SYNC_VISIBLE_NAMESPACES) != 0; + brain->no_mail_sync = (flags & DSYNC_BRAIN_FLAG_NO_MAIL_SYNC) != 0; + brain->no_backup_overwrite = + (flags & DSYNC_BRAIN_FLAG_NO_BACKUP_OVERWRITE) != 0; + brain->no_mail_prefetch = + (flags & DSYNC_BRAIN_FLAG_NO_MAIL_PREFETCH) != 0; + brain->no_notify = (flags & DSYNC_BRAIN_FLAG_NO_NOTIFY) != 0; + brain->empty_hdr_workaround = (flags & DSYNC_BRAIN_FLAG_EMPTY_HDR_WORKAROUND) != 0; +} + +static void +dsync_brain_open_virtual_all_box(struct dsync_brain *brain, + const char *vname) +{ + struct mail_namespace *ns; + + ns = mail_namespace_find(brain->user->namespaces, vname); + brain->virtual_all_box = + mailbox_alloc(ns->list, vname, MAILBOX_FLAG_READONLY); +} + +struct dsync_brain * +dsync_brain_master_init(struct mail_user *user, struct dsync_ibc *ibc, + enum dsync_brain_sync_type sync_type, + enum dsync_brain_flags flags, + const struct dsync_brain_settings *set) +{ + struct dsync_ibc_settings ibc_set; + struct dsync_brain *brain; + struct mail_namespace *ns; + string_t *sync_ns_str = NULL; + const char *error; + + i_assert(sync_type != DSYNC_BRAIN_SYNC_TYPE_UNKNOWN); + i_assert(sync_type != DSYNC_BRAIN_SYNC_TYPE_STATE || + (set->state != NULL && *set->state != '\0')); + i_assert(N_ELEMENTS(dsync_state_names) == DSYNC_STATE_DONE+1); + + brain = dsync_brain_common_init(user, ibc); + brain->process_title_prefix = + p_strdup(brain->pool, set->process_title_prefix); + brain->sync_type = sync_type; + if (array_count(&set->sync_namespaces) > 0) { + sync_ns_str = t_str_new(128); + p_array_init(&brain->sync_namespaces, brain->pool, + array_count(&set->sync_namespaces)); + array_foreach_elem(&set->sync_namespaces, ns) { + str_append(sync_ns_str, ns->prefix); + str_append_c(sync_ns_str, '\n'); + array_push_back(&brain->sync_namespaces, &ns); + } + str_delete(sync_ns_str, str_len(sync_ns_str)-1, 1); + } + brain->alt_char = set->mailbox_alt_char == '\0' ? '_' : + set->mailbox_alt_char; + brain->sync_since_timestamp = set->sync_since_timestamp; + brain->sync_until_timestamp = set->sync_until_timestamp; + brain->sync_max_size = set->sync_max_size; + brain->sync_flag = p_strdup(brain->pool, set->sync_flag); + brain->sync_box = p_strdup(brain->pool, set->sync_box); + brain->exclude_mailboxes = set->exclude_mailboxes == NULL ? NULL : + p_strarray_dup(brain->pool, set->exclude_mailboxes); + memcpy(brain->sync_box_guid, set->sync_box_guid, + sizeof(brain->sync_box_guid)); + brain->lock_timeout = set->lock_timeout_secs; + if (brain->lock_timeout != 0) + brain->mailbox_lock_timeout_secs = brain->lock_timeout; + else + brain->mailbox_lock_timeout_secs = + DSYNC_MAILBOX_DEFAULT_LOCK_TIMEOUT_SECS; + brain->import_commit_msgs_interval = set->import_commit_msgs_interval; + brain->master_brain = TRUE; + brain->hashed_headers = + (const char*const*)p_strarray_dup(brain->pool, set->hashed_headers); + dsync_brain_set_flags(brain, flags); + + if (set->virtual_all_box != NULL) + dsync_brain_open_virtual_all_box(brain, set->virtual_all_box); + + if (sync_type != DSYNC_BRAIN_SYNC_TYPE_STATE) + ; + else if (dsync_mailbox_states_import(brain->mailbox_states, brain->pool, + set->state, &error) < 0) { + hash_table_clear(brain->mailbox_states, FALSE); + i_error("Saved sync state is invalid, " + "falling back to full sync: %s", error); + brain->sync_type = sync_type = DSYNC_BRAIN_SYNC_TYPE_FULL; + } else { + if (brain->debug) { + i_debug("brain %c: Imported mailbox states:", + brain->master_brain ? 'M' : 'S'); + dsync_brain_mailbox_states_dump(brain); + } + } + dsync_brain_mailbox_trees_init(brain); + + i_zero(&ibc_set); + ibc_set.hostname = my_hostdomain(); + ibc_set.sync_ns_prefixes = sync_ns_str == NULL ? + NULL : str_c(sync_ns_str); + ibc_set.sync_box = set->sync_box; + ibc_set.virtual_all_box = set->virtual_all_box; + ibc_set.exclude_mailboxes = set->exclude_mailboxes; + ibc_set.sync_since_timestamp = set->sync_since_timestamp; + ibc_set.sync_until_timestamp = set->sync_until_timestamp; + ibc_set.sync_max_size = set->sync_max_size; + ibc_set.sync_flags = set->sync_flag; + memcpy(ibc_set.sync_box_guid, set->sync_box_guid, + sizeof(ibc_set.sync_box_guid)); + ibc_set.alt_char = brain->alt_char; + ibc_set.sync_type = sync_type; + ibc_set.hdr_hash_v2 = TRUE; + ibc_set.lock_timeout = set->lock_timeout_secs; + ibc_set.import_commit_msgs_interval = set->import_commit_msgs_interval; + ibc_set.hashed_headers = set->hashed_headers; + /* reverse the backup direction for the slave */ + ibc_set.brain_flags = flags & ENUM_NEGATE(DSYNC_BRAIN_FLAG_BACKUP_SEND | + DSYNC_BRAIN_FLAG_BACKUP_RECV); + if ((flags & DSYNC_BRAIN_FLAG_BACKUP_SEND) != 0) + ibc_set.brain_flags |= DSYNC_BRAIN_FLAG_BACKUP_RECV; + else if ((flags & DSYNC_BRAIN_FLAG_BACKUP_RECV) != 0) + ibc_set.brain_flags |= DSYNC_BRAIN_FLAG_BACKUP_SEND; + dsync_ibc_send_handshake(ibc, &ibc_set); + + dsync_ibc_set_io_callback(ibc, dsync_brain_run_io, brain); + brain->state = DSYNC_STATE_MASTER_RECV_HANDSHAKE; + + if (brain->verbose_proctitle) + process_title_set(dsync_brain_get_proctitle(brain)); + return brain; +} + +struct dsync_brain * +dsync_brain_slave_init(struct mail_user *user, struct dsync_ibc *ibc, + bool local, const char *process_title_prefix, + char default_alt_char) +{ + struct dsync_ibc_settings ibc_set; + struct dsync_brain *brain; + + i_assert(default_alt_char != '\0'); + + brain = dsync_brain_common_init(user, ibc); + brain->alt_char = default_alt_char; + brain->process_title_prefix = + p_strdup(brain->pool, process_title_prefix); + brain->state = DSYNC_STATE_SLAVE_RECV_HANDSHAKE; + + if (local) { + /* both master and slave are running within the same process, + update the proctitle only for master. */ + brain->verbose_proctitle = FALSE; + } + + i_zero(&ibc_set); + ibc_set.hdr_hash_v2 = TRUE; + ibc_set.hostname = my_hostdomain(); + dsync_ibc_send_handshake(ibc, &ibc_set); + + if (brain->verbose_proctitle) + process_title_set(dsync_brain_get_proctitle(brain)); + dsync_ibc_set_io_callback(ibc, dsync_brain_run_io, brain); + return brain; +} + +static void dsync_brain_purge(struct dsync_brain *brain) +{ + struct mail_namespace *ns; + struct mail_storage *storage; + + for (ns = brain->user->namespaces; ns != NULL; ns = ns->next) { + if (!dsync_brain_want_namespace(brain, ns)) + continue; + + storage = mail_namespace_get_default_storage(ns); + if (mail_storage_purge(storage) < 0) { + i_error("Purging namespace '%s' failed: %s", ns->prefix, + mail_storage_get_last_internal_error(storage, NULL)); + } + } +} + +int dsync_brain_deinit(struct dsync_brain **_brain, enum mail_error *error_r) +{ + struct dsync_brain *brain = *_brain; + int ret; + + *_brain = NULL; + + if (dsync_ibc_has_timed_out(brain->ibc)) { + i_error("Timeout during state=%s%s", + dsync_state_names[brain->state], + brain->state != DSYNC_STATE_SYNC_MAILS ? "" : + t_strdup_printf(" (send=%s recv=%s)", + dsync_box_state_names[brain->box_send_state], + dsync_box_state_names[brain->box_recv_state])); + } + if (dsync_ibc_has_failed(brain->ibc) || + brain->state != DSYNC_STATE_DONE) + brain->failed = TRUE; + dsync_ibc_close_mail_streams(brain->ibc); + + if (brain->purge && !brain->failed) + dsync_brain_purge(brain); + + if (brain->box != NULL) + dsync_brain_sync_mailbox_deinit(brain); + if (brain->virtual_all_box != NULL) + mailbox_free(&brain->virtual_all_box); + if (brain->local_tree_iter != NULL) + dsync_mailbox_tree_iter_deinit(&brain->local_tree_iter); + if (brain->local_mailbox_tree != NULL) + dsync_mailbox_tree_deinit(&brain->local_mailbox_tree); + if (brain->remote_mailbox_tree != NULL) + dsync_mailbox_tree_deinit(&brain->remote_mailbox_tree); + hash_table_iterate_deinit(&brain->mailbox_states_iter); + hash_table_destroy(&brain->mailbox_states); + + pool_unref(&brain->dsync_box_pool); + + if (brain->lock_fd != -1) { + /* unlink the lock file before it gets unlocked */ + i_unlink(brain->lock_path); + if (brain->debug) { + i_debug("brain %c: Unlocked %s", + brain->master_brain ? 'M' : 'S', + brain->lock_path); + } + file_lock_free(&brain->lock); + i_close_fd(&brain->lock_fd); + } + + ret = brain->failed ? -1 : 0; + mail_user_unref(&brain->user); + + *error_r = !brain->failed ? 0 : + (brain->mail_error == 0 ? MAIL_ERROR_TEMP : brain->mail_error); + pool_unref(&brain->pool); + return ret; +} + +static int +dsync_brain_lock(struct dsync_brain *brain, const char *remote_hostname) +{ + const struct file_create_settings lock_set = { + .lock_timeout_secs = brain->lock_timeout, + .lock_settings = { + .lock_method = FILE_LOCK_METHOD_FCNTL, + }, + }; + const char *home, *error, *local_hostname = my_hostdomain(); + bool created; + int ret; + + if ((ret = strcmp(remote_hostname, local_hostname)) < 0) { + /* locking done by remote */ + if (brain->debug) { + i_debug("brain %c: Locking done by remote " + "(local hostname=%s, remote hostname=%s)", + brain->master_brain ? 'M' : 'S', + local_hostname, remote_hostname); + } + return 0; + } + if (ret == 0 && !brain->master_brain) { + /* running dsync within the same server. + locking done by master brain. */ + if (brain->debug) { + i_debug("brain %c: Locking done by local master-brain " + "(local hostname=%s, remote hostname=%s)", + brain->master_brain ? 'M' : 'S', + local_hostname, remote_hostname); + } + return 0; + } + + if ((ret = mail_user_get_home(brain->user, &home)) < 0) { + i_error("Couldn't look up user's home dir"); + return -1; + } + if (ret == 0) { + i_error("User has no home directory"); + return -1; + } + + if (brain->verbose_proctitle) + process_title_set(dsync_brain_get_proctitle_full(brain, DSYNC_BRAIN_TITLE_LOCKING)); + brain->lock_path = p_strconcat(brain->pool, home, + "/"DSYNC_LOCK_FILENAME, NULL); + brain->lock_fd = file_create_locked(brain->lock_path, &lock_set, + &brain->lock, &created, &error); + if (brain->lock_fd == -1 && errno == ENOENT) { + /* home directory not created */ + if (mail_user_home_mkdir(brain->user) < 0) + return -1; + brain->lock_fd = file_create_locked(brain->lock_path, &lock_set, + &brain->lock, &created, &error); + } + if (brain->lock_fd == -1) + i_error("Couldn't lock %s: %s", brain->lock_path, error); + else if (brain->debug) { + i_debug("brain %c: Locking done locally in %s " + "(local hostname=%s, remote hostname=%s)", + brain->master_brain ? 'M' : 'S', + brain->lock_path, local_hostname, remote_hostname); + } + if (brain->verbose_proctitle) + process_title_set(dsync_brain_get_proctitle(brain)); + return brain->lock_fd == -1 ? -1 : 0; +} + +static void +dsync_brain_set_hdr_hash_version(struct dsync_brain *brain, + const struct dsync_ibc_settings *ibc_set) +{ + if (ibc_set->hdr_hash_v3) + brain->hdr_hash_version = 3; + else if (ibc_set->hdr_hash_v2) + brain->hdr_hash_version = 3; + else + brain->hdr_hash_version = 1; +} + +static bool dsync_brain_master_recv_handshake(struct dsync_brain *brain) +{ + const struct dsync_ibc_settings *ibc_set; + + i_assert(brain->master_brain); + + if (dsync_ibc_recv_handshake(brain->ibc, &ibc_set) == 0) + return FALSE; + + if (brain->lock_timeout > 0) { + if (dsync_brain_lock(brain, ibc_set->hostname) < 0) { + brain->failed = TRUE; + return FALSE; + } + } + dsync_brain_set_hdr_hash_version(brain, ibc_set); + + brain->state = brain->sync_type == DSYNC_BRAIN_SYNC_TYPE_STATE ? + DSYNC_STATE_MASTER_SEND_LAST_COMMON : + DSYNC_STATE_SEND_MAILBOX_TREE; + return TRUE; +} + +static bool dsync_brain_slave_recv_handshake(struct dsync_brain *brain) +{ + const struct dsync_ibc_settings *ibc_set; + struct mail_namespace *ns; + const char *const *prefixes; + + i_assert(!brain->master_brain); + + if (dsync_ibc_recv_handshake(brain->ibc, &ibc_set) == 0) + return FALSE; + dsync_brain_set_hdr_hash_version(brain, ibc_set); + + if (ibc_set->lock_timeout > 0) { + brain->lock_timeout = ibc_set->lock_timeout; + brain->mailbox_lock_timeout_secs = brain->lock_timeout; + if (dsync_brain_lock(brain, ibc_set->hostname) < 0) { + brain->failed = TRUE; + return FALSE; + } + } else { + brain->mailbox_lock_timeout_secs = + DSYNC_MAILBOX_DEFAULT_LOCK_TIMEOUT_SECS; + } + + if (ibc_set->sync_ns_prefixes != NULL) { + p_array_init(&brain->sync_namespaces, brain->pool, 4); + prefixes = t_strsplit(ibc_set->sync_ns_prefixes, "\n"); + if (prefixes[0] == NULL) { + /* ugly workaround for strsplit API: there was one + prefix="" entry */ + static const char *empty_prefix[] = { "", NULL }; + prefixes = empty_prefix; + } + for (; *prefixes != NULL; prefixes++) { + ns = mail_namespace_find(brain->user->namespaces, + *prefixes); + if (ns == NULL) { + i_error("Namespace not found: '%s'", *prefixes); + brain->failed = TRUE; + return FALSE; + } + array_push_back(&brain->sync_namespaces, &ns); + } + } + brain->sync_box = p_strdup(brain->pool, ibc_set->sync_box); + brain->exclude_mailboxes = ibc_set->exclude_mailboxes == NULL ? NULL : + p_strarray_dup(brain->pool, ibc_set->exclude_mailboxes); + brain->sync_since_timestamp = ibc_set->sync_since_timestamp; + brain->sync_until_timestamp = ibc_set->sync_until_timestamp; + brain->sync_max_size = ibc_set->sync_max_size; + brain->sync_flag = p_strdup(brain->pool, ibc_set->sync_flags); + memcpy(brain->sync_box_guid, ibc_set->sync_box_guid, + sizeof(brain->sync_box_guid)); + if (ibc_set->alt_char != '\0') + brain->alt_char = ibc_set->alt_char; + i_assert(brain->sync_type == DSYNC_BRAIN_SYNC_TYPE_UNKNOWN); + brain->sync_type = ibc_set->sync_type; + + dsync_brain_set_flags(brain, ibc_set->brain_flags); + if (ibc_set->hashed_headers != NULL) + brain->hashed_headers = + p_strarray_dup(brain->pool, (const char*const*)ibc_set->hashed_headers); + /* this flag is only set on the remote slave brain */ + brain->purge = (ibc_set->brain_flags & + DSYNC_BRAIN_FLAG_PURGE_REMOTE) != 0; + + if (ibc_set->virtual_all_box != NULL) + dsync_brain_open_virtual_all_box(brain, ibc_set->virtual_all_box); + dsync_brain_mailbox_trees_init(brain); + + if (brain->sync_type == DSYNC_BRAIN_SYNC_TYPE_STATE) + brain->state = DSYNC_STATE_SLAVE_RECV_LAST_COMMON; + else + brain->state = DSYNC_STATE_SEND_MAILBOX_TREE; + return TRUE; +} + +static void dsync_brain_master_send_last_common(struct dsync_brain *brain) +{ + struct dsync_mailbox_state *state; + uint8_t *guid; + enum dsync_ibc_send_ret ret = DSYNC_IBC_SEND_RET_OK; + + i_assert(brain->master_brain); + + if (brain->mailbox_states_iter == NULL) { + brain->mailbox_states_iter = + hash_table_iterate_init(brain->mailbox_states); + } + + for (;;) { + if (ret == DSYNC_IBC_SEND_RET_FULL) + return; + if (!hash_table_iterate(brain->mailbox_states_iter, + brain->mailbox_states, &guid, &state)) + break; + ret = dsync_ibc_send_mailbox_state(brain->ibc, state); + } + hash_table_iterate_deinit(&brain->mailbox_states_iter); + + dsync_ibc_send_end_of_list(brain->ibc, DSYNC_IBC_EOL_MAILBOX_STATE); + brain->state = DSYNC_STATE_SEND_MAILBOX_TREE; +} + +static void dsync_mailbox_state_add(struct dsync_brain *brain, + const struct dsync_mailbox_state *state) +{ + struct dsync_mailbox_state *dupstate; + uint8_t *guid_p; + + dupstate = p_new(brain->pool, struct dsync_mailbox_state, 1); + *dupstate = *state; + guid_p = dupstate->mailbox_guid; + hash_table_insert(brain->mailbox_states, guid_p, dupstate); +} + +static bool dsync_brain_slave_recv_last_common(struct dsync_brain *brain) +{ + struct dsync_mailbox_state state; + enum dsync_ibc_recv_ret ret; + bool changed = FALSE; + + i_assert(!brain->master_brain); + + while ((ret = dsync_ibc_recv_mailbox_state(brain->ibc, &state)) > 0) { + dsync_mailbox_state_add(brain, &state); + changed = TRUE; + } + if (ret == DSYNC_IBC_RECV_RET_FINISHED) { + brain->state = DSYNC_STATE_SEND_MAILBOX_TREE; + changed = TRUE; + } + return changed; +} + +static bool dsync_brain_finish(struct dsync_brain *brain) +{ + const char *error; + enum mail_error mail_error; + bool require_full_resync; + enum dsync_ibc_recv_ret ret; + + if (!brain->master_brain) { + dsync_ibc_send_finish(brain->ibc, + brain->failed ? "dsync failed" : NULL, + brain->mail_error, + brain->require_full_resync); + brain->state = DSYNC_STATE_DONE; + return TRUE; + } + ret = dsync_ibc_recv_finish(brain->ibc, &error, &mail_error, + &require_full_resync); + if (ret == DSYNC_IBC_RECV_RET_TRYAGAIN) + return FALSE; + if (error != NULL) { + i_error("Remote dsync failed: %s", error); + brain->failed = TRUE; + if (mail_error != 0 && + (brain->mail_error == 0 || brain->mail_error == MAIL_ERROR_TEMP)) + brain->mail_error = mail_error; + } + if (require_full_resync) + brain->require_full_resync = TRUE; + brain->state = DSYNC_STATE_DONE; + return TRUE; +} + +static bool dsync_brain_run_real(struct dsync_brain *brain, bool *changed_r) +{ + enum dsync_state orig_state = brain->state; + enum dsync_box_state orig_box_recv_state = brain->box_recv_state; + enum dsync_box_state orig_box_send_state = brain->box_send_state; + bool changed = FALSE, ret = TRUE; + + if (brain->failed) + return FALSE; + + switch (brain->state) { + case DSYNC_STATE_MASTER_RECV_HANDSHAKE: + changed = dsync_brain_master_recv_handshake(brain); + break; + case DSYNC_STATE_SLAVE_RECV_HANDSHAKE: + changed = dsync_brain_slave_recv_handshake(brain); + break; + case DSYNC_STATE_MASTER_SEND_LAST_COMMON: + dsync_brain_master_send_last_common(brain); + changed = TRUE; + break; + case DSYNC_STATE_SLAVE_RECV_LAST_COMMON: + changed = dsync_brain_slave_recv_last_common(brain); + break; + case DSYNC_STATE_SEND_MAILBOX_TREE: + dsync_brain_send_mailbox_tree(brain); + changed = TRUE; + break; + case DSYNC_STATE_RECV_MAILBOX_TREE: + changed = dsync_brain_recv_mailbox_tree(brain); + break; + case DSYNC_STATE_SEND_MAILBOX_TREE_DELETES: + dsync_brain_send_mailbox_tree_deletes(brain); + changed = TRUE; + break; + case DSYNC_STATE_RECV_MAILBOX_TREE_DELETES: + changed = dsync_brain_recv_mailbox_tree_deletes(brain); + break; + case DSYNC_STATE_MASTER_SEND_MAILBOX: + dsync_brain_master_send_mailbox(brain); + changed = TRUE; + break; + case DSYNC_STATE_SLAVE_RECV_MAILBOX: + changed = dsync_brain_slave_recv_mailbox(brain); + break; + case DSYNC_STATE_SYNC_MAILS: + changed = dsync_brain_sync_mails(brain); + break; + case DSYNC_STATE_FINISH: + changed = dsync_brain_finish(brain); + break; + case DSYNC_STATE_DONE: + changed = TRUE; + ret = FALSE; + break; + } + if (brain->verbose_proctitle) { + if (orig_state != brain->state || + orig_box_recv_state != brain->box_recv_state || + orig_box_send_state != brain->box_send_state || + ++brain->proctitle_update_counter % 100 == 0) + process_title_set(dsync_brain_get_proctitle(brain)); + } + *changed_r = changed; + return brain->failed ? FALSE : ret; +} + +bool dsync_brain_run(struct dsync_brain *brain, bool *changed_r) +{ + bool ret; + + *changed_r = FALSE; + + if (dsync_ibc_has_failed(brain->ibc)) { + brain->failed = TRUE; + return FALSE; + } + + T_BEGIN { + ret = dsync_brain_run_real(brain, changed_r); + } T_END; + return ret; +} + +static void dsync_brain_mailbox_states_dump(struct dsync_brain *brain) +{ + struct hash_iterate_context *iter; + struct dsync_mailbox_state *state; + uint8_t *guid; + + iter = hash_table_iterate_init(brain->mailbox_states); + while (hash_table_iterate(iter, brain->mailbox_states, &guid, &state)) { + i_debug("brain %c: Mailbox %s state: uidvalidity=%u uid=%u modseq=%"PRIu64" pvt_modseq=%"PRIu64" messages=%u changes_during_sync=%d", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(guid), + state->last_uidvalidity, + state->last_common_uid, + state->last_common_modseq, + state->last_common_pvt_modseq, + state->last_messages_count, + state->changes_during_sync ? 1 : 0); + } + hash_table_iterate_deinit(&iter); +} + +void dsync_brain_get_state(struct dsync_brain *brain, string_t *output) +{ + struct hash_iterate_context *iter; + struct dsync_mailbox_node *node; + const struct dsync_mailbox_state *new_state; + struct dsync_mailbox_state *state; + const uint8_t *guid_p; + uint8_t *guid; + + if (brain->require_full_resync) + return; + + /* update mailbox states */ + array_foreach(&brain->remote_mailbox_states, new_state) { + guid_p = new_state->mailbox_guid; + state = hash_table_lookup(brain->mailbox_states, guid_p); + if (state != NULL) + *state = *new_state; + else + dsync_mailbox_state_add(brain, new_state); + } + + /* remove nonexistent mailboxes */ + iter = hash_table_iterate_init(brain->mailbox_states); + while (hash_table_iterate(iter, brain->mailbox_states, &guid, &state)) { + node = dsync_mailbox_tree_lookup_guid(brain->local_mailbox_tree, + guid); + if (node == NULL || + node->existence != DSYNC_MAILBOX_NODE_EXISTS) { + if (brain->debug) { + i_debug("brain %c: Removed state for deleted mailbox %s", + brain->master_brain ? 'M' : 'S', + guid_128_to_string(guid)); + } + hash_table_remove(brain->mailbox_states, guid); + } + } + hash_table_iterate_deinit(&iter); + + if (brain->debug) { + i_debug("brain %c: Exported mailbox states:", + brain->master_brain ? 'M' : 'S'); + dsync_brain_mailbox_states_dump(brain); + } + dsync_mailbox_states_export(brain->mailbox_states, output); +} + +enum dsync_brain_sync_type dsync_brain_get_sync_type(struct dsync_brain *brain) +{ + return brain->sync_type; +} + +bool dsync_brain_has_failed(struct dsync_brain *brain) +{ + return brain->failed; +} + +const char *dsync_brain_get_unexpected_changes_reason(struct dsync_brain *brain, + bool *remote_only_r) +{ + if (brain->changes_during_sync == NULL && + brain->changes_during_remote_sync) { + *remote_only_r = TRUE; + return "Remote notified that changes happened during sync"; + } + *remote_only_r = FALSE; + return brain->changes_during_sync; +} + +static bool dsync_brain_want_shared_namespace(const struct mail_namespace *ns, + const struct mail_namespace *sync_ns) +{ + /* Include shared namespaces and all its + children in the sync (e.g. "Shared/example.com" + will be synced to "Shared/"). + This also allows "dsync -n Shared/example.com/" + with "Shared/example.com/username/" style + shared namespace config. */ + return (ns->type == MAIL_NAMESPACE_TYPE_SHARED) && + (sync_ns->type == MAIL_NAMESPACE_TYPE_SHARED) && + str_begins(ns->prefix, sync_ns->prefix); +} + +bool dsync_brain_want_namespace(struct dsync_brain *brain, + struct mail_namespace *ns) +{ + struct mail_namespace *sync_ns; + + if (array_is_created(&brain->sync_namespaces)) { + array_foreach_elem(&brain->sync_namespaces, sync_ns) { + if (ns == sync_ns) + return TRUE; + if (dsync_brain_want_shared_namespace(ns, sync_ns)) + return TRUE; + } + return FALSE; + } + if (ns->alias_for != NULL) { + /* always skip aliases */ + return FALSE; + } + if (brain->sync_visible_namespaces) { + if ((ns->flags & NAMESPACE_FLAG_HIDDEN) == 0) + return TRUE; + if ((ns->flags & (NAMESPACE_FLAG_LIST_PREFIX | + NAMESPACE_FLAG_LIST_CHILDREN)) != 0) + return TRUE; + return FALSE; + } else { + return strcmp(ns->unexpanded_set->location, + SETTING_STRVAR_UNEXPANDED) == 0; + } +} + +void dsync_brain_set_changes_during_sync(struct dsync_brain *brain, + const char *reason) +{ + if (brain->debug) { + i_debug("brain %c: Change during sync: %s", + brain->master_brain ? 'M' : 'S', reason); + } + if (brain->changes_during_sync == NULL) + brain->changes_during_sync = p_strdup(brain->pool, reason); +} diff --git a/src/doveadm/dsync/dsync-brain.h b/src/doveadm/dsync/dsync-brain.h new file mode 100644 index 0000000..5813148 --- /dev/null +++ b/src/doveadm/dsync/dsync-brain.h @@ -0,0 +1,130 @@ +#ifndef DSYNC_BRAIN_H +#define DSYNC_BRAIN_H + +#include "module-context.h" +#include "guid.h" +#include "mail-error.h" +#include "mailbox-list-private.h" + +struct mail_namespace; +struct mail_user; +struct dsync_ibc; + +enum dsync_brain_flags { + DSYNC_BRAIN_FLAG_SEND_MAIL_REQUESTS = 0x01, + DSYNC_BRAIN_FLAG_BACKUP_SEND = 0x02, + DSYNC_BRAIN_FLAG_BACKUP_RECV = 0x04, + DSYNC_BRAIN_FLAG_DEBUG = 0x08, + DSYNC_BRAIN_FLAG_SYNC_VISIBLE_NAMESPACES= 0x10, + /* Sync everything but the actual mails (e.g. mailbox creates, + deletes) */ + DSYNC_BRAIN_FLAG_NO_MAIL_SYNC = 0x20, + /* Used with BACKUP_SEND/RECV: Don't force the + Use the two-way syncing algorithm, but don't actually modify + anything locally. (Useful during migration.) */ + DSYNC_BRAIN_FLAG_NO_BACKUP_OVERWRITE = 0x40, + /* Run storage purge on the remote after syncing. + Useful with e.g. a nightly doveadm backup. */ + DSYNC_BRAIN_FLAG_PURGE_REMOTE = 0x80, + /* Don't prefetch mail bodies until they're actually needed. This works + only with pipe ibc. It's useful if most of the mails can be copied + directly within filesystem without having to read them. */ + DSYNC_BRAIN_FLAG_NO_MAIL_PREFETCH = 0x100, + /* Add MAILBOX_TRANSACTION_FLAG_NO_NOTIFY to transactions. */ + DSYNC_BRAIN_FLAG_NO_NOTIFY = 0x400, + /* Workaround missing Date/Message-ID headers */ + DSYNC_BRAIN_FLAG_EMPTY_HDR_WORKAROUND = 0x800, +}; + +enum dsync_brain_sync_type { + DSYNC_BRAIN_SYNC_TYPE_UNKNOWN, + /* Go through all mailboxes to make sure everything is synced */ + DSYNC_BRAIN_SYNC_TYPE_FULL, + /* Go through all mailboxes that have changed (based on UIDVALIDITY, + UIDNEXT, HIGHESTMODSEQ). If both sides have had equal amount of + changes in some mailbox, it may get incorrectly skipped. */ + DSYNC_BRAIN_SYNC_TYPE_CHANGED, + /* Use saved state to find out what has changed. */ + DSYNC_BRAIN_SYNC_TYPE_STATE +}; + +struct dsync_brain_settings { + const char *process_title_prefix; + /* Sync only these namespaces */ + ARRAY(struct mail_namespace *) sync_namespaces; + /* Sync only this mailbox name */ + const char *sync_box; + /* Use this virtual \All mailbox to be able to copy mails with the same + GUID instead of saving them twice. With most storages this results + in less disk space usage. */ + const char *virtual_all_box; + /* Sync only this mailbox GUID */ + guid_128_t sync_box_guid; + /* Exclude these mailboxes from the sync. They can contain '*' + wildcards and be \special-use flags. */ + const char *const *exclude_mailboxes; + /* Alternative character to use in mailbox names where the original + character cannot be used. */ + char mailbox_alt_char; + /* Sync only mails with received timestamp at least this high. */ + time_t sync_since_timestamp; + /* Sync only mails with received timestamp less or equal than this */ + time_t sync_until_timestamp; + /* Don't sync mails larger than this. */ + uoff_t sync_max_size; + /* Sync only mails which contains / doesn't contain this flag. + '-' at the beginning means this flag must not exist. */ + const char *sync_flag; + /* Headers to hash (defaults to Date, Message-ID) */ + const char *const *hashed_headers; + + /* If non-zero, use dsync lock file for this user */ + unsigned int lock_timeout_secs; + /* If non-zero, importing will attempt to commit transaction after + saving this many messages. */ + unsigned int import_commit_msgs_interval; + /* Input state for DSYNC_BRAIN_SYNC_TYPE_STATE */ + const char *state; +}; + +#define DSYNC_LIST_CONTEXT(obj) \ + MODULE_CONTEXT(obj, dsync_mailbox_list_module) +struct dsync_mailbox_list { + union mailbox_list_module_context module_ctx; + bool have_orig_escape_char; +}; +extern MODULE_CONTEXT_DEFINE(dsync_mailbox_list_module, + &mailbox_list_module_register); + +struct dsync_brain * +dsync_brain_master_init(struct mail_user *user, struct dsync_ibc *ibc, + enum dsync_brain_sync_type sync_type, + enum dsync_brain_flags flags, + const struct dsync_brain_settings *set); +struct dsync_brain * +dsync_brain_slave_init(struct mail_user *user, struct dsync_ibc *ibc, + bool local, const char *process_title_prefix, + char default_alt_char); +/* Returns 0 if everything was successful, -1 if syncing failed in some way */ +int dsync_brain_deinit(struct dsync_brain **brain, enum mail_error *error_r); + +/* Returns TRUE if brain needs to run more, FALSE if it's finished. + changed_r is TRUE if anything happened during this run. */ +bool dsync_brain_run(struct dsync_brain *brain, bool *changed_r); +/* Returns TRUE if brain has failed, and there's no point in continuing. */ +bool dsync_brain_has_failed(struct dsync_brain *brain); +/* Returns the current sync state string, which can be given as parameter to + dsync_brain_master_init() to quickly sync only the new changes. */ +void dsync_brain_get_state(struct dsync_brain *brain, string_t *output); +/* Returns the sync type that was used. Mainly useful with slave brain. */ +enum dsync_brain_sync_type dsync_brain_get_sync_type(struct dsync_brain *brain); +/* If there were any unexpected changes during the sync, return the reason + for them. Otherwise return NULL. If remote_only_r=TRUE, this brain itself + didn't see any changes, but the remote brain did. */ +const char *dsync_brain_get_unexpected_changes_reason(struct dsync_brain *brain, + bool *remote_only_r); +/* Returns TRUE if we want to sync this namespace. */ +bool dsync_brain_want_namespace(struct dsync_brain *brain, + struct mail_namespace *ns); + +#endif diff --git a/src/doveadm/dsync/dsync-deserializer.c b/src/doveadm/dsync/dsync-deserializer.c new file mode 100644 index 0000000..2a90c47 --- /dev/null +++ b/src/doveadm/dsync/dsync-deserializer.c @@ -0,0 +1,193 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "strescape.h" +#include "dsync-serializer.h" +#include "dsync-deserializer.h" + +struct dsync_deserializer { + pool_t pool; + const char *name; + const char *const *required_fields; + const char *const *keys; + unsigned int *required_field_indexes; + unsigned int required_field_count; +}; + +struct dsync_deserializer_decoder { + pool_t pool; + struct dsync_deserializer *deserializer; + const char *const *values; + unsigned int values_count; +}; + +static bool field_find(const char *const *names, const char *name, + unsigned int *idx_r) +{ + unsigned int i; + + for (i = 0; names[i] != NULL; i++) { + if (strcmp(names[i], name) == 0) { + *idx_r = i; + return TRUE; + } + } + return FALSE; +} + +int dsync_deserializer_init(const char *name, const char *const *required_fields, + const char *header_line, + struct dsync_deserializer **deserializer_r, + const char **error_r) +{ + struct dsync_deserializer *deserializer; + const char **dup_required_fields; + unsigned int i, required_count; + pool_t pool; + + *deserializer_r = NULL; + + pool = pool_alloconly_create("dsync deserializer", 1024); + deserializer = p_new(pool, struct dsync_deserializer, 1); + deserializer->pool = pool; + deserializer->name = p_strdup(pool, name); + deserializer->keys = (void *)p_strsplit_tabescaped(pool, header_line); + + deserializer->required_field_count = required_count = + required_fields == NULL ? 0 : + str_array_length(required_fields); + dup_required_fields = p_new(pool, const char *, required_count + 1); + deserializer->required_field_indexes = + p_new(pool, unsigned int, required_count + 1); + for (i = 0; i < required_count; i++) { + dup_required_fields[i] = + p_strdup(pool, required_fields[i]); + if (!field_find(deserializer->keys, required_fields[i], + &deserializer->required_field_indexes[i])) { + *error_r = t_strdup_printf( + "Header missing required field %s", + required_fields[i]); + pool_unref(&pool); + return -1; + } + } + deserializer->required_fields = dup_required_fields; + + *deserializer_r = deserializer; + return 0; +} + +void dsync_deserializer_deinit(struct dsync_deserializer **_deserializer) +{ + struct dsync_deserializer *deserializer = *_deserializer; + + *_deserializer = NULL; + + pool_unref(&deserializer->pool); +} + +int dsync_deserializer_decode_begin(struct dsync_deserializer *deserializer, + const char *input, + struct dsync_deserializer_decoder **decoder_r, + const char **error_r) +{ + struct dsync_deserializer_decoder *decoder; + unsigned int i; + char **values; + pool_t pool; + + *decoder_r = NULL; + + pool = pool_alloconly_create("dsync deserializer decode", 1024); + decoder = p_new(pool, struct dsync_deserializer_decoder, 1); + decoder->pool = pool; + decoder->deserializer = deserializer; + values = p_strsplit_tabescaped(pool, input); + + /* fix NULLs */ + for (i = 0; values[i] != NULL; i++) { + if (values[i][0] == NULL_CHR) { + /* NULL? */ + if (values[i][1] == '\0') + values[i] = NULL; + else + values[i] += 1; + } + } + decoder->values_count = i; + + /* see if all required fields exist */ + for (i = 0; i < deserializer->required_field_count; i++) { + unsigned int ridx = deserializer->required_field_indexes[i]; + + if (ridx >= decoder->values_count || values[ridx] == NULL) { + *error_r = t_strdup_printf("Missing required field %s", + deserializer->required_fields[i]); + pool_unref(&pool); + return -1; + } + } + decoder->values = (void *)values; + + *decoder_r = decoder; + return 0; +} + +static bool +dsync_deserializer_find_field(struct dsync_deserializer *deserializer, + const char *key, unsigned int *idx_r) +{ + unsigned int i; + + for (i = 0; deserializer->keys[i] != NULL; i++) { + if (strcmp(deserializer->keys[i], key) == 0) { + *idx_r = i; + return TRUE; + } + } + return FALSE; +} + +bool dsync_deserializer_decode_try(struct dsync_deserializer_decoder *decoder, + const char *key, const char **value_r) +{ + unsigned int idx; + + if (!dsync_deserializer_find_field(decoder->deserializer, key, &idx) || + idx >= decoder->values_count) { + *value_r = NULL; + return FALSE; + } else { + *value_r = decoder->values[idx]; + return *value_r != NULL; + } +} + +const char * +dsync_deserializer_decode_get(struct dsync_deserializer_decoder *decoder, + const char *key) +{ + const char *value; + + if (!dsync_deserializer_decode_try(decoder, key, &value)) { + i_panic("dsync_deserializer_decode_get() " + "used for non-required key %s", key); + } + return value; +} + +const char * +dsync_deserializer_decoder_get_name(struct dsync_deserializer_decoder *decoder) +{ + return decoder->deserializer->name; +} + +void dsync_deserializer_decode_finish(struct dsync_deserializer_decoder **_decoder) +{ + struct dsync_deserializer_decoder *decoder = *_decoder; + + *_decoder = NULL; + + pool_unref(&decoder->pool); +} diff --git a/src/doveadm/dsync/dsync-deserializer.h b/src/doveadm/dsync/dsync-deserializer.h new file mode 100644 index 0000000..45b19cc --- /dev/null +++ b/src/doveadm/dsync/dsync-deserializer.h @@ -0,0 +1,27 @@ +#ifndef DSYNC_DESERIALIZER_H +#define DSYNC_DESERIALIZER_H + +struct dsync_deserializer; +struct dsync_deserializer_decoder; + +int dsync_deserializer_init(const char *name, const char *const *required_fields, + const char *header_line, + struct dsync_deserializer **deserializer_r, + const char **error_r); +void dsync_deserializer_deinit(struct dsync_deserializer **deserializer); + +int dsync_deserializer_decode_begin(struct dsync_deserializer *deserializer, + const char *input, + struct dsync_deserializer_decoder **decoder_r, + const char **error_r); +bool dsync_deserializer_decode_try(struct dsync_deserializer_decoder *decoder, + const char *key, const char **value_r); +/* key must be in required fields. The return value is never NULL. */ +const char * +dsync_deserializer_decode_get(struct dsync_deserializer_decoder *decoder, + const char *key); +const char * +dsync_deserializer_decoder_get_name(struct dsync_deserializer_decoder *decoder); +void dsync_deserializer_decode_finish(struct dsync_deserializer_decoder **decoder); + +#endif diff --git a/src/doveadm/dsync/dsync-ibc-pipe.c b/src/doveadm/dsync/dsync-ibc-pipe.c new file mode 100644 index 0000000..1b8886e --- /dev/null +++ b/src/doveadm/dsync/dsync-ibc-pipe.c @@ -0,0 +1,599 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "istream.h" +#include "dsync-mail.h" +#include "dsync-mailbox.h" +#include "dsync-mailbox-state.h" +#include "dsync-mailbox-tree.h" +#include "dsync-ibc-private.h" + +enum item_type { + ITEM_END_OF_LIST, + ITEM_HANDSHAKE, + ITEM_MAILBOX_STATE, + ITEM_MAILBOX_TREE_NODE, + ITEM_MAILBOX_DELETE, + ITEM_MAILBOX, + ITEM_MAILBOX_ATTRIBUTE, + ITEM_MAIL_CHANGE, + ITEM_MAIL_REQUEST, + ITEM_MAIL, + ITEM_FINISH +}; + +struct item { + enum item_type type; + pool_t pool; + + union { + struct dsync_ibc_settings set; + struct dsync_mailbox_state state; + struct dsync_mailbox_node node; + guid_128_t mailbox_guid; + struct dsync_mailbox dsync_box; + struct dsync_mailbox_attribute attr; + struct dsync_mail_change change; + struct dsync_mail_request request; + struct dsync_mail mail; + struct { + const struct dsync_mailbox_delete *deletes; + unsigned int count; + char hierarchy_sep; + char escape_char; + } mailbox_delete; + struct { + const char *error; + enum mail_error mail_error; + bool require_full_resync; + } finish; + } u; +}; + +struct dsync_ibc_pipe { + struct dsync_ibc ibc; + + ARRAY(pool_t) pools; + ARRAY(struct item) item_queue; + struct dsync_ibc_pipe *remote; + + pool_t pop_pool; + struct item pop_item; +}; + +static pool_t dsync_ibc_pipe_get_pool(struct dsync_ibc_pipe *pipe) +{ + pool_t *pools, ret; + unsigned int count; + + pools = array_get_modifiable(&pipe->pools, &count); + if (count == 0) + return pool_alloconly_create(MEMPOOL_GROWING"pipe item pool", 1024); + + ret = pools[count-1]; + array_delete(&pipe->pools, count-1, 1); + p_clear(ret); + return ret; +} + +static struct item * ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_pipe_push_item(struct dsync_ibc_pipe *pipe, enum item_type type) +{ + struct item *item; + + item = array_append_space(&pipe->item_queue); + item->type = type; + + switch (type) { + case ITEM_END_OF_LIST: + case ITEM_MAILBOX_STATE: + case ITEM_MAILBOX_DELETE: + break; + case ITEM_HANDSHAKE: + case ITEM_MAILBOX: + case ITEM_MAILBOX_TREE_NODE: + case ITEM_MAILBOX_ATTRIBUTE: + case ITEM_MAIL_CHANGE: + case ITEM_MAIL_REQUEST: + case ITEM_MAIL: + case ITEM_FINISH: + item->pool = dsync_ibc_pipe_get_pool(pipe); + break; + } + return item; +} + +static struct item * +dsync_ibc_pipe_pop_item(struct dsync_ibc_pipe *pipe, enum item_type type) +{ + struct item *item; + + if (array_count(&pipe->item_queue) == 0) + return NULL; + + item = array_front_modifiable(&pipe->item_queue); + i_assert(item->type == type); + pipe->pop_item = *item; + array_pop_front(&pipe->item_queue); + item = NULL; + + pool_unref(&pipe->pop_pool); + pipe->pop_pool = pipe->pop_item.pool; + return &pipe->pop_item; +} + +static bool dsync_ibc_pipe_try_pop_eol(struct dsync_ibc_pipe *pipe) +{ + const struct item *item; + + if (array_count(&pipe->item_queue) == 0) + return FALSE; + + item = array_front(&pipe->item_queue); + if (item->type != ITEM_END_OF_LIST) + return FALSE; + + array_pop_front(&pipe->item_queue); + return TRUE; +} + +static void dsync_ibc_pipe_deinit(struct dsync_ibc *ibc) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + pool_t pool; + + if (pipe->remote != NULL) { + i_assert(pipe->remote->remote == pipe); + pipe->remote->remote = NULL; + } + + pool_unref(&pipe->pop_pool); + array_foreach_modifiable(&pipe->item_queue, item) { + pool_unref(&item->pool); + } + array_foreach_elem(&pipe->pools, pool) + pool_unref(&pool); + array_free(&pipe->pools); + array_free(&pipe->item_queue); + i_free(pipe); +} + +static void +dsync_ibc_pipe_send_handshake(struct dsync_ibc *ibc, + const struct dsync_ibc_settings *set) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_HANDSHAKE); + item->u.set = *set; + item->u.set.sync_ns_prefixes = + p_strdup(item->pool, set->sync_ns_prefixes); + item->u.set.sync_box = p_strdup(item->pool, set->sync_box); + item->u.set.virtual_all_box = p_strdup(item->pool, set->virtual_all_box); + item->u.set.exclude_mailboxes = set->exclude_mailboxes == NULL ? NULL : + p_strarray_dup(item->pool, set->exclude_mailboxes); + memcpy(item->u.set.sync_box_guid, set->sync_box_guid, + sizeof(item->u.set.sync_box_guid)); + item->u.set.sync_since_timestamp = set->sync_since_timestamp; + item->u.set.sync_until_timestamp = set->sync_until_timestamp; + item->u.set.sync_max_size = set->sync_max_size; + item->u.set.sync_flags = p_strdup(item->pool, set->sync_flags); +} + +static enum dsync_ibc_recv_ret +dsync_ibc_pipe_recv_handshake(struct dsync_ibc *ibc, + const struct dsync_ibc_settings **set_r) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_pop_item(pipe, ITEM_HANDSHAKE); + if (item == NULL) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + *set_r = &item->u.set; + return DSYNC_IBC_RECV_RET_OK; +} + +static bool dsync_ibc_pipe_is_send_queue_full(struct dsync_ibc *ibc) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + + return array_count(&pipe->remote->item_queue) > 0; +} + +static bool dsync_ibc_pipe_has_pending_data(struct dsync_ibc *ibc) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + + return array_count(&pipe->item_queue) > 0; +} + +static void +dsync_ibc_pipe_send_end_of_list(struct dsync_ibc *ibc, + enum dsync_ibc_eol_type type ATTR_UNUSED) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + + dsync_ibc_pipe_push_item(pipe->remote, ITEM_END_OF_LIST); +} + +static void +dsync_ibc_pipe_send_mailbox_state(struct dsync_ibc *ibc, + const struct dsync_mailbox_state *state) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX_STATE); + item->u.state = *state; +} + +static enum dsync_ibc_recv_ret +dsync_ibc_pipe_recv_mailbox_state(struct dsync_ibc *ibc, + struct dsync_mailbox_state *state_r) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + if (dsync_ibc_pipe_try_pop_eol(pipe)) + return DSYNC_IBC_RECV_RET_FINISHED; + + item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX_STATE); + if (item == NULL) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + *state_r = item->u.state; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_pipe_send_mailbox_tree_node(struct dsync_ibc *ibc, + const char *const *name, + const struct dsync_mailbox_node *node) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX_TREE_NODE); + + /* a little bit kludgy way to send it */ + item->u.node.name = (void *)p_strarray_dup(item->pool, name); + dsync_mailbox_node_copy_data(&item->u.node, node); +} + +static enum dsync_ibc_recv_ret +dsync_ibc_pipe_recv_mailbox_tree_node(struct dsync_ibc *ibc, + const char *const **name_r, + const struct dsync_mailbox_node **node_r) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + if (dsync_ibc_pipe_try_pop_eol(pipe)) + return DSYNC_IBC_RECV_RET_FINISHED; + + item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX_TREE_NODE); + if (item == NULL) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + *name_r = (const void *)item->u.node.name; + item->u.node.name = NULL; + + *node_r = &item->u.node; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_pipe_send_mailbox_deletes(struct dsync_ibc *ibc, + const struct dsync_mailbox_delete *deletes, + unsigned int count, char hierarchy_sep, + char escape_char) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX_DELETE); + + /* we'll assume that the deletes are permanent. this works for now.. */ + /* a little bit kludgy way to send it */ + item->u.mailbox_delete.deletes = deletes; + item->u.mailbox_delete.count = count; + item->u.mailbox_delete.hierarchy_sep = hierarchy_sep; + item->u.mailbox_delete.escape_char = escape_char; +} + +static enum dsync_ibc_recv_ret +dsync_ibc_pipe_recv_mailbox_deletes(struct dsync_ibc *ibc, + const struct dsync_mailbox_delete **deletes_r, + unsigned int *count_r, + char *hierarchy_sep_r, + char *escape_char_r) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + if (dsync_ibc_pipe_try_pop_eol(pipe)) + return DSYNC_IBC_RECV_RET_FINISHED; + + item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX_DELETE); + if (item == NULL) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + *deletes_r = item->u.mailbox_delete.deletes; + *count_r = item->u.mailbox_delete.count; + *hierarchy_sep_r = item->u.mailbox_delete.hierarchy_sep; + *escape_char_r = item->u.mailbox_delete.escape_char; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_pipe_send_mailbox(struct dsync_ibc *ibc, + const struct dsync_mailbox *dsync_box) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + const struct mailbox_cache_field *cf; + struct mailbox_cache_field *ncf; + + item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX); + item->u.dsync_box = *dsync_box; + p_array_init(&item->u.dsync_box.cache_fields, item->pool, + array_count(&dsync_box->cache_fields)); + array_foreach(&dsync_box->cache_fields, cf) { + ncf = array_append_space(&item->u.dsync_box.cache_fields); + ncf->name = p_strdup(item->pool, cf->name); + ncf->decision = cf->decision; + ncf->last_used = cf->last_used; + } +} + +static enum dsync_ibc_recv_ret +dsync_ibc_pipe_recv_mailbox(struct dsync_ibc *ibc, + const struct dsync_mailbox **dsync_box_r) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + if (dsync_ibc_pipe_try_pop_eol(pipe)) + return DSYNC_IBC_RECV_RET_FINISHED; + + item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX); + if (item == NULL) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + *dsync_box_r = &item->u.dsync_box; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_pipe_send_mailbox_attribute(struct dsync_ibc *ibc, + const struct dsync_mailbox_attribute *attr) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX_ATTRIBUTE); + dsync_mailbox_attribute_dup(item->pool, attr, &item->u.attr); +} + +static enum dsync_ibc_recv_ret +dsync_ibc_pipe_recv_mailbox_attribute(struct dsync_ibc *ibc, + const struct dsync_mailbox_attribute **attr_r) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + if (dsync_ibc_pipe_try_pop_eol(pipe)) + return DSYNC_IBC_RECV_RET_FINISHED; + + item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX_ATTRIBUTE); + if (item == NULL) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + *attr_r = &item->u.attr; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_pipe_send_change(struct dsync_ibc *ibc, + const struct dsync_mail_change *change) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAIL_CHANGE); + dsync_mail_change_dup(item->pool, change, &item->u.change); +} + +static enum dsync_ibc_recv_ret +dsync_ibc_pipe_recv_change(struct dsync_ibc *ibc, + const struct dsync_mail_change **change_r) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + if (dsync_ibc_pipe_try_pop_eol(pipe)) + return DSYNC_IBC_RECV_RET_FINISHED; + + item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAIL_CHANGE); + if (item == NULL) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + *change_r = &item->u.change; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_pipe_send_mail_request(struct dsync_ibc *ibc, + const struct dsync_mail_request *request) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAIL_REQUEST); + item->u.request.guid = p_strdup(item->pool, request->guid); + item->u.request.uid = request->uid; +} + +static enum dsync_ibc_recv_ret +dsync_ibc_pipe_recv_mail_request(struct dsync_ibc *ibc, + const struct dsync_mail_request **request_r) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + if (dsync_ibc_pipe_try_pop_eol(pipe)) + return DSYNC_IBC_RECV_RET_FINISHED; + + item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAIL_REQUEST); + if (item == NULL) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + *request_r = &item->u.request; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_pipe_send_mail(struct dsync_ibc *ibc, const struct dsync_mail *mail) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAIL); + item->u.mail.guid = p_strdup(item->pool, mail->guid); + item->u.mail.uid = mail->uid; + item->u.mail.pop3_uidl = p_strdup(item->pool, mail->pop3_uidl); + item->u.mail.pop3_order = mail->pop3_order; + item->u.mail.received_date = mail->received_date; + if (mail->input != NULL) { + item->u.mail.input = mail->input; + i_stream_ref(mail->input); + } + item->u.mail.input_mail = mail->input_mail; + item->u.mail.input_mail_uid = mail->input_mail_uid; +} + +static enum dsync_ibc_recv_ret +dsync_ibc_pipe_recv_mail(struct dsync_ibc *ibc, struct dsync_mail **mail_r) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + if (dsync_ibc_pipe_try_pop_eol(pipe)) + return DSYNC_IBC_RECV_RET_FINISHED; + + item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAIL); + if (item == NULL) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + *mail_r = &item->u.mail; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_pipe_send_finish(struct dsync_ibc *ibc, const char *error, + enum mail_error mail_error, + bool require_full_resync) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_FINISH); + item->u.finish.error = p_strdup(item->pool, error); + item->u.finish.mail_error = mail_error; + item->u.finish.require_full_resync = require_full_resync; +} + +static enum dsync_ibc_recv_ret +dsync_ibc_pipe_recv_finish(struct dsync_ibc *ibc, const char **error_r, + enum mail_error *mail_error_r, + bool *require_full_resync_r) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + struct item *item; + + item = dsync_ibc_pipe_pop_item(pipe, ITEM_FINISH); + if (item == NULL) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + *error_r = item->u.finish.error; + *mail_error_r = item->u.finish.mail_error; + *require_full_resync_r = item->u.finish.require_full_resync; + return DSYNC_IBC_RECV_RET_OK; +} + +static void pipe_close_mail_streams(struct dsync_ibc_pipe *pipe) +{ + struct item *item; + + if (array_count(&pipe->item_queue) > 0) { + item = array_front_modifiable(&pipe->item_queue); + if (item->type == ITEM_MAIL && + item->u.mail.input != NULL) + i_stream_unref(&item->u.mail.input); + } +} + +static void dsync_ibc_pipe_close_mail_streams(struct dsync_ibc *ibc) +{ + struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc; + + pipe_close_mail_streams(pipe); + pipe_close_mail_streams(pipe->remote); +} + +static const struct dsync_ibc_vfuncs dsync_ibc_pipe_vfuncs = { + dsync_ibc_pipe_deinit, + dsync_ibc_pipe_send_handshake, + dsync_ibc_pipe_recv_handshake, + dsync_ibc_pipe_send_end_of_list, + dsync_ibc_pipe_send_mailbox_state, + dsync_ibc_pipe_recv_mailbox_state, + dsync_ibc_pipe_send_mailbox_tree_node, + dsync_ibc_pipe_recv_mailbox_tree_node, + dsync_ibc_pipe_send_mailbox_deletes, + dsync_ibc_pipe_recv_mailbox_deletes, + dsync_ibc_pipe_send_mailbox, + dsync_ibc_pipe_recv_mailbox, + dsync_ibc_pipe_send_mailbox_attribute, + dsync_ibc_pipe_recv_mailbox_attribute, + dsync_ibc_pipe_send_change, + dsync_ibc_pipe_recv_change, + dsync_ibc_pipe_send_mail_request, + dsync_ibc_pipe_recv_mail_request, + dsync_ibc_pipe_send_mail, + dsync_ibc_pipe_recv_mail, + dsync_ibc_pipe_send_finish, + dsync_ibc_pipe_recv_finish, + dsync_ibc_pipe_close_mail_streams, + dsync_ibc_pipe_is_send_queue_full, + dsync_ibc_pipe_has_pending_data +}; + +static struct dsync_ibc_pipe * +dsync_ibc_pipe_alloc(void) +{ + struct dsync_ibc_pipe *pipe; + + pipe = i_new(struct dsync_ibc_pipe, 1); + pipe->ibc.v = dsync_ibc_pipe_vfuncs; + i_array_init(&pipe->pools, 4); + i_array_init(&pipe->item_queue, 4); + return pipe; +} + +void dsync_ibc_init_pipe(struct dsync_ibc **ibc1_r, struct dsync_ibc **ibc2_r) +{ + struct dsync_ibc_pipe *pipe1, *pipe2; + + pipe1 = dsync_ibc_pipe_alloc(); + pipe2 = dsync_ibc_pipe_alloc(); + pipe1->remote = pipe2; + pipe2->remote = pipe1; + *ibc1_r = &pipe1->ibc; + *ibc2_r = &pipe2->ibc; +} diff --git a/src/doveadm/dsync/dsync-ibc-private.h b/src/doveadm/dsync/dsync-ibc-private.h new file mode 100644 index 0000000..c055aca --- /dev/null +++ b/src/doveadm/dsync/dsync-ibc-private.h @@ -0,0 +1,96 @@ +#ifndef DSYNC_IBC_PRIVATE_H +#define DSYNC_IBC_PRIVATE_H + +#include "dsync-ibc.h" + +struct dsync_ibc_vfuncs { + void (*deinit)(struct dsync_ibc *ibc); + + void (*send_handshake)(struct dsync_ibc *ibc, + const struct dsync_ibc_settings *set); + enum dsync_ibc_recv_ret + (*recv_handshake)(struct dsync_ibc *ibc, + const struct dsync_ibc_settings **set_r); + + void (*send_end_of_list)(struct dsync_ibc *ibc, + enum dsync_ibc_eol_type type); + + void (*send_mailbox_state)(struct dsync_ibc *ibc, + const struct dsync_mailbox_state *state); + enum dsync_ibc_recv_ret + (*recv_mailbox_state)(struct dsync_ibc *ibc, + struct dsync_mailbox_state *state_r); + + void (*send_mailbox_tree_node)(struct dsync_ibc *ibc, + const char *const *name, + const struct dsync_mailbox_node *node); + enum dsync_ibc_recv_ret + (*recv_mailbox_tree_node)(struct dsync_ibc *ibc, + const char *const **name_r, + const struct dsync_mailbox_node **node_r); + + void (*send_mailbox_deletes)(struct dsync_ibc *ibc, + const struct dsync_mailbox_delete *deletes, + unsigned int count, char hierarchy_sep, + char escape_char); + enum dsync_ibc_recv_ret + (*recv_mailbox_deletes)(struct dsync_ibc *ibc, + const struct dsync_mailbox_delete **deletes_r, + unsigned int *count_r, + char *hierarchy_sep_r, + char *escape_char_r); + + void (*send_mailbox)(struct dsync_ibc *ibc, + const struct dsync_mailbox *dsync_box); + enum dsync_ibc_recv_ret + (*recv_mailbox)(struct dsync_ibc *ibc, + const struct dsync_mailbox **dsync_box_r); + + void (*send_mailbox_attribute)(struct dsync_ibc *ibc, + const struct dsync_mailbox_attribute *attr); + enum dsync_ibc_recv_ret + (*recv_mailbox_attribute)(struct dsync_ibc *ibc, + const struct dsync_mailbox_attribute **attr_r); + + void (*send_change)(struct dsync_ibc *ibc, + const struct dsync_mail_change *change); + enum dsync_ibc_recv_ret + (*recv_change)(struct dsync_ibc *ibc, + const struct dsync_mail_change **change_r); + + void (*send_mail_request)(struct dsync_ibc *ibc, + const struct dsync_mail_request *request); + enum dsync_ibc_recv_ret + (*recv_mail_request)(struct dsync_ibc *ibc, + const struct dsync_mail_request **request_r); + + void (*send_mail)(struct dsync_ibc *ibc, + const struct dsync_mail *mail); + enum dsync_ibc_recv_ret + (*recv_mail)(struct dsync_ibc *ibc, + struct dsync_mail **mail_r); + + void (*send_finish)(struct dsync_ibc *ibc, const char *error, + enum mail_error mail_error, + bool require_full_resync); + enum dsync_ibc_recv_ret + (*recv_finish)(struct dsync_ibc *ibc, const char **error_r, + enum mail_error *mail_error_r, + bool *require_full_resync_r); + + void (*close_mail_streams)(struct dsync_ibc *ibc); + bool (*is_send_queue_full)(struct dsync_ibc *ibc); + bool (*has_pending_data)(struct dsync_ibc *ibc); +}; + +struct dsync_ibc { + struct dsync_ibc_vfuncs v; + + io_callback_t *io_callback; + void *io_context; + + bool failed:1; + bool timeout:1; +}; + +#endif diff --git a/src/doveadm/dsync/dsync-ibc-stream.c b/src/doveadm/dsync/dsync-ibc-stream.c new file mode 100644 index 0000000..17115b0 --- /dev/null +++ b/src/doveadm/dsync/dsync-ibc-stream.c @@ -0,0 +1,2138 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "safe-mkstemp.h" +#include "ioloop.h" +#include "istream.h" +#include "istream-seekable.h" +#include "istream-dot.h" +#include "ostream.h" +#include "str.h" +#include "strescape.h" +#include "master-service.h" +#include "mail-cache.h" +#include "mail-storage-private.h" +#include "dsync-serializer.h" +#include "dsync-deserializer.h" +#include "dsync-mail.h" +#include "dsync-mailbox.h" +#include "dsync-mailbox-state.h" +#include "dsync-mailbox-tree.h" +#include "dsync-ibc-private.h" + + +#define DSYNC_IBC_STREAM_OUTBUF_THROTTLE_SIZE (1024*128) + +#define DSYNC_PROTOCOL_VERSION_MAJOR 3 +#define DSYNC_PROTOCOL_VERSION_MINOR 5 +#define DSYNC_HANDSHAKE_VERSION "VERSION\tdsync\t3\t5\n" + +#define DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES 1 +#define DSYNC_PROTOCOL_MINOR_HAVE_SAVE_GUID 2 +#define DSYNC_PROTOCOL_MINOR_HAVE_FINISH 3 +#define DSYNC_PROTOCOL_MINOR_HAVE_HDR_HASH_V2 4 +#define DSYNC_PROTOCOL_MINOR_HAVE_HDR_HASH_V3 5 + +enum item_type { + ITEM_NONE, + ITEM_DONE, + + ITEM_HANDSHAKE, + ITEM_MAILBOX_STATE, + ITEM_MAILBOX_TREE_NODE, + ITEM_MAILBOX_DELETE, + ITEM_MAILBOX, + + ITEM_MAILBOX_ATTRIBUTE, + ITEM_MAIL_CHANGE, + ITEM_MAIL_REQUEST, + ITEM_MAIL, + ITEM_FINISH, + + ITEM_MAILBOX_CACHE_FIELD, + + ITEM_END_OF_LIST +}; + +#define END_OF_LIST_LINE "." +static const struct { + /* full human readable name of the item */ + const char *name; + /* unique character identifying the item */ + char chr; + const char *required_keys; + const char *optional_keys; + unsigned int min_minor_version; +} items[ITEM_END_OF_LIST+1] = { + { NULL, '\0', NULL, NULL, 0 }, + { .name = "done", + .chr = 'X', + .optional_keys = "" + }, + { .name = "handshake", + .chr = 'H', + .required_keys = "hostname", + .optional_keys = "sync_ns_prefix sync_box sync_box_guid sync_type " + "debug sync_visible_namespaces exclude_mailboxes " + "send_mail_requests backup_send backup_recv lock_timeout " + "no_mail_sync no_backup_overwrite purge_remote " + "no_notify sync_since_timestamp sync_max_size sync_flags sync_until_timestamp " + "virtual_all_box empty_hdr_workaround import_commit_msgs_interval " + "hashed_headers alt_char" + }, + { .name = "mailbox_state", + .chr = 'S', + .required_keys = "mailbox_guid last_uidvalidity last_common_uid " + "last_common_modseq last_common_pvt_modseq", + .optional_keys = "last_messages_count changes_during_sync" + }, + { .name = "mailbox_tree_node", + .chr = 'N', + .required_keys = "name existence", + .optional_keys = "mailbox_guid uid_validity uid_next " + "last_renamed_or_created subscribed last_subscription_change" + }, + { .name = "mailbox_delete", + .chr = 'D', + .required_keys = "hierarchy_sep", + .optional_keys = "escape_char mailboxes dirs unsubscribes" + }, + { .name = "mailbox", + .chr = 'B', + .required_keys = "mailbox_guid uid_validity uid_next messages_count " + "first_recent_uid highest_modseq highest_pvt_modseq", + .optional_keys = "mailbox_lost mailbox_ignore " + "cache_fields have_guids have_save_guids have_only_guid128" + }, + { .name = "mailbox_attribute", + .chr = 'A', + .required_keys = "type key", + .optional_keys = "value stream deleted last_change modseq", + .min_minor_version = DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES + }, + { .name = "mail_change", + .chr = 'C', + .required_keys = "type uid", + .optional_keys = "guid hdr_hash modseq pvt_modseq " + "add_flags remove_flags final_flags " + "keywords_reset keyword_changes received_timestamp virtual_size" + }, + { .name = "mail_request", + .chr = 'R', + .optional_keys = "guid uid" + }, + { .name = "mail", + .chr = 'M', + .optional_keys = "guid uid pop3_uidl pop3_order received_date saved_date stream" + }, + { .name = "finish", + .chr = 'F', + .optional_keys = "error mail_error require_full_resync", + .min_minor_version = DSYNC_PROTOCOL_MINOR_HAVE_FINISH + }, + { .name = "mailbox_cache_field", + .chr = 'c', + .required_keys = "name decision", + .optional_keys = "last_used" + }, + + { "end_of_list", '\0', NULL, NULL, 0 } +}; + +struct dsync_ibc_stream { + struct dsync_ibc ibc; + + char *name, *temp_path_prefix; + unsigned int timeout_secs; + struct istream *input; + struct ostream *output; + struct io *io; + struct timeout *to; + + unsigned int minor_version; + struct dsync_serializer *serializers[ITEM_END_OF_LIST]; + struct dsync_deserializer *deserializers[ITEM_END_OF_LIST]; + + pool_t ret_pool; + struct dsync_deserializer_decoder *cur_decoder; + + struct istream *value_output, *value_input; + struct dsync_mail *cur_mail; + struct dsync_mailbox_attribute *cur_attr; + char value_output_last; + + enum item_type last_recv_item, last_sent_item; + bool last_recv_item_eol:1; + bool last_sent_item_eol:1; + + bool version_received:1; + bool handshake_received:1; + bool has_pending_data:1; + bool finish_received:1; + bool done_received:1; + bool stopped:1; +}; + +static const char *dsync_ibc_stream_get_state(struct dsync_ibc_stream *ibc) +{ + if (!ibc->version_received) + return "version not received"; + else if (!ibc->handshake_received) + return "handshake not received"; + + return t_strdup_printf("last sent=%s%s, last recv=%s%s", + items[ibc->last_sent_item].name, + ibc->last_sent_item_eol ? " (EOL)" : "", + items[ibc->last_recv_item].name, + ibc->last_recv_item_eol ? " (EOL)" : ""); +} + +static void dsync_ibc_stream_stop(struct dsync_ibc_stream *ibc) +{ + ibc->stopped = TRUE; + i_stream_close(ibc->input); + o_stream_close(ibc->output); + io_loop_stop(current_ioloop); +} + +static int dsync_ibc_stream_read_mail_stream(struct dsync_ibc_stream *ibc) +{ + do { + i_stream_skip(ibc->value_input, + i_stream_get_data_size(ibc->value_input)); + } while (i_stream_read(ibc->value_input) > 0); + if (ibc->value_input->eof) { + if (ibc->value_input->stream_errno != 0) { + i_error("dsync(%s): read(%s) failed: %s (%s)", ibc->name, + i_stream_get_name(ibc->value_input), + i_stream_get_error(ibc->value_input), + dsync_ibc_stream_get_state(ibc)); + dsync_ibc_stream_stop(ibc); + return -1; + } + /* finished reading the mail stream */ + i_assert(ibc->value_input->eof); + i_stream_seek(ibc->value_input, 0); + ibc->has_pending_data = TRUE; + ibc->value_input = NULL; + return 1; + } + return 0; +} + +static void dsync_ibc_stream_input(struct dsync_ibc_stream *ibc) +{ + timeout_reset(ibc->to); + if (ibc->value_input != NULL) { + if (dsync_ibc_stream_read_mail_stream(ibc) == 0) + return; + } + o_stream_cork(ibc->output); + ibc->ibc.io_callback(ibc->ibc.io_context); + o_stream_uncork(ibc->output); +} + +static int dsync_ibc_stream_send_value_stream(struct dsync_ibc_stream *ibc) +{ + const unsigned char *data; + unsigned char add; + size_t i, size; + int ret; + + while ((ret = i_stream_read_more(ibc->value_output, &data, &size)) > 0) { + add = '\0'; + for (i = 0; i < size; i++) { + if (data[i] == '.' && + ((i == 0 && ibc->value_output_last == '\n') || + (i > 0 && data[i-1] == '\n'))) { + /* escape the dot */ + add = '.'; + break; + } + } + + if (i > 0) { + o_stream_nsend(ibc->output, data, i); + ibc->value_output_last = data[i-1]; + i_stream_skip(ibc->value_output, i); + } + + if (o_stream_get_buffer_used_size(ibc->output) >= 4096) { + if ((ret = o_stream_flush(ibc->output)) < 0) { + dsync_ibc_stream_stop(ibc); + return -1; + } + if (ret == 0) { + /* continue later */ + o_stream_set_flush_pending(ibc->output, TRUE); + return 0; + } + } + + if (add != '\0') { + o_stream_nsend(ibc->output, &add, 1); + ibc->value_output_last = add; + } + } + i_assert(ret == -1); + + if (ibc->value_output->stream_errno != 0) { + i_error("dsync(%s): read(%s) failed: %s (%s)", + ibc->name, i_stream_get_name(ibc->value_output), + i_stream_get_error(ibc->value_output), + dsync_ibc_stream_get_state(ibc)); + dsync_ibc_stream_stop(ibc); + return -1; + } + + /* finished sending the stream. use "CRLF." instead of "LF." just in + case we're sending binary data that ends with CR. */ + o_stream_nsend_str(ibc->output, "\r\n.\r\n"); + i_stream_unref(&ibc->value_output); + return 1; +} + +static int dsync_ibc_stream_output(struct dsync_ibc_stream *ibc) +{ + struct ostream *output = ibc->output; + int ret; + + if ((ret = o_stream_flush(output)) < 0) + ret = 1; + else if (ibc->value_output != NULL) { + if (dsync_ibc_stream_send_value_stream(ibc) < 0) + ret = 1; + } + timeout_reset(ibc->to); + + if (!dsync_ibc_is_send_queue_full(&ibc->ibc)) + ibc->ibc.io_callback(ibc->ibc.io_context); + return ret; +} + +static void dsync_ibc_stream_timeout(struct dsync_ibc_stream *ibc) +{ + i_error("dsync(%s): I/O has stalled, no activity for %u seconds (%s)", + ibc->name, ibc->timeout_secs, dsync_ibc_stream_get_state(ibc)); + ibc->ibc.timeout = TRUE; + dsync_ibc_stream_stop(ibc); +} + +static void dsync_ibc_stream_init(struct dsync_ibc_stream *ibc) +{ + unsigned int i; + + ibc->io = io_add_istream(ibc->input, dsync_ibc_stream_input, ibc); + io_set_pending(ibc->io); + o_stream_set_no_error_handling(ibc->output, TRUE); + o_stream_set_flush_callback(ibc->output, dsync_ibc_stream_output, ibc); + ibc->to = timeout_add(ibc->timeout_secs * 1000, + dsync_ibc_stream_timeout, ibc); + o_stream_cork(ibc->output); + o_stream_nsend_str(ibc->output, DSYNC_HANDSHAKE_VERSION); + + /* initialize serializers and send their headers to remote */ + for (i = ITEM_DONE + 1; i < ITEM_END_OF_LIST; i++) T_BEGIN { + const char *keys; + + keys = items[i].required_keys == NULL ? items[i].optional_keys : + t_strconcat(items[i].required_keys, " ", + items[i].optional_keys, NULL); + if (keys != NULL) { + i_assert(items[i].chr != '\0'); + + ibc->serializers[i] = + dsync_serializer_init(t_strsplit_spaces(keys, " ")); + o_stream_nsend(ibc->output, &items[i].chr, 1); + o_stream_nsend_str(ibc->output, + dsync_serializer_encode_header_line(ibc->serializers[i])); + } + } T_END; + o_stream_nsend_str(ibc->output, ".\n"); + o_stream_uncork(ibc->output); +} + +static void dsync_ibc_stream_deinit(struct dsync_ibc *_ibc) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + unsigned int i; + + for (i = ITEM_DONE + 1; i < ITEM_END_OF_LIST; i++) { + if (ibc->serializers[i] != NULL) + dsync_serializer_deinit(&ibc->serializers[i]); + if (ibc->deserializers[i] != NULL) + dsync_deserializer_deinit(&ibc->deserializers[i]); + } + if (ibc->cur_decoder != NULL) + dsync_deserializer_decode_finish(&ibc->cur_decoder); + if (ibc->value_output != NULL) + i_stream_unref(&ibc->value_output); + else { + /* If the remote has not told us that they are closing we + notify remote that we're closing. this is mainly to avoid + "read() failed: EOF" errors on failing dsyncs. + + Avoid a race condition: + We do not tell the remote we are closing if they have + already told us because they close the + connection after sending ITEM_DONE and will + not be ever receive anything else from us unless + it just happens to get combined into the same packet + as a previous response and is already in the buffer. + */ + if (!ibc->done_received && !ibc->finish_received) { + o_stream_nsend_str(ibc->output, + t_strdup_printf("%c\n", items[ITEM_DONE].chr)); + } + (void)o_stream_finish(ibc->output); + } + + timeout_remove(&ibc->to); + io_remove(&ibc->io); + i_stream_destroy(&ibc->input); + o_stream_destroy(&ibc->output); + pool_unref(&ibc->ret_pool); + i_free(ibc->temp_path_prefix); + i_free(ibc->name); + i_free(ibc); +} + +static int dsync_ibc_stream_next_line(struct dsync_ibc_stream *ibc, + const char **line_r) +{ + string_t *error; + const char *line; + ssize_t ret; + + line = i_stream_next_line(ibc->input); + if (line != NULL) { + *line_r = line; + return 1; + } + /* try reading some */ + if ((ret = i_stream_read(ibc->input)) == -1) { + if (ibc->stopped) + return -1; + error = t_str_new(128); + if (ibc->input->stream_errno != 0) { + str_printfa(error, "read(%s) failed: %s", ibc->name, + i_stream_get_error(ibc->input)); + } else { + i_assert(ibc->input->eof); + str_printfa(error, "read(%s) failed: EOF", ibc->name); + } + str_printfa(error, " (%s)", dsync_ibc_stream_get_state(ibc)); + i_error("%s", str_c(error)); + dsync_ibc_stream_stop(ibc); + return -1; + } + i_assert(ret >= 0); + *line_r = i_stream_next_line(ibc->input); + if (*line_r == NULL) { + ibc->has_pending_data = FALSE; + return 0; + } + ibc->has_pending_data = TRUE; + return 1; +} + +static void ATTR_FORMAT(3, 4) ATTR_NULL(2) +dsync_ibc_input_error(struct dsync_ibc_stream *ibc, + struct dsync_deserializer_decoder *decoder, + const char *fmt, ...) +{ + va_list args; + const char *error; + + va_start(args, fmt); + error = t_strdup_vprintf(fmt, args); + if (decoder == NULL) + i_error("dsync(%s): %s", ibc->name, error); + else { + i_error("dsync(%s): %s: %s", ibc->name, + dsync_deserializer_decoder_get_name(decoder), error); + } + va_end(args); + + dsync_ibc_stream_stop(ibc); +} + +static void +dsync_ibc_stream_send_string(struct dsync_ibc_stream *ibc, + const string_t *str) +{ + i_assert(ibc->value_output == NULL); + o_stream_nsend(ibc->output, str_data(str), str_len(str)); +} + +static int seekable_fd_callback(const char **path_r, void *context) +{ + struct dsync_ibc_stream *ibc = context; + string_t *path; + int fd; + + path = t_str_new(128); + str_append(path, ibc->temp_path_prefix); + fd = safe_mkstemp(path, 0600, (uid_t)-1, (gid_t)-1); + if (fd == -1) { + i_error("safe_mkstemp(%s) failed: %m", str_c(path)); + return -1; + } + + /* we just want the fd, unlink it */ + if (i_unlink(str_c(path)) < 0) { + /* shouldn't happen.. */ + i_close_fd(&fd); + return -1; + } + + *path_r = str_c(path); + return fd; +} + +static struct istream * +dsync_ibc_stream_input_stream(struct dsync_ibc_stream *ibc) +{ + struct istream *inputs[2]; + + inputs[0] = i_stream_create_dot(ibc->input, FALSE); + inputs[1] = NULL; + ibc->value_input = i_stream_create_seekable(inputs, MAIL_READ_FULL_BLOCK_SIZE, + seekable_fd_callback, ibc); + i_stream_unref(&inputs[0]); + + return ibc->value_input; +} + +static int +dsync_ibc_check_missing_deserializers(struct dsync_ibc_stream *ibc) +{ + unsigned int i; + int ret = 0; + + for (i = ITEM_DONE + 1; i < ITEM_END_OF_LIST; i++) { + if (ibc->deserializers[i] == NULL && + ibc->minor_version >= items[i].min_minor_version && + (items[i].required_keys != NULL || + items[i].optional_keys != NULL)) { + dsync_ibc_input_error(ibc, NULL, + "Remote didn't handshake deserializer for %s", + items[i].name); + ret = -1; + } + } + return ret; +} + +static bool +dsync_ibc_stream_handshake(struct dsync_ibc_stream *ibc, const char *line) +{ + enum item_type item = ITEM_NONE; + const char *const *required_keys, *error; + unsigned int i; + + if (ibc->handshake_received) + return TRUE; + + if (!ibc->version_received) { + if (!version_string_verify_full(line, "dsync", + DSYNC_PROTOCOL_VERSION_MAJOR, + &ibc->minor_version)) { + dsync_ibc_input_error(ibc, NULL, + "Remote dsync doesn't use compatible protocol"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + ibc->version_received = TRUE; + return FALSE; + } + + if (strcmp(line, END_OF_LIST_LINE) == 0) { + /* finished handshaking */ + if (dsync_ibc_check_missing_deserializers(ibc) < 0) + return FALSE; + ibc->handshake_received = TRUE; + ibc->last_recv_item = ITEM_HANDSHAKE; + return FALSE; + } + + for (i = 1; i < ITEM_END_OF_LIST; i++) { + if (items[i].chr == line[0]) { + item = i; + break; + } + } + if (item == ITEM_NONE) { + /* unknown deserializer, ignore */ + return FALSE; + } + + required_keys = items[item].required_keys == NULL ? NULL : + t_strsplit(items[item].required_keys, " "); + if (dsync_deserializer_init(items[item].name, + required_keys, line + 1, + &ibc->deserializers[item], &error) < 0) { + dsync_ibc_input_error(ibc, NULL, + "Remote sent invalid handshake for %s: %s", + items[item].name, error); + } + return FALSE; +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_input_next(struct dsync_ibc_stream *ibc, enum item_type item, + struct dsync_deserializer_decoder **decoder_r) +{ + enum item_type line_item = ITEM_NONE; + const char *line, *error; + unsigned int i; + + i_assert(ibc->value_input == NULL); + + timeout_reset(ibc->to); + + do { + if (dsync_ibc_stream_next_line(ibc, &line) <= 0) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } while (!dsync_ibc_stream_handshake(ibc, line)); + + ibc->last_recv_item = item; + ibc->last_recv_item_eol = FALSE; + + if (strcmp(line, END_OF_LIST_LINE) == 0) { + /* end of this list */ + ibc->last_recv_item_eol = TRUE; + return DSYNC_IBC_RECV_RET_FINISHED; + } + if (line[0] == items[ITEM_DONE].chr) { + /* remote cleanly closed the connection, possibly because of + some failure (which it should have logged). we don't want to + log any stream errors anyway after this. */ + ibc->done_received = TRUE; + dsync_ibc_stream_stop(ibc); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + + } + for (i = 1; i < ITEM_END_OF_LIST; i++) { + if (*line == items[i].chr) { + line_item = i; + break; + } + } + if (line_item != item) { + dsync_ibc_input_error(ibc, NULL, + "Received unexpected input %c != %c", + *line, items[item].chr); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + if (ibc->cur_decoder != NULL) + dsync_deserializer_decode_finish(&ibc->cur_decoder); + if (dsync_deserializer_decode_begin(ibc->deserializers[item], + line+1, &ibc->cur_decoder, + &error) < 0) { + dsync_ibc_input_error(ibc, NULL, "Invalid input to %s: %s", + items[item].name, error); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + *decoder_r = ibc->cur_decoder; + return DSYNC_IBC_RECV_RET_OK; +} + +static struct dsync_serializer_encoder * +dsync_ibc_send_encode_begin(struct dsync_ibc_stream *ibc, enum item_type item) +{ + ibc->last_sent_item = item; + ibc->last_sent_item_eol = FALSE; + return dsync_serializer_encode_begin(ibc->serializers[item]); +} + +static void +dsync_ibc_stream_send_handshake(struct dsync_ibc *_ibc, + const struct dsync_ibc_settings *set) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; + string_t *str = t_str_new(128); + char sync_type[2]; + + str_append_c(str, items[ITEM_HANDSHAKE].chr); + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_HANDSHAKE); + dsync_serializer_encode_add(encoder, "hostname", set->hostname); + if (set->sync_ns_prefixes != NULL) { + dsync_serializer_encode_add(encoder, "sync_ns_prefix", + set->sync_ns_prefixes); + } + if (set->sync_box != NULL) + dsync_serializer_encode_add(encoder, "sync_box", set->sync_box); + if (set->virtual_all_box != NULL) { + dsync_serializer_encode_add(encoder, "virtual_all_box", + set->virtual_all_box); + } + if (set->exclude_mailboxes != NULL) { + string_t *substr = t_str_new(64); + unsigned int i; + + for (i = 0; set->exclude_mailboxes[i] != NULL; i++) { + if (i != 0) + str_append_c(substr, '\t'); + str_append_tabescaped(substr, set->exclude_mailboxes[i]); + } + dsync_serializer_encode_add(encoder, "exclude_mailboxes", + str_c(substr)); + } + if (!guid_128_is_empty(set->sync_box_guid)) { + dsync_serializer_encode_add(encoder, "sync_box_guid", + guid_128_to_string(set->sync_box_guid)); + } + + sync_type[0] = sync_type[1] = '\0'; + switch (set->sync_type) { + case DSYNC_BRAIN_SYNC_TYPE_UNKNOWN: + break; + case DSYNC_BRAIN_SYNC_TYPE_FULL: + sync_type[0] = 'f'; + break; + case DSYNC_BRAIN_SYNC_TYPE_CHANGED: + sync_type[0] = 'c'; + break; + case DSYNC_BRAIN_SYNC_TYPE_STATE: + sync_type[0] = 's'; + break; + } + if (sync_type[0] != '\0') + dsync_serializer_encode_add(encoder, "sync_type", sync_type); + if (set->lock_timeout > 0) { + dsync_serializer_encode_add(encoder, "lock_timeout", + t_strdup_printf("%u", set->lock_timeout)); + } + if (set->import_commit_msgs_interval > 0) { + dsync_serializer_encode_add(encoder, "import_commit_msgs_interval", + t_strdup_printf("%u", set->import_commit_msgs_interval)); + } + if (set->sync_since_timestamp > 0) { + dsync_serializer_encode_add(encoder, "sync_since_timestamp", + t_strdup_printf("%ld", (long)set->sync_since_timestamp)); + } + if (set->sync_until_timestamp > 0) { + dsync_serializer_encode_add(encoder, "sync_until_timestamp", + t_strdup_printf("%ld", (long)set->sync_since_timestamp)); + } + if (set->sync_max_size > 0) { + dsync_serializer_encode_add(encoder, "sync_max_size", + t_strdup_printf("%"PRIu64, set->sync_max_size)); + } + if (set->sync_flags != NULL) { + dsync_serializer_encode_add(encoder, "sync_flags", + set->sync_flags); + } + if (set->alt_char != '\0') { + dsync_serializer_encode_add(encoder, "alt_char", + t_strdup_printf("%c", set->alt_char)); + } + if ((set->brain_flags & DSYNC_BRAIN_FLAG_SEND_MAIL_REQUESTS) != 0) + dsync_serializer_encode_add(encoder, "send_mail_requests", ""); + if ((set->brain_flags & DSYNC_BRAIN_FLAG_BACKUP_SEND) != 0) + dsync_serializer_encode_add(encoder, "backup_send", ""); + if ((set->brain_flags & DSYNC_BRAIN_FLAG_BACKUP_RECV) != 0) + dsync_serializer_encode_add(encoder, "backup_recv", ""); + if ((set->brain_flags & DSYNC_BRAIN_FLAG_DEBUG) != 0) + dsync_serializer_encode_add(encoder, "debug", ""); + if ((set->brain_flags & DSYNC_BRAIN_FLAG_SYNC_VISIBLE_NAMESPACES) != 0) + dsync_serializer_encode_add(encoder, "sync_visible_namespaces", ""); + if ((set->brain_flags & DSYNC_BRAIN_FLAG_NO_MAIL_SYNC) != 0) + dsync_serializer_encode_add(encoder, "no_mail_sync", ""); + if ((set->brain_flags & DSYNC_BRAIN_FLAG_NO_BACKUP_OVERWRITE) != 0) + dsync_serializer_encode_add(encoder, "no_backup_overwrite", ""); + if ((set->brain_flags & DSYNC_BRAIN_FLAG_PURGE_REMOTE) != 0) + dsync_serializer_encode_add(encoder, "purge_remote", ""); + if ((set->brain_flags & DSYNC_BRAIN_FLAG_NO_NOTIFY) != 0) + dsync_serializer_encode_add(encoder, "no_notify", ""); + if ((set->brain_flags & DSYNC_BRAIN_FLAG_EMPTY_HDR_WORKAROUND) != 0) + dsync_serializer_encode_add(encoder, "empty_hdr_workaround", ""); + /* this can be NULL in slave */ + string_t *str2 = t_str_new(32); + if (set->hashed_headers != NULL) { + for(const char *const *ptr = set->hashed_headers; *ptr != NULL; ptr++) { + str_append_tabescaped(str2, *ptr); + str_append_c(str2, '\t'); + } + } + dsync_serializer_encode_add(encoder, "hashed_headers", str_c(str2)); + dsync_serializer_encode_finish(&encoder, str); + dsync_ibc_stream_send_string(ibc, str); +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_recv_handshake(struct dsync_ibc *_ibc, + const struct dsync_ibc_settings **set_r) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_deserializer_decoder *decoder; + struct dsync_ibc_settings *set; + const char *value; + pool_t pool = ibc->ret_pool; + enum dsync_ibc_recv_ret ret; + + ret = dsync_ibc_stream_input_next(ibc, ITEM_HANDSHAKE, &decoder); + if (ret != DSYNC_IBC_RECV_RET_OK) { + if (ret != DSYNC_IBC_RECV_RET_TRYAGAIN) { + i_error("dsync(%s): Unexpected input in handshake", + ibc->name); + dsync_ibc_stream_stop(ibc); + } + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + p_clear(pool); + set = p_new(pool, struct dsync_ibc_settings, 1); + + value = dsync_deserializer_decode_get(decoder, "hostname"); + set->hostname = p_strdup(pool, value); + /* now that we know the remote's hostname, use it for the + stream's name */ + i_free(ibc->name); + ibc->name = i_strdup(set->hostname); + + if (dsync_deserializer_decode_try(decoder, "sync_ns_prefix", &value)) + set->sync_ns_prefixes = p_strdup(pool, value); + if (dsync_deserializer_decode_try(decoder, "sync_box", &value)) + set->sync_box = p_strdup(pool, value); + if (dsync_deserializer_decode_try(decoder, "virtual_all_box", &value)) + set->virtual_all_box = p_strdup(pool, value); + if (dsync_deserializer_decode_try(decoder, "sync_box_guid", &value) && + guid_128_from_string(value, set->sync_box_guid) < 0) { + dsync_ibc_input_error(ibc, decoder, + "Invalid sync_box_guid: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "exclude_mailboxes", &value) && + *value != '\0') { + char **boxes = p_strsplit_tabescaped(pool, value); + set->exclude_mailboxes = (const void *)boxes; + } + if (dsync_deserializer_decode_try(decoder, "sync_type", &value)) { + switch (value[0]) { + case 'f': + set->sync_type = DSYNC_BRAIN_SYNC_TYPE_FULL; + break; + case 'c': + set->sync_type = DSYNC_BRAIN_SYNC_TYPE_CHANGED; + break; + case 's': + set->sync_type = DSYNC_BRAIN_SYNC_TYPE_STATE; + break; + default: + dsync_ibc_input_error(ibc, decoder, + "Unknown sync_type: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + } + if (dsync_deserializer_decode_try(decoder, "lock_timeout", &value)) { + if (str_to_uint(value, &set->lock_timeout) < 0 || + set->lock_timeout == 0) { + dsync_ibc_input_error(ibc, decoder, + "Invalid lock_timeout: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + } + if (dsync_deserializer_decode_try(decoder, "import_commit_msgs_interval", &value)) { + if (str_to_uint(value, &set->import_commit_msgs_interval) < 0 || + set->import_commit_msgs_interval == 0) { + dsync_ibc_input_error(ibc, decoder, + "Invalid import_commit_msgs_interval: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + } + if (dsync_deserializer_decode_try(decoder, "sync_since_timestamp", &value)) { + if (str_to_time(value, &set->sync_since_timestamp) < 0 || + set->sync_since_timestamp == 0) { + dsync_ibc_input_error(ibc, decoder, + "Invalid sync_since_timestamp: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + } + if (dsync_deserializer_decode_try(decoder, "sync_until_timestamp", &value)) { + if (str_to_time(value, &set->sync_until_timestamp) < 0 || + set->sync_until_timestamp == 0) { + dsync_ibc_input_error(ibc, decoder, + "Invalid sync_until_timestamp: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + } + if (dsync_deserializer_decode_try(decoder, "sync_max_size", &value)) { + if (str_to_uoff(value, &set->sync_max_size) < 0 || + set->sync_max_size == 0) { + dsync_ibc_input_error(ibc, decoder, + "Invalid sync_max_size: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + } + if (dsync_deserializer_decode_try(decoder, "sync_flags", &value)) + set->sync_flags = p_strdup(pool, value); + if (dsync_deserializer_decode_try(decoder, "alt_char", &value)) + set->alt_char = value[0]; + if (dsync_deserializer_decode_try(decoder, "send_mail_requests", &value)) + set->brain_flags |= DSYNC_BRAIN_FLAG_SEND_MAIL_REQUESTS; + if (dsync_deserializer_decode_try(decoder, "backup_send", &value)) + set->brain_flags |= DSYNC_BRAIN_FLAG_BACKUP_SEND; + if (dsync_deserializer_decode_try(decoder, "backup_recv", &value)) + set->brain_flags |= DSYNC_BRAIN_FLAG_BACKUP_RECV; + if (dsync_deserializer_decode_try(decoder, "debug", &value)) + set->brain_flags |= DSYNC_BRAIN_FLAG_DEBUG; + if (dsync_deserializer_decode_try(decoder, "sync_visible_namespaces", &value)) + set->brain_flags |= DSYNC_BRAIN_FLAG_SYNC_VISIBLE_NAMESPACES; + if (dsync_deserializer_decode_try(decoder, "no_mail_sync", &value)) + set->brain_flags |= DSYNC_BRAIN_FLAG_NO_MAIL_SYNC; + if (dsync_deserializer_decode_try(decoder, "no_backup_overwrite", &value)) + set->brain_flags |= DSYNC_BRAIN_FLAG_NO_BACKUP_OVERWRITE; + if (dsync_deserializer_decode_try(decoder, "purge_remote", &value)) + set->brain_flags |= DSYNC_BRAIN_FLAG_PURGE_REMOTE; + if (dsync_deserializer_decode_try(decoder, "no_notify", &value)) + set->brain_flags |= DSYNC_BRAIN_FLAG_NO_NOTIFY; + if (dsync_deserializer_decode_try(decoder, "empty_hdr_workaround", &value)) + set->brain_flags |= DSYNC_BRAIN_FLAG_EMPTY_HDR_WORKAROUND; + if (dsync_deserializer_decode_try(decoder, "hashed_headers", &value)) + set->hashed_headers = (const char*const*)p_strsplit_tabescaped(pool, value); + set->hdr_hash_v2 = ibc->minor_version >= DSYNC_PROTOCOL_MINOR_HAVE_HDR_HASH_V2; + set->hdr_hash_v3 = ibc->minor_version >= DSYNC_PROTOCOL_MINOR_HAVE_HDR_HASH_V3; + + *set_r = set; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_stream_send_end_of_list(struct dsync_ibc *_ibc, + enum dsync_ibc_eol_type type) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + + i_assert(ibc->value_output == NULL); + + switch (type) { + case DSYNC_IBC_EOL_MAILBOX_ATTRIBUTE: + if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES) + return; + break; + default: + break; + } + + ibc->last_sent_item_eol = TRUE; + o_stream_nsend_str(ibc->output, END_OF_LIST_LINE"\n"); +} + +static void +dsync_ibc_stream_send_mailbox_state(struct dsync_ibc *_ibc, + const struct dsync_mailbox_state *state) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; + string_t *str = t_str_new(128); + + str_append_c(str, items[ITEM_MAILBOX_STATE].chr); + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX_STATE); + dsync_serializer_encode_add(encoder, "mailbox_guid", + guid_128_to_string(state->mailbox_guid)); + dsync_serializer_encode_add(encoder, "last_uidvalidity", + dec2str(state->last_uidvalidity)); + dsync_serializer_encode_add(encoder, "last_common_uid", + dec2str(state->last_common_uid)); + dsync_serializer_encode_add(encoder, "last_common_modseq", + dec2str(state->last_common_modseq)); + dsync_serializer_encode_add(encoder, "last_common_pvt_modseq", + dec2str(state->last_common_pvt_modseq)); + dsync_serializer_encode_add(encoder, "last_messages_count", + dec2str(state->last_messages_count)); + if (state->changes_during_sync) + dsync_serializer_encode_add(encoder, "changes_during_sync", ""); + + dsync_serializer_encode_finish(&encoder, str); + dsync_ibc_stream_send_string(ibc, str); +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_recv_mailbox_state(struct dsync_ibc *_ibc, + struct dsync_mailbox_state *state_r) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_deserializer_decoder *decoder; + const char *value; + enum dsync_ibc_recv_ret ret; + + i_zero(state_r); + + ret = dsync_ibc_stream_input_next(ibc, ITEM_MAILBOX_STATE, &decoder); + if (ret != DSYNC_IBC_RECV_RET_OK) + return ret; + + value = dsync_deserializer_decode_get(decoder, "mailbox_guid"); + if (guid_128_from_string(value, state_r->mailbox_guid) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid mailbox_guid"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + value = dsync_deserializer_decode_get(decoder, "last_uidvalidity"); + if (str_to_uint32(value, &state_r->last_uidvalidity) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid last_uidvalidity"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + value = dsync_deserializer_decode_get(decoder, "last_common_uid"); + if (str_to_uint32(value, &state_r->last_common_uid) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid last_common_uid"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + value = dsync_deserializer_decode_get(decoder, "last_common_modseq"); + if (str_to_uint64(value, &state_r->last_common_modseq) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid last_common_modseq"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + value = dsync_deserializer_decode_get(decoder, "last_common_pvt_modseq"); + if (str_to_uint64(value, &state_r->last_common_pvt_modseq) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid last_common_pvt_modseq"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "last_messages_count", &value) && + str_to_uint32(value, &state_r->last_messages_count) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid last_messages_count"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "changes_during_sync", &value)) + state_r->changes_during_sync = TRUE; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_stream_send_mailbox_tree_node(struct dsync_ibc *_ibc, + const char *const *name, + const struct dsync_mailbox_node *node) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; + string_t *str, *namestr; + + i_assert(*name != NULL); + + str = t_str_new(128); + str_append_c(str, items[ITEM_MAILBOX_TREE_NODE].chr); + + /* convert all hierarchy separators to tabs. mailbox names really + aren't supposed to have any tabs, but escape them anyway if there + are. */ + namestr = t_str_new(128); + for (; *name != NULL; name++) { + str_append_tabescaped(namestr, *name); + str_append_c(namestr, '\t'); + } + str_truncate(namestr, str_len(namestr)-1); + + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX_TREE_NODE); + dsync_serializer_encode_add(encoder, "name", str_c(namestr)); + switch (node->existence) { + case DSYNC_MAILBOX_NODE_NONEXISTENT: + dsync_serializer_encode_add(encoder, "existence", "n"); + break; + case DSYNC_MAILBOX_NODE_EXISTS: + dsync_serializer_encode_add(encoder, "existence", "y"); + break; + case DSYNC_MAILBOX_NODE_DELETED: + dsync_serializer_encode_add(encoder, "existence", "d"); + break; + } + + if (!guid_128_is_empty(node->mailbox_guid)) { + dsync_serializer_encode_add(encoder, "mailbox_guid", + guid_128_to_string(node->mailbox_guid)); + } + if (node->uid_validity != 0) { + dsync_serializer_encode_add(encoder, "uid_validity", + dec2str(node->uid_validity)); + } + if (node->uid_next != 0) { + dsync_serializer_encode_add(encoder, "uid_next", + dec2str(node->uid_next)); + } + if (node->last_renamed_or_created != 0) { + dsync_serializer_encode_add(encoder, "last_renamed_or_created", + dec2str(node->last_renamed_or_created)); + } + if (node->last_subscription_change != 0) { + dsync_serializer_encode_add(encoder, "last_subscription_change", + dec2str(node->last_subscription_change)); + } + if (node->subscribed) + dsync_serializer_encode_add(encoder, "subscribed", ""); + dsync_serializer_encode_finish(&encoder, str); + dsync_ibc_stream_send_string(ibc, str); +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_recv_mailbox_tree_node(struct dsync_ibc *_ibc, + const char *const **name_r, + const struct dsync_mailbox_node **node_r) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_deserializer_decoder *decoder; + struct dsync_mailbox_node *node; + const char *value; + enum dsync_ibc_recv_ret ret; + + ret = dsync_ibc_stream_input_next(ibc, ITEM_MAILBOX_TREE_NODE, &decoder); + if (ret != DSYNC_IBC_RECV_RET_OK) + return ret; + + p_clear(ibc->ret_pool); + node = p_new(ibc->ret_pool, struct dsync_mailbox_node, 1); + + value = dsync_deserializer_decode_get(decoder, "name"); + if (*value == '\0') { + dsync_ibc_input_error(ibc, decoder, "Empty name"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + *name_r = (void *)p_strsplit_tabescaped(ibc->ret_pool, value); + + value = dsync_deserializer_decode_get(decoder, "existence"); + switch (*value) { + case 'n': + node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT; + break; + case 'y': + node->existence = DSYNC_MAILBOX_NODE_EXISTS; + break; + case 'd': + node->existence = DSYNC_MAILBOX_NODE_DELETED; + break; + } + + if (dsync_deserializer_decode_try(decoder, "mailbox_guid", &value) && + guid_128_from_string(value, node->mailbox_guid) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid mailbox_guid"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "uid_validity", &value) && + str_to_uint32(value, &node->uid_validity) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid uid_validity"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "uid_next", &value) && + str_to_uint32(value, &node->uid_next) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid uid_next"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "last_renamed_or_created", &value) && + str_to_time(value, &node->last_renamed_or_created) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid last_renamed_or_created"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "last_subscription_change", &value) && + str_to_time(value, &node->last_subscription_change) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid last_subscription_change"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "subscribed", &value)) + node->subscribed = TRUE; + + *node_r = node; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_stream_encode_delete(string_t *str, + struct dsync_serializer_encoder *encoder, + const struct dsync_mailbox_delete *deletes, + unsigned int count, const char *key, + enum dsync_mailbox_delete_type type) +{ + unsigned int i; + + str_truncate(str, 0); + for (i = 0; i < count; i++) { + if (deletes[i].type == type) { + str_append(str, guid_128_to_string(deletes[i].guid)); + str_printfa(str, " %ld ", (long)deletes[i].timestamp); + } + } + if (str_len(str) > 0) { + str_truncate(str, str_len(str)-1); + dsync_serializer_encode_add(encoder, key, str_c(str)); + } +} + +static void +dsync_ibc_stream_send_mailbox_deletes(struct dsync_ibc *_ibc, + const struct dsync_mailbox_delete *deletes, + unsigned int count, char hierarchy_sep, + char escape_char) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; + string_t *str, *substr; + char sep[2]; + + str = t_str_new(128); + str_append_c(str, items[ITEM_MAILBOX_DELETE].chr); + + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX_DELETE); + sep[0] = hierarchy_sep; sep[1] = '\0'; + dsync_serializer_encode_add(encoder, "hierarchy_sep", sep); + sep[0] = escape_char; sep[1] = '\0'; + dsync_serializer_encode_add(encoder, "escape_char", sep); + + substr = t_str_new(128); + dsync_ibc_stream_encode_delete(substr, encoder, deletes, count, + "mailboxes", + DSYNC_MAILBOX_DELETE_TYPE_MAILBOX); + dsync_ibc_stream_encode_delete(substr, encoder, deletes, count, + "dirs", + DSYNC_MAILBOX_DELETE_TYPE_DIR); + dsync_ibc_stream_encode_delete(substr, encoder, deletes, count, + "unsubscribes", + DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE); + dsync_serializer_encode_finish(&encoder, str); + dsync_ibc_stream_send_string(ibc, str); +} + +ARRAY_DEFINE_TYPE(dsync_mailbox_delete, struct dsync_mailbox_delete); +static int +decode_mailbox_deletes(ARRAY_TYPE(dsync_mailbox_delete) *deletes, + const char *value, enum dsync_mailbox_delete_type type) +{ + struct dsync_mailbox_delete *del; + const char *const *tmp; + unsigned int i; + + tmp = t_strsplit(value, " "); + for (i = 0; tmp[i] != NULL; i += 2) { + del = array_append_space(deletes); + del->type = type; + if (guid_128_from_string(tmp[i], del->guid) < 0) + return -1; + if (tmp[i+1] == NULL || + str_to_time(tmp[i+1], &del->timestamp) < 0) + return -1; + } + return 0; +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_recv_mailbox_deletes(struct dsync_ibc *_ibc, + const struct dsync_mailbox_delete **deletes_r, + unsigned int *count_r, char *hierarchy_sep_r, + char *escape_char_r) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_deserializer_decoder *decoder; + ARRAY_TYPE(dsync_mailbox_delete) deletes; + const char *value; + enum dsync_ibc_recv_ret ret; + + ret = dsync_ibc_stream_input_next(ibc, ITEM_MAILBOX_DELETE, &decoder); + if (ret != DSYNC_IBC_RECV_RET_OK) + return ret; + + p_clear(ibc->ret_pool); + p_array_init(&deletes, ibc->ret_pool, 16); + + value = dsync_deserializer_decode_get(decoder, "hierarchy_sep"); + if (strlen(value) != 1) { + dsync_ibc_input_error(ibc, decoder, "Invalid hierarchy_sep"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + *hierarchy_sep_r = value[0]; + + if (!dsync_deserializer_decode_try(decoder, "escape_char", &value)) + *escape_char_r = '\0'; + else { + if (strlen(value) > 1) { + dsync_ibc_input_error(ibc, decoder, "Invalid escape_char '%s'", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + *escape_char_r = value[0]; + } + + if (dsync_deserializer_decode_try(decoder, "mailboxes", &value) && + decode_mailbox_deletes(&deletes, value, + DSYNC_MAILBOX_DELETE_TYPE_MAILBOX) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid mailboxes"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "dirs", &value) && + decode_mailbox_deletes(&deletes, value, + DSYNC_MAILBOX_DELETE_TYPE_DIR) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid dirs"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "unsubscribes", &value) && + decode_mailbox_deletes(&deletes, value, + DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid dirs"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + *deletes_r = array_get(&deletes, count_r); + return DSYNC_IBC_RECV_RET_OK; +} + +static const char * +get_cache_fields(struct dsync_ibc_stream *ibc, + const struct dsync_mailbox *dsync_box) +{ + struct dsync_serializer_encoder *encoder; + string_t *str; + const struct mailbox_cache_field *cache_fields; + unsigned int i, count; + char decision[3]; + + cache_fields = array_get(&dsync_box->cache_fields, &count); + if (count == 0) + return ""; + + str = t_str_new(128); + for (i = 0; i < count; i++) { + const struct mailbox_cache_field *field = &cache_fields[i]; + + encoder = dsync_serializer_encode_begin(ibc->serializers[ITEM_MAILBOX_CACHE_FIELD]); + dsync_serializer_encode_add(encoder, "name", field->name); + + memset(decision, 0, sizeof(decision)); + switch (field->decision & ENUM_NEGATE(MAIL_CACHE_DECISION_FORCED)) { + case MAIL_CACHE_DECISION_NO: + decision[0] = 'n'; + break; + case MAIL_CACHE_DECISION_TEMP: + decision[0] = 't'; + break; + case MAIL_CACHE_DECISION_YES: + decision[0] = 'y'; + break; + } + i_assert(decision[0] != '\0'); + if ((field->decision & MAIL_CACHE_DECISION_FORCED) != 0) + decision[1] = 'F'; + dsync_serializer_encode_add(encoder, "decision", decision); + if (field->last_used != 0) { + dsync_serializer_encode_add(encoder, "last_used", + dec2str(field->last_used)); + } + dsync_serializer_encode_finish(&encoder, str); + } + if (i > 0) { + /* remove the trailing LF */ + str_truncate(str, str_len(str)-1); + } + return str_c(str); +} + +static void +dsync_ibc_stream_send_mailbox(struct dsync_ibc *_ibc, + const struct dsync_mailbox *dsync_box) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; + string_t *str = t_str_new(128); + const char *value; + + str_append_c(str, items[ITEM_MAILBOX].chr); + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX); + dsync_serializer_encode_add(encoder, "mailbox_guid", + guid_128_to_string(dsync_box->mailbox_guid)); + + if (dsync_box->mailbox_lost) + dsync_serializer_encode_add(encoder, "mailbox_lost", ""); + if (dsync_box->mailbox_ignore) + dsync_serializer_encode_add(encoder, "mailbox_ignore", ""); + if (dsync_box->have_guids) + dsync_serializer_encode_add(encoder, "have_guids", ""); + if (dsync_box->have_save_guids) + dsync_serializer_encode_add(encoder, "have_save_guids", ""); + if (dsync_box->have_only_guid128) + dsync_serializer_encode_add(encoder, "have_only_guid128", ""); + dsync_serializer_encode_add(encoder, "uid_validity", + dec2str(dsync_box->uid_validity)); + dsync_serializer_encode_add(encoder, "uid_next", + dec2str(dsync_box->uid_next)); + dsync_serializer_encode_add(encoder, "messages_count", + dec2str(dsync_box->messages_count)); + dsync_serializer_encode_add(encoder, "first_recent_uid", + dec2str(dsync_box->first_recent_uid)); + dsync_serializer_encode_add(encoder, "highest_modseq", + dec2str(dsync_box->highest_modseq)); + dsync_serializer_encode_add(encoder, "highest_pvt_modseq", + dec2str(dsync_box->highest_pvt_modseq)); + + value = get_cache_fields(ibc, dsync_box); + if (value != NULL) + dsync_serializer_encode_add(encoder, "cache_fields", value); + + dsync_serializer_encode_finish(&encoder, str); + dsync_ibc_stream_send_string(ibc, str); +} + +static int +parse_cache_field(struct dsync_ibc_stream *ibc, struct dsync_mailbox *box, + const char *value) +{ + struct dsync_deserializer_decoder *decoder; + struct mailbox_cache_field field; + const char *error; + int ret = 0; + + if (dsync_deserializer_decode_begin(ibc->deserializers[ITEM_MAILBOX_CACHE_FIELD], + value, &decoder, &error) < 0) { + dsync_ibc_input_error(ibc, NULL, + "cache_field: Invalid input: %s", error); + return -1; + } + + i_zero(&field); + value = dsync_deserializer_decode_get(decoder, "name"); + field.name = p_strdup(ibc->ret_pool, value); + + value = dsync_deserializer_decode_get(decoder, "decision"); + switch (*value) { + case 'n': + field.decision = MAIL_CACHE_DECISION_NO; + break; + case 't': + field.decision = MAIL_CACHE_DECISION_TEMP; + break; + case 'y': + field.decision = MAIL_CACHE_DECISION_YES; + break; + default: + dsync_ibc_input_error(ibc, decoder, "Invalid decision: %s", + value); + ret = -1; + break; + } + if (value[1] == 'F') + field.decision |= MAIL_CACHE_DECISION_FORCED; + + if (dsync_deserializer_decode_try(decoder, "last_used", &value) && + str_to_time(value, &field.last_used) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid last_used"); + ret = -1; + } + array_push_back(&box->cache_fields, &field); + + dsync_deserializer_decode_finish(&decoder); + return ret; +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_recv_mailbox(struct dsync_ibc *_ibc, + const struct dsync_mailbox **dsync_box_r) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + pool_t pool = ibc->ret_pool; + struct dsync_deserializer_decoder *decoder; + struct dsync_mailbox *box; + const char *value; + enum dsync_ibc_recv_ret ret; + + p_clear(pool); + box = p_new(pool, struct dsync_mailbox, 1); + + ret = dsync_ibc_stream_input_next(ibc, ITEM_MAILBOX, &decoder); + if (ret != DSYNC_IBC_RECV_RET_OK) + return ret; + + value = dsync_deserializer_decode_get(decoder, "mailbox_guid"); + if (guid_128_from_string(value, box->mailbox_guid) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid mailbox_guid"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + if (dsync_deserializer_decode_try(decoder, "mailbox_lost", &value)) + box->mailbox_lost = TRUE; + if (dsync_deserializer_decode_try(decoder, "mailbox_ignore", &value)) + box->mailbox_ignore = TRUE; + if (dsync_deserializer_decode_try(decoder, "have_guids", &value)) + box->have_guids = TRUE; + if (dsync_deserializer_decode_try(decoder, "have_save_guids", &value) || + (box->have_guids && ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_SAVE_GUID)) + box->have_save_guids = TRUE; + if (dsync_deserializer_decode_try(decoder, "have_only_guid128", &value)) + box->have_only_guid128 = TRUE; + value = dsync_deserializer_decode_get(decoder, "uid_validity"); + if (str_to_uint32(value, &box->uid_validity) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid uid_validity"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + value = dsync_deserializer_decode_get(decoder, "uid_next"); + if (str_to_uint32(value, &box->uid_next) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid uid_next"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + value = dsync_deserializer_decode_get(decoder, "messages_count"); + if (str_to_uint32(value, &box->messages_count) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid messages_count"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + value = dsync_deserializer_decode_get(decoder, "first_recent_uid"); + if (str_to_uint32(value, &box->first_recent_uid) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid first_recent_uid"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + value = dsync_deserializer_decode_get(decoder, "highest_modseq"); + if (str_to_uint64(value, &box->highest_modseq) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid highest_modseq"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + value = dsync_deserializer_decode_get(decoder, "highest_pvt_modseq"); + if (str_to_uint64(value, &box->highest_pvt_modseq) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid highest_pvt_modseq"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + p_array_init(&box->cache_fields, pool, 32); + if (dsync_deserializer_decode_try(decoder, "cache_fields", &value)) { + const char *const *fields = t_strsplit(value, "\n"); + for (; *fields != NULL; fields++) { + if (parse_cache_field(ibc, box, *fields) < 0) + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + } + + *dsync_box_r = box; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_stream_send_mailbox_attribute(struct dsync_ibc *_ibc, + const struct dsync_mailbox_attribute *attr) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; + string_t *str = t_str_new(128); + char type[2]; + + if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES) + return; + + str_append_c(str, items[ITEM_MAILBOX_ATTRIBUTE].chr); + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX_ATTRIBUTE); + + type[0] = type[1] = '\0'; + switch (attr->type) { + case MAIL_ATTRIBUTE_TYPE_PRIVATE: + type[0] = 'p'; + break; + case MAIL_ATTRIBUTE_TYPE_SHARED: + type[0] = 's'; + break; + } + i_assert(type[0] != '\0'); + dsync_serializer_encode_add(encoder, "type", type); + dsync_serializer_encode_add(encoder, "key", attr->key); + if (attr->value != NULL) + dsync_serializer_encode_add(encoder, "value", attr->value); + else if (attr->value_stream != NULL) + dsync_serializer_encode_add(encoder, "stream", ""); + + if (attr->deleted) + dsync_serializer_encode_add(encoder, "deleted", ""); + if (attr->last_change != 0) { + dsync_serializer_encode_add(encoder, "last_change", + dec2str(attr->last_change)); + } + if (attr->modseq != 0) { + dsync_serializer_encode_add(encoder, "modseq", + dec2str(attr->modseq)); + } + + dsync_serializer_encode_finish(&encoder, str); + dsync_ibc_stream_send_string(ibc, str); + + if (attr->value_stream != NULL) { + ibc->value_output_last = '\0'; + ibc->value_output = attr->value_stream; + i_stream_ref(ibc->value_output); + (void)dsync_ibc_stream_send_value_stream(ibc); + } +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_recv_mailbox_attribute(struct dsync_ibc *_ibc, + const struct dsync_mailbox_attribute **attr_r) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + pool_t pool = ibc->ret_pool; + struct dsync_deserializer_decoder *decoder; + struct dsync_mailbox_attribute *attr; + const char *value; + enum dsync_ibc_recv_ret ret; + + if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES) + return DSYNC_IBC_RECV_RET_FINISHED; + + if (ibc->value_input != NULL) { + /* wait until the mail's stream has been read */ + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + if (ibc->cur_attr != NULL) { + /* finished reading the stream, return the mail now */ + *attr_r = ibc->cur_attr; + ibc->cur_attr = NULL; + return DSYNC_IBC_RECV_RET_OK; + } + + p_clear(pool); + attr = p_new(pool, struct dsync_mailbox_attribute, 1); + + ret = dsync_ibc_stream_input_next(ibc, ITEM_MAILBOX_ATTRIBUTE, &decoder); + if (ret != DSYNC_IBC_RECV_RET_OK) + return ret; + + value = dsync_deserializer_decode_get(decoder, "type"); + switch (*value) { + case 'p': + attr->type = MAIL_ATTRIBUTE_TYPE_PRIVATE; + break; + case 's': + attr->type = MAIL_ATTRIBUTE_TYPE_SHARED; + break; + default: + dsync_ibc_input_error(ibc, decoder, "Invalid type: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + value = dsync_deserializer_decode_get(decoder, "key"); + attr->key = p_strdup(pool, value); + + if (dsync_deserializer_decode_try(decoder, "deleted", &value)) + attr->deleted = TRUE; + if (dsync_deserializer_decode_try(decoder, "last_change", &value) && + str_to_time(value, &attr->last_change) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid last_change"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "modseq", &value) && + str_to_uint64(value, &attr->modseq) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid modseq"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + /* NOTE: stream reading must be the last here, because reading a large + stream will be finished later by return TRYAGAIN. We need to + deserialize all the other fields before that or they'll get lost. */ + if (dsync_deserializer_decode_try(decoder, "stream", &value)) { + attr->value_stream = dsync_ibc_stream_input_stream(ibc); + if (dsync_ibc_stream_read_mail_stream(ibc) <= 0) { + ibc->cur_attr = attr; + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + /* already finished reading the stream */ + i_assert(ibc->value_input == NULL); + } else if (dsync_deserializer_decode_try(decoder, "value", &value)) + attr->value = p_strdup(pool, value); + + *attr_r = attr; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_stream_send_change(struct dsync_ibc *_ibc, + const struct dsync_mail_change *change) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; + string_t *str = t_str_new(128); + char type[2]; + + str_append_c(str, items[ITEM_MAIL_CHANGE].chr); + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAIL_CHANGE); + + type[0] = type[1] = '\0'; + switch (change->type) { + case DSYNC_MAIL_CHANGE_TYPE_SAVE: + type[0] = 's'; + break; + case DSYNC_MAIL_CHANGE_TYPE_EXPUNGE: + type[0] = 'e'; + break; + case DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE: + type[0] = 'f'; + break; + } + i_assert(type[0] != '\0'); + dsync_serializer_encode_add(encoder, "type", type); + dsync_serializer_encode_add(encoder, "uid", dec2str(change->uid)); + if (change->guid != NULL) + dsync_serializer_encode_add(encoder, "guid", change->guid); + if (change->hdr_hash != NULL) { + dsync_serializer_encode_add(encoder, "hdr_hash", + change->hdr_hash); + } + if (change->modseq != 0) { + dsync_serializer_encode_add(encoder, "modseq", + dec2str(change->modseq)); + } + if (change->pvt_modseq != 0) { + dsync_serializer_encode_add(encoder, "pvt_modseq", + dec2str(change->pvt_modseq)); + } + if (change->add_flags != 0) { + dsync_serializer_encode_add(encoder, "add_flags", + t_strdup_printf("%x", change->add_flags)); + } + if (change->remove_flags != 0) { + dsync_serializer_encode_add(encoder, "remove_flags", + t_strdup_printf("%x", change->remove_flags)); + } + if (change->final_flags != 0) { + dsync_serializer_encode_add(encoder, "final_flags", + t_strdup_printf("%x", change->final_flags)); + } + if (change->keywords_reset) + dsync_serializer_encode_add(encoder, "keywords_reset", ""); + + if (array_is_created(&change->keyword_changes) && + array_count(&change->keyword_changes) > 0) { + string_t *kw_str = t_str_new(128); + const char *const *changes; + unsigned int i, count; + + changes = array_get(&change->keyword_changes, &count); + str_append_tabescaped(kw_str, changes[0]); + for (i = 1; i < count; i++) { + str_append_c(kw_str, '\t'); + str_append_tabescaped(kw_str, changes[i]); + } + dsync_serializer_encode_add(encoder, "keyword_changes", + str_c(kw_str)); + } + if (change->received_timestamp > 0) { + dsync_serializer_encode_add(encoder, "received_timestamp", + t_strdup_printf("%"PRIxTIME_T, change->received_timestamp)); + } + if (change->virtual_size > 0) { + dsync_serializer_encode_add(encoder, "virtual_size", + t_strdup_printf("%llx", (unsigned long long)change->virtual_size)); + } + + dsync_serializer_encode_finish(&encoder, str); + dsync_ibc_stream_send_string(ibc, str); +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_recv_change(struct dsync_ibc *_ibc, + const struct dsync_mail_change **change_r) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + pool_t pool = ibc->ret_pool; + struct dsync_deserializer_decoder *decoder; + struct dsync_mail_change *change; + const char *value; + unsigned int uintval; + unsigned long long ullongval; + enum dsync_ibc_recv_ret ret; + + p_clear(pool); + change = p_new(pool, struct dsync_mail_change, 1); + + ret = dsync_ibc_stream_input_next(ibc, ITEM_MAIL_CHANGE, &decoder); + if (ret != DSYNC_IBC_RECV_RET_OK) + return ret; + + value = dsync_deserializer_decode_get(decoder, "type"); + switch (*value) { + case 's': + change->type = DSYNC_MAIL_CHANGE_TYPE_SAVE; + break; + case 'e': + change->type = DSYNC_MAIL_CHANGE_TYPE_EXPUNGE; + break; + case 'f': + change->type = DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE; + break; + default: + dsync_ibc_input_error(ibc, decoder, "Invalid type: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + value = dsync_deserializer_decode_get(decoder, "uid"); + if (str_to_uint32(value, &change->uid) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid uid"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + if (dsync_deserializer_decode_try(decoder, "guid", &value)) + change->guid = p_strdup(pool, value); + if (dsync_deserializer_decode_try(decoder, "hdr_hash", &value)) + change->hdr_hash = p_strdup(pool, value); + if (dsync_deserializer_decode_try(decoder, "modseq", &value) && + str_to_uint64(value, &change->modseq) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid modseq"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "pvt_modseq", &value) && + str_to_uint64(value, &change->pvt_modseq) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid pvt_modseq"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + if (dsync_deserializer_decode_try(decoder, "add_flags", &value)) { + if (str_to_uint_hex(value, &uintval) < 0 || + uintval > (uint8_t)-1) { + dsync_ibc_input_error(ibc, decoder, + "Invalid add_flags: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + change->add_flags = uintval; + } + if (dsync_deserializer_decode_try(decoder, "remove_flags", &value)) { + if (str_to_uint_hex(value, &uintval) < 0 || + uintval > (uint8_t)-1) { + dsync_ibc_input_error(ibc, decoder, + "Invalid remove_flags: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + change->remove_flags = uintval; + } + if (dsync_deserializer_decode_try(decoder, "final_flags", &value)) { + if (str_to_uint_hex(value, &uintval) < 0 || + uintval > (uint8_t)-1) { + dsync_ibc_input_error(ibc, decoder, + "Invalid final_flags: %s", value); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + change->final_flags = uintval; + } + if (dsync_deserializer_decode_try(decoder, "keywords_reset", &value)) + change->keywords_reset = TRUE; + + if (dsync_deserializer_decode_try(decoder, "keyword_changes", &value) && + *value != '\0') { + const char *const *changes = t_strsplit_tabescaped(value); + unsigned int i, count = str_array_length(changes); + + p_array_init(&change->keyword_changes, pool, count); + for (i = 0; i < count; i++) { + value = p_strdup(pool, changes[i]); + array_push_back(&change->keyword_changes, &value); + } + } + if (dsync_deserializer_decode_try(decoder, "received_timestamp", &value)) { + if (str_to_ullong_hex(value, &ullongval) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid received_timestamp"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + change->received_timestamp = ullongval; + } + if (dsync_deserializer_decode_try(decoder, "virtual_size", &value)) { + if (str_to_ullong_hex(value, &ullongval) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid virtual_size"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + change->virtual_size = ullongval; + } + + *change_r = change; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_stream_send_mail_request(struct dsync_ibc *_ibc, + const struct dsync_mail_request *request) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; + string_t *str = t_str_new(128); + + str_append_c(str, items[ITEM_MAIL_REQUEST].chr); + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAIL_REQUEST); + if (request->guid != NULL) + dsync_serializer_encode_add(encoder, "guid", request->guid); + if (request->uid != 0) { + dsync_serializer_encode_add(encoder, "uid", + dec2str(request->uid)); + } + dsync_serializer_encode_finish(&encoder, str); + dsync_ibc_stream_send_string(ibc, str); +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_recv_mail_request(struct dsync_ibc *_ibc, + const struct dsync_mail_request **request_r) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_deserializer_decoder *decoder; + struct dsync_mail_request *request; + const char *value; + enum dsync_ibc_recv_ret ret; + + p_clear(ibc->ret_pool); + request = p_new(ibc->ret_pool, struct dsync_mail_request, 1); + + ret = dsync_ibc_stream_input_next(ibc, ITEM_MAIL_REQUEST, &decoder); + if (ret != DSYNC_IBC_RECV_RET_OK) + return ret; + + if (dsync_deserializer_decode_try(decoder, "guid", &value)) + request->guid = p_strdup(ibc->ret_pool, value); + if (dsync_deserializer_decode_try(decoder, "uid", &value) && + str_to_uint32(value, &request->uid) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid uid"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + + *request_r = request; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_stream_send_mail(struct dsync_ibc *_ibc, + const struct dsync_mail *mail) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; + string_t *str = t_str_new(128); + + i_assert(!mail->minimal_fields); + i_assert(ibc->value_output == NULL); + + str_append_c(str, items[ITEM_MAIL].chr); + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAIL); + if (mail->guid != NULL) + dsync_serializer_encode_add(encoder, "guid", mail->guid); + if (mail->uid != 0) + dsync_serializer_encode_add(encoder, "uid", dec2str(mail->uid)); + if (mail->pop3_uidl != NULL) { + dsync_serializer_encode_add(encoder, "pop3_uidl", + mail->pop3_uidl); + } + if (mail->pop3_order > 0) { + dsync_serializer_encode_add(encoder, "pop3_order", + dec2str(mail->pop3_order)); + } + if (mail->received_date > 0) { + dsync_serializer_encode_add(encoder, "received_date", + dec2str(mail->received_date)); + } + if (mail->saved_date != 0) { + dsync_serializer_encode_add(encoder, "saved_date", + dec2str(mail->saved_date)); + } + if (mail->input != NULL) + dsync_serializer_encode_add(encoder, "stream", ""); + + dsync_serializer_encode_finish(&encoder, str); + dsync_ibc_stream_send_string(ibc, str); + + if (mail->input != NULL) { + ibc->value_output_last = '\0'; + ibc->value_output = mail->input; + i_stream_ref(ibc->value_output); + (void)dsync_ibc_stream_send_value_stream(ibc); + } +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_recv_mail(struct dsync_ibc *_ibc, struct dsync_mail **mail_r) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + pool_t pool = ibc->ret_pool; + struct dsync_deserializer_decoder *decoder; + struct dsync_mail *mail; + const char *value; + enum dsync_ibc_recv_ret ret; + + if (ibc->value_input != NULL) { + /* wait until the mail's stream has been read */ + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (ibc->cur_mail != NULL) { + /* finished reading the stream, return the mail now */ + *mail_r = ibc->cur_mail; + ibc->cur_mail = NULL; + return DSYNC_IBC_RECV_RET_OK; + } + + p_clear(pool); + mail = p_new(pool, struct dsync_mail, 1); + + ret = dsync_ibc_stream_input_next(ibc, ITEM_MAIL, &decoder); + if (ret != DSYNC_IBC_RECV_RET_OK) + return ret; + + if (dsync_deserializer_decode_try(decoder, "guid", &value)) + mail->guid = p_strdup(pool, value); + if (dsync_deserializer_decode_try(decoder, "uid", &value) && + str_to_uint32(value, &mail->uid) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid uid"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "pop3_uidl", &value)) + mail->pop3_uidl = p_strdup(pool, value); + if (dsync_deserializer_decode_try(decoder, "pop3_order", &value) && + str_to_uint32(value, &mail->pop3_order) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid pop3_order"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "received_date", &value) && + str_to_time(value, &mail->received_date) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid received_date"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "saved_date", &value) && + str_to_time(value, &mail->saved_date) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid saved_date"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "stream", &value)) { + mail->input = dsync_ibc_stream_input_stream(ibc); + if (dsync_ibc_stream_read_mail_stream(ibc) <= 0) { + ibc->cur_mail = mail; + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + /* already finished reading the stream */ + i_assert(ibc->value_input == NULL); + } + + *mail_r = mail; + return DSYNC_IBC_RECV_RET_OK; +} + +static void +dsync_ibc_stream_send_finish(struct dsync_ibc *_ibc, const char *error, + enum mail_error mail_error, + bool require_full_resync) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_serializer_encoder *encoder; + string_t *str = t_str_new(128); + + str_append_c(str, items[ITEM_FINISH].chr); + encoder = dsync_ibc_send_encode_begin(ibc, ITEM_FINISH); + if (error != NULL) + dsync_serializer_encode_add(encoder, "error", error); + if (mail_error != 0) { + dsync_serializer_encode_add(encoder, "mail_error", + dec2str(mail_error)); + } + if (require_full_resync) + dsync_serializer_encode_add(encoder, "require_full_resync", ""); + dsync_serializer_encode_finish(&encoder, str); + dsync_ibc_stream_send_string(ibc, str); +} + +static enum dsync_ibc_recv_ret +dsync_ibc_stream_recv_finish(struct dsync_ibc *_ibc, const char **error_r, + enum mail_error *mail_error_r, + bool *require_full_resync_r) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + struct dsync_deserializer_decoder *decoder; + const char *value; + enum dsync_ibc_recv_ret ret; + int i = 0; + + *error_r = NULL; + *mail_error_r = 0; + *require_full_resync_r = FALSE; + + p_clear(ibc->ret_pool); + + if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_FINISH) + return DSYNC_IBC_RECV_RET_OK; + + ret = dsync_ibc_stream_input_next(ibc, ITEM_FINISH, &decoder); + if (ret != DSYNC_IBC_RECV_RET_OK) + return ret; + + if (dsync_deserializer_decode_try(decoder, "error", &value)) + *error_r = p_strdup(ibc->ret_pool, value); + if (dsync_deserializer_decode_try(decoder, "mail_error", &value) && + str_to_int(value, &i) < 0) { + dsync_ibc_input_error(ibc, decoder, "Invalid mail_error"); + return DSYNC_IBC_RECV_RET_TRYAGAIN; + } + if (dsync_deserializer_decode_try(decoder, "require_full_resync", &value)) + *require_full_resync_r = TRUE; + *mail_error_r = i; + + ibc->finish_received = TRUE; + return DSYNC_IBC_RECV_RET_OK; +} + +static void dsync_ibc_stream_close_mail_streams(struct dsync_ibc *_ibc) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + + if (ibc->value_output != NULL) { + i_stream_unref(&ibc->value_output); + dsync_ibc_stream_stop(ibc); + } +} + +static bool dsync_ibc_stream_is_send_queue_full(struct dsync_ibc *_ibc) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + size_t bytes; + + if (ibc->value_output != NULL) + return TRUE; + + bytes = o_stream_get_buffer_used_size(ibc->output); + if (bytes < DSYNC_IBC_STREAM_OUTBUF_THROTTLE_SIZE) + return FALSE; + + o_stream_set_flush_pending(ibc->output, TRUE); + return TRUE; +} + +static bool dsync_ibc_stream_has_pending_data(struct dsync_ibc *_ibc) +{ + struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc; + + return ibc->has_pending_data; +} + +static const struct dsync_ibc_vfuncs dsync_ibc_stream_vfuncs = { + dsync_ibc_stream_deinit, + dsync_ibc_stream_send_handshake, + dsync_ibc_stream_recv_handshake, + dsync_ibc_stream_send_end_of_list, + dsync_ibc_stream_send_mailbox_state, + dsync_ibc_stream_recv_mailbox_state, + dsync_ibc_stream_send_mailbox_tree_node, + dsync_ibc_stream_recv_mailbox_tree_node, + dsync_ibc_stream_send_mailbox_deletes, + dsync_ibc_stream_recv_mailbox_deletes, + dsync_ibc_stream_send_mailbox, + dsync_ibc_stream_recv_mailbox, + dsync_ibc_stream_send_mailbox_attribute, + dsync_ibc_stream_recv_mailbox_attribute, + dsync_ibc_stream_send_change, + dsync_ibc_stream_recv_change, + dsync_ibc_stream_send_mail_request, + dsync_ibc_stream_recv_mail_request, + dsync_ibc_stream_send_mail, + dsync_ibc_stream_recv_mail, + dsync_ibc_stream_send_finish, + dsync_ibc_stream_recv_finish, + dsync_ibc_stream_close_mail_streams, + dsync_ibc_stream_is_send_queue_full, + dsync_ibc_stream_has_pending_data +}; + +struct dsync_ibc * +dsync_ibc_init_stream(struct istream *input, struct ostream *output, + const char *name, const char *temp_path_prefix, + unsigned int timeout_secs) +{ + struct dsync_ibc_stream *ibc; + + ibc = i_new(struct dsync_ibc_stream, 1); + ibc->ibc.v = dsync_ibc_stream_vfuncs; + ibc->input = input; + ibc->output = output; + i_stream_ref(ibc->input); + o_stream_ref(ibc->output); + ibc->name = i_strdup(name); + ibc->temp_path_prefix = i_strdup(temp_path_prefix); + ibc->timeout_secs = timeout_secs; + ibc->ret_pool = pool_alloconly_create("ibc stream data", 2048); + dsync_ibc_stream_init(ibc); + return &ibc->ibc; +} diff --git a/src/doveadm/dsync/dsync-ibc.c b/src/doveadm/dsync/dsync-ibc.c new file mode 100644 index 0000000..ede7b83 --- /dev/null +++ b/src/doveadm/dsync/dsync-ibc.c @@ -0,0 +1,239 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "dsync-mail.h" +#include "dsync-ibc-private.h" + +void dsync_ibc_deinit(struct dsync_ibc **_ibc) +{ + struct dsync_ibc *ibc = *_ibc; + + *_ibc = NULL; + ibc->v.deinit(ibc); +} + +void dsync_ibc_set_io_callback(struct dsync_ibc *ibc, + io_callback_t *callback, void *context) +{ + ibc->io_callback = callback; + ibc->io_context = context; +} + +void dsync_ibc_send_handshake(struct dsync_ibc *ibc, + const struct dsync_ibc_settings *set) +{ + ibc->v.send_handshake(ibc, set); +} + +enum dsync_ibc_recv_ret +dsync_ibc_recv_handshake(struct dsync_ibc *ibc, + const struct dsync_ibc_settings **set_r) +{ + return ibc->v.recv_handshake(ibc, set_r); +} + +static enum dsync_ibc_send_ret +dsync_ibc_send_ret(struct dsync_ibc *ibc) +{ + return ibc->v.is_send_queue_full(ibc) ? + DSYNC_IBC_SEND_RET_FULL : + DSYNC_IBC_SEND_RET_OK; +} + +enum dsync_ibc_send_ret +dsync_ibc_send_end_of_list(struct dsync_ibc *ibc, enum dsync_ibc_eol_type type) +{ + ibc->v.send_end_of_list(ibc, type); + return dsync_ibc_send_ret(ibc); +} + +enum dsync_ibc_send_ret +dsync_ibc_send_mailbox_state(struct dsync_ibc *ibc, + const struct dsync_mailbox_state *state) +{ + T_BEGIN { + ibc->v.send_mailbox_state(ibc, state); + } T_END; + return dsync_ibc_send_ret(ibc); +} + +enum dsync_ibc_recv_ret +dsync_ibc_recv_mailbox_state(struct dsync_ibc *ibc, + struct dsync_mailbox_state *state_r) +{ + return ibc->v.recv_mailbox_state(ibc, state_r); +} + +enum dsync_ibc_send_ret +dsync_ibc_send_mailbox_tree_node(struct dsync_ibc *ibc, + const char *const *name, + const struct dsync_mailbox_node *node) +{ + i_assert(*name != NULL); + + T_BEGIN { + ibc->v.send_mailbox_tree_node(ibc, name, node); + } T_END; + return dsync_ibc_send_ret(ibc); +} + +enum dsync_ibc_recv_ret +dsync_ibc_recv_mailbox_tree_node(struct dsync_ibc *ibc, + const char *const **name_r, + const struct dsync_mailbox_node **node_r) +{ + return ibc->v.recv_mailbox_tree_node(ibc, name_r, node_r); +} + +enum dsync_ibc_send_ret +dsync_ibc_send_mailbox_deletes(struct dsync_ibc *ibc, + const struct dsync_mailbox_delete *deletes, + unsigned int count, char hierarchy_sep, + char escape_char) +{ + T_BEGIN { + ibc->v.send_mailbox_deletes(ibc, deletes, count, + hierarchy_sep, escape_char); + } T_END; + return dsync_ibc_send_ret(ibc); +} + +enum dsync_ibc_recv_ret +dsync_ibc_recv_mailbox_deletes(struct dsync_ibc *ibc, + const struct dsync_mailbox_delete **deletes_r, + unsigned int *count_r, char *hierarchy_sep_r, + char *escape_char_r) +{ + return ibc->v.recv_mailbox_deletes(ibc, deletes_r, count_r, + hierarchy_sep_r, escape_char_r); +} + +enum dsync_ibc_send_ret +dsync_ibc_send_mailbox(struct dsync_ibc *ibc, + const struct dsync_mailbox *dsync_box) +{ + T_BEGIN { + ibc->v.send_mailbox(ibc, dsync_box); + } T_END; + return dsync_ibc_send_ret(ibc); +} + +enum dsync_ibc_recv_ret +dsync_ibc_recv_mailbox(struct dsync_ibc *ibc, + const struct dsync_mailbox **dsync_box_r) +{ + return ibc->v.recv_mailbox(ibc, dsync_box_r); +} + +enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_send_mailbox_attribute(struct dsync_ibc *ibc, + const struct dsync_mailbox_attribute *attr) +{ + T_BEGIN { + ibc->v.send_mailbox_attribute(ibc, attr); + } T_END; + return dsync_ibc_send_ret(ibc); +} + +enum dsync_ibc_recv_ret +dsync_ibc_recv_mailbox_attribute(struct dsync_ibc *ibc, + const struct dsync_mailbox_attribute **attr_r) +{ + return ibc->v.recv_mailbox_attribute(ibc, attr_r); +} + +enum dsync_ibc_send_ret +dsync_ibc_send_change(struct dsync_ibc *ibc, + const struct dsync_mail_change *change) +{ + i_assert(change->uid > 0); + + T_BEGIN { + ibc->v.send_change(ibc, change); + } T_END; + return dsync_ibc_send_ret(ibc); +} + +enum dsync_ibc_recv_ret +dsync_ibc_recv_change(struct dsync_ibc *ibc, + const struct dsync_mail_change **change_r) +{ + return ibc->v.recv_change(ibc, change_r); +} + +enum dsync_ibc_send_ret +dsync_ibc_send_mail_request(struct dsync_ibc *ibc, + const struct dsync_mail_request *request) +{ + i_assert(request->guid != NULL || request->uid != 0); + + T_BEGIN { + ibc->v.send_mail_request(ibc, request); + } T_END; + return dsync_ibc_send_ret(ibc); +} + +enum dsync_ibc_recv_ret +dsync_ibc_recv_mail_request(struct dsync_ibc *ibc, + const struct dsync_mail_request **request_r) +{ + return ibc->v.recv_mail_request(ibc, request_r); +} + +enum dsync_ibc_send_ret +dsync_ibc_send_mail(struct dsync_ibc *ibc, const struct dsync_mail *mail) +{ + i_assert(*mail->guid != '\0' || mail->uid != 0); + + T_BEGIN { + ibc->v.send_mail(ibc, mail); + } T_END; + return dsync_ibc_send_ret(ibc); +} + +enum dsync_ibc_recv_ret +dsync_ibc_recv_mail(struct dsync_ibc *ibc, struct dsync_mail **mail_r) +{ + return ibc->v.recv_mail(ibc, mail_r); +} + +void dsync_ibc_send_finish(struct dsync_ibc *ibc, const char *error, + enum mail_error mail_error, + bool require_full_resync) +{ + ibc->v.send_finish(ibc, error, mail_error, require_full_resync); +} + +enum dsync_ibc_recv_ret +dsync_ibc_recv_finish(struct dsync_ibc *ibc, const char **error_r, + enum mail_error *mail_error_r, + bool *require_full_resync_r) +{ + return ibc->v.recv_finish(ibc, error_r, mail_error_r, + require_full_resync_r); +} + +void dsync_ibc_close_mail_streams(struct dsync_ibc *ibc) +{ + ibc->v.close_mail_streams(ibc); +} + +bool dsync_ibc_has_failed(struct dsync_ibc *ibc) +{ + return ibc->failed; +} + +bool dsync_ibc_has_timed_out(struct dsync_ibc *ibc) +{ + return ibc->timeout; +} + +bool dsync_ibc_is_send_queue_full(struct dsync_ibc *ibc) +{ + return ibc->v.is_send_queue_full(ibc); +} + +bool dsync_ibc_has_pending_data(struct dsync_ibc *ibc) +{ + return ibc->v.has_pending_data(ibc); +} diff --git a/src/doveadm/dsync/dsync-ibc.h b/src/doveadm/dsync/dsync-ibc.h new file mode 100644 index 0000000..dc85560 --- /dev/null +++ b/src/doveadm/dsync/dsync-ibc.h @@ -0,0 +1,178 @@ +#ifndef DSYNC_IBC_H +#define DSYNC_IBC_H + +/* dsync inter-brain communicator */ + +#include "ioloop.h" +#include "guid.h" +#include "mail-error.h" +#include "dsync-brain.h" + +struct dsync_mailbox; +struct dsync_mailbox_state; +struct dsync_mailbox_node; +struct dsync_mailbox_delete; +struct dsync_mailbox_attribute; +struct dsync_mail; +struct dsync_mail_change; +struct dsync_mail_request; + +enum dsync_ibc_send_ret { + DSYNC_IBC_SEND_RET_OK = 1, + /* send queue is full, stop sending more */ + DSYNC_IBC_SEND_RET_FULL = 0 +}; + +enum dsync_ibc_recv_ret { + DSYNC_IBC_RECV_RET_FINISHED = -1, + /* try again / error (the error handling delayed until io callback) */ + DSYNC_IBC_RECV_RET_TRYAGAIN = 0, + DSYNC_IBC_RECV_RET_OK = 1 +}; + +enum dsync_ibc_eol_type { + DSYNC_IBC_EOL_MAILBOX_STATE, + DSYNC_IBC_EOL_MAILBOX_TREE, + DSYNC_IBC_EOL_MAILBOX_ATTRIBUTE, + DSYNC_IBC_EOL_MAILBOX, + DSYNC_IBC_EOL_MAIL_CHANGES, + DSYNC_IBC_EOL_MAIL_REQUESTS, + DSYNC_IBC_EOL_MAILS +}; + +struct dsync_ibc_settings { + /* Server hostname. Used for determining which server does the + locking. */ + const char *hostname; + /* if non-NULL, sync only these namespaces (LF-separated) */ + const char *sync_ns_prefixes; + /* if non-NULL, sync only this mailbox name */ + const char *sync_box; + /* if non-NULL, use this mailbox for finding messages with GUIDs and + copying them instead of saving them again. */ + const char *virtual_all_box; + /* if non-empty, sync only this mailbox GUID */ + guid_128_t sync_box_guid; + /* Exclude these mailboxes from the sync. They can contain '*' + wildcards and be \special-use flags. */ + const char *const *exclude_mailboxes; + /* Sync only mails with received timestamp at least this high. */ + time_t sync_since_timestamp; + /* Sync only mails with received timestamp less or equal than this */ + time_t sync_until_timestamp; + /* Don't sync mails larger than this. */ + uoff_t sync_max_size; + /* Sync only mails with specified flags. */ + const char *sync_flags; + /* Hashed headers */ + const char *const *hashed_headers; + + char alt_char; + enum dsync_brain_sync_type sync_type; + enum dsync_brain_flags brain_flags; + bool hdr_hash_v2; + bool hdr_hash_v3; + unsigned int lock_timeout; + unsigned int import_commit_msgs_interval; +}; + +void dsync_ibc_init_pipe(struct dsync_ibc **ibc1_r, + struct dsync_ibc **ibc2_r); +struct dsync_ibc * +dsync_ibc_init_stream(struct istream *input, struct ostream *output, + const char *name, const char *temp_path_prefix, + unsigned int timeout_secs); +void dsync_ibc_deinit(struct dsync_ibc **ibc); + +/* I/O callback is called whenever new data is available. It's also called on + errors, so check first the error status. */ +void dsync_ibc_set_io_callback(struct dsync_ibc *ibc, + io_callback_t *callback, void *context); + +void dsync_ibc_send_handshake(struct dsync_ibc *ibc, + const struct dsync_ibc_settings *set); +enum dsync_ibc_recv_ret +dsync_ibc_recv_handshake(struct dsync_ibc *ibc, + const struct dsync_ibc_settings **set_r); + +enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_send_end_of_list(struct dsync_ibc *ibc, enum dsync_ibc_eol_type type); + +enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_send_mailbox_state(struct dsync_ibc *ibc, + const struct dsync_mailbox_state *state); +enum dsync_ibc_recv_ret +dsync_ibc_recv_mailbox_state(struct dsync_ibc *ibc, + struct dsync_mailbox_state *state_r); + +enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_send_mailbox_tree_node(struct dsync_ibc *ibc, + const char *const *name, + const struct dsync_mailbox_node *node); +enum dsync_ibc_recv_ret +dsync_ibc_recv_mailbox_tree_node(struct dsync_ibc *ibc, + const char *const **name_r, + const struct dsync_mailbox_node **node_r); + +enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_send_mailbox_deletes(struct dsync_ibc *ibc, + const struct dsync_mailbox_delete *deletes, + unsigned int count, char hierarchy_sep, + char escape_char); +enum dsync_ibc_recv_ret +dsync_ibc_recv_mailbox_deletes(struct dsync_ibc *ibc, + const struct dsync_mailbox_delete **deletes_r, + unsigned int *count_r, char *hierarchy_sep_r, + char *escape_char_r); + +enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_send_mailbox(struct dsync_ibc *ibc, + const struct dsync_mailbox *dsync_box); +enum dsync_ibc_recv_ret +dsync_ibc_recv_mailbox(struct dsync_ibc *ibc, + const struct dsync_mailbox **dsync_box_r); + +enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_send_mailbox_attribute(struct dsync_ibc *ibc, + const struct dsync_mailbox_attribute *attr); +enum dsync_ibc_recv_ret +dsync_ibc_recv_mailbox_attribute(struct dsync_ibc *ibc, + const struct dsync_mailbox_attribute **attr_r); + +enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_send_change(struct dsync_ibc *ibc, + const struct dsync_mail_change *change); +enum dsync_ibc_recv_ret +dsync_ibc_recv_change(struct dsync_ibc *ibc, + const struct dsync_mail_change **change_r); + +enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_send_mail_request(struct dsync_ibc *ibc, + const struct dsync_mail_request *request); +enum dsync_ibc_recv_ret +dsync_ibc_recv_mail_request(struct dsync_ibc *ibc, + const struct dsync_mail_request **request_r); + +enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT +dsync_ibc_send_mail(struct dsync_ibc *ibc, const struct dsync_mail *mail); +enum dsync_ibc_recv_ret +dsync_ibc_recv_mail(struct dsync_ibc *ibc, struct dsync_mail **mail_r); + +void dsync_ibc_send_finish(struct dsync_ibc *ibc, const char *error, + enum mail_error mail_error, + bool require_full_resync); +enum dsync_ibc_recv_ret +dsync_ibc_recv_finish(struct dsync_ibc *ibc, const char **error_r, + enum mail_error *mail_error_r, + bool *require_full_resync_r); + +/* Close any mail input streams that are kept open. This needs to be called + before the mail is attempted to be freed (usually on error conditions). */ +void dsync_ibc_close_mail_streams(struct dsync_ibc *ibc); + +bool dsync_ibc_has_failed(struct dsync_ibc *ibc); +bool dsync_ibc_has_timed_out(struct dsync_ibc *ibc); +bool dsync_ibc_is_send_queue_full(struct dsync_ibc *ibc); +bool dsync_ibc_has_pending_data(struct dsync_ibc *ibc); + +#endif diff --git a/src/doveadm/dsync/dsync-mail.c b/src/doveadm/dsync/dsync-mail.c new file mode 100644 index 0000000..b82818f --- /dev/null +++ b/src/doveadm/dsync/dsync-mail.c @@ -0,0 +1,156 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hex-binary.h" +#include "md5.h" +#include "istream.h" +#include "istream-crlf.h" +#include "message-header-hash.h" +#include "message-size.h" +#include "mail-storage.h" +#include "dsync-mail.h" + +struct mailbox_header_lookup_ctx * +dsync_mail_get_hash_headers(struct mailbox *box, const char *const *hashed_headers) +{ + return mailbox_header_lookup_init(box, hashed_headers); +} + +int dsync_mail_get_hdr_hash(struct mail *mail, unsigned int version, + const char *const *hashed_headers, const char **hdr_hash_r) +{ + struct istream *hdr_input, *input; + struct mailbox_header_lookup_ctx *hdr_ctx; + struct message_header_hash_context hash_ctx; + struct md5_context md5_ctx; + unsigned char md5_result[MD5_RESULTLEN]; + const unsigned char *data; + size_t size; + ssize_t sret; + int ret = 0; + + hdr_ctx = mailbox_header_lookup_init(mail->box, hashed_headers); + ret = mail_get_header_stream(mail, hdr_ctx, &hdr_input); + mailbox_header_lookup_unref(&hdr_ctx); + if (ret < 0) + return -1; + + input = i_stream_create_lf(hdr_input); + + md5_init(&md5_ctx); + i_zero(&hash_ctx); + while ((sret = i_stream_read_more(input, &data, &size)) > 0) { + message_header_hash_more(&hash_ctx, &hash_method_md5, &md5_ctx, + version, data, size); + i_stream_skip(input, size); + } + i_assert(sret == -1); + if (input->stream_errno != 0) + ret = -1; + i_stream_unref(&input); + + md5_final(&md5_ctx, md5_result); + *hdr_hash_r = binary_to_hex(md5_result, sizeof(md5_result)); + return ret; +} + +int dsync_mail_fill(struct mail *mail, bool minimal_fill, + struct dsync_mail *dmail_r, const char **error_field_r) +{ + const char *guid; + + i_zero(dmail_r); + + if (mail_get_special(mail, MAIL_FETCH_GUID, &guid) < 0) { + *error_field_r = "GUID"; + return -1; + } + dmail_r->guid = guid; + dmail_r->uid = mail->uid; + + dmail_r->input_mail = mail; + dmail_r->input_mail_uid = mail->uid; + + if (mail_get_save_date(mail, &dmail_r->saved_date) < 0) { + *error_field_r = "saved-date"; + return -1; + } + if (!minimal_fill) + return dsync_mail_fill_nonminimal(mail, dmail_r, error_field_r); + dmail_r->minimal_fields = TRUE; + return 0; +} + +int dsync_mail_fill_nonminimal(struct mail *mail, struct dsync_mail *dmail_r, + const char **error_field_r) +{ + const char *str; + + if (mail_get_stream(mail, NULL, NULL, &dmail_r->input) < 0) { + *error_field_r = "body"; + return -1; + } + + if (mail_get_special(mail, MAIL_FETCH_UIDL_BACKEND, &dmail_r->pop3_uidl) < 0) { + *error_field_r = "pop3-uidl"; + return -1; + } + if (mail_get_special(mail, MAIL_FETCH_POP3_ORDER, &str) < 0) { + *error_field_r = "pop3-order"; + return -1; + } + if (*str != '\0') { + if (str_to_uint32(str, &dmail_r->pop3_order) < 0) + i_unreached(); + } + if (mail_get_received_date(mail, &dmail_r->received_date) < 0) { + *error_field_r = "received-date"; + return -1; + } + return 0; +} + +static void +const_string_array_dup(pool_t pool, const ARRAY_TYPE(const_string) *src, + ARRAY_TYPE(const_string) *dest) +{ + const char *const *strings, *str; + unsigned int i, count; + + if (!array_is_created(src)) + return; + + strings = array_get(src, &count); + if (count == 0) + return; + + p_array_init(dest, pool, count); + for (i = 0; i < count; i++) { + str = p_strdup(pool, strings[i]); + array_push_back(dest, &str); + } +} + +void dsync_mail_change_dup(pool_t pool, const struct dsync_mail_change *src, + struct dsync_mail_change *dest_r) +{ + dest_r->type = src->type; + dest_r->uid = src->uid; + if (src->guid != NULL) { + dest_r->guid = *src->guid == '\0' ? "" : + p_strdup(pool, src->guid); + } + dest_r->hdr_hash = p_strdup(pool, src->hdr_hash); + dest_r->modseq = src->modseq; + dest_r->pvt_modseq = src->pvt_modseq; + + dest_r->add_flags = src->add_flags; + dest_r->remove_flags = src->remove_flags; + dest_r->final_flags = src->final_flags; + dest_r->keywords_reset = src->keywords_reset; + const_string_array_dup(pool, &src->keyword_changes, + &dest_r->keyword_changes); + dest_r->received_timestamp = src->received_timestamp; + dest_r->virtual_size = src->virtual_size; +} diff --git a/src/doveadm/dsync/dsync-mail.h b/src/doveadm/dsync/dsync-mail.h new file mode 100644 index 0000000..fc00059 --- /dev/null +++ b/src/doveadm/dsync/dsync-mail.h @@ -0,0 +1,108 @@ +#ifndef DSYNC_MAIL_H +#define DSYNC_MAIL_H + +#include "mail-types.h" + +struct md5_context; +struct mail; +struct mailbox; + +struct dsync_mail { + /* either GUID="" or uid=0 */ + const char *guid; + uint32_t uid; + time_t saved_date; + + /* If non-NULL, we're syncing within the dsync process using ibc-pipe. + This mail can be used to mailbox_copy() the mail. */ + struct mail *input_mail; + /* Verify that this equals to input_mail->uid */ + uint32_t input_mail_uid; + + /* TRUE if the following fields aren't set, because minimal_fill=TRUE + parameter was used. */ + bool minimal_fields; + + const char *pop3_uidl; + uint32_t pop3_order; + time_t received_date; + /* Input stream containing the message text, or NULL if all instances + of the message were already expunged from this mailbox. */ + struct istream *input; +}; + +struct dsync_mail_request { + /* either GUID=NULL or uid=0 */ + const char *guid; + uint32_t uid; +}; + +enum dsync_mail_change_type { + DSYNC_MAIL_CHANGE_TYPE_SAVE, + DSYNC_MAIL_CHANGE_TYPE_EXPUNGE, + DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE +}; + +#define KEYWORD_CHANGE_ADD '+' +#define KEYWORD_CHANGE_REMOVE '-' +#define KEYWORD_CHANGE_FINAL '=' +#define KEYWORD_CHANGE_ADD_AND_FINAL '&' + +struct dsync_mail_change { + enum dsync_mail_change_type type; + + uint32_t uid; + /* Message's GUID: + - for expunges either 128bit hex or NULL if unknown + - "" if backend doesn't support GUIDs */ + const char *guid; + /* If GUID is "", this contains hash of the message header, + otherwise NULL */ + const char *hdr_hash; + + /* Message's current modseq (saves, flag changes) */ + uint64_t modseq; + /* Message's current private modseq (for private flags in + shared mailboxes, otherwise 0) */ + uint64_t pvt_modseq; + + /* List of flag/keyword changes: (saves, flag changes) */ + + /* Flags added/removed since last sync, and final flags containing + flags that exist now but haven't changed */ + uint8_t add_flags, remove_flags, final_flags; + uint8_t add_pvt_flags, remove_pvt_flags; + /* Remove all keywords before applying changes. This is used only with + old transaction logs, new ones never reset keywords (just explicitly + remove unwanted keywords) */ + bool keywords_reset; + /* +add, -remove, =final, &add_and_final. */ + ARRAY_TYPE(const_string) keyword_changes; + + /* Received timestamp for saves, if brain.sync_since/until_timestamp is set */ + time_t received_timestamp; + /* Mail's size for saves if brain.sync_max_size is set, + UOFF_T_MAX otherwise. */ + uoff_t virtual_size; +}; + +struct mailbox_header_lookup_ctx * +dsync_mail_get_hash_headers(struct mailbox *box, const char *const *hashed_headers); + +int dsync_mail_get_hdr_hash(struct mail *mail, unsigned int version, + const char *const *hashed_headers, const char **hdr_hash_r); +static inline bool dsync_mail_hdr_hash_is_empty(const char *hdr_hash) +{ + /* md5(\n) */ + return strcmp(hdr_hash, "68b329da9893e34099c7d8ad5cb9c940") == 0; +} + +int dsync_mail_fill(struct mail *mail, bool minimal_fill, + struct dsync_mail *dmail_r, const char **error_field_r); +int dsync_mail_fill_nonminimal(struct mail *mail, struct dsync_mail *dmail_r, + const char **error_field_r); + +void dsync_mail_change_dup(pool_t pool, const struct dsync_mail_change *src, + struct dsync_mail_change *dest_r); + +#endif diff --git a/src/doveadm/dsync/dsync-mailbox-export.c b/src/doveadm/dsync/dsync-mailbox-export.c new file mode 100644 index 0000000..a58fbec --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-export.c @@ -0,0 +1,961 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "istream.h" +#include "mail-index-modseq.h" +#include "mail-storage-private.h" +#include "mail-search-build.h" +#include "dsync-transaction-log-scan.h" +#include "dsync-mail.h" +#include "dsync-mailbox.h" +#include "dsync-mailbox-export.h" + +struct dsync_mail_guid_instances { + ARRAY_TYPE(seq_range) seqs; + bool requested; + bool searched; +}; + +struct dsync_mailbox_exporter { + pool_t pool; + struct mailbox *box; + struct dsync_transaction_log_scan *log_scan; + uint32_t last_common_uid; + + struct mailbox_header_lookup_ctx *wanted_headers; + struct mailbox_transaction_context *trans; + struct mail_search_context *search_ctx; + unsigned int search_pos, search_count; + unsigned int hdr_hash_version; + + const char *const *hashed_headers; + + /* GUID => instances */ + HASH_TABLE(char *, struct dsync_mail_guid_instances *) export_guids; + ARRAY_TYPE(seq_range) requested_uids; + ARRAY_TYPE(seq_range) search_uids; + + ARRAY_TYPE(seq_range) expunged_seqs; + ARRAY_TYPE(const_string) expunged_guids; + unsigned int expunged_guid_idx; + + /* uint32_t UID => struct dsync_mail_change */ + HASH_TABLE(void *, struct dsync_mail_change *) changes; + /* changes sorted by UID */ + ARRAY(struct dsync_mail_change *) sorted_changes; + unsigned int change_idx; + uint32_t highest_changed_uid; + + struct mailbox_attribute_iter *attr_iter; + struct hash_iterate_context *attr_change_iter; + enum mail_attribute_type attr_type; + struct dsync_mailbox_attribute attr; + + struct dsync_mail_change change; + struct dsync_mail dsync_mail; + + const char *error; + enum mail_error mail_error; + + bool body_search_initialized:1; + bool auto_export_mails:1; + bool mails_have_guids:1; + bool minimal_dmail_fill:1; + bool return_all_mails:1; + bool export_received_timestamps:1; + bool export_virtual_sizes:1; + bool no_hdr_hashes:1; +}; + +static int dsync_mail_error(struct dsync_mailbox_exporter *exporter, + struct mail *mail, const char *field) +{ + const char *errstr; + enum mail_error error; + + errstr = mailbox_get_last_internal_error(exporter->box, &error); + if (error == MAIL_ERROR_EXPUNGED) + return 0; + + exporter->mail_error = error; + exporter->error = p_strdup_printf(exporter->pool, + "Can't lookup %s for UID=%u: %s", + field, mail->uid, errstr); + return -1; +} + +static bool +final_keyword_check(struct dsync_mail_change *change, const char *name, + char *type_r) +{ + const char *const *changes; + unsigned int i, count; + + *type_r = KEYWORD_CHANGE_FINAL; + + changes = array_get(&change->keyword_changes, &count); + for (i = 0; i < count; i++) { + if (strcmp(changes[i]+1, name) != 0) + continue; + + switch (changes[i][0]) { + case KEYWORD_CHANGE_ADD: + /* replace with ADD_AND_FINAL */ + array_delete(&change->keyword_changes, i, 1); + *type_r = KEYWORD_CHANGE_ADD_AND_FINAL; + return FALSE; + case KEYWORD_CHANGE_REMOVE: + /* a final keyword is marked as removed. + this shouldn't normally happen. */ + array_delete(&change->keyword_changes, i, 1); + return FALSE; + case KEYWORD_CHANGE_ADD_AND_FINAL: + case KEYWORD_CHANGE_FINAL: + /* no change */ + return TRUE; + } + } + return FALSE; +} + +static void +search_update_flag_changes(struct dsync_mailbox_exporter *exporter, + struct mail *mail, struct dsync_mail_change *change) +{ + const char *const *keywords; + unsigned int i; + char type; + + i_assert((change->add_flags & change->remove_flags) == 0); + + change->modseq = mail_get_modseq(mail); + change->pvt_modseq = mail_get_pvt_modseq(mail); + change->final_flags = mail_get_flags(mail); + + keywords = mail_get_keywords(mail); + if (!array_is_created(&change->keyword_changes) && + keywords[0] != NULL) { + p_array_init(&change->keyword_changes, exporter->pool, + str_array_length(keywords)); + } + for (i = 0; keywords[i] != NULL; i++) { + /* add the final keyword if it's not already there + as +keyword */ + if (!final_keyword_check(change, keywords[i], &type)) { + const char *keyword_change = + p_strdup_printf(exporter->pool, "%c%s", + type, keywords[i]); + array_push_back(&change->keyword_changes, + &keyword_change); + } + } +} + +static int +exporter_get_guids(struct dsync_mailbox_exporter *exporter, + struct mail *mail, const char **guid_r, + const char **hdr_hash_r) +{ + *guid_r = ""; + *hdr_hash_r = NULL; + + /* always try to get GUID, even if we're also getting header hash */ + if (mail_get_special(mail, MAIL_FETCH_GUID, guid_r) < 0) + return dsync_mail_error(exporter, mail, "GUID"); + + if (!exporter->mails_have_guids) { + /* get header hash also */ + if (exporter->no_hdr_hashes) { + *hdr_hash_r = ""; + return 1; + } + if (dsync_mail_get_hdr_hash(mail, exporter->hdr_hash_version, + exporter->hashed_headers, hdr_hash_r) < 0) + return dsync_mail_error(exporter, mail, "hdr-stream"); + return 1; + } else if (**guid_r == '\0') { + exporter->mail_error = MAIL_ERROR_TEMP; + exporter->error = "Backend doesn't support GUIDs, " + "sync with header hashes instead"; + return -1; + } else { + /* GUIDs are required, we don't need header hash */ + return 1; + } +} + +static int +search_update_flag_change_guid(struct dsync_mailbox_exporter *exporter, + struct mail *mail) +{ + struct dsync_mail_change *change, *log_change; + const char *guid, *hdr_hash; + int ret; + + change = hash_table_lookup(exporter->changes, POINTER_CAST(mail->uid)); + if (change != NULL) { + i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE); + } else { + i_assert(exporter->return_all_mails); + + change = p_new(exporter->pool, struct dsync_mail_change, 1); + change->uid = mail->uid; + change->type = DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE; + hash_table_insert(exporter->changes, + POINTER_CAST(mail->uid), change); + } + + if ((ret = exporter_get_guids(exporter, mail, &guid, &hdr_hash)) < 0) + return -1; + if (ret == 0) { + /* the message was expunged during export */ + i_zero(change); + change->type = DSYNC_MAIL_CHANGE_TYPE_EXPUNGE; + change->uid = mail->uid; + + /* find its GUID from log if possible */ + log_change = dsync_transaction_log_scan_find_new_expunge( + exporter->log_scan, mail->uid); + if (log_change != NULL) + change->guid = log_change->guid; + } else { + change->guid = *guid == '\0' ? "" : + p_strdup(exporter->pool, guid); + change->hdr_hash = p_strdup(exporter->pool, hdr_hash); + search_update_flag_changes(exporter, mail, change); + } + return 0; +} + +static struct dsync_mail_change * +export_save_change_get(struct dsync_mailbox_exporter *exporter, uint32_t uid) +{ + struct dsync_mail_change *change; + + change = hash_table_lookup(exporter->changes, POINTER_CAST(uid)); + if (change == NULL) { + change = p_new(exporter->pool, struct dsync_mail_change, 1); + change->uid = uid; + hash_table_insert(exporter->changes, POINTER_CAST(uid), change); + } else { + /* move flag changes into a save. this happens only when + last_common_uid isn't known */ + i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE); + i_assert(exporter->last_common_uid == 0); + } + + change->type = DSYNC_MAIL_CHANGE_TYPE_SAVE; + return change; +} + +static void +export_add_mail_instance(struct dsync_mailbox_exporter *exporter, + struct dsync_mail_change *change, uint32_t seq) +{ + struct dsync_mail_guid_instances *instances; + + if (exporter->auto_export_mails && !exporter->mails_have_guids) { + /* GUIDs not supported, mail is requested by UIDs */ + seq_range_array_add(&exporter->requested_uids, change->uid); + return; + } + if (*change->guid == '\0') { + /* mail UIDs are manually requested */ + i_assert(!exporter->mails_have_guids); + return; + } + + instances = hash_table_lookup(exporter->export_guids, change->guid); + if (instances == NULL) { + instances = p_new(exporter->pool, + struct dsync_mail_guid_instances, 1); + p_array_init(&instances->seqs, exporter->pool, 2); + hash_table_insert(exporter->export_guids, + p_strdup(exporter->pool, change->guid), + instances); + if (exporter->auto_export_mails) + instances->requested = TRUE; + } + seq_range_array_add(&instances->seqs, seq); +} + +static int +search_add_save(struct dsync_mailbox_exporter *exporter, struct mail *mail) +{ + struct dsync_mail_change *change; + const char *guid, *hdr_hash; + enum mail_fetch_field wanted_fields = MAIL_FETCH_GUID; + time_t received_timestamp = 0; + uoff_t virtual_size = UOFF_T_MAX; + int ret; + + /* update wanted fields in case we didn't already set them for the + search */ + if (exporter->export_received_timestamps) + wanted_fields |= MAIL_FETCH_RECEIVED_DATE; + if (exporter->export_virtual_sizes) + wanted_fields |= MAIL_FETCH_VIRTUAL_SIZE; + mail_add_temp_wanted_fields(mail, wanted_fields, + exporter->wanted_headers); + + /* If message is already expunged here, just skip it */ + if ((ret = exporter_get_guids(exporter, mail, &guid, &hdr_hash)) <= 0) + return ret; + + if (exporter->export_received_timestamps) { + if (mail_get_received_date(mail, &received_timestamp) < 0) + return dsync_mail_error(exporter, mail, "received-time"); + if (received_timestamp == 0) { + /* don't allow timestamps to be zero. we want to have + asserts verify that the timestamp is set properly. */ + received_timestamp = 1; + } + } + if (exporter->export_virtual_sizes) { + if (mail_get_virtual_size(mail, &virtual_size) < 0) + return dsync_mail_error(exporter, mail, "virtual-size"); + i_assert(virtual_size != UOFF_T_MAX); + } + + change = export_save_change_get(exporter, mail->uid); + change->guid = *guid == '\0' ? "" : + p_strdup(exporter->pool, guid); + change->hdr_hash = p_strdup(exporter->pool, hdr_hash); + change->received_timestamp = received_timestamp; + change->virtual_size = virtual_size; + search_update_flag_changes(exporter, mail, change); + + export_add_mail_instance(exporter, change, mail->seq); + return 1; +} + +static void +dsync_mailbox_export_add_flagchange_uids(struct dsync_mailbox_exporter *exporter, + ARRAY_TYPE(seq_range) *uids) +{ + struct hash_iterate_context *iter; + void *key; + struct dsync_mail_change *change; + + iter = hash_table_iterate_init(exporter->changes); + while (hash_table_iterate(iter, exporter->changes, &key, &change)) { + if (change->type == DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE) + seq_range_array_add(uids, change->uid); + } + hash_table_iterate_deinit(&iter); +} + +static void +dsync_mailbox_export_drop_expunged_flag_changes(struct dsync_mailbox_exporter *exporter) +{ + struct hash_iterate_context *iter; + void *key; + struct dsync_mail_change *change; + + /* any flag changes for UIDs above last_common_uid weren't found by + mailbox search, which means they were already expunged. for some + reason the log scanner found flag changes for the message, but not + the expunge. just remove these. */ + iter = hash_table_iterate_init(exporter->changes); + while (hash_table_iterate(iter, exporter->changes, &key, &change)) { + if (change->type == DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE && + change->uid > exporter->last_common_uid) + hash_table_remove(exporter->changes, key); + } + hash_table_iterate_deinit(&iter); +} + +static void +dsync_mailbox_export_search(struct dsync_mailbox_exporter *exporter) +{ + struct mail_search_context *search_ctx; + struct mail_search_args *search_args; + struct mail_search_arg *sarg; + struct mail *mail; + enum mail_fetch_field wanted_fields = 0; + struct mailbox_header_lookup_ctx *wanted_headers = NULL; + int ret = 0; + + search_args = mail_search_build_init(); + sarg = mail_search_build_add(search_args, SEARCH_UIDSET); + p_array_init(&sarg->value.seqset, search_args->pool, 1); + + if (exporter->return_all_mails || exporter->last_common_uid == 0) { + /* we want to know about all mails */ + seq_range_array_add_range(&sarg->value.seqset, 1, (uint32_t)-1); + } else { + /* lookup GUIDs for messages with flag changes */ + dsync_mailbox_export_add_flagchange_uids(exporter, + &sarg->value.seqset); + /* lookup new messages */ + seq_range_array_add_range(&sarg->value.seqset, + exporter->last_common_uid + 1, + (uint32_t)-1); + } + + if (exporter->last_common_uid == 0) { + /* we're syncing all mails, so we can request the wanted + fields for all the mails */ + wanted_fields = MAIL_FETCH_GUID; + wanted_headers = exporter->wanted_headers; + } + + exporter->trans = mailbox_transaction_begin(exporter->box, + MAILBOX_TRANSACTION_FLAG_SYNC, + __func__); + search_ctx = mailbox_search_init(exporter->trans, search_args, NULL, + wanted_fields, wanted_headers); + mail_search_args_unref(&search_args); + + while (mailbox_search_next(search_ctx, &mail)) { + T_BEGIN { + if (mail->uid <= exporter->last_common_uid) + ret = search_update_flag_change_guid(exporter, mail); + else + ret = search_add_save(exporter, mail); + } T_END; + if (ret < 0) + break; + } + i_assert(ret >= 0 || exporter->error != NULL); + + dsync_mailbox_export_drop_expunged_flag_changes(exporter); + + if (mailbox_search_deinit(&search_ctx) < 0 && + exporter->error == NULL) { + exporter->error = p_strdup_printf(exporter->pool, + "Mail search failed: %s", + mailbox_get_last_internal_error(exporter->box, + &exporter->mail_error)); + } +} + +static int dsync_mail_change_p_uid_cmp(struct dsync_mail_change *const *c1, + struct dsync_mail_change *const *c2) +{ + if ((*c1)->uid < (*c2)->uid) + return -1; + if ((*c1)->uid > (*c2)->uid) + return 1; + return 0; +} + +static void +dsync_mailbox_export_sort_changes(struct dsync_mailbox_exporter *exporter) +{ + struct hash_iterate_context *iter; + void *key; + struct dsync_mail_change *change; + + p_array_init(&exporter->sorted_changes, exporter->pool, + hash_table_count(exporter->changes)); + + iter = hash_table_iterate_init(exporter->changes); + while (hash_table_iterate(iter, exporter->changes, &key, &change)) + array_push_back(&exporter->sorted_changes, &change); + hash_table_iterate_deinit(&iter); + array_sort(&exporter->sorted_changes, dsync_mail_change_p_uid_cmp); +} + +static void +dsync_mailbox_export_attr_init(struct dsync_mailbox_exporter *exporter, + enum mail_attribute_type type) +{ + exporter->attr_iter = + mailbox_attribute_iter_init(exporter->box, type, ""); + exporter->attr_type = type; +} + +static void +dsync_mailbox_export_log_scan(struct dsync_mailbox_exporter *exporter, + struct dsync_transaction_log_scan *log_scan) +{ + HASH_TABLE_TYPE(dsync_uid_mail_change) log_changes; + struct hash_iterate_context *iter; + void *key; + struct dsync_mail_change *change, *dup_change; + + log_changes = dsync_transaction_log_scan_get_hash(log_scan); + if (dsync_transaction_log_scan_has_all_changes(log_scan)) { + /* we tried to access too old/invalid modseqs. to make sure + no changes get lost, we need to send all of the messages */ + exporter->return_all_mails = TRUE; + } + + /* clone the hash table, since we're changing it. */ + hash_table_create_direct(&exporter->changes, exporter->pool, + hash_table_count(log_changes)); + iter = hash_table_iterate_init(log_changes); + while (hash_table_iterate(iter, log_changes, &key, &change)) { + dup_change = p_new(exporter->pool, struct dsync_mail_change, 1); + *dup_change = *change; + hash_table_insert(exporter->changes, key, dup_change); + if (exporter->highest_changed_uid < change->uid) + exporter->highest_changed_uid = change->uid; + } + hash_table_iterate_deinit(&iter); +} + +struct dsync_mailbox_exporter * +dsync_mailbox_export_init(struct mailbox *box, + struct dsync_transaction_log_scan *log_scan, + uint32_t last_common_uid, + enum dsync_mailbox_exporter_flags flags, + unsigned int hdr_hash_version, + const char *const *hashed_headers) +{ + struct dsync_mailbox_exporter *exporter; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"dsync mailbox export", + 4096); + exporter = p_new(pool, struct dsync_mailbox_exporter, 1); + exporter->pool = pool; + exporter->box = box; + exporter->log_scan = log_scan; + exporter->last_common_uid = last_common_uid; + exporter->auto_export_mails = + (flags & DSYNC_MAILBOX_EXPORTER_FLAG_AUTO_EXPORT_MAILS) != 0; + exporter->mails_have_guids = + (flags & DSYNC_MAILBOX_EXPORTER_FLAG_MAILS_HAVE_GUIDS) != 0; + exporter->minimal_dmail_fill = + (flags & DSYNC_MAILBOX_EXPORTER_FLAG_MINIMAL_DMAIL_FILL) != 0; + exporter->export_received_timestamps = + (flags & DSYNC_MAILBOX_EXPORTER_FLAG_TIMESTAMPS) != 0; + exporter->export_virtual_sizes = + (flags & DSYNC_MAILBOX_EXPORTER_FLAG_VSIZES) != 0; + exporter->hdr_hash_version = hdr_hash_version; + exporter->no_hdr_hashes = + (flags & DSYNC_MAILBOX_EXPORTER_FLAG_NO_HDR_HASHES) != 0; + exporter->hashed_headers = hashed_headers; + + p_array_init(&exporter->requested_uids, pool, 16); + p_array_init(&exporter->search_uids, pool, 16); + hash_table_create(&exporter->export_guids, pool, 0, str_hash, strcmp); + p_array_init(&exporter->expunged_seqs, pool, 16); + p_array_init(&exporter->expunged_guids, pool, 16); + + if (!exporter->mails_have_guids && !exporter->no_hdr_hashes) + exporter->wanted_headers = + dsync_mail_get_hash_headers(box, exporter->hashed_headers); + + /* first scan transaction log and save any expunges and flag changes */ + dsync_mailbox_export_log_scan(exporter, log_scan); + /* get saves and also find GUIDs for flag changes */ + dsync_mailbox_export_search(exporter); + /* get the changes sorted by UID */ + dsync_mailbox_export_sort_changes(exporter); + + dsync_mailbox_export_attr_init(exporter, MAIL_ATTRIBUTE_TYPE_PRIVATE); + return exporter; +} + +static int +dsync_mailbox_export_iter_next_nonexistent_attr(struct dsync_mailbox_exporter *exporter) +{ + struct dsync_mailbox_attribute *attr; + struct mail_attribute_value value; + + while (hash_table_iterate(exporter->attr_change_iter, + dsync_transaction_log_scan_get_attr_hash(exporter->log_scan), + &attr, &attr)) { + if (attr->exported || !attr->deleted) + continue; + + /* lookup the value mainly to get its last_change value. */ + if (mailbox_attribute_get_stream(exporter->box, attr->type, + attr->key, &value) < 0) { + exporter->error = p_strdup_printf(exporter->pool, + "Mailbox attribute %s lookup failed: %s", attr->key, + mailbox_get_last_internal_error(exporter->box, + &exporter->mail_error)); + break; + } + if ((value.flags & MAIL_ATTRIBUTE_VALUE_FLAG_READONLY) != 0) { + i_stream_unref(&value.value_stream); + continue; + } + + attr->last_change = value.last_change; + if (value.value != NULL || value.value_stream != NULL) { + attr->value = p_strdup(exporter->pool, value.value); + attr->value_stream = value.value_stream; + attr->deleted = FALSE; + } + attr->exported = TRUE; + exporter->attr = *attr; + return 1; + } + hash_table_iterate_deinit(&exporter->attr_change_iter); + return 0; +} + +static int +dsync_mailbox_export_iter_next_attr(struct dsync_mailbox_exporter *exporter) +{ + HASH_TABLE_TYPE(dsync_attr_change) attr_changes; + struct dsync_mailbox_attribute lookup_attr, *attr; + struct dsync_mailbox_attribute *attr_change; + const char *key; + struct mail_attribute_value value; + bool export_all_attrs; + + export_all_attrs = exporter->return_all_mails || + exporter->last_common_uid == 0; + attr_changes = dsync_transaction_log_scan_get_attr_hash(exporter->log_scan); + lookup_attr.type = exporter->attr_type; + + /* note that the order of processing may be important for some + attributes. for example sieve can't set a script active until it's + first been created */ + while ((key = mailbox_attribute_iter_next(exporter->attr_iter)) != NULL) { + lookup_attr.key = key; + attr_change = hash_table_lookup(attr_changes, &lookup_attr); + if (attr_change == NULL && !export_all_attrs) + continue; + + if (mailbox_attribute_get_stream(exporter->box, + exporter->attr_type, key, + &value) < 0) { + exporter->error = p_strdup_printf(exporter->pool, + "Mailbox attribute %s lookup failed: %s", key, + mailbox_get_last_internal_error(exporter->box, + &exporter->mail_error)); + return -1; + } + if ((value.flags & MAIL_ATTRIBUTE_VALUE_FLAG_READONLY) != 0) { + /* readonly attributes can't be changed, + no point in exporting them */ + if (value.value_stream != NULL) + i_stream_unref(&value.value_stream); + continue; + } + if (value.value == NULL && value.value_stream == NULL && + (attr_change == NULL || !attr_change->deleted)) { + /* the attribute was just deleted? + skip for this sync. */ + continue; + } + if (attr_change != NULL && attr_change->exported) { + /* duplicate attribute returned. + shouldn't normally happen, but don't crash. */ + i_warning("Ignoring duplicate attributes '%s'", key); + continue; + } + + attr = &exporter->attr; + i_zero(attr); + attr->type = exporter->attr_type; + attr->value = p_strdup(exporter->pool, value.value); + attr->value_stream = value.value_stream; + attr->last_change = value.last_change; + if (attr_change != NULL) { + attr_change->exported = TRUE; + attr->key = attr_change->key; + attr->deleted = attr_change->deleted && + !DSYNC_ATTR_HAS_VALUE(attr); + attr->modseq = attr_change->modseq; + } else { + attr->key = p_strdup(exporter->pool, key); + } + return 1; + } + if (mailbox_attribute_iter_deinit(&exporter->attr_iter) < 0) { + exporter->error = p_strdup_printf(exporter->pool, + "Mailbox attribute iteration failed: %s", + mailbox_get_last_internal_error(exporter->box, + &exporter->mail_error)); + return -1; + } + if (exporter->attr_type == MAIL_ATTRIBUTE_TYPE_PRIVATE) { + /* export shared attributes */ + dsync_mailbox_export_attr_init(exporter, + MAIL_ATTRIBUTE_TYPE_SHARED); + return dsync_mailbox_export_iter_next_attr(exporter); + } + exporter->attr_change_iter = hash_table_iterate_init(attr_changes); + return dsync_mailbox_export_iter_next_nonexistent_attr(exporter); +} + +int dsync_mailbox_export_next_attr(struct dsync_mailbox_exporter *exporter, + const struct dsync_mailbox_attribute **attr_r) +{ + int ret; + + if (exporter->error != NULL) + return -1; + + i_stream_unref(&exporter->attr.value_stream); + + if (exporter->attr_iter != NULL) { + ret = dsync_mailbox_export_iter_next_attr(exporter); + } else { + ret = dsync_mailbox_export_iter_next_nonexistent_attr(exporter); + } + if (ret > 0) + *attr_r = &exporter->attr; + return ret; +} + +int dsync_mailbox_export_next(struct dsync_mailbox_exporter *exporter, + const struct dsync_mail_change **change_r) +{ + struct dsync_mail_change *const *changes; + unsigned int count; + + if (exporter->error != NULL) + return -1; + + changes = array_get(&exporter->sorted_changes, &count); + if (exporter->change_idx == count) + return 0; + *change_r = changes[exporter->change_idx++]; + return 1; +} + +static int +dsync_mailbox_export_body_search_init(struct dsync_mailbox_exporter *exporter) +{ + struct mail_search_args *search_args; + struct mail_search_arg *sarg; + struct hash_iterate_context *iter; + const struct seq_range *uids; + char *guid; + const char *const_guid; + enum mail_fetch_field wanted_fields; + struct dsync_mail_guid_instances *instances; + const struct seq_range *range; + unsigned int i, count; + uint32_t seq, seq1, seq2; + + i_assert(exporter->search_ctx == NULL); + + search_args = mail_search_build_init(); + sarg = mail_search_build_add(search_args, SEARCH_SEQSET); + p_array_init(&sarg->value.seqset, search_args->pool, 128); + + /* get a list of messages we want to fetch. if there are more than one + instance for a GUID, use the first one. */ + iter = hash_table_iterate_init(exporter->export_guids); + while (hash_table_iterate(iter, exporter->export_guids, + &guid, &instances)) { + if (!instances->requested || + array_count(&instances->seqs) == 0) + continue; + + uids = array_front(&instances->seqs); + seq = uids[0].seq1; + if (!instances->searched) { + instances->searched = TRUE; + seq_range_array_add(&sarg->value.seqset, seq); + } else if (seq_range_exists(&exporter->expunged_seqs, seq)) { + /* we're on a second round, refetching expunged + messages */ + seq_range_array_remove(&instances->seqs, seq); + seq_range_array_remove(&exporter->expunged_seqs, seq); + if (array_count(&instances->seqs) == 0) { + /* no instances left */ + const_guid = guid; + array_push_back(&exporter->expunged_guids, + &const_guid); + continue; + } + uids = array_front(&instances->seqs); + seq = uids[0].seq1; + seq_range_array_add(&sarg->value.seqset, seq); + } + } + hash_table_iterate_deinit(&iter); + + /* add requested UIDs */ + range = array_get(&exporter->requested_uids, &count); + for (i = 0; i < count; i++) { + mailbox_get_seq_range(exporter->box, + range[i].seq1, range[i].seq2, + &seq1, &seq2); + seq_range_array_add_range(&sarg->value.seqset, + seq1, seq2); + } + array_clear(&exporter->search_uids); + array_append_array(&exporter->search_uids, &exporter->requested_uids); + array_clear(&exporter->requested_uids); + + wanted_fields = MAIL_FETCH_GUID | MAIL_FETCH_SAVE_DATE; + if (!exporter->minimal_dmail_fill) { + wanted_fields |= MAIL_FETCH_RECEIVED_DATE | + MAIL_FETCH_UIDL_BACKEND | MAIL_FETCH_POP3_ORDER | + MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY; + } + exporter->search_count += seq_range_count(&sarg->value.seqset); + exporter->search_ctx = + mailbox_search_init(exporter->trans, search_args, NULL, + wanted_fields, NULL); + mail_search_args_unref(&search_args); + return array_count(&sarg->value.seqset) > 0 ? 1 : 0; +} + +static void +dsync_mailbox_export_body_search_deinit(struct dsync_mailbox_exporter *exporter) +{ + if (exporter->search_ctx == NULL) + return; + + if (mailbox_search_deinit(&exporter->search_ctx) < 0 && + exporter->error == NULL) { + exporter->error = p_strdup_printf(exporter->pool, + "Mail search failed: %s", + mailbox_get_last_internal_error(exporter->box, + &exporter->mail_error)); + } +} + +static int dsync_mailbox_export_mail(struct dsync_mailbox_exporter *exporter, + struct mail *mail) +{ + struct dsync_mail_guid_instances *instances; + const char *error_field; + + if (dsync_mail_fill(mail, exporter->minimal_dmail_fill, + &exporter->dsync_mail, &error_field) < 0) + return dsync_mail_error(exporter, mail, error_field); + + instances = *exporter->dsync_mail.guid == '\0' ? NULL : + hash_table_lookup(exporter->export_guids, + exporter->dsync_mail.guid); + if (instances != NULL) { + /* GUID found */ + } else if (exporter->dsync_mail.uid != 0) { + /* mail requested by UID */ + } else { + exporter->mail_error = MAIL_ERROR_TEMP; + exporter->error = p_strdup_printf(exporter->pool, + "GUID unexpectedly changed for UID=%u GUID=%s", + mail->uid, exporter->dsync_mail.guid); + return -1; + } + + if (!seq_range_exists(&exporter->search_uids, mail->uid)) + exporter->dsync_mail.uid = 0; + else + exporter->dsync_mail.guid = ""; + + /* this message was successfully returned, don't try retrying it */ + if (instances != NULL) + array_clear(&instances->seqs); + return 1; +} + +void dsync_mailbox_export_want_mail(struct dsync_mailbox_exporter *exporter, + const struct dsync_mail_request *request) +{ + struct dsync_mail_guid_instances *instances; + + i_assert(!exporter->auto_export_mails); + + if (request->guid == NULL) { + i_assert(request->uid > 0); + seq_range_array_add(&exporter->requested_uids, request->uid); + return; + } + + instances = hash_table_lookup(exporter->export_guids, request->guid); + if (instances == NULL) { + exporter->mail_error = MAIL_ERROR_TEMP; + exporter->error = p_strdup_printf(exporter->pool, + "Remote requested unexpected GUID %s", request->guid); + return; + } + instances->requested = TRUE; +} + +int dsync_mailbox_export_next_mail(struct dsync_mailbox_exporter *exporter, + const struct dsync_mail **mail_r) +{ + struct mail *mail; + const char *const *guids; + unsigned int count; + int ret; + + if (exporter->error != NULL) + return -1; + if (!exporter->body_search_initialized) { + exporter->body_search_initialized = TRUE; + if (dsync_mailbox_export_body_search_init(exporter) < 0) { + i_assert(exporter->error != NULL); + return -1; + } + } + + while (mailbox_search_next(exporter->search_ctx, &mail)) { + exporter->search_pos++; + if ((ret = dsync_mailbox_export_mail(exporter, mail)) > 0) { + *mail_r = &exporter->dsync_mail; + return 1; + } + if (ret < 0) { + i_assert(exporter->error != NULL); + return -1; + } + /* the message was expunged. if the GUID has another instance, + try sending it later. */ + seq_range_array_add(&exporter->expunged_seqs, mail->seq); + } + /* if some instances of messages were expunged, retry fetching them + with other instances */ + dsync_mailbox_export_body_search_deinit(exporter); + if ((ret = dsync_mailbox_export_body_search_init(exporter)) < 0) { + i_assert(exporter->error != NULL); + return -1; + } + if (ret > 0) { + /* not finished yet */ + return dsync_mailbox_export_next_mail(exporter, mail_r); + } + + /* finished with messages. if there are any expunged messages, + return them */ + guids = array_get(&exporter->expunged_guids, &count); + if (exporter->expunged_guid_idx < count) { + i_zero(&exporter->dsync_mail); + exporter->dsync_mail.guid = + guids[exporter->expunged_guid_idx++]; + *mail_r = &exporter->dsync_mail; + return 1; + } + return 0; +} + +int dsync_mailbox_export_deinit(struct dsync_mailbox_exporter **_exporter, + const char **errstr_r, enum mail_error *error_r) +{ + struct dsync_mailbox_exporter *exporter = *_exporter; + + *_exporter = NULL; + + if (exporter->attr_iter != NULL) + (void)mailbox_attribute_iter_deinit(&exporter->attr_iter); + dsync_mailbox_export_body_search_deinit(exporter); + (void)mailbox_transaction_commit(&exporter->trans); + mailbox_header_lookup_unref(&exporter->wanted_headers); + + i_stream_unref(&exporter->attr.value_stream); + hash_table_destroy(&exporter->export_guids); + hash_table_destroy(&exporter->changes); + + i_assert((exporter->error != NULL) == (exporter->mail_error != 0)); + + *error_r = exporter->mail_error; + *errstr_r = t_strdup(exporter->error); + pool_unref(&exporter->pool); + return *errstr_r != NULL ? -1 : 0; +} + +const char *dsync_mailbox_export_get_proctitle(struct dsync_mailbox_exporter *exporter) +{ + if (exporter->search_ctx == NULL) + return ""; + return t_strdup_printf("%u/%u", exporter->search_pos, + exporter->search_count); +} diff --git a/src/doveadm/dsync/dsync-mailbox-export.h b/src/doveadm/dsync/dsync-mailbox-export.h new file mode 100644 index 0000000..67e3216 --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-export.h @@ -0,0 +1,37 @@ +#ifndef DSYNC_MAILBOX_EXPORT_H +#define DSYNC_MAILBOX_EXPORT_H + +enum dsync_mailbox_exporter_flags { + DSYNC_MAILBOX_EXPORTER_FLAG_AUTO_EXPORT_MAILS = 0x01, + DSYNC_MAILBOX_EXPORTER_FLAG_MAILS_HAVE_GUIDS = 0x02, + DSYNC_MAILBOX_EXPORTER_FLAG_MINIMAL_DMAIL_FILL = 0x04, + DSYNC_MAILBOX_EXPORTER_FLAG_TIMESTAMPS = 0x08, + DSYNC_MAILBOX_EXPORTER_FLAG_NO_HDR_HASHES = 0x20, + DSYNC_MAILBOX_EXPORTER_FLAG_VSIZES = 0x40, +}; + +struct dsync_mailbox_exporter * +dsync_mailbox_export_init(struct mailbox *box, + struct dsync_transaction_log_scan *log_scan, + uint32_t last_common_uid, + enum dsync_mailbox_exporter_flags flags, + unsigned int hdr_hash_version, + const char *const *hashed_headers); +/* Returns 1 if attribute was returned, 0 if no more attributes, -1 on error */ +int dsync_mailbox_export_next_attr(struct dsync_mailbox_exporter *exporter, + const struct dsync_mailbox_attribute **attr_r); +/* Returns 1 if change was returned, 0 if no more changes, -1 on error */ +int dsync_mailbox_export_next(struct dsync_mailbox_exporter *exporter, + const struct dsync_mail_change **change_r); + +void dsync_mailbox_export_want_mail(struct dsync_mailbox_exporter *exporter, + const struct dsync_mail_request *request); +/* Returns 1 if mail was returned, 0 if no more mails, -1 on error */ +int dsync_mailbox_export_next_mail(struct dsync_mailbox_exporter *exporter, + const struct dsync_mail **mail_r); +int dsync_mailbox_export_deinit(struct dsync_mailbox_exporter **exporter, + const char **errstr_r, enum mail_error *error_r); + +const char *dsync_mailbox_export_get_proctitle(struct dsync_mailbox_exporter *exporter); + +#endif diff --git a/src/doveadm/dsync/dsync-mailbox-import.c b/src/doveadm/dsync/dsync-mailbox-import.c new file mode 100644 index 0000000..8a8fbe4 --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-import.c @@ -0,0 +1,2997 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "str.h" +#include "hex-binary.h" +#include "istream.h" +#include "seq-range-array.h" +#include "imap-util.h" +#include "mail-storage-private.h" +#include "mail-search-build.h" +#include "dsync-transaction-log-scan.h" +#include "dsync-mail.h" +#include "dsync-mailbox.h" +#include "dsync-mailbox-import.h" + +struct importer_mail { + const char *guid; + uint32_t uid; +}; + +struct importer_new_mail { + /* linked list of mails for this GUID */ + struct importer_new_mail *next; + /* if non-NULL, this mail exists in both local and remote. this link + points to the other side. */ + struct importer_new_mail *link; + + const char *guid; + struct dsync_mail_change *change; + + /* the final UID for the message */ + uint32_t final_uid; + /* the original local UID, or 0 if exists only remotely */ + uint32_t local_uid; + /* the original remote UID, or 0 if exists only remotely */ + uint32_t remote_uid; + /* UID for the mail in the virtual \All mailbox */ + uint32_t virtual_all_uid; + + bool uid_in_local:1; + bool uid_is_usable:1; + bool skip:1; + bool expunged:1; + bool copy_failed:1; + bool saved:1; +}; + +/* for quickly testing that two-way sync doesn't actually do any unexpected + modifications. */ +#define IMPORTER_DEBUG_CHANGE(importer) /*i_assert(!importer->master_brain)*/ + +HASH_TABLE_DEFINE_TYPE(guid_new_mail, const char *, struct importer_new_mail *); +HASH_TABLE_DEFINE_TYPE(uid_new_mail, void *, struct importer_new_mail *); + +struct dsync_mailbox_importer { + pool_t pool; + struct mailbox *box; + uint32_t last_common_uid; + uint64_t last_common_modseq, last_common_pvt_modseq; + uint32_t remote_uid_next; + uint32_t remote_first_recent_uid; + uint64_t remote_highest_modseq, remote_highest_pvt_modseq; + time_t sync_since_timestamp; + time_t sync_until_timestamp; + uoff_t sync_max_size; + enum mailbox_transaction_flags transaction_flags; + unsigned int hdr_hash_version; + unsigned int commit_msgs_interval; + + const char *const *hashed_headers; + + enum mail_flags sync_flag; + const char *sync_keyword; + bool sync_flag_dontwant; + + struct mailbox_transaction_context *trans, *ext_trans; + struct mail_search_context *search_ctx; + struct mail *mail, *ext_mail; + + struct mailbox *virtual_all_box; + struct mailbox_transaction_context *virtual_trans; + struct mail *virtual_mail; + + struct mail *cur_mail; + const char *cur_guid; + const char *cur_hdr_hash; + + /* UID => struct dsync_mail_change */ + HASH_TABLE_TYPE(dsync_uid_mail_change) local_changes; + HASH_TABLE_TYPE(dsync_attr_change) local_attr_changes; + + ARRAY_TYPE(seq_range) maybe_expunge_uids; + ARRAY(struct dsync_mail_change *) maybe_saves; + + /* GUID => struct importer_new_mail */ + HASH_TABLE_TYPE(guid_new_mail) import_guids; + /* UID => struct importer_new_mail */ + HASH_TABLE_TYPE(uid_new_mail) import_uids; + + ARRAY(struct importer_new_mail *) newmails; + ARRAY_TYPE(uint32_t) wanted_uids; + ARRAY_TYPE(uint32_t) saved_uids; + uint32_t highest_wanted_uid; + + ARRAY(struct dsync_mail_request) mail_requests; + unsigned int mail_request_idx; + + uint32_t prev_uid, next_local_seq, local_uid_next; + uint64_t local_initial_highestmodseq, local_initial_highestpvtmodseq; + unsigned int import_pos, import_count; + unsigned int first_unsaved_idx, saves_since_commit; + + enum mail_error mail_error; + + bool failed:1; + bool require_full_resync:1; + bool debug:1; + bool stateful_import:1; + bool last_common_uid_found:1; + bool cur_uid_has_change:1; + bool cur_mail_skip:1; + bool local_expunged_guids_set:1; + bool new_uids_assigned:1; + bool want_mail_requests:1; + bool master_brain:1; + bool revert_local_changes:1; + bool mails_have_guids:1; + bool mails_use_guid128:1; + bool delete_mailbox:1; + bool empty_hdr_workaround:1; +}; + +static const char *dsync_mail_change_type_names[] = { + "save", "expunge", "flag-change" +}; + +static bool dsync_mailbox_save_newmails(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail, + struct importer_new_mail *all_newmails, + bool remote_mail); +static int dsync_mailbox_import_commit(struct dsync_mailbox_importer *importer, + bool final); + +static void ATTR_FORMAT(2, 3) +imp_debug(struct dsync_mailbox_importer *importer, const char *fmt, ...) +{ + va_list args; + + if (importer->debug) T_BEGIN { + va_start(args, fmt); + i_debug("brain %c: Import %s: %s", + importer->master_brain ? 'M' : 'S', + mailbox_get_vname(importer->box), + t_strdup_vprintf(fmt, args)); + va_end(args); + } T_END; +} + +static void +dsync_import_unexpected_state(struct dsync_mailbox_importer *importer, + const char *error) +{ + if (!importer->stateful_import) { + i_error("Mailbox %s: %s", mailbox_get_vname(importer->box), + error); + } else { + i_warning("Mailbox %s doesn't match previous state: %s " + "(dsync must be run again without the state)", + mailbox_get_vname(importer->box), error); + } + importer->require_full_resync = TRUE; +} + +static void +dsync_mailbox_import_search_init(struct dsync_mailbox_importer *importer) +{ + struct mail_search_args *search_args; + struct mail_search_arg *sarg; + + search_args = mail_search_build_init(); + sarg = mail_search_build_add(search_args, SEARCH_UIDSET); + p_array_init(&sarg->value.seqset, search_args->pool, 128); + seq_range_array_add_range(&sarg->value.seqset, + importer->last_common_uid+1, (uint32_t)-1); + + importer->search_ctx = + mailbox_search_init(importer->trans, search_args, NULL, + 0, NULL); + mail_search_args_unref(&search_args); + + if (mailbox_search_next(importer->search_ctx, &importer->cur_mail)) + importer->next_local_seq = importer->cur_mail->seq; + /* this flag causes cur_guid to be looked up later */ + importer->cur_mail_skip = TRUE; +} + +static void +dsync_mailbox_import_transaction_begin(struct dsync_mailbox_importer *importer) +{ + const enum mailbox_transaction_flags ext_trans_flags = + importer->transaction_flags | + MAILBOX_TRANSACTION_FLAG_EXTERNAL | + MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS; + + importer->trans = mailbox_transaction_begin(importer->box, + importer->transaction_flags, + "dsync import"); + importer->ext_trans = mailbox_transaction_begin(importer->box, + ext_trans_flags, + "dsync ext import"); + importer->mail = mail_alloc(importer->trans, 0, NULL); + importer->ext_mail = mail_alloc(importer->ext_trans, 0, NULL); +} + +struct dsync_mailbox_importer * +dsync_mailbox_import_init(struct mailbox *box, + struct mailbox *virtual_all_box, + struct dsync_transaction_log_scan *log_scan, + uint32_t last_common_uid, + uint64_t last_common_modseq, + uint64_t last_common_pvt_modseq, + uint32_t remote_uid_next, + uint32_t remote_first_recent_uid, + uint64_t remote_highest_modseq, + uint64_t remote_highest_pvt_modseq, + time_t sync_since_timestamp, + time_t sync_until_timestamp, + uoff_t sync_max_size, + const char *sync_flag, + unsigned int commit_msgs_interval, + enum dsync_mailbox_import_flags flags, + unsigned int hdr_hash_version, + const char *const *hashed_headers) +{ + struct dsync_mailbox_importer *importer; + struct mailbox_status status; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"dsync mailbox importer", + 10240); + importer = p_new(pool, struct dsync_mailbox_importer, 1); + importer->pool = pool; + importer->box = box; + importer->virtual_all_box = virtual_all_box; + importer->last_common_uid = last_common_uid; + importer->last_common_modseq = last_common_modseq; + importer->last_common_pvt_modseq = last_common_pvt_modseq; + importer->last_common_uid_found = + last_common_uid != 0 || last_common_modseq != 0; + importer->remote_uid_next = remote_uid_next; + importer->remote_first_recent_uid = remote_first_recent_uid; + importer->remote_highest_modseq = remote_highest_modseq; + importer->remote_highest_pvt_modseq = remote_highest_pvt_modseq; + importer->sync_since_timestamp = sync_since_timestamp; + importer->sync_until_timestamp = sync_until_timestamp; + importer->sync_max_size = sync_max_size; + importer->stateful_import = importer->last_common_uid_found; + importer->hashed_headers = hashed_headers; + + if (sync_flag != NULL) { + if (sync_flag[0] == '-') { + importer->sync_flag_dontwant = TRUE; + sync_flag++; + } + if (sync_flag[0] == '\\') + importer->sync_flag = imap_parse_system_flag(sync_flag); + else + importer->sync_keyword = p_strdup(pool, sync_flag); + } + importer->commit_msgs_interval = commit_msgs_interval; + importer->transaction_flags = MAILBOX_TRANSACTION_FLAG_SYNC; + if ((flags & DSYNC_MAILBOX_IMPORT_FLAG_NO_NOTIFY) != 0) + importer->transaction_flags |= MAILBOX_TRANSACTION_FLAG_NO_NOTIFY; + + hash_table_create(&importer->import_guids, pool, 0, str_hash, strcmp); + hash_table_create_direct(&importer->import_uids, pool, 0); + i_array_init(&importer->maybe_expunge_uids, 16); + i_array_init(&importer->maybe_saves, 128); + i_array_init(&importer->newmails, 128); + i_array_init(&importer->wanted_uids, 128); + i_array_init(&importer->saved_uids, 128); + + dsync_mailbox_import_transaction_begin(importer); + + if ((flags & DSYNC_MAILBOX_IMPORT_FLAG_WANT_MAIL_REQUESTS) != 0) { + i_array_init(&importer->mail_requests, 128); + importer->want_mail_requests = TRUE; + } + importer->master_brain = + (flags & DSYNC_MAILBOX_IMPORT_FLAG_MASTER_BRAIN) != 0; + importer->revert_local_changes = + (flags & DSYNC_MAILBOX_IMPORT_FLAG_REVERT_LOCAL_CHANGES) != 0; + importer->debug = (flags & DSYNC_MAILBOX_IMPORT_FLAG_DEBUG) != 0; + importer->mails_have_guids = + (flags & DSYNC_MAILBOX_IMPORT_FLAG_MAILS_HAVE_GUIDS) != 0; + importer->mails_use_guid128 = + (flags & DSYNC_MAILBOX_IMPORT_FLAG_MAILS_USE_GUID128) != 0; + importer->hdr_hash_version = hdr_hash_version; + importer->empty_hdr_workaround = + (flags & DSYNC_MAILBOX_IMPORT_FLAG_EMPTY_HDR_WORKAROUND) != 0; + + mailbox_get_open_status(importer->box, STATUS_UIDNEXT | + STATUS_HIGHESTMODSEQ | STATUS_HIGHESTPVTMODSEQ, + &status); + if (status.nonpermanent_modseqs) + status.highest_modseq = 0; + importer->local_uid_next = status.uidnext; + importer->local_initial_highestmodseq = status.highest_modseq; + importer->local_initial_highestpvtmodseq = status.highest_pvt_modseq; + dsync_mailbox_import_search_init(importer); + + if (!importer->stateful_import) + ; + else if (importer->local_uid_next <= last_common_uid) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "local UIDNEXT %u <= last common UID %u", + importer->local_uid_next, last_common_uid)); + } else if (importer->local_initial_highestmodseq < last_common_modseq) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "local HIGHESTMODSEQ %"PRIu64" < last common HIGHESTMODSEQ %"PRIu64, + importer->local_initial_highestmodseq, + last_common_modseq)); + } else if (importer->local_initial_highestpvtmodseq < last_common_pvt_modseq) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "local HIGHESTMODSEQ %"PRIu64" < last common HIGHESTMODSEQ %"PRIu64, + importer->local_initial_highestpvtmodseq, + last_common_pvt_modseq)); + } + + importer->local_changes = dsync_transaction_log_scan_get_hash(log_scan); + importer->local_attr_changes = dsync_transaction_log_scan_get_attr_hash(log_scan); + return importer; +} + +static int +dsync_mailbox_import_lookup_attr(struct dsync_mailbox_importer *importer, + enum mail_attribute_type type, const char *key, + struct dsync_mailbox_attribute **attr_r) +{ + struct dsync_mailbox_attribute lookup_attr, *attr; + const struct dsync_mailbox_attribute *attr_change; + struct mail_attribute_value value; + + *attr_r = NULL; + + if (mailbox_attribute_get_stream(importer->box, type, key, &value) < 0) { + i_error("Mailbox %s: Failed to get attribute %s: %s", + mailbox_get_vname(importer->box), key, + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + return -1; + } + + lookup_attr.type = type; + lookup_attr.key = key; + + attr_change = hash_table_lookup(importer->local_attr_changes, + &lookup_attr); + if (attr_change == NULL && + value.value == NULL && value.value_stream == NULL) { + /* we have no knowledge of this attribute */ + return 0; + } + attr = t_new(struct dsync_mailbox_attribute, 1); + attr->type = type; + attr->key = key; + attr->value = value.value; + attr->value_stream = value.value_stream; + attr->last_change = value.last_change; + if (attr_change != NULL) { + attr->deleted = attr_change->deleted && + !DSYNC_ATTR_HAS_VALUE(attr); + attr->modseq = attr_change->modseq; + } + *attr_r = attr; + return 0; +} + +static int +dsync_istreams_cmp(struct istream *input1, struct istream *input2, int *cmp_r) +{ + const unsigned char *data1, *data2; + size_t size1, size2, size; + + *cmp_r = -1; /* quiet gcc */ + + for (;;) { + (void)i_stream_read_more(input1, &data1, &size1); + (void)i_stream_read_more(input2, &data2, &size2); + + if (size1 == 0 || size2 == 0) + break; + size = I_MIN(size1, size2); + *cmp_r = memcmp(data1, data2, size); + if (*cmp_r != 0) + return 0; + i_stream_skip(input1, size); + i_stream_skip(input2, size); + } + if (input1->stream_errno != 0) { + i_error("read(%s) failed: %s", i_stream_get_name(input1), + i_stream_get_error(input1)); + return -1; + } + if (input2->stream_errno != 0) { + i_error("read(%s) failed: %s", i_stream_get_name(input2), + i_stream_get_error(input2)); + return -1; + } + if (size1 == 0 && size2 == 0) + *cmp_r = 0; + else + *cmp_r = size1 == 0 ? -1 : 1; + return 0; +} + +static int +dsync_attributes_cmp_values(const struct dsync_mailbox_attribute *attr1, + const struct dsync_mailbox_attribute *attr2, + int *cmp_r) +{ + struct istream *input1, *input2; + int ret; + + i_assert(attr1->value_stream != NULL || attr1->value != NULL); + i_assert(attr2->value_stream != NULL || attr2->value != NULL); + + if (attr1->value != NULL && attr2->value != NULL) { + *cmp_r = strcmp(attr1->value, attr2->value); + return 0; + } + /* at least one of them is a stream. make both of them streams. */ + input1 = attr1->value_stream != NULL ? attr1->value_stream : + i_stream_create_from_data(attr1->value, strlen(attr1->value)); + input2 = attr2->value_stream != NULL ? attr2->value_stream : + i_stream_create_from_data(attr2->value, strlen(attr2->value)); + i_stream_seek(input1, 0); + i_stream_seek(input2, 0); + ret = dsync_istreams_cmp(input1, input2, cmp_r); + if (attr1->value_stream == NULL) + i_stream_unref(&input1); + if (attr2->value_stream == NULL) + i_stream_unref(&input2); + return ret; +} + +static int +dsync_attributes_cmp(const struct dsync_mailbox_attribute *attr, + const struct dsync_mailbox_attribute *local_attr, + int *cmp_r) +{ + if (DSYNC_ATTR_HAS_VALUE(attr) && + !DSYNC_ATTR_HAS_VALUE(local_attr)) { + /* remote has a value and local doesn't -> use it */ + *cmp_r = 1; + return 0; + } else if (!DSYNC_ATTR_HAS_VALUE(attr) && + DSYNC_ATTR_HAS_VALUE(local_attr)) { + /* remote doesn't have a value, bt local does -> skip */ + *cmp_r = -1; + return 0; + } + + return dsync_attributes_cmp_values(attr, local_attr, cmp_r); +} + +static int +dsync_mailbox_import_attribute_real(struct dsync_mailbox_importer *importer, + const struct dsync_mailbox_attribute *attr, + const struct dsync_mailbox_attribute *local_attr, + const char **result_r) +{ + struct mail_attribute_value value; + int cmp; + bool ignore = FALSE; + + i_assert(DSYNC_ATTR_HAS_VALUE(attr) || attr->deleted); + + if (attr->deleted && + (local_attr == NULL || !DSYNC_ATTR_HAS_VALUE(local_attr))) { + /* attribute doesn't exist on either side -> ignore */ + *result_r = "Nonexistent in both sides"; + return 0; + } + if (local_attr == NULL) { + /* we haven't seen this locally -> use whatever remote has */ + *result_r = "Nonexistent locally"; + } else if (local_attr->modseq <= importer->last_common_modseq && + attr->modseq > importer->last_common_modseq && + importer->last_common_modseq > 0) { + /* we're doing incremental syncing, and we can see that the + attribute was changed remotely, but not locally -> use it */ + *result_r = "Changed remotely"; + } else if (local_attr->modseq > importer->last_common_modseq && + attr->modseq <= importer->last_common_modseq && + importer->last_common_modseq > 0) { + /* we're doing incremental syncing, and we can see that the + attribute was changed locally, but not remotely -> ignore */ + *result_r = "Changed locally"; + ignore = TRUE; + } else if (attr->last_change > local_attr->last_change) { + /* remote has a newer timestamp -> use it */ + *result_r = "Remote has newer timestamp"; + } else if (attr->last_change < local_attr->last_change) { + /* remote has an older timestamp -> ignore */ + *result_r = "Local has newer timestamp"; + ignore = TRUE; + } else { + /* the timestamps are the same. now we're down to guessing + the right answer, unless the values are actually equal, + so check that first. next try to use modseqs, but if even + they are the same, fallback to just picking one based on the + value. */ + if (dsync_attributes_cmp(attr, local_attr, &cmp) < 0) { + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + return -1; + } + if (cmp == 0) { + /* identical scripts */ + *result_r = "Unchanged value"; + return 0; + } + + if (attr->modseq > local_attr->modseq) { + /* remote has a higher modseq -> use it */ + *result_r = "Remote has newer modseq"; + } else if (attr->modseq < local_attr->modseq) { + /* remote has an older modseq -> ignore */ + *result_r = "Local has newer modseq"; + ignore = TRUE; + } else if (cmp < 0) { + ignore = TRUE; + *result_r = "Value changed, but unknown which is newer - picking local"; + } else { + *result_r = "Value changed, but unknown which is newer - picking remote"; + } + } + if (ignore) + return 0; + + i_zero(&value); + value.value = attr->value; + value.value_stream = attr->value_stream; + value.last_change = attr->last_change; + if (mailbox_attribute_set(importer->trans, attr->type, + attr->key, &value) < 0) { + i_error("Mailbox %s: Failed to set attribute %s: %s", + mailbox_get_vname(importer->box), attr->key, + mailbox_get_last_internal_error(importer->box, NULL)); + /* the attributes aren't vital, don't fail everything just + because of them. */ + } + return 0; +} + +int dsync_mailbox_import_attribute(struct dsync_mailbox_importer *importer, + const struct dsync_mailbox_attribute *attr) +{ + struct dsync_mailbox_attribute *local_attr; + const char *result = ""; + int ret; + + if (dsync_mailbox_import_lookup_attr(importer, attr->type, + attr->key, &local_attr) < 0) + ret = -1; + else { + ret = dsync_mailbox_import_attribute_real(importer, attr, + local_attr, &result); + if (local_attr != NULL && local_attr->value_stream != NULL) + i_stream_unref(&local_attr->value_stream); + } + imp_debug(importer, "Import attribute %s: %s", attr->key, + ret < 0 ? "failed" : result); + return ret; +} + +static void dsync_mail_error(struct dsync_mailbox_importer *importer, + struct mail *mail, const char *field) +{ + const char *errstr; + enum mail_error error; + + errstr = mailbox_get_last_internal_error(importer->box, &error); + if (error == MAIL_ERROR_EXPUNGED) + return; + + i_error("Mailbox %s: Can't lookup %s for UID=%u: %s", + mailbox_get_vname(mail->box), field, mail->uid, errstr); + importer->mail_error = error; + importer->failed = TRUE; +} + +static bool +dsync_mail_change_guid_equals(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change, + const char *guid, const char **cmp_guid_r) +{ + guid_128_t guid_128, change_guid_128; + + if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + if (guid_128_from_string(change->guid, change_guid_128) < 0) + i_unreached(); + } else if (importer->mails_use_guid128) { + mail_generate_guid_128_hash(change->guid, change_guid_128); + } else { + if (cmp_guid_r != NULL) + *cmp_guid_r = change->guid; + return strcmp(change->guid, guid) == 0; + } + + mail_generate_guid_128_hash(guid, guid_128); + if (memcmp(change_guid_128, guid_128, GUID_128_SIZE) != 0) { + if (cmp_guid_r != NULL) { + *cmp_guid_r = t_strdup_printf("%s(guid128, orig=%s)", + binary_to_hex(change_guid_128, sizeof(change_guid_128)), + change->guid); + } + return FALSE; + } + return TRUE; +} + +static int +importer_try_next_mail(struct dsync_mailbox_importer *importer, + uint32_t wanted_uid) +{ + struct mail_private *pmail; + const char *hdr_hash; + + if (importer->cur_mail == NULL) { + /* end of search */ + return -1; + } + while (importer->cur_mail->seq < importer->next_local_seq || + importer->cur_mail->uid < wanted_uid) { + if (!importer->cur_uid_has_change && + !importer->last_common_uid_found) { + /* this message exists locally, but remote didn't send + expunge-change for it. if the message's + uid <= last-common-uid, it should be deleted */ + seq_range_array_add(&importer->maybe_expunge_uids, + importer->cur_mail->uid); + } + + importer->cur_mail_skip = FALSE; + if (!mailbox_search_next(importer->search_ctx, + &importer->cur_mail)) { + importer->cur_mail = NULL; + importer->cur_guid = NULL; + importer->cur_hdr_hash = NULL; + return -1; + } + importer->cur_uid_has_change = FALSE; + } + importer->cur_uid_has_change = importer->cur_mail->uid == wanted_uid; + if (importer->mails_have_guids) { + if (mail_get_special(importer->cur_mail, MAIL_FETCH_GUID, + &importer->cur_guid) < 0) { + dsync_mail_error(importer, importer->cur_mail, "GUID"); + return 0; + } + } else { + if (dsync_mail_get_hdr_hash(importer->cur_mail, + importer->hdr_hash_version, + importer->hashed_headers, + &hdr_hash) < 0) { + dsync_mail_error(importer, importer->cur_mail, + "header hash"); + return 0; + } + pmail = (struct mail_private *)importer->cur_mail; + importer->cur_hdr_hash = p_strdup(pmail->pool, hdr_hash); + importer->cur_guid = ""; + } + /* make sure next_local_seq gets updated in case we came here + because of min_uid */ + importer->next_local_seq = importer->cur_mail->seq; + return 1; +} + +static bool +importer_next_mail(struct dsync_mailbox_importer *importer, uint32_t wanted_uid) +{ + int ret; + + for (;;) { + T_BEGIN { + ret = importer_try_next_mail(importer, wanted_uid); + } T_END; + if (ret != 0 || importer->failed) + break; + importer->next_local_seq = importer->cur_mail->seq + 1; + } + return ret > 0; +} + +static int +importer_mail_cmp(const struct importer_mail *m1, + const struct importer_mail *m2) +{ + int ret; + + if (m1->guid == NULL) + return 1; + if (m2->guid == NULL) + return -1; + + ret = strcmp(m1->guid, m2->guid); + if (ret != 0) + return ret; + + if (m1->uid < m2->uid) + return -1; + if (m1->uid > m2->uid) + return 1; + return 0; +} + +static void newmail_link(struct dsync_mailbox_importer *importer, + struct importer_new_mail *newmail, uint32_t remote_uid) +{ + struct importer_new_mail *first_mail, **last, *mail, *link = NULL; + + if (*newmail->guid != '\0') { + first_mail = hash_table_lookup(importer->import_guids, + newmail->guid); + if (first_mail == NULL) { + /* first mail for this GUID */ + hash_table_insert(importer->import_guids, + newmail->guid, newmail); + return; + } + } else { + if (remote_uid == 0) { + /* mail exists locally. we don't want to request + it, and we'll assume it has no duplicate + instances. */ + return; + } + first_mail = hash_table_lookup(importer->import_uids, + POINTER_CAST(remote_uid)); + if (first_mail == NULL) { + /* first mail for this UID */ + hash_table_insert(importer->import_uids, + POINTER_CAST(remote_uid), newmail); + return; + } + } + /* 1) add the newmail to the end of the linked list + 2) find our link + + FIXME: this loop is slow if the same GUID has a ton of instances. + Could it be improved in some way? */ + last = &first_mail->next; + for (mail = first_mail; mail != NULL; mail = mail->next) { + if (mail->final_uid == newmail->final_uid) + mail->uid_is_usable = TRUE; + if (link == NULL && mail->link == NULL && + mail->uid_in_local != newmail->uid_in_local) + link = mail; + last = &mail->next; + } + *last = newmail; + if (link != NULL && newmail->link == NULL) { + link->link = newmail; + newmail->link = link; + } +} + +static void +dsync_mailbox_revert_existing_uid(struct dsync_mailbox_importer *importer, + uint32_t uid, const char *reason) +{ + i_assert(importer->revert_local_changes); + + /* UID either already exists or UIDNEXT is too high. we can't set the + wanted UID, so we'll need to delete the whole mailbox and resync */ + i_warning("Deleting mailbox '%s': UID=%u already exists locally for a different mail: %s", + mailbox_get_vname(importer->box), uid, reason); + importer->delete_mailbox = TRUE; + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; +} + +static bool dsync_mailbox_try_save_cur(struct dsync_mailbox_importer *importer, + struct dsync_mail_change *save_change) +{ + struct importer_mail m1, m2; + struct importer_new_mail *newmail; + int diff; + bool remote_saved; + + i_zero(&m1); + if (importer->cur_mail != NULL) { + m1.guid = importer->mails_have_guids ? + importer->cur_guid : importer->cur_hdr_hash; + m1.uid = importer->cur_mail->uid; + } + i_zero(&m2); + if (save_change != NULL) { + m2.guid = importer->mails_have_guids ? + save_change->guid : save_change->hdr_hash; + m2.uid = save_change->uid; + i_assert(save_change->type != DSYNC_MAIL_CHANGE_TYPE_EXPUNGE); + } + + if (importer->empty_hdr_workaround && !importer->mails_have_guids && + importer->cur_mail != NULL && save_change != NULL && + (dsync_mail_hdr_hash_is_empty(m1.guid) || + dsync_mail_hdr_hash_is_empty(m2.guid))) { + /* one of the headers is empty. assume it's broken and that + the header matches what we have currently. */ + diff = 0; + } else { + diff = importer_mail_cmp(&m1, &m2); + } + if (diff < 0) { + /* add a record for local mail */ + i_assert(importer->cur_mail != NULL); + if (importer->revert_local_changes) { + if (save_change == NULL && + importer->cur_mail->uid >= importer->remote_uid_next) { + dsync_mailbox_revert_existing_uid(importer, importer->cur_mail->uid, + t_strdup_printf("higher than remote's UIDs (remote UIDNEXT=%u)", importer->remote_uid_next)); + return TRUE; + } + mail_expunge(importer->cur_mail); + importer->cur_mail_skip = TRUE; + importer->next_local_seq++; + return FALSE; + } + newmail = p_new(importer->pool, struct importer_new_mail, 1); + newmail->guid = p_strdup(importer->pool, importer->cur_guid); + newmail->final_uid = importer->cur_mail->uid; + newmail->local_uid = importer->cur_mail->uid; + newmail->uid_in_local = TRUE; + newmail->uid_is_usable = + newmail->final_uid >= importer->remote_uid_next; + remote_saved = FALSE; + } else if (diff > 0) { + i_assert(save_change != NULL); + newmail = p_new(importer->pool, struct importer_new_mail, 1); + newmail->guid = save_change->guid; + newmail->final_uid = save_change->uid; + newmail->remote_uid = save_change->uid; + newmail->uid_in_local = FALSE; + newmail->uid_is_usable = + newmail->final_uid >= importer->local_uid_next; + if (!newmail->uid_is_usable && importer->revert_local_changes) { + dsync_mailbox_revert_existing_uid(importer, newmail->final_uid, + t_strdup_printf("UID >= local UIDNEXT=%u", importer->local_uid_next)); + return TRUE; + } + remote_saved = TRUE; + } else { + /* identical */ + i_assert(importer->cur_mail != NULL); + i_assert(save_change != NULL); + newmail = p_new(importer->pool, struct importer_new_mail, 1); + newmail->guid = save_change->guid; + newmail->final_uid = importer->cur_mail->uid; + newmail->local_uid = importer->cur_mail->uid; + newmail->remote_uid = save_change->uid; + newmail->uid_in_local = TRUE; + newmail->uid_is_usable = TRUE; + newmail->link = newmail; + remote_saved = TRUE; + } + + if (newmail->uid_in_local) { + importer->cur_mail_skip = TRUE; + importer->next_local_seq++; + } + /* NOTE: assumes save_change is allocated from importer pool */ + newmail->change = save_change; + + array_push_back(&importer->newmails, &newmail); + if (newmail->uid_in_local) + newmail_link(importer, newmail, 0); + else { + i_assert(save_change != NULL); + newmail_link(importer, newmail, save_change->uid); + } + return remote_saved; +} + +static bool ATTR_NULL(2) +dsync_mailbox_try_save(struct dsync_mailbox_importer *importer, + struct dsync_mail_change *save_change) +{ + if (importer->cur_mail_skip) { + if (!importer_next_mail(importer, 0) && save_change == NULL) + return FALSE; + } + return dsync_mailbox_try_save_cur(importer, save_change); +} + +static void dsync_mailbox_save(struct dsync_mailbox_importer *importer, + struct dsync_mail_change *save_change) +{ + while (!dsync_mailbox_try_save(importer, save_change)) ; +} + +static bool +dsync_import_set_mail(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + const char *guid, *cmp_guid; + + if (!mail_set_uid(importer->mail, change->uid)) + return FALSE; + if (change->guid == NULL) { + /* GUID is unknown */ + return TRUE; + } + if (*change->guid == '\0') { + /* backend doesn't support GUIDs. if hdr_hash is set, we could + verify it, but since this message really is supposed to + match, it's probably too much trouble. */ + return TRUE; + } + + /* verify that GUID matches, just in case */ + if (mail_get_special(importer->mail, MAIL_FETCH_GUID, &guid) < 0) { + dsync_mail_error(importer, importer->mail, "GUID"); + return FALSE; + } + if (!dsync_mail_change_guid_equals(importer, change, guid, &cmp_guid)) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "Unexpected GUID mismatch for UID=%u: %s != %s", + change->uid, guid, cmp_guid)); + return FALSE; + } + return TRUE; +} + +static bool dsync_check_cur_guid(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + const char *cmp_guid; + + if (change->guid == NULL || change->guid[0] == '\0' || + importer->cur_guid[0] == '\0') + return TRUE; + if (!dsync_mail_change_guid_equals(importer, change, + importer->cur_guid, &cmp_guid)) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "Unexpected GUID mismatch (2) for UID=%u: %s != %s", + change->uid, importer->cur_guid, cmp_guid)); + return FALSE; + } + return TRUE; +} + +static void +merge_flags(uint32_t local_final, uint32_t local_add, uint32_t local_remove, + uint32_t remote_final, uint32_t remote_add, uint32_t remote_remove, + uint32_t pvt_mask, bool prefer_remote, bool prefer_pvt_remote, + uint32_t *change_add_r, uint32_t *change_remove_r, + bool *remote_changed, bool *remote_pvt_changed) +{ + uint32_t combined_add, combined_remove, conflict_flags; + uint32_t local_wanted, remote_wanted, conflict_pvt_flags; + + /* resolve conflicts */ + conflict_flags = local_add & remote_remove; + if (conflict_flags != 0) { + conflict_pvt_flags = conflict_flags & pvt_mask; + conflict_flags &= ~pvt_mask; + if (prefer_remote) + local_add &= ~conflict_flags; + else + remote_remove &= ~conflict_flags; + if (prefer_pvt_remote) + local_add &= ~conflict_pvt_flags; + else + remote_remove &= ~conflict_pvt_flags; + } + conflict_flags = local_remove & remote_add; + if (conflict_flags != 0) { + conflict_pvt_flags = conflict_flags & pvt_mask; + conflict_flags &= ~pvt_mask; + if (prefer_remote) + local_remove &= ~conflict_flags; + else + remote_add &= ~conflict_flags; + if (prefer_pvt_remote) + local_remove &= ~conflict_pvt_flags; + else + remote_add &= ~conflict_pvt_flags; + } + + combined_add = local_add|remote_add; + combined_remove = local_remove|remote_remove; + i_assert((combined_add & combined_remove) == 0); + + /* don't change flags that are currently identical in both sides */ + conflict_flags = local_final ^ remote_final; + combined_add &= conflict_flags; + combined_remove &= conflict_flags; + + /* see if there are conflicting final flags */ + local_wanted = (local_final|combined_add) & ~combined_remove; + remote_wanted = (remote_final|combined_add) & ~combined_remove; + + conflict_flags = local_wanted ^ remote_wanted; + if (conflict_flags != 0) { + if (prefer_remote && prefer_pvt_remote) + local_wanted = remote_wanted; + else if (prefer_remote && !prefer_pvt_remote) { + local_wanted = (local_wanted & pvt_mask) | + (remote_wanted & ~pvt_mask); + } else if (!prefer_remote && prefer_pvt_remote) { + local_wanted = (local_wanted & ~pvt_mask) | + (remote_wanted & pvt_mask); + } + } + + *change_add_r = local_wanted & ~local_final; + *change_remove_r = local_final & ~local_wanted; + if ((local_wanted & ~pvt_mask) != (remote_final & ~pvt_mask)) + *remote_changed = TRUE; + if ((local_wanted & pvt_mask) != (remote_final & pvt_mask)) + *remote_pvt_changed = TRUE; +} + +static bool +keyword_find(ARRAY_TYPE(const_string) *keywords, const char *name, + unsigned int *idx_r) +{ + const char *const *names; + unsigned int i, count; + + names = array_get(keywords, &count); + for (i = 0; i < count; i++) { + if (strcmp(names[i], name) == 0) { + *idx_r = i; + return TRUE; + } + } + return FALSE; +} + +static void keywords_append(ARRAY_TYPE(const_string) *dest, + const ARRAY_TYPE(const_string) *keywords, + uint32_t bits, unsigned int start_idx) +{ + const char *name; + unsigned int i; + + for (i = 0; i < 32; i++) { + if ((bits & (1U << i)) == 0) + continue; + + name = array_idx_elem(keywords, start_idx+i); + array_push_back(dest, &name); + } +} + +static void +merge_keywords(struct mail *mail, const ARRAY_TYPE(const_string) *local_changes, + const ARRAY_TYPE(const_string) *remote_changes, + bool prefer_remote, + bool *remote_changed, bool *remote_pvt_changed) +{ + /* local_changes and remote_changes are assumed to have no + duplicates names */ + uint32_t *local_add, *local_remove, *local_final; + uint32_t *remote_add, *remote_remove, *remote_final; + uint32_t *change_add, *change_remove; + ARRAY_TYPE(const_string) all_keywords, add_keywords, remove_keywords; + const char *const *changes, *name, *const *local_keywords; + struct mail_keywords *kw; + unsigned int i, count, name_idx, array_size; + + local_keywords = mail_get_keywords(mail); + + /* we'll assign a common index for each keyword name and place + the changes to separate bit arrays. */ + if (array_is_created(remote_changes)) + changes = array_get(remote_changes, &count); + else { + changes = NULL; + count = 0; + } + + array_size = str_array_length(local_keywords) + count; + if (array_is_created(local_changes)) + array_size += array_count(local_changes); + if (array_size == 0) { + /* this message has no keywords */ + return; + } + t_array_init(&all_keywords, array_size); + t_array_init(&add_keywords, array_size); + t_array_init(&remove_keywords, array_size); + + /* @UNSAFE: create large enough arrays to fit all keyword indexes. */ + array_size = (array_size+31)/32; + local_add = t_new(uint32_t, array_size); + local_remove = t_new(uint32_t, array_size); + local_final = t_new(uint32_t, array_size); + remote_add = t_new(uint32_t, array_size); + remote_remove = t_new(uint32_t, array_size); + remote_final = t_new(uint32_t, array_size); + change_add = t_new(uint32_t, array_size); + change_remove = t_new(uint32_t, array_size); + + /* get remote changes */ + for (i = 0; i < count; i++) { + name = changes[i]+1; + name_idx = array_count(&all_keywords); + array_push_back(&all_keywords, &name); + + switch (changes[i][0]) { + case KEYWORD_CHANGE_ADD: + remote_add[name_idx/32] |= 1U << (name_idx%32); + break; + case KEYWORD_CHANGE_REMOVE: + remote_remove[name_idx/32] |= 1U << (name_idx%32); + break; + case KEYWORD_CHANGE_FINAL: + remote_final[name_idx/32] |= 1U << (name_idx%32); + break; + case KEYWORD_CHANGE_ADD_AND_FINAL: + remote_add[name_idx/32] |= 1U << (name_idx%32); + remote_final[name_idx/32] |= 1U << (name_idx%32); + break; + } + } + + /* get local changes. use existing indexes for names when they exist. */ + if (array_is_created(local_changes)) + changes = array_get(local_changes, &count); + else { + changes = NULL; + count = 0; + } + for (i = 0; i < count; i++) { + name = changes[i]+1; + if (!keyword_find(&all_keywords, name, &name_idx)) { + name_idx = array_count(&all_keywords); + array_push_back(&all_keywords, &name); + } + + switch (changes[i][0]) { + case KEYWORD_CHANGE_ADD: + case KEYWORD_CHANGE_ADD_AND_FINAL: + local_add[name_idx/32] |= 1U << (name_idx%32); + break; + case KEYWORD_CHANGE_REMOVE: + local_remove[name_idx/32] |= 1U << (name_idx%32); + break; + case KEYWORD_CHANGE_FINAL: + break; + } + } + for (i = 0; local_keywords[i] != NULL; i++) { + name = local_keywords[i]; + if (!keyword_find(&all_keywords, name, &name_idx)) { + name_idx = array_count(&all_keywords); + array_push_back(&all_keywords, &name); + } + local_final[name_idx/32] |= 1U << (name_idx%32); + } + i_assert(array_count(&all_keywords) <= array_size*32); + array_size = (array_count(&all_keywords)+31) / 32; + + /* merge keywords */ + for (i = 0; i < array_size; i++) { + merge_flags(local_final[i], local_add[i], local_remove[i], + remote_final[i], remote_add[i], remote_remove[i], + 0, prefer_remote, prefer_remote, + &change_add[i], &change_remove[i], + remote_changed, remote_pvt_changed); + if (change_add[i] != 0) { + keywords_append(&add_keywords, &all_keywords, + change_add[i], i*32); + } + if (change_remove[i] != 0) { + keywords_append(&remove_keywords, &all_keywords, + change_remove[i], i*32); + } + } + + /* apply changes */ + if (array_count(&add_keywords) > 0) { + array_append_zero(&add_keywords); + kw = mailbox_keywords_create_valid(mail->box, + array_front(&add_keywords)); + mail_update_keywords(mail, MODIFY_ADD, kw); + mailbox_keywords_unref(&kw); + } + if (array_count(&remove_keywords) > 0) { + array_append_zero(&remove_keywords); + kw = mailbox_keywords_create_valid(mail->box, + array_front(&remove_keywords)); + mail_update_keywords(mail, MODIFY_REMOVE, kw); + mailbox_keywords_unref(&kw); + } +} + +static void +dsync_mailbox_import_replace_flags(struct mail *mail, + const struct dsync_mail_change *change) +{ + ARRAY_TYPE(const_string) keywords; + struct mail_keywords *kw; + const char *const *changes, *name; + unsigned int i, count; + + if (array_is_created(&change->keyword_changes)) + changes = array_get(&change->keyword_changes, &count); + else { + changes = NULL; + count = 0; + } + t_array_init(&keywords, count+1); + for (i = 0; i < count; i++) { + switch (changes[i][0]) { + case KEYWORD_CHANGE_ADD: + case KEYWORD_CHANGE_FINAL: + case KEYWORD_CHANGE_ADD_AND_FINAL: + name = changes[i]+1; + array_push_back(&keywords, &name); + break; + case KEYWORD_CHANGE_REMOVE: + break; + } + } + array_append_zero(&keywords); + + kw = mailbox_keywords_create_valid(mail->box, array_front(&keywords)); + mail_update_keywords(mail, MODIFY_REPLACE, kw); + mailbox_keywords_unref(&kw); + + mail_update_flags(mail, MODIFY_REPLACE, + change->add_flags | change->final_flags); + if (mail_get_modseq(mail) < change->modseq) + mail_update_modseq(mail, change->modseq); + if (mail_get_pvt_modseq(mail) < change->pvt_modseq) + mail_update_pvt_modseq(mail, change->pvt_modseq); +} + +static void +dsync_mailbox_import_flag_change(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + const struct dsync_mail_change *local_change; + enum mail_flags local_add, local_remove; + uint32_t change_add, change_remove; + uint64_t new_modseq; + ARRAY_TYPE(const_string) local_keyword_changes = ARRAY_INIT; + struct mail *mail; + bool prefer_remote, prefer_pvt_remote; + bool remote_changed = FALSE, remote_pvt_changed = FALSE; + + i_assert((change->add_flags & change->remove_flags) == 0); + + if (importer->cur_mail != NULL && + importer->cur_mail->uid == change->uid) { + if (!dsync_check_cur_guid(importer, change)) + return; + mail = importer->cur_mail; + } else { + if (!dsync_import_set_mail(importer, change)) + return; + mail = importer->mail; + } + + if (importer->revert_local_changes) { + /* dsync backup: just make the local look like remote. */ + dsync_mailbox_import_replace_flags(mail, change); + return; + } + + local_change = hash_table_lookup(importer->local_changes, + POINTER_CAST(change->uid)); + if (local_change == NULL) { + local_add = local_remove = 0; + } else { + local_add = local_change->add_flags; + local_remove = local_change->remove_flags; + local_keyword_changes = local_change->keyword_changes; + } + + if (mail_get_modseq(mail) < change->modseq) + prefer_remote = TRUE; + else if (mail_get_modseq(mail) > change->modseq) + prefer_remote = FALSE; + else { + /* identical modseq, we'll just have to pick one. + Note that both brains need to pick the same one, otherwise + they become unsynced. */ + prefer_remote = !importer->master_brain; + } + if (mail_get_pvt_modseq(mail) < change->pvt_modseq) + prefer_pvt_remote = TRUE; + else if (mail_get_pvt_modseq(mail) > change->pvt_modseq) + prefer_pvt_remote = FALSE; + else + prefer_pvt_remote = !importer->master_brain; + + /* merge flags */ + merge_flags(mail_get_flags(mail), local_add, local_remove, + change->final_flags, change->add_flags, change->remove_flags, + mailbox_get_private_flags_mask(mail->box), + prefer_remote, prefer_pvt_remote, + &change_add, &change_remove, + &remote_changed, &remote_pvt_changed); + + if (change_add != 0) + mail_update_flags(mail, MODIFY_ADD, change_add); + if (change_remove != 0) + mail_update_flags(mail, MODIFY_REMOVE, change_remove); + + /* merge keywords */ + merge_keywords(mail, &local_keyword_changes, &change->keyword_changes, + prefer_remote, &remote_changed, &remote_pvt_changed); + + /* update modseqs. try to anticipate when we have to increase modseq + to get it closer to what remote has (although we can't guess it + exactly correctly) */ + new_modseq = change->modseq; + if (remote_changed && new_modseq <= importer->remote_highest_modseq) + new_modseq = importer->remote_highest_modseq+1; + if (mail_get_modseq(mail) < new_modseq) + mail_update_modseq(mail, new_modseq); + + new_modseq = change->pvt_modseq; + if (remote_pvt_changed && new_modseq <= importer->remote_highest_pvt_modseq) + new_modseq = importer->remote_highest_pvt_modseq+1; + if (mail_get_pvt_modseq(mail) < new_modseq) + mail_update_pvt_modseq(mail, new_modseq); +} + +static bool +dsync_mail_change_have_keyword(const struct dsync_mail_change *change, + const char *keyword) +{ + const char *str; + + if (!array_is_created(&change->keyword_changes)) + return FALSE; + + array_foreach_elem(&change->keyword_changes, str) { + switch (str[0]) { + case KEYWORD_CHANGE_FINAL: + case KEYWORD_CHANGE_ADD_AND_FINAL: + if (strcasecmp(str+1, keyword) == 0) + return TRUE; + break; + default: + break; + } + } + return FALSE; +} + +static bool +dsync_mailbox_import_want_change(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change, + const char **result_r) +{ + if (importer->sync_since_timestamp > 0) { + i_assert(change->received_timestamp > 0); + if (change->received_timestamp < importer->sync_since_timestamp) { + /* mail has too old timestamp - skip it */ + *result_r = "Ignoring missing local mail with too old timestamp"; + return FALSE; + } + } + if (importer->sync_until_timestamp > 0) { + i_assert(change->received_timestamp > 0); + if (change->received_timestamp > importer->sync_until_timestamp) { + /* mail has too new timestamp - skip it */ + *result_r = "Ignoring missing local mail with too new timestamp"; + return FALSE; + } + } + if (importer->sync_max_size > 0) { + i_assert(change->virtual_size != UOFF_T_MAX); + if (change->virtual_size > importer->sync_max_size) { + /* mail is too large - skip it */ + *result_r = "Ignoring missing local mail with too large size"; + return FALSE; + } + } + if (importer->sync_flag != 0) { + bool have_flag = (change->final_flags & importer->sync_flag) != 0; + + if (have_flag && importer->sync_flag_dontwant) { + *result_r = "Ignoring missing local mail that doesn't have wanted flags"; + return FALSE; + } + if (!have_flag && !importer->sync_flag_dontwant) { + *result_r = "Ignoring missing local mail that has unwanted flags"; + return FALSE; + } + } + if (importer->sync_keyword != NULL) { + bool have_kw = dsync_mail_change_have_keyword(change, importer->sync_keyword); + + if (have_kw && importer->sync_flag_dontwant) { + *result_r = "Ignoring missing local mail that doesn't have wanted keywords"; + return FALSE; + } + if (!have_kw && !importer->sync_flag_dontwant) { + *result_r = "Ignoring missing local mail that has unwanted keywords"; + return FALSE; + } + } + return TRUE; +} + +static void +dsync_mailbox_import_save(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + struct dsync_mail_change *save; + const char *result; + + i_assert(change->guid != NULL); + + if (change->uid == importer->last_common_uid) { + /* we've already verified that the GUID matches. + apply flag changes if there are any. */ + i_assert(!importer->last_common_uid_found); + dsync_mailbox_import_flag_change(importer, change); + return; + } + if (!dsync_mailbox_import_want_change(importer, change, &result)) + return; + + save = p_new(importer->pool, struct dsync_mail_change, 1); + dsync_mail_change_dup(importer->pool, change, save); + + if (importer->last_common_uid_found) { + /* this is a new mail. its UID may or may not conflict with + an existing local mail, we'll figure it out later. */ + i_assert(change->uid > importer->last_common_uid); + dsync_mailbox_save(importer, save); + } else { + /* the local mail is expunged. we'll decide later if we want + to save this mail locally or expunge it form remote. */ + i_assert(change->uid > importer->last_common_uid); + i_assert(importer->cur_mail == NULL || + change->uid < importer->cur_mail->uid); + array_push_back(&importer->maybe_saves, &save); + } +} + +static void +dsync_mailbox_import_expunge(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + + if (importer->last_common_uid_found) { + /* expunge the message, unless its GUID unexpectedly doesn't + match */ + i_assert(change->uid <= importer->last_common_uid); + if (dsync_import_set_mail(importer, change)) + mail_expunge(importer->mail); + } else if (importer->cur_mail == NULL || + change->uid < importer->cur_mail->uid) { + /* already expunged locally, we can ignore this. + uid=last_common_uid if we managed to verify from + transaction log that the GUIDs match */ + i_assert(change->uid >= importer->last_common_uid); + } else if (change->uid == importer->last_common_uid) { + /* already verified that the GUID matches */ + i_assert(importer->cur_mail->uid == change->uid); + if (dsync_check_cur_guid(importer, change)) + mail_expunge(importer->cur_mail); + } else { + /* we don't know yet if we should expunge this + message or not. queue it until we do. */ + i_assert(change->uid > importer->last_common_uid); + seq_range_array_add(&importer->maybe_expunge_uids, change->uid); + } +} + +static void +dsync_mailbox_rewind_search(struct dsync_mailbox_importer *importer) +{ + /* If there are local mails after last_common_uid which we skipped + while trying to match the next message, we need to now go back */ + if (importer->cur_mail != NULL && + importer->cur_mail->uid <= importer->last_common_uid+1) + return; + + importer->cur_mail = NULL; + importer->cur_guid = NULL; + importer->cur_hdr_hash = NULL; + importer->next_local_seq = 0; + + (void)mailbox_search_deinit(&importer->search_ctx); + dsync_mailbox_import_search_init(importer); +} + +static void +dsync_mailbox_common_uid_found(struct dsync_mailbox_importer *importer) +{ + struct dsync_mail_change *const *saves; + struct seq_range_iter iter; + unsigned int n, i, count; + uint32_t uid; + + if (importer->debug) T_BEGIN { + string_t *expunges = t_str_new(64); + + imap_write_seq_range(expunges, &importer->maybe_expunge_uids); + imp_debug(importer, "Last common UID=%u. Delayed expunges=%s", + importer->last_common_uid, str_c(expunges)); + } T_END; + + importer->last_common_uid_found = TRUE; + dsync_mailbox_rewind_search(importer); + + /* expunge the messages whose expunge-decision we delayed previously */ + seq_range_array_iter_init(&iter, &importer->maybe_expunge_uids); n = 0; + while (seq_range_array_iter_nth(&iter, n++, &uid)) { + if (uid > importer->last_common_uid) { + /* we expunge messages only up to last_common_uid, + ignore the rest */ + break; + } + + if (mail_set_uid(importer->mail, uid)) + mail_expunge(importer->mail); + } + + /* handle pending saves */ + saves = array_get(&importer->maybe_saves, &count); + for (i = 0; i < count; i++) { + if (saves[i]->uid > importer->last_common_uid) { + imp_debug(importer, "Delayed save UID=%u: Save", + saves[i]->uid); + dsync_mailbox_save(importer, saves[i]); + } else { + imp_debug(importer, "Delayed save UID=%u: Ignore", + saves[i]->uid); + } + } +} + +static int +dsync_mailbox_import_match_msg(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change, + const char **result_r) +{ + const char *hdr_hash, *cmp_guid; + + if (*change->guid != '\0' && *importer->cur_guid != '\0') { + /* we have GUIDs, verify them */ + if (dsync_mail_change_guid_equals(importer, change, + importer->cur_guid, &cmp_guid)) { + *result_r = "GUIDs match"; + return 1; + } else { + *result_r = t_strdup_printf("GUIDs don't match (%s vs %s)", + importer->cur_guid, cmp_guid); + return 0; + } + } + + /* verify hdr_hash if it exists */ + if (change->hdr_hash == NULL) { + i_assert(*importer->cur_guid == '\0'); + if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + /* the message was already expunged, so we don't know + its header. return "unknown". */ + *result_r = "Unknown match for expunge"; + return -1; + } + i_error("Mailbox %s: GUIDs not supported, " + "sync with header hashes instead", + mailbox_get_vname(importer->box)); + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + *result_r = "Error, invalid parameters"; + return -1; + } + + if (dsync_mail_get_hdr_hash(importer->cur_mail, + importer->hdr_hash_version, + importer->hashed_headers, &hdr_hash) < 0) { + dsync_mail_error(importer, importer->cur_mail, "hdr-stream"); + *result_r = "Error fetching header stream"; + return -1; + } + if (importer->empty_hdr_workaround && + (dsync_mail_hdr_hash_is_empty(change->hdr_hash) || + dsync_mail_hdr_hash_is_empty(hdr_hash))) { + *result_r = "Empty headers found with workaround enabled - assuming a match"; + return 1; + } else if (strcmp(change->hdr_hash, hdr_hash) == 0) { + *result_r = "Headers hashes match"; + return 1; + } else { + *result_r = t_strdup_printf("Headers hashes don't match (%s vs %s)", + change->hdr_hash, hdr_hash); + return 0; + } +} + +static bool +dsync_mailbox_find_common_expunged_uid(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change, + const char **result_r) +{ + const struct dsync_mail_change *local_change; + + if (*change->guid == '\0') { + /* remote doesn't support GUIDs, can't verify expunge */ + *result_r = "GUIDs not supported, can't verify expunge"; + return FALSE; + } + + /* local message is expunged. see if we can find its GUID from + transaction log and check if the GUIDs match. The GUID in + log is a 128bit GUID, so we may need to convert the remote's + GUID string to 128bit GUID first. */ + local_change = hash_table_lookup(importer->local_changes, + POINTER_CAST(change->uid)); + if (local_change == NULL || local_change->guid == NULL) { + *result_r = "Expunged local mail's GUID not found"; + return FALSE; + } + + i_assert(local_change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE); + if (dsync_mail_change_guid_equals(importer, local_change, + change->guid, NULL)) { + importer->last_common_uid = change->uid; + *result_r = "Expunged local mail's GUID matches remote"; + } else if (change->type != DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + dsync_mailbox_common_uid_found(importer); + *result_r = "Expunged local mail's GUID doesn't match remote GUID"; + } else { + /* GUID mismatch for two expunged mails. dsync can't update + GUIDs for already expunged messages, so we can't immediately + determine that the rest of the messages are a mismatch. so + for now we'll just skip over this pair. */ + *result_r = "Expunged mails' GUIDs don't match - delaying decision"; + /* NOTE: the return value here doesn't matter, because the only + caller that checks for it never reaches this code path */ + } + return TRUE; +} + +static void +dsync_mailbox_revert_missing(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + i_assert(importer->revert_local_changes); + + /* mail exists on remote, but not locally. we'll need to + insert this mail back, which means deleting the whole + mailbox and resyncing. */ + i_warning("Deleting mailbox '%s': UID=%u GUID=%s is missing locally", + mailbox_get_vname(importer->box), + change->uid, change->guid); + importer->delete_mailbox = TRUE; + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; +} + +static void +dsync_mailbox_find_common_uid(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change, + const char **result_r) +{ + int ret; + + i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE || + ((change->received_timestamp > 0 || + (importer->sync_since_timestamp == 0 && + importer->sync_until_timestamp == 0)) && + (change->virtual_size != UOFF_T_MAX || importer->sync_max_size == 0))); + + /* try to find the matching local mail */ + if (!importer_next_mail(importer, change->uid)) { + /* no more local mails. we can still try to match + expunged mails though. */ + if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + /* mail doesn't exist remotely either, don't bother + looking it up locally. */ + *result_r = "Expunged mail not found locally"; + return; + } + i_assert(change->guid != NULL); + if (!dsync_mailbox_import_want_change(importer, change, result_r)) + ; + else if (importer->local_uid_next <= change->uid) { + dsync_mailbox_common_uid_found(importer); + *result_r = "Mail's UID is above local UIDNEXT"; + } else if (importer->revert_local_changes) { + dsync_mailbox_revert_missing(importer, change); + *result_r = "Reverting local change by deleting mailbox"; + } else if (!dsync_mailbox_find_common_expunged_uid(importer, change, result_r)) { + /* it's unknown if this mail existed locally and was + expunged. since we don't want to lose any mails, + assume that we need to preserve the mail. use the + last message with a matching GUID as the last common + UID. */ + dsync_mailbox_common_uid_found(importer); + } + *result_r = t_strdup_printf("%s - No more local mails found", *result_r); + return; + } + + if (change->guid == NULL) { + /* we can't know if this UID matches */ + i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE); + *result_r = "Expunged mail has no GUID, can't verify it"; + return; + } + if (importer->cur_mail->uid == change->uid) { + /* we have a matching local UID. check GUID to see if it's + really the same mail or not */ + if ((ret = dsync_mailbox_import_match_msg(importer, change, result_r)) < 0) { + /* unknown */ + return; + } + if (ret > 0) { + importer->last_common_uid = change->uid; + } else if (!importer->revert_local_changes) { + /* mismatch - found the first non-common UID */ + dsync_mailbox_common_uid_found(importer); + } else { + /* mismatch and we want to revert local changes - + need to delete the mailbox. */ + dsync_mailbox_revert_existing_uid(importer, change->uid, *result_r); + } + return; + } + /* mail exists remotely, but doesn't exist locally. */ + if (!dsync_mailbox_import_want_change(importer, change, result_r)) + return; + if (importer->revert_local_changes && + change->type != DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + dsync_mailbox_revert_missing(importer, change); + *result_r = "Reverting local change by deleting mailbox"; + } else { + (void)dsync_mailbox_find_common_expunged_uid(importer, change, result_r); + } + *result_r = t_strdup_printf("%s (next local mail UID=%u)", + *result_r, importer->cur_mail == NULL ? 0 : importer->cur_mail->uid); +} + +int dsync_mailbox_import_change(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + const char *result; + + i_assert(!importer->new_uids_assigned); + i_assert(importer->prev_uid < change->uid); + + importer->prev_uid = change->uid; + + if (importer->failed) + return -1; + if (importer->require_full_resync) + return 0; + + if (!importer->last_common_uid_found) { + result = NULL; + dsync_mailbox_find_common_uid(importer, change, &result); + i_assert(result != NULL); + } else { + result = "New mail"; + } + + imp_debug(importer, "Import change type=%s GUID=%s UID=%u hdr_hash=%s result=%s", + dsync_mail_change_type_names[change->type], + change->guid != NULL ? change->guid : "<unknown>", change->uid, + change->hdr_hash != NULL ? change->hdr_hash : "", result); + + if (importer->failed) + return -1; + if (importer->require_full_resync) + return 0; + + if (importer->last_common_uid_found) { + /* a) uid <= last_common_uid for flag changes and expunges. + this happens only when last_common_uid was originally given + as parameter to importer. + + when we're finding the last_common_uid ourself, + uid>last_common_uid always in here, because + last_common_uid_found=TRUE only after we find the first + mismatch. + + b) uid > last_common_uid for i) new messages, ii) expunges + that were sent "just in case" */ + if (change->uid <= importer->last_common_uid) { + i_assert(change->type != DSYNC_MAIL_CHANGE_TYPE_SAVE); + } else if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + /* ignore */ + return 0; + } else { + i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_SAVE); + } + } else { + /* a) uid < last_common_uid can never happen */ + i_assert(change->uid >= importer->last_common_uid); + /* b) uid = last_common_uid if we've verified that the + messages' GUIDs match so far. + + c) uid > last_common_uid: i) TYPE_EXPUNGE change has + GUID=NULL, so we couldn't verify yet if it matches our + local message, ii) local message is expunged and we couldn't + find its GUID */ + if (change->uid > importer->last_common_uid) { + i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE || + importer->cur_mail == NULL || + change->uid < importer->cur_mail->uid); + } + } + + switch (change->type) { + case DSYNC_MAIL_CHANGE_TYPE_SAVE: + dsync_mailbox_import_save(importer, change); + break; + case DSYNC_MAIL_CHANGE_TYPE_EXPUNGE: + dsync_mailbox_import_expunge(importer, change); + break; + case DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE: + i_assert(importer->last_common_uid_found); + dsync_mailbox_import_flag_change(importer, change); + break; + } + return importer->failed ? -1 : 0; +} + +static int +importer_new_mail_final_uid_cmp(struct importer_new_mail *const *newmail1, + struct importer_new_mail *const *newmail2) +{ + if ((*newmail1)->final_uid < (*newmail2)->final_uid) + return -1; + if ((*newmail1)->final_uid > (*newmail2)->final_uid) + return 1; + return 0; +} + +static void +dsync_mailbox_import_assign_new_uids(struct dsync_mailbox_importer *importer) +{ + struct importer_new_mail *newmail; + uint32_t common_uid_next, new_uid; + + common_uid_next = I_MAX(importer->local_uid_next, + importer->remote_uid_next); + array_foreach_elem(&importer->newmails, newmail) { + if (newmail->skip) { + /* already assigned */ + i_assert(newmail->final_uid != 0); + continue; + } + + /* figure out what UID to use for the mail */ + if (newmail->uid_is_usable) { + /* keep the UID */ + new_uid = newmail->final_uid; + } else if (newmail->link != NULL && + newmail->link->uid_is_usable) { + /* we can use the linked message's UID and expunge + this mail */ + new_uid = newmail->link->final_uid; + } else { + i_assert(!importer->revert_local_changes); + new_uid = common_uid_next++; + imp_debug(importer, "UID %u isn't usable, assigning new UID %u", + newmail->final_uid, new_uid); + } + + newmail->final_uid = new_uid; + if (newmail->link != NULL && newmail->link != newmail) { + /* skip processing the linked mail */ + newmail->link->skip = TRUE; + } + } + importer->last_common_uid = common_uid_next-1; + importer->new_uids_assigned = TRUE; + /* Sort the newmails by their final_uid. This is used for tracking + whether an intermediate commit is allowed. */ + array_sort(&importer->newmails, importer_new_mail_final_uid_cmp); +} + +static int +dsync_mailbox_import_local_uid(struct dsync_mailbox_importer *importer, + struct mail *mail, uint32_t uid, const char *guid, + struct dsync_mail *dmail_r) +{ + const char *error_field, *errstr; + enum mail_error error; + + if (!mail_set_uid(mail, uid)) + return 0; + + /* NOTE: Errors are logged, but they don't cause the entire import + to fail. */ + if (dsync_mail_fill(mail, TRUE, dmail_r, &error_field) < 0) { + errstr = mailbox_get_last_internal_error(mail->box, &error); + if (error == MAIL_ERROR_EXPUNGED) + return 0; + + i_error("Mailbox %s: Can't lookup %s for UID=%u: %s", + mailbox_get_vname(importer->box), + error_field, uid, errstr); + return -1; + } + if (*guid != '\0' && strcmp(guid, dmail_r->guid) != 0) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "Unexpected GUID mismatch (3) for UID=%u: %s != %s", + uid, dmail_r->guid, guid)); + return -1; + } + return 1; +} + +static void +dsync_mailbox_import_saved_uid(struct dsync_mailbox_importer *importer, + uint32_t uid) +{ + i_assert(importer->search_ctx == NULL); + + if (importer->highest_wanted_uid < uid) + importer->highest_wanted_uid = uid; + array_push_back(&importer->wanted_uids, &uid); +} + +static void +dsync_mailbox_import_update_first_saved(struct dsync_mailbox_importer *importer) +{ + struct importer_new_mail *const *newmails; + unsigned int count; + + newmails = array_get(&importer->newmails, &count); + while (importer->first_unsaved_idx < count) { + if (!newmails[importer->first_unsaved_idx]->saved) + break; + importer->first_unsaved_idx++; + } +} + +static void +dsync_mailbox_import_saved_newmail(struct dsync_mailbox_importer *importer, + struct importer_new_mail *newmail) +{ + dsync_mailbox_import_saved_uid(importer, newmail->final_uid); + newmail->saved = TRUE; + + dsync_mailbox_import_update_first_saved(importer); + importer->saves_since_commit++; + /* we can commit only if all the upcoming mails will have UIDs that + are larger than we're committing. + + Note that if any existing UIDs have been changed, the new UID is + usually higher than anything that is being saved so we can't do + an intermediate commit. It's too much extra work to try to handle + that situation. So here this never happens, because then + array_count(wanted_uids) is always higher than first_unsaved_idx. */ + if (importer->saves_since_commit >= importer->commit_msgs_interval && + importer->first_unsaved_idx == array_count(&importer->wanted_uids)) { + if (dsync_mailbox_import_commit(importer, FALSE) < 0) + importer->failed = TRUE; + importer->saves_since_commit = 0; + } +} + +static bool +dsync_msg_change_uid(struct dsync_mailbox_importer *importer, + uint32_t old_uid, uint32_t new_uid) +{ + struct mail_save_context *save_ctx; + + IMPORTER_DEBUG_CHANGE(importer); + + if (!mail_set_uid(importer->mail, old_uid)) + return FALSE; + + save_ctx = mailbox_save_alloc(importer->ext_trans); + mailbox_save_copy_flags(save_ctx, importer->mail); + mailbox_save_set_uid(save_ctx, new_uid); + if (mailbox_move(&save_ctx, importer->mail) < 0) + return FALSE; + dsync_mailbox_import_saved_uid(importer, new_uid); + return TRUE; +} + +static bool +dsync_mailbox_import_change_uid(struct dsync_mailbox_importer *importer, + ARRAY_TYPE(seq_range) *unwanted_uids, + uint32_t wanted_uid) +{ + const struct seq_range *range; + unsigned int count, n; + struct seq_range_iter iter; + uint32_t uid; + + /* optimize by first trying to use the latest UID */ + range = array_get(unwanted_uids, &count); + if (count == 0) + return FALSE; + if (dsync_msg_change_uid(importer, range[count-1].seq2, wanted_uid)) { + seq_range_array_remove(unwanted_uids, range[count-1].seq2); + return TRUE; + } + if (mailbox_get_last_mail_error(importer->box) == MAIL_ERROR_EXPUNGED) + seq_range_array_remove(unwanted_uids, range[count-1].seq2); + + /* now try to use any of them by iterating through them. (would be + easier&faster to just iterate backwards, but probably too much + trouble to add such API) */ + n = 0; seq_range_array_iter_init(&iter, unwanted_uids); + while (seq_range_array_iter_nth(&iter, n++, &uid)) { + if (dsync_msg_change_uid(importer, uid, wanted_uid)) { + seq_range_array_remove(unwanted_uids, uid); + return TRUE; + } + if (mailbox_get_last_mail_error(importer->box) == MAIL_ERROR_EXPUNGED) + seq_range_array_remove(unwanted_uids, uid); + } + return FALSE; +} + +static bool +dsync_mailbox_import_try_local(struct dsync_mailbox_importer *importer, + struct importer_new_mail *all_newmails, + ARRAY_TYPE(seq_range) *local_uids, + ARRAY_TYPE(seq_range) *wanted_uids) +{ + ARRAY_TYPE(seq_range) assigned_uids, unwanted_uids; + struct seq_range_iter local_iter, wanted_iter; + unsigned int local_n, wanted_n; + uint32_t local_uid, wanted_uid; + struct importer_new_mail *mail; + struct dsync_mail dmail; + + if (array_count(local_uids) == 0) + return FALSE; + + local_n = wanted_n = 0; + seq_range_array_iter_init(&local_iter, local_uids); + seq_range_array_iter_init(&wanted_iter, wanted_uids); + + /* wanted_uids contains UIDs that need to exist at the end. those that + don't already exist in local_uids have a higher UID than any + existing local UID */ + t_array_init(&assigned_uids, array_count(wanted_uids)); + t_array_init(&unwanted_uids, 8); + while (seq_range_array_iter_nth(&local_iter, local_n++, &local_uid)) { + if (seq_range_array_iter_nth(&wanted_iter, wanted_n, + &wanted_uid)) { + if (local_uid == wanted_uid) { + /* we have exactly the UID we want. keep it. */ + seq_range_array_add(&assigned_uids, wanted_uid); + wanted_n++; + continue; + } + i_assert(local_uid < wanted_uid); + } + /* we no longer want this local UID. */ + seq_range_array_add(&unwanted_uids, local_uid); + } + + /* reuse as many existing messages as possible by changing their UIDs */ + while (seq_range_array_iter_nth(&wanted_iter, wanted_n, &wanted_uid)) { + if (!dsync_mailbox_import_change_uid(importer, &unwanted_uids, + wanted_uid)) + break; + seq_range_array_add(&assigned_uids, wanted_uid); + wanted_n++; + } + + /* expunge all unwanted messages */ + local_n = 0; seq_range_array_iter_init(&local_iter, &unwanted_uids); + while (seq_range_array_iter_nth(&local_iter, local_n++, &local_uid)) { + IMPORTER_DEBUG_CHANGE(importer); + if (mail_set_uid(importer->mail, local_uid)) + mail_expunge(importer->mail); + } + + /* mark mails whose UIDs we got to be skipped over later */ + for (mail = all_newmails; mail != NULL; mail = mail->next) { + if (!mail->skip && + seq_range_exists(&assigned_uids, mail->final_uid)) + mail->skip = TRUE; + } + + if (!seq_range_array_iter_nth(&wanted_iter, wanted_n, &wanted_uid)) { + /* we've assigned all wanted UIDs */ + return TRUE; + } + + /* try to find one existing message that we can use to copy to the + other instances */ + local_n = 0; seq_range_array_iter_init(&local_iter, local_uids); + while (seq_range_array_iter_nth(&local_iter, local_n++, &local_uid)) { + if (dsync_mailbox_import_local_uid(importer, importer->mail, + local_uid, all_newmails->guid, + &dmail) > 0) { + if (dsync_mailbox_save_newmails(importer, &dmail, + all_newmails, FALSE)) + return TRUE; + } + } + return FALSE; +} + +static bool +dsync_mailbox_import_try_virtual_all(struct dsync_mailbox_importer *importer, + struct importer_new_mail *all_newmails) +{ + struct dsync_mail dmail; + + if (all_newmails->virtual_all_uid == 0) + return FALSE; + + if (dsync_mailbox_import_local_uid(importer, importer->virtual_mail, + all_newmails->virtual_all_uid, + all_newmails->guid, &dmail) > 0) { + if (dsync_mailbox_save_newmails(importer, &dmail, + all_newmails, FALSE)) + return TRUE; + } + return FALSE; +} + +static bool +dsync_mailbox_import_handle_mail(struct dsync_mailbox_importer *importer, + struct importer_new_mail *all_newmails) +{ + ARRAY_TYPE(seq_range) local_uids, wanted_uids; + struct dsync_mail_request *request; + struct importer_new_mail *mail; + const char *request_guid = NULL; + uint32_t request_uid = 0; + + i_assert(all_newmails != NULL); + + /* get the list of the current local UIDs and the wanted UIDs. + find the first remote instance that we can request in case there are + no local instances */ + t_array_init(&local_uids, 8); + t_array_init(&wanted_uids, 8); + for (mail = all_newmails; mail != NULL; mail = mail->next) { + if (mail->uid_in_local) + seq_range_array_add(&local_uids, mail->local_uid); + else if (request_guid == NULL) { + if (*mail->guid != '\0') + request_guid = mail->guid; + request_uid = mail->remote_uid; + i_assert(request_uid != 0); + } + if (!mail->skip) + seq_range_array_add(&wanted_uids, mail->final_uid); + } + i_assert(array_count(&wanted_uids) > 0); + + if (!dsync_mailbox_import_try_local(importer, all_newmails, + &local_uids, &wanted_uids) && + !dsync_mailbox_import_try_virtual_all(importer, all_newmails)) { + /* no local instance. request from remote */ + IMPORTER_DEBUG_CHANGE(importer); + if (importer->want_mail_requests) { + request = array_append_space(&importer->mail_requests); + request->guid = request_guid; + request->uid = request_uid; + } + return FALSE; + } + /* successfully handled all the mails locally */ + importer->import_pos++; + return TRUE; +} + +static void +dsync_mailbox_import_find_virtual_uids(struct dsync_mailbox_importer *importer) +{ + struct mail_search_context *search_ctx; + struct mail_search_args *search_args; + struct importer_new_mail *newmail; + struct mail *mail; + const char *guid; + + if (mailbox_sync(importer->virtual_all_box, 0) < 0) { + i_error("Couldn't sync \\All mailbox '%s': %s", + mailbox_get_vname(importer->virtual_all_box), + mailbox_get_last_internal_error(importer->virtual_all_box, NULL)); + return; + } + + search_args = mail_search_build_init(); + mail_search_build_add_all(search_args); + + importer->virtual_trans = + mailbox_transaction_begin(importer->virtual_all_box, + importer->transaction_flags, + __func__); + search_ctx = mailbox_search_init(importer->virtual_trans, search_args, + NULL, MAIL_FETCH_GUID, NULL); + mail_search_args_unref(&search_args); + + while (mailbox_search_next(search_ctx, &mail)) { + if (mail_get_special(mail, MAIL_FETCH_GUID, &guid) < 0) { + /* ignore errors */ + continue; + } + newmail = hash_table_lookup(importer->import_guids, guid); + if (newmail != NULL && newmail->virtual_all_uid == 0) + newmail->virtual_all_uid = mail->uid; + } + if (mailbox_search_deinit(&search_ctx) < 0) { + i_error("Couldn't search \\All mailbox '%s': %s", + mailbox_get_vname(importer->virtual_all_box), + mailbox_get_last_internal_error(importer->virtual_all_box, NULL)); + } + + importer->virtual_mail = mail_alloc(importer->virtual_trans, 0, NULL); +} + +static void +dsync_mailbox_import_handle_local_mails(struct dsync_mailbox_importer *importer) +{ + struct hash_iterate_context *iter; + const char *key; + void *key2; + struct importer_new_mail *mail; + + if (importer->virtual_all_box != NULL && + hash_table_count(importer->import_guids) > 0) { + /* find UIDs in \All mailbox for all wanted GUIDs. */ + dsync_mailbox_import_find_virtual_uids(importer); + } + + iter = hash_table_iterate_init(importer->import_guids); + while (hash_table_iterate(iter, importer->import_guids, &key, &mail)) { + T_BEGIN { + if (dsync_mailbox_import_handle_mail(importer, mail)) + hash_table_remove(importer->import_guids, key); + } T_END; + } + hash_table_iterate_deinit(&iter); + + iter = hash_table_iterate_init(importer->import_uids); + while (hash_table_iterate(iter, importer->import_uids, &key2, &mail)) { + T_BEGIN { + if (dsync_mailbox_import_handle_mail(importer, mail)) + hash_table_remove(importer->import_uids, key2); + } T_END; + } + hash_table_iterate_deinit(&iter); + if (!importer->mails_have_guids) { + array_foreach_elem(&importer->newmails, mail) { + if (mail->uid_in_local) + (void)dsync_mailbox_import_handle_mail(importer, mail); + } + } +} + +int dsync_mailbox_import_changes_finish(struct dsync_mailbox_importer *importer) +{ + i_assert(!importer->new_uids_assigned); + + if (!importer->last_common_uid_found) { + /* handle pending expunges and flag updates */ + dsync_mailbox_common_uid_found(importer); + } + /* skip common local mails */ + (void)importer_next_mail(importer, importer->last_common_uid+1); + /* if there are any local mails left, add them to newmails list */ + while (importer->cur_mail != NULL && !importer->failed) + (void)dsync_mailbox_try_save(importer, NULL); + + if (importer->search_ctx != NULL) { + if (mailbox_search_deinit(&importer->search_ctx) < 0) { + i_error("Mailbox %s: Search failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + } + } + importer->import_count = hash_table_count(importer->import_guids) + + hash_table_count(importer->import_uids); + + dsync_mailbox_import_assign_new_uids(importer); + /* save mails from local sources where possible, + request the rest from remote */ + if (!importer->failed) + dsync_mailbox_import_handle_local_mails(importer); + return importer->failed ? -1 : 0; +} + +const struct dsync_mail_request * +dsync_mailbox_import_next_request(struct dsync_mailbox_importer *importer) +{ + const struct dsync_mail_request *requests; + unsigned int count; + + requests = array_get(&importer->mail_requests, &count); + if (importer->mail_request_idx == count) + return NULL; + return &requests[importer->mail_request_idx++]; +} + +static const char *const * +dsync_mailbox_get_final_keywords(const struct dsync_mail_change *change) +{ + ARRAY_TYPE(const_string) keywords; + const char *const *changes; + unsigned int i, count; + + if (!array_is_created(&change->keyword_changes)) + return NULL; + + changes = array_get(&change->keyword_changes, &count); + t_array_init(&keywords, count); + for (i = 0; i < count; i++) { + if (changes[i][0] == KEYWORD_CHANGE_ADD || + changes[i][0] == KEYWORD_CHANGE_FINAL || + changes[i][0] == KEYWORD_CHANGE_ADD_AND_FINAL) { + const char *name = changes[i]+1; + + array_push_back(&keywords, &name); + } + } + if (array_count(&keywords) == 0) + return NULL; + + array_append_zero(&keywords); + return array_front(&keywords); +} + +static void +dsync_mailbox_save_set_metadata(struct dsync_mailbox_importer *importer, + struct mail_save_context *save_ctx, + const struct dsync_mail_change *change) +{ + const char *const *keyword_names; + struct mail_keywords *keywords; + + keyword_names = dsync_mailbox_get_final_keywords(change); + keywords = keyword_names == NULL ? NULL : + mailbox_keywords_create_valid(importer->box, + keyword_names); + mailbox_save_set_flags(save_ctx, change->final_flags, keywords); + if (keywords != NULL) + mailbox_keywords_unref(&keywords); + + if (change->modseq > 1) { + (void)mailbox_enable(importer->box, MAILBOX_FEATURE_CONDSTORE); + mailbox_save_set_min_modseq(save_ctx, change->modseq); + } + /* FIXME: if there already are private flags, they get lost because + saving can't handle updating private index. they get added on the + next sync though. if this is fixed here, set min_pvt_modseq also. */ +} + +static int +dsync_msg_try_copy(struct dsync_mailbox_importer *importer, + struct mail_save_context **save_ctx_p, + struct importer_new_mail **all_newmails_forcopy) +{ + struct importer_new_mail *inst; + + for (inst = *all_newmails_forcopy; inst != NULL; inst = inst->next) { + if (inst->uid_in_local && !inst->copy_failed && + mail_set_uid(importer->mail, inst->local_uid)) { + if (mailbox_copy(save_ctx_p, importer->mail) < 0) { + enum mail_error error; + const char *errstr; + + errstr = mailbox_get_last_internal_error(importer->box, &error); + if (error != MAIL_ERROR_EXPUNGED) { + i_warning("Failed to copy mail from UID=%u: " + "%s - falling back to other means", + inst->local_uid, errstr); + } + inst->copy_failed = TRUE; + return -1; + } + *all_newmails_forcopy = inst; + return 1; + } + } + *all_newmails_forcopy = NULL; + return 0; +} + +static void +dsync_mailbox_save_set_nonminimal(struct mail_save_context *save_ctx, + const struct dsync_mail *mail) +{ + if (mail->pop3_uidl != NULL && *mail->pop3_uidl != '\0') + mailbox_save_set_pop3_uidl(save_ctx, mail->pop3_uidl); + if (mail->pop3_order > 0) + mailbox_save_set_pop3_order(save_ctx, mail->pop3_order); + mailbox_save_set_received_date(save_ctx, mail->received_date, 0); +} + +static struct mail_save_context * +dsync_mailbox_save_init(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail, + struct importer_new_mail *newmail) +{ + struct mail_save_context *save_ctx; + + save_ctx = mailbox_save_alloc(importer->ext_trans); + mailbox_save_set_uid(save_ctx, newmail->final_uid); + if (*mail->guid != '\0') + mailbox_save_set_guid(save_ctx, mail->guid); + if (mail->saved_date != 0) + mailbox_save_set_save_date(save_ctx, mail->saved_date); + dsync_mailbox_save_set_metadata(importer, save_ctx, newmail->change); + + if (!mail->minimal_fields) + dsync_mailbox_save_set_nonminimal(save_ctx, mail); + return save_ctx; +} + +static bool +dsync_mailbox_save_body(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail, + struct importer_new_mail *newmail, + struct importer_new_mail **all_newmails_forcopy, + bool remote_mail) +{ + struct mail_save_context *save_ctx; + struct istream *input; + ssize_t ret; + bool save_failed = FALSE; + + /* try to save the mail by copying an existing mail */ + save_ctx = dsync_mailbox_save_init(importer, mail, newmail); + if ((ret = dsync_msg_try_copy(importer, &save_ctx, all_newmails_forcopy)) < 0) { + if (save_ctx == NULL) + save_ctx = dsync_mailbox_save_init(importer, mail, newmail); + } + if (ret <= 0 && mail->input_mail != NULL) { + /* copy using the source mail */ + i_assert(mail->input_mail->uid == mail->input_mail_uid); + if (mailbox_copy(&save_ctx, mail->input_mail) == 0) + ret = 1; + else { + enum mail_error error; + const char *errstr; + + errstr = mailbox_get_last_internal_error(importer->box, &error); + if (error != MAIL_ERROR_EXPUNGED) { + i_warning("Failed to copy source UID=%u mail: " + "%s - falling back to regular saving", + mail->input_mail->uid, errstr); + } + ret = -1; + save_ctx = dsync_mailbox_save_init(importer, mail, newmail); + } + + } + if (ret > 0) { + i_assert(save_ctx == NULL); + dsync_mailbox_import_saved_newmail(importer, newmail); + return TRUE; + } + /* fallback to saving from remote stream */ + if (!remote_mail) { + /* the mail isn't remote yet. we were just trying to copy a + local mail to avoid downloading the remote mail. */ + mailbox_save_cancel(&save_ctx); + return FALSE; + } + if (mail->minimal_fields) { + struct dsync_mail mail2; + const char *error_field; + + i_assert(mail->input_mail != NULL); + + if (dsync_mail_fill_nonminimal(mail->input_mail, &mail2, + &error_field) < 0) { + i_error("Mailbox %s: Failed to read mail %s uid=%u: %s", + mailbox_get_vname(importer->box), + error_field, mail->uid, + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + mailbox_save_cancel(&save_ctx); + return TRUE; + } + dsync_mailbox_save_set_nonminimal(save_ctx, &mail2); + input = mail2.input; + } else { + input = mail->input; + } + + if (input == NULL) { + /* it was just expunged in remote, skip it */ + mailbox_save_cancel(&save_ctx); + return TRUE; + } + + i_stream_seek(input, 0); + if (mailbox_save_begin(&save_ctx, input) < 0) { + i_error("Mailbox %s: Saving failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + return TRUE; + } + while ((ret = i_stream_read(input)) > 0 || ret == -2) { + if (mailbox_save_continue(save_ctx) < 0) { + save_failed = TRUE; + ret = -1; + break; + } + } + i_assert(ret == -1); + + if (input->stream_errno != 0) { + i_error("Mailbox %s: read(msg input) failed: %s", + mailbox_get_vname(importer->box), + i_stream_get_error(input)); + mailbox_save_cancel(&save_ctx); + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + } else if (save_failed) { + i_error("Mailbox %s: Saving failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + mailbox_save_cancel(&save_ctx); + importer->failed = TRUE; + } else { + i_assert(input->eof); + if (mailbox_save_finish(&save_ctx) < 0) { + i_error("Mailbox %s: Saving failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + } else { + dsync_mailbox_import_saved_newmail(importer, newmail); + } + } + return TRUE; +} + +static bool dsync_mailbox_save_newmails(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail, + struct importer_new_mail *all_newmails, + bool remote_mail) +{ + struct importer_new_mail *newmail, *all_newmails_forcopy; + bool ret = TRUE; + + /* if all_newmails list is large, avoid scanning through the + uninteresting ones for each newmail */ + all_newmails_forcopy = all_newmails; + + /* save all instances of the message */ + for (newmail = all_newmails; newmail != NULL && ret; newmail = newmail->next) { + if (!newmail->skip) T_BEGIN { + if (!dsync_mailbox_save_body(importer, mail, newmail, + &all_newmails_forcopy, + remote_mail)) + ret = FALSE; + } T_END; + } + return ret; +} + +int dsync_mailbox_import_mail(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail) +{ + struct importer_new_mail *all_newmails; + + i_assert(mail->input == NULL || mail->input->seekable); + i_assert(importer->new_uids_assigned); + + if (importer->failed) + return -1; + if (importer->require_full_resync) + return 0; + + imp_debug(importer, "Import mail body for GUID=%s UID=%u", + mail->guid, mail->uid); + + all_newmails = *mail->guid != '\0' ? + hash_table_lookup(importer->import_guids, mail->guid) : + hash_table_lookup(importer->import_uids, POINTER_CAST(mail->uid)); + if (all_newmails == NULL) { + if (importer->want_mail_requests) { + i_error("Mailbox %s: Remote sent unwanted message body for " + "GUID=%s UID=%u", + mailbox_get_vname(importer->box), + mail->guid, mail->uid); + } else { + imp_debug(importer, "Skip unwanted mail body for " + "GUID=%s UID=%u", mail->guid, mail->uid); + } + return 0; + } + if (*mail->guid != '\0') + hash_table_remove(importer->import_guids, mail->guid); + else { + hash_table_remove(importer->import_uids, + POINTER_CAST(mail->uid)); + } + importer->import_pos++; + if (!dsync_mailbox_save_newmails(importer, mail, all_newmails, TRUE)) + i_unreached(); + return importer->failed ? -1 : 0; +} + +static int +reassign_uids_in_seq_range(struct dsync_mailbox_importer *importer, + const ARRAY_TYPE(seq_range) *unwanted_uids) +{ + struct mailbox *box = importer->box; + const enum mailbox_transaction_flags trans_flags = + importer->transaction_flags | + MAILBOX_TRANSACTION_FLAG_EXTERNAL | + MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS; + struct mailbox_transaction_context *trans; + struct mail_search_args *search_args; + struct mail_search_arg *arg; + struct mail_search_context *search_ctx; + struct mail_save_context *save_ctx; + struct mail *mail; + unsigned int renumber_count = 0; + int ret = 1; + + if (array_count(unwanted_uids) == 0) + return 1; + + if (importer->debug) T_BEGIN { + string_t *str = t_str_new(256); + imap_write_seq_range(str, unwanted_uids); + imp_debug(importer, "Reassign UIDs: %s", str_c(str)); + } T_END; + + search_args = mail_search_build_init(); + arg = mail_search_build_add(search_args, SEARCH_UIDSET); + p_array_init(&arg->value.seqset, search_args->pool, + array_count(unwanted_uids)); + array_append_array(&arg->value.seqset, unwanted_uids); + + trans = mailbox_transaction_begin(box, trans_flags, __func__); + search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL); + mail_search_args_unref(&search_args); + + while (mailbox_search_next(search_ctx, &mail)) { + save_ctx = mailbox_save_alloc(trans); + mailbox_save_copy_flags(save_ctx, mail); + if (mailbox_move(&save_ctx, mail) < 0) { + i_error("Mailbox %s: Couldn't move mail within mailbox: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &importer->mail_error)); + ret = -1; + } else if (ret > 0) { + ret = 0; + } + renumber_count++; + } + if (mailbox_search_deinit(&search_ctx) < 0) { + i_error("Mailbox %s: mail search failed: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &importer->mail_error)); + ret = -1; + } + + if (mailbox_transaction_commit(&trans) < 0) { + i_error("Mailbox %s: UID reassign commit failed: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &importer->mail_error)); + ret = -1; + } + if (ret == 0) { + imp_debug(importer, "Mailbox %s: Change during sync: " + "Renumbered %u of %u unwanted UIDs", + mailbox_get_vname(box), + renumber_count, array_count(unwanted_uids)); + } + return ret; +} + +static int +reassign_unwanted_uids(struct dsync_mailbox_importer *importer, + const char **changes_during_sync_r) +{ + ARRAY_TYPE(seq_range) unwanted_uids; + const uint32_t *wanted_uids, *saved_uids; + uint32_t highest_seen_uid; + unsigned int i, wanted_count, saved_count; + int ret = 0; + + wanted_uids = array_get(&importer->wanted_uids, &wanted_count); + saved_uids = array_get(&importer->saved_uids, &saved_count); + i_assert(wanted_count == saved_count); + if (wanted_count == 0) + return 0; + /* wanted_uids contains the UIDs we tried to save mails with. + if nothing changed during dsync, we should have the expected UIDs + (saved_uids) and all is well. + + if any new messages got inserted during dsync, we'll need to fix up + the UIDs and let the next dsync fix up the other side. for example: + + remote uids = 5,7,9 = wanted_uids + remote uidnext = 12 + locally added new uid=5 -> + saved_uids = 10,7,9 + + we'll now need to reassign UIDs 5 and 10. to be fully future-proof + we'll reassign all UIDs between [original local uidnext .. highest + UID we think we know] that aren't in saved_uids. */ + + /* create uidset for the list of UIDs we don't want to exist */ + t_array_init(&unwanted_uids, 8); + highest_seen_uid = I_MAX(importer->remote_uid_next-1, + importer->highest_wanted_uid); + i_assert(importer->local_uid_next <= highest_seen_uid); + seq_range_array_add_range(&unwanted_uids, + importer->local_uid_next, highest_seen_uid); + for (i = 0; i < wanted_count; i++) { + i_assert(i < wanted_count); + if (saved_uids[i] == wanted_uids[i]) + seq_range_array_remove(&unwanted_uids, saved_uids[i]); + } + + ret = reassign_uids_in_seq_range(importer, &unwanted_uids); + if (ret == 0) { + *changes_during_sync_r = t_strdup_printf( + "%u UIDs changed due to UID conflicts", + seq_range_count(&unwanted_uids)); + /* conflicting changes during sync, revert our last-common-uid + back to a safe value. */ + importer->last_common_uid = importer->local_uid_next - 1; + } + return ret < 0 ? -1 : 0; +} + +static int +dsync_mailbox_import_commit(struct dsync_mailbox_importer *importer, bool final) +{ + struct mail_transaction_commit_changes changes; + struct seq_range_iter iter; + uint32_t uid; + unsigned int n; + int ret = importer->failed ? -1 : 0; + + mail_free(&importer->mail); + mail_free(&importer->ext_mail); + + /* commit saves */ + if (mailbox_transaction_commit_get_changes(&importer->ext_trans, + &changes) < 0) { + i_error("Mailbox %s: Save commit failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, &importer->mail_error)); + /* removed wanted_uids that weren't actually saved */ + array_delete(&importer->wanted_uids, + array_count(&importer->saved_uids), + array_count(&importer->wanted_uids) - + array_count(&importer->saved_uids)); + mailbox_transaction_rollback(&importer->trans); + ret = -1; + } else { + /* remember the UIDs that were successfully saved */ + if (importer->debug) T_BEGIN { + string_t *str = t_str_new(256); + imap_write_seq_range(str, &changes.saved_uids); + imp_debug(importer, "Saved UIDs: %s", str_c(str)); + } T_END; + seq_range_array_iter_init(&iter, &changes.saved_uids); n = 0; + while (seq_range_array_iter_nth(&iter, n++, &uid)) + array_push_back(&importer->saved_uids, &uid); + pool_unref(&changes.pool); + + /* commit flag changes and expunges */ + if (mailbox_transaction_commit(&importer->trans) < 0) { + i_error("Mailbox %s: Commit failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + ret = -1; + } + } + + if (!final) + dsync_mailbox_import_transaction_begin(importer); + return ret; +} + +static int dsync_mailbox_import_finish(struct dsync_mailbox_importer *importer, + const char **changes_during_sync_r) +{ + struct mailbox_update update; + int ret; + + ret = dsync_mailbox_import_commit(importer, TRUE); + + if (ret == 0) { + /* update mailbox metadata if we successfully saved + everything. */ + i_zero(&update); + update.min_next_uid = importer->remote_uid_next; + update.min_first_recent_uid = + I_MIN(importer->last_common_uid+1, + importer->remote_first_recent_uid); + update.min_highest_modseq = importer->remote_highest_modseq; + update.min_highest_pvt_modseq = importer->remote_highest_pvt_modseq; + + imp_debug(importer, "Finish update: min_next_uid=%u " + "min_first_recent_uid=%u min_highest_modseq=%"PRIu64" " + "min_highest_pvt_modseq=%"PRIu64, + update.min_next_uid, update.min_first_recent_uid, + update.min_highest_modseq, + update.min_highest_pvt_modseq); + + if (mailbox_update(importer->box, &update) < 0) { + i_error("Mailbox %s: Update failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + ret = -1; + } + } + + /* sync mailbox to finish flag changes and expunges. */ + if (mailbox_sync(importer->box, 0) < 0) { + i_error("Mailbox %s: Sync failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + ret = -1; + } + if (ret == 0) { + /* give new UIDs to messages that got saved with unwanted UIDs. + do it only if the whole transaction succeeded. */ + if (reassign_unwanted_uids(importer, changes_during_sync_r) < 0) + ret = -1; + } + return ret; +} + +static void +dsync_mailbox_import_check_missing_guid_imports(struct dsync_mailbox_importer *importer) +{ + struct hash_iterate_context *iter; + const char *key; + struct importer_new_mail *mail; + + iter = hash_table_iterate_init(importer->import_guids); + while (hash_table_iterate(iter, importer->import_guids, &key, &mail)) { + for (; mail != NULL; mail = mail->next) { + if (mail->skip) + continue; + + i_error("Mailbox %s: Remote didn't send mail GUID=%s (UID=%u)", + mailbox_get_vname(importer->box), + mail->guid, mail->remote_uid); + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + } + } + hash_table_iterate_deinit(&iter); +} + +static void +dsync_mailbox_import_check_missing_uid_imports(struct dsync_mailbox_importer *importer) +{ + struct hash_iterate_context *iter; + void *key; + struct importer_new_mail *mail; + + iter = hash_table_iterate_init(importer->import_uids); + while (hash_table_iterate(iter, importer->import_uids, &key, &mail)) { + for (; mail != NULL; mail = mail->next) { + if (mail->skip) + continue; + + i_error("Mailbox %s: Remote didn't send mail UID=%u", + mailbox_get_vname(importer->box), + mail->remote_uid); + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + } + } + hash_table_iterate_deinit(&iter); +} + +int dsync_mailbox_import_deinit(struct dsync_mailbox_importer **_importer, + bool success, + uint32_t *last_common_uid_r, + uint64_t *last_common_modseq_r, + uint64_t *last_common_pvt_modseq_r, + uint32_t *last_messages_count_r, + const char **changes_during_sync_r, + bool *require_full_resync_r, + enum mail_error *error_r) +{ + struct dsync_mailbox_importer *importer = *_importer; + struct mailbox_status status; + int ret; + + *_importer = NULL; + *changes_during_sync_r = NULL; + *require_full_resync_r = importer->require_full_resync; + + if ((!success || importer->require_full_resync) && !importer->failed) { + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + } + + if (!importer->new_uids_assigned && !importer->failed) + dsync_mailbox_import_assign_new_uids(importer); + + if (!importer->failed) { + dsync_mailbox_import_check_missing_guid_imports(importer); + dsync_mailbox_import_check_missing_uid_imports(importer); + } + + if (importer->search_ctx != NULL) { + if (mailbox_search_deinit(&importer->search_ctx) < 0) { + i_error("Mailbox %s: Search failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + } + } + if (dsync_mailbox_import_finish(importer, changes_during_sync_r) < 0) + importer->failed = TRUE; + + if (importer->virtual_mail != NULL) + mail_free(&importer->virtual_mail); + if (importer->virtual_trans != NULL) + (void)mailbox_transaction_commit(&importer->virtual_trans); + + hash_table_destroy(&importer->import_guids); + hash_table_destroy(&importer->import_uids); + array_free(&importer->maybe_expunge_uids); + array_free(&importer->maybe_saves); + array_free(&importer->wanted_uids); + array_free(&importer->saved_uids); + array_free(&importer->newmails); + if (array_is_created(&importer->mail_requests)) + array_free(&importer->mail_requests); + + *last_common_uid_r = importer->last_common_uid; + if (*changes_during_sync_r == NULL) { + *last_common_modseq_r = importer->remote_highest_modseq; + *last_common_pvt_modseq_r = importer->remote_highest_pvt_modseq; + } else { + /* local changes occurred during dsync. we exported changes up + to local_initial_highestmodseq, so all of the changes have + happened after it. we want the next run to see those changes, + so return it as the last common modseq */ + *last_common_modseq_r = importer->local_initial_highestmodseq; + *last_common_pvt_modseq_r = importer->local_initial_highestpvtmodseq; + } + if (importer->delete_mailbox) { + if (mailbox_delete(importer->box) < 0) { + i_error("Couldn't delete mailbox %s: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + } + *last_messages_count_r = 0; + } else { + mailbox_get_open_status(importer->box, STATUS_MESSAGES, &status); + *last_messages_count_r = status.messages; + } + + i_assert(importer->failed == (importer->mail_error != 0)); + ret = importer->failed ? -1 : 0; + *error_r = importer->mail_error; + pool_unref(&importer->pool); + return ret; +} + +const char *dsync_mailbox_import_get_proctitle(struct dsync_mailbox_importer *importer) +{ + if (importer->search_ctx != NULL) + return ""; + return t_strdup_printf("%u/%u", importer->import_pos, + importer->import_count); +} diff --git a/src/doveadm/dsync/dsync-mailbox-import.h b/src/doveadm/dsync/dsync-mailbox-import.h new file mode 100644 index 0000000..6a2020e --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-import.h @@ -0,0 +1,63 @@ +#ifndef DSYNC_MAILBOX_IMPORT_H +#define DSYNC_MAILBOX_IMPORT_H + +#include "mail-error.h" + +enum dsync_mailbox_import_flags { + DSYNC_MAILBOX_IMPORT_FLAG_MASTER_BRAIN = 0x01, + DSYNC_MAILBOX_IMPORT_FLAG_WANT_MAIL_REQUESTS = 0x02, + DSYNC_MAILBOX_IMPORT_FLAG_REVERT_LOCAL_CHANGES = 0x04, + DSYNC_MAILBOX_IMPORT_FLAG_DEBUG = 0x08, + DSYNC_MAILBOX_IMPORT_FLAG_MAILS_HAVE_GUIDS = 0x10, + DSYNC_MAILBOX_IMPORT_FLAG_MAILS_USE_GUID128 = 0x20, + DSYNC_MAILBOX_IMPORT_FLAG_NO_NOTIFY = 0x40, + DSYNC_MAILBOX_IMPORT_FLAG_EMPTY_HDR_WORKAROUND = 0x100 +}; + +struct mailbox; +struct dsync_mailbox_attribute; +struct dsync_mail; +struct dsync_mail_change; +struct dsync_transaction_log_scan; + +struct dsync_mailbox_importer * +dsync_mailbox_import_init(struct mailbox *box, + struct mailbox *virtual_all_box, + struct dsync_transaction_log_scan *log_scan, + uint32_t last_common_uid, + uint64_t last_common_modseq, + uint64_t last_common_pvt_modseq, + uint32_t remote_uid_next, + uint32_t remote_first_recent_uid, + uint64_t remote_highest_modseq, + uint64_t remote_highest_pvt_modseq, + time_t sync_since_timestamp, + time_t sync_until_timestamp, + uoff_t sync_max_size, + const char *sync_flag, + unsigned int commit_msgs_interval, + enum dsync_mailbox_import_flags flags, + unsigned int hdr_hash_version, + const char *const *hashed_headers); +int dsync_mailbox_import_attribute(struct dsync_mailbox_importer *importer, + const struct dsync_mailbox_attribute *attr); +int dsync_mailbox_import_change(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change); +int dsync_mailbox_import_changes_finish(struct dsync_mailbox_importer *importer); +const struct dsync_mail_request * +dsync_mailbox_import_next_request(struct dsync_mailbox_importer *importer); +int dsync_mailbox_import_mail(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail); +int dsync_mailbox_import_deinit(struct dsync_mailbox_importer **importer, + bool success, + uint32_t *last_common_uid_r, + uint64_t *last_common_modseq_r, + uint64_t *last_common_pvt_modseq_r, + uint32_t *last_messages_count_r, + const char **changes_during_sync_r, + bool *require_full_resync_r, + enum mail_error *error_r); + +const char *dsync_mailbox_import_get_proctitle(struct dsync_mailbox_importer *importer); + +#endif diff --git a/src/doveadm/dsync/dsync-mailbox-state.c b/src/doveadm/dsync/dsync-mailbox-state.c new file mode 100644 index 0000000..cca24d4 --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-state.c @@ -0,0 +1,127 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "base64.h" +#include "crc32.h" +#include "hash.h" +#include "dsync-mailbox-state.h" + +#define DSYNC_STATE_MAJOR_VERSION 1 +#define DSYNC_STATE_MINOR_VERSION 0 + +#define V0_MAILBOX_SIZE (GUID_128_SIZE + 4 + 4 + 8 + 8) +#define MAILBOX_SIZE (GUID_128_SIZE + 4 + 4 + 8 + 8 + 4) + +static void put_uint32(buffer_t *output, uint32_t num) +{ + uint8_t tmp[sizeof(uint32_t)]; + + cpu32_to_le_unaligned(num, tmp); + + buffer_append(output, tmp, sizeof(tmp)); +} + +void dsync_mailbox_states_export(const HASH_TABLE_TYPE(dsync_mailbox_state) states, + string_t *output) +{ + struct hash_iterate_context *iter; + struct dsync_mailbox_state *state; + uint8_t *guid; + buffer_t *buf = t_buffer_create(128); + uint32_t crc = 0; + + buffer_append_c(buf, DSYNC_STATE_MAJOR_VERSION); + buffer_append_c(buf, DSYNC_STATE_MINOR_VERSION); + buffer_append_c(buf, '\0'); + buffer_append_c(buf, '\0'); + + iter = hash_table_iterate_init(states); + while (hash_table_iterate(iter, states, &guid, &state)) { + buffer_append(buf, state->mailbox_guid, + sizeof(state->mailbox_guid)); + put_uint32(buf, state->last_uidvalidity); + put_uint32(buf, state->last_common_uid); + put_uint32(buf, state->last_common_modseq & 0xffffffffU); + put_uint32(buf, state->last_common_modseq >> 32); + put_uint32(buf, state->last_common_pvt_modseq & 0xffffffffU); + put_uint32(buf, state->last_common_pvt_modseq >> 32); + put_uint32(buf, state->last_messages_count); /* v1 */ + if (buf->used % 3 == 0) { + crc = crc32_data_more(crc, buf->data, buf->used); + base64_encode(buf->data, buf->used, output); + buffer_set_used_size(buf, 0); + } + } + hash_table_iterate_deinit(&iter); + + crc = crc32_data_more(crc, buf->data, buf->used); + put_uint32(buf, crc); + base64_encode(buf->data, buf->used, output); +} + +static int dsync_mailbox_states_retry_import_v0(const buffer_t *buf) +{ + const unsigned char *data = buf->data; + + /* v0 had no version header and no last_messages_count */ + + if ((buf->used-4) % V0_MAILBOX_SIZE != 0 || + le32_to_cpu_unaligned(data + buf->used-4) != crc32_data(data, buf->used-4)) + return -1; + /* looks like valid v0 format, silently treat it as empty state */ + return 0; +} + +int dsync_mailbox_states_import(HASH_TABLE_TYPE(dsync_mailbox_state) states, + pool_t pool, const char *input, + const char **error_r) +{ + struct dsync_mailbox_state *state; + buffer_t *buf; + uint8_t *guid_p; + const unsigned char *data; + unsigned int i, count; + + buf = t_buffer_create(strlen(input)); + if (base64_decode(input, strlen(input), NULL, buf) < 0) { + *error_r = "Invalid base64 data"; + return -1; + } + /* v1: 4 byte header, mailboxes[], CRC32 */ + data = buf->data; + + if (buf->used == 4 && le32_to_cpu_unaligned(data) == 0) { + /* v0: Empty state */ + return 0; + } + if (buf->used < 8) { + *error_r = "Input too small"; + return -1; + } + + if ((buf->used-8) % MAILBOX_SIZE != 0) { + *error_r = "Invalid input size"; + return dsync_mailbox_states_retry_import_v0(buf); + } + + if (le32_to_cpu_unaligned(data + buf->used-4) != crc32_data(data, buf->used-4)) { + *error_r = "CRC32 mismatch"; + return dsync_mailbox_states_retry_import_v0(buf); + } + data += 4; + count = (buf->used-8) / MAILBOX_SIZE; + + for (i = 0; i < count; i++, data += MAILBOX_SIZE) { + state = p_new(pool, struct dsync_mailbox_state, 1); + memcpy(state->mailbox_guid, data, GUID_128_SIZE); + state->last_uidvalidity = le32_to_cpu_unaligned(data + GUID_128_SIZE); + state->last_common_uid = le32_to_cpu_unaligned(data + GUID_128_SIZE + 4); + state->last_common_modseq = le64_to_cpu_unaligned(data + GUID_128_SIZE + 8); + state->last_common_pvt_modseq = le64_to_cpu_unaligned(data + GUID_128_SIZE + 16); + state->last_messages_count = le32_to_cpu_unaligned(data + GUID_128_SIZE + 24); + guid_p = state->mailbox_guid; + hash_table_insert(states, guid_p, state); + } + return 0; +} diff --git a/src/doveadm/dsync/dsync-mailbox-state.h b/src/doveadm/dsync/dsync-mailbox-state.h new file mode 100644 index 0000000..a01f531 --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-state.h @@ -0,0 +1,24 @@ +#ifndef DSYNC_MAILBOX_STATE_H +#define DSYNC_MAILBOX_STATE_H + +#include "guid.h" + +struct dsync_mailbox_state { + guid_128_t mailbox_guid; + uint32_t last_uidvalidity; + uint32_t last_common_uid; + uint64_t last_common_modseq; + uint64_t last_common_pvt_modseq; + uint32_t last_messages_count; + bool changes_during_sync; +}; +ARRAY_DEFINE_TYPE(dsync_mailbox_state, struct dsync_mailbox_state); +HASH_TABLE_DEFINE_TYPE(dsync_mailbox_state, uint8_t *, struct dsync_mailbox_state *); + +void dsync_mailbox_states_export(const HASH_TABLE_TYPE(dsync_mailbox_state) states, + string_t *output); +int dsync_mailbox_states_import(HASH_TABLE_TYPE(dsync_mailbox_state) states, + pool_t pool, const char *input, + const char **error_r); + +#endif diff --git a/src/doveadm/dsync/dsync-mailbox-tree-fill.c b/src/doveadm/dsync/dsync-mailbox-tree-fill.c new file mode 100644 index 0000000..c523e6b --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-tree-fill.c @@ -0,0 +1,405 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "guid.h" +#include "str.h" +#include "wildcard-match.h" +#include "mailbox-log.h" +#include "mail-namespace.h" +#include "mail-storage.h" +#include "mailbox-list-iter.h" +#include "dsync-mailbox-tree-private.h" + +static int +dsync_mailbox_tree_add_node(struct dsync_mailbox_tree *tree, + const struct mailbox_info *info, + struct dsync_mailbox_node **node_r) +{ + struct dsync_mailbox_node *node; + + node = dsync_mailbox_tree_get(tree, info->vname); + if (node->ns == info->ns) + ; + else if (node->ns == NULL) { + i_assert(tree->root.ns == NULL); + node->ns = info->ns; + } else { + i_error("Mailbox '%s' exists in two namespaces: '%s' and '%s'", + info->vname, node->ns->prefix, info->ns->prefix); + return -1; + } + *node_r = node; + return 0; +} + +static int +dsync_mailbox_tree_add_exists_node(struct dsync_mailbox_tree *tree, + const struct mailbox_info *info, + struct dsync_mailbox_node **node_r, + enum mail_error *error_r) +{ + if (dsync_mailbox_tree_add_node(tree, info, node_r) < 0) { + *error_r = MAIL_ERROR_TEMP; + return -1; + } + (*node_r)->existence = DSYNC_MAILBOX_NODE_EXISTS; + return 0; +} + +static int +dsync_mailbox_tree_get_selectable(struct mailbox *box, + struct mailbox_metadata *metadata_r, + struct mailbox_status *status_r) +{ + /* try the fast path */ + if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, metadata_r) < 0) + return -1; + if (mailbox_get_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT, status_r) < 0) + return -1; + + i_assert(!guid_128_is_empty(metadata_r->guid)); + if (status_r->uidvalidity != 0) + return 0; + + /* no UIDVALIDITY assigned yet. syncing a mailbox should add it. */ + if (mailbox_sync(box, 0) < 0) + return -1; + if (mailbox_get_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT, status_r) < 0) + return -1; + i_assert(status_r->uidvalidity != 0); + return 0; +} + +static int dsync_mailbox_tree_add(struct dsync_mailbox_tree *tree, + const struct mailbox_info *info, + const guid_128_t box_guid, + enum mail_error *error_r) +{ + struct dsync_mailbox_node *node; + struct mailbox *box; + struct mailbox_metadata metadata; + struct mailbox_status status; + const char *errstr; + enum mail_error error; + int ret = 0; + + if ((info->flags & MAILBOX_NONEXISTENT) != 0) + return 0; + if ((info->flags & MAILBOX_NOSELECT) != 0) { + return !guid_128_is_empty(box_guid) ? 0 : + dsync_mailbox_tree_add_exists_node(tree, info, &node, error_r); + } + + /* get GUID and UIDVALIDITY for selectable mailbox */ + box = mailbox_alloc(info->ns->list, info->vname, MAILBOX_FLAG_READONLY); + if (dsync_mailbox_tree_get_selectable(box, &metadata, &status) < 0) { + errstr = mailbox_get_last_internal_error(box, &error); + switch (error) { + case MAIL_ERROR_NOTFOUND: + /* mailbox was just deleted? */ + break; + case MAIL_ERROR_NOTPOSSIBLE: + /* invalid mbox files? ignore */ + break; + default: + i_error("Failed to access mailbox %s: %s", + info->vname, errstr); + *error_r = error; + ret = -1; + } + mailbox_free(&box); + return ret; + } + mailbox_free(&box); + + if (!guid_128_is_empty(box_guid) && + !guid_128_equals(box_guid, metadata.guid)) { + /* unwanted mailbox */ + return 0; + } + if (dsync_mailbox_tree_add_exists_node(tree, info, &node, error_r) < 0) + return -1; + memcpy(node->mailbox_guid, metadata.guid, + sizeof(node->mailbox_guid)); + node->uid_validity = status.uidvalidity; + node->uid_next = status.uidnext; + return 0; +} + +static struct dsync_mailbox_node * +dsync_mailbox_tree_find_sha(struct dsync_mailbox_tree *tree, + struct mail_namespace *ns, const guid_128_t sha128) +{ + struct dsync_mailbox_node *node; + + if (!hash_table_is_created(tree->name128_hash)) + dsync_mailbox_tree_build_name128_hash(tree); + + node = hash_table_lookup(tree->name128_hash, sha128); + return node == NULL || node->ns != ns ? NULL : node; +} + +static int +dsync_mailbox_tree_add_change_timestamps(struct dsync_mailbox_tree *tree, + struct mail_namespace *ns) +{ + struct dsync_mailbox_node *node; + struct dsync_mailbox_delete *del; + struct mailbox_log *log; + struct mailbox_log_iter *iter; + const struct mailbox_log_record *rec; + const uint8_t *guid_p; + time_t timestamp; + + log = mailbox_list_get_changelog(ns->list); + if (log == NULL) + return 0; + + iter = mailbox_log_iter_init(log); + while ((rec = mailbox_log_iter_next(iter)) != NULL) { + /* For DELETE_MAILBOX the record_guid is the mailbox GUID. + Otherwise it's 128bit SHA1 of the mailbox vname. */ + node = rec->type == MAILBOX_LOG_RECORD_DELETE_MAILBOX ? NULL : + dsync_mailbox_tree_find_sha(tree, ns, rec->mailbox_guid); + + timestamp = mailbox_log_record_get_timestamp(rec); + switch (rec->type) { + case MAILBOX_LOG_RECORD_DELETE_MAILBOX: + guid_p = rec->mailbox_guid; + if (hash_table_lookup(tree->guid_hash, guid_p) != NULL) { + /* mailbox still exists. maybe it was restored + from backup or something. */ + break; + } + del = array_append_space(&tree->deletes); + del->type = DSYNC_MAILBOX_DELETE_TYPE_MAILBOX; + del->timestamp = timestamp; + memcpy(del->guid, rec->mailbox_guid, sizeof(del->guid)); + break; + case MAILBOX_LOG_RECORD_DELETE_DIR: + if (node != NULL && + node->existence == DSYNC_MAILBOX_NODE_EXISTS) { + /* directory exists again, skip it */ + break; + } + /* we don't know what directory name was deleted, + just its hash. if the name still exists on the other + dsync side, it can match this deletion to the + name. */ + del = array_append_space(&tree->deletes); + del->type = DSYNC_MAILBOX_DELETE_TYPE_DIR; + del->timestamp = timestamp; + memcpy(del->guid, rec->mailbox_guid, sizeof(del->guid)); + break; + case MAILBOX_LOG_RECORD_CREATE_DIR: + if (node == NULL) { + /* directory has been deleted again, skip it */ + break; + } + /* notify the remote that we want to keep this + directory created (unless remote has a newer delete + timestamp) */ + node->last_renamed_or_created = timestamp; + break; + case MAILBOX_LOG_RECORD_RENAME: + if (node != NULL) + node->last_renamed_or_created = timestamp; + break; + case MAILBOX_LOG_RECORD_SUBSCRIBE: + if (node != NULL) + node->last_subscription_change = timestamp; + break; + case MAILBOX_LOG_RECORD_UNSUBSCRIBE: + if (node != NULL) { + node->last_subscription_change = timestamp; + break; + } + /* The mailbox is already deleted, but it may still + exist on the other side (even the subscription + alone). */ + del = array_append_space(&tree->deletes); + del->type = DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE; + del->timestamp = timestamp; + memcpy(del->guid, rec->mailbox_guid, sizeof(del->guid)); + break; + } + } + if (mailbox_log_iter_deinit(&iter) < 0) { + i_error("Mailbox log iteration for namespace '%s' failed", + ns->prefix); + return -1; + } + return 0; +} + +static int +dsync_mailbox_tree_fix_guid_duplicate(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node1, + struct dsync_mailbox_node *node2) +{ + struct mailbox *box; + struct mailbox_update update; + struct dsync_mailbox_node *change_node; + const char *change_vname; + int ret = 0; + + i_zero(&update); + guid_128_generate(update.mailbox_guid); + + /* just in case the duplication exists in both sides, + make them choose the same node */ + if (strcmp(dsync_mailbox_node_get_full_name(tree, node1), + dsync_mailbox_node_get_full_name(tree, node2)) <= 0) + change_node = node1; + else + change_node = node2; + + change_vname = dsync_mailbox_node_get_full_name(tree, change_node); + i_error("Duplicate mailbox GUID %s for mailboxes %s and %s - " + "giving a new GUID %s to %s", + guid_128_to_string(node1->mailbox_guid), + dsync_mailbox_node_get_full_name(tree, node1), + dsync_mailbox_node_get_full_name(tree, node2), + guid_128_to_string(update.mailbox_guid), change_vname); + + i_assert(node1->ns != NULL && node2->ns != NULL); + box = mailbox_alloc(change_node->ns->list, change_vname, 0); + if (mailbox_update(box, &update) < 0) { + i_error("Couldn't update mailbox %s GUID: %s", + change_vname, mailbox_get_last_internal_error(box, NULL)); + ret = -1; + } else { + memcpy(change_node->mailbox_guid, update.mailbox_guid, + sizeof(change_node->mailbox_guid)); + } + mailbox_free(&box); + return ret; +} + +static bool +dsync_mailbox_info_is_wanted(const struct mailbox_info *info, + const char *box_name, + const char *const *exclude_mailboxes) +{ + const char *const *info_specialuses; + unsigned int i; + + if (exclude_mailboxes == NULL && + (box_name == NULL || box_name[0] != '\\')) + return TRUE; + + info_specialuses = info->special_use == NULL ? NULL : + t_strsplit(info->special_use, " "); + /* include */ + if (box_name != NULL && box_name[0] == '\\') { + if (info_specialuses == NULL || + !str_array_icase_find(info_specialuses, box_name)) + return FALSE; + } + /* exclude */ + if (exclude_mailboxes == NULL) + return TRUE; + for (i = 0; exclude_mailboxes[i] != NULL; i++) { + const char *exclude = exclude_mailboxes[i]; + + if (exclude[0] == '\\') { + /* special-use */ + if (info_specialuses != NULL && + str_array_icase_find(info_specialuses, exclude)) + return FALSE; + } else { + /* mailbox with wildcards */ + if (wildcard_match(info->vname, exclude)) + return FALSE; + } + } + return TRUE; +} + +int dsync_mailbox_tree_fill(struct dsync_mailbox_tree *tree, + struct mail_namespace *ns, const char *box_name, + const guid_128_t box_guid, + const char *const *exclude_mailboxes, + enum mail_error *error_r) +{ + const enum mailbox_list_iter_flags list_flags = + /* FIXME: we'll skip symlinks, because we can't handle them + currently. in future we could detect them and create them + by creating the symlink. */ + MAILBOX_LIST_ITER_SKIP_ALIASES | + MAILBOX_LIST_ITER_NO_AUTO_BOXES; + const enum mailbox_list_iter_flags subs_list_flags = + MAILBOX_LIST_ITER_NO_AUTO_BOXES | + MAILBOX_LIST_ITER_SELECT_SUBSCRIBED | + MAILBOX_LIST_ITER_RETURN_NO_FLAGS; + struct mailbox_list_iterate_context *iter; + struct dsync_mailbox_node *node, *dup_node1, *dup_node2; + const struct mailbox_info *info; + const char *list_pattern = + box_name != NULL && box_name[0] != '\\' ? box_name : "*"; + int ret = 0; + + i_assert(mail_namespace_get_sep(ns) == tree->sep); + + /* assign namespace to its root, so it gets copied to children */ + if (ns->prefix_len > 0) { + const char *vname = t_strndup(ns->prefix, ns->prefix_len-1); + node = dsync_mailbox_tree_get(tree, vname); + node->ns = ns; + + struct mailbox_info ns_info = { + .vname = vname, + .ns = ns, + }; + if (dsync_mailbox_tree_add(tree, &ns_info, box_guid, error_r) < 0) + return -1; + } else { + tree->root.ns = ns; + } + + /* first add all of the existing mailboxes */ + iter = mailbox_list_iter_init(ns->list, list_pattern, list_flags); + while ((info = mailbox_list_iter_next(iter)) != NULL) T_BEGIN { + if (dsync_mailbox_info_is_wanted(info, box_name, + exclude_mailboxes)) { + if (dsync_mailbox_tree_add(tree, info, box_guid, error_r) < 0) + ret = -1; + } + } T_END; + if (mailbox_list_iter_deinit(&iter) < 0) { + i_error("Mailbox listing for namespace '%s' failed: %s", + ns->prefix, mailbox_list_get_last_internal_error(ns->list, error_r)); + ret = -1; + } + + /* add subscriptions */ + iter = mailbox_list_iter_init(ns->list, list_pattern, subs_list_flags); + while ((info = mailbox_list_iter_next(iter)) != NULL) { + if (dsync_mailbox_tree_add_node(tree, info, &node) == 0) + node->subscribed = TRUE; + else { + *error_r = MAIL_ERROR_TEMP; + ret = -1; + } + } + if (mailbox_list_iter_deinit(&iter) < 0) { + i_error("Mailbox listing for namespace '%s' failed: %s", + ns->prefix, mailbox_list_get_last_internal_error(ns->list, error_r)); + ret = -1; + } + if (ret < 0) + return -1; + + while (dsync_mailbox_tree_build_guid_hash(tree, &dup_node1, + &dup_node2) < 0) { + if (dsync_mailbox_tree_fix_guid_duplicate(tree, dup_node1, dup_node2) < 0) + return -1; + } + + /* add timestamps */ + if (dsync_mailbox_tree_add_change_timestamps(tree, ns) < 0) + return -1; + return 0; +} diff --git a/src/doveadm/dsync/dsync-mailbox-tree-private.h b/src/doveadm/dsync/dsync-mailbox-tree-private.h new file mode 100644 index 0000000..0614151 --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-tree-private.h @@ -0,0 +1,38 @@ +#ifndef DSYNC_MAILBOX_TREE_PRIVATE_H +#define DSYNC_MAILBOX_TREE_PRIVATE_H + +#include "dsync-mailbox-tree.h" + +struct dsync_mailbox_tree { + pool_t pool; + char sep, sep_str[2], remote_sep, alt_char; + char escape_char, remote_escape_char; + /* root node isn't part of the real mailbox tree. its name is "" and + it has no siblings */ + struct dsync_mailbox_node root; + + unsigned int iter_count; + + ARRAY(struct dsync_mailbox_delete) deletes; + + /* guid_128_t => struct dsync_mailbox_node */ + HASH_TABLE(uint8_t *, struct dsync_mailbox_node *) name128_hash; + HASH_TABLE(uint8_t *, struct dsync_mailbox_node *) name128_remotesep_hash; + HASH_TABLE(uint8_t *, struct dsync_mailbox_node *) guid_hash; +}; + +void dsync_mailbox_tree_build_name128_hash(struct dsync_mailbox_tree *tree); + +int dsync_mailbox_node_name_cmp(struct dsync_mailbox_node *const *n1, + struct dsync_mailbox_node *const *n2); + +void dsync_mailbox_tree_node_attach(struct dsync_mailbox_node *node, + struct dsync_mailbox_node *parent); +void dsync_mailbox_tree_node_detach(struct dsync_mailbox_node *node); + +struct dsync_mailbox_tree * +dsync_mailbox_tree_dup(const struct dsync_mailbox_tree *src); +bool dsync_mailbox_trees_equal(struct dsync_mailbox_tree *tree1, + struct dsync_mailbox_tree *tree2); + +#endif diff --git a/src/doveadm/dsync/dsync-mailbox-tree-sync.c b/src/doveadm/dsync/dsync-mailbox-tree-sync.c new file mode 100644 index 0000000..e4a50ae --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-tree-sync.c @@ -0,0 +1,1473 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "buffer.h" +#include "str.h" +#include "md5.h" +#include "hex-binary.h" +#include "aqueue.h" +#include "hash.h" +#include "dsync-brain-private.h" +#include "dsync-mailbox-tree-private.h" + +#define TEMP_MAX_NAME_LEN 100 +#define TEMP_SUFFIX_MAX_LEN (sizeof("temp-")-1 + 8) +#define TEMP_SUFFIX_FORMAT "temp-%x" + +struct dsync_mailbox_tree_bfs_iter { + struct dsync_mailbox_tree *tree; + + ARRAY(struct dsync_mailbox_node *) queue_arr; + struct aqueue *queue; + struct dsync_mailbox_node *cur; +}; + +struct dsync_mailbox_tree_sync_ctx { + pool_t pool; + struct dsync_mailbox_tree *local_tree, *remote_tree; + enum dsync_mailbox_trees_sync_type sync_type; + enum dsync_mailbox_trees_sync_flags sync_flags; + unsigned int combined_mailboxes_count; + + ARRAY(struct dsync_mailbox_tree_sync_change) changes; + unsigned int change_idx; + bool failed; +}; + +static struct dsync_mailbox_tree_bfs_iter * +dsync_mailbox_tree_bfs_iter_init(struct dsync_mailbox_tree *tree) +{ + struct dsync_mailbox_tree_bfs_iter *iter; + + iter = i_new(struct dsync_mailbox_tree_bfs_iter, 1); + iter->tree = tree; + i_array_init(&iter->queue_arr, 32); + iter->queue = aqueue_init(&iter->queue_arr.arr); + iter->cur = tree->root.first_child; + return iter; +} + +static bool +dsync_mailbox_tree_bfs_iter_next(struct dsync_mailbox_tree_bfs_iter *iter, + struct dsync_mailbox_node **node_r) +{ + if (iter->cur == NULL) { + if (aqueue_count(iter->queue) == 0) + return FALSE; + iter->cur = array_idx_elem(&iter->queue_arr, + aqueue_idx(iter->queue, 0)); + aqueue_delete_tail(iter->queue); + } + *node_r = iter->cur; + + if (iter->cur->first_child != NULL) + aqueue_append(iter->queue, &iter->cur->first_child); + iter->cur = iter->cur->next; + return TRUE; +} + +static void +dsync_mailbox_tree_bfs_iter_deinit(struct dsync_mailbox_tree_bfs_iter **_iter) +{ + struct dsync_mailbox_tree_bfs_iter *iter = *_iter; + + *_iter = NULL; + + aqueue_deinit(&iter->queue); + array_free(&iter->queue_arr); + i_free(iter); +} + +static void +sync_add_dir_change(struct dsync_mailbox_tree_sync_ctx *ctx, + const struct dsync_mailbox_node *node, + enum dsync_mailbox_tree_sync_type type) +{ + struct dsync_mailbox_tree_sync_change *change; + const char *name; + + i_assert(ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL); + + name = dsync_mailbox_node_get_full_name(ctx->local_tree, node); + + change = array_append_space(&ctx->changes); + change->type = type; + change->ns = node->ns; + change->full_name = p_strdup(ctx->pool, name); +} + +static void +sync_add_create_change(struct dsync_mailbox_tree_sync_ctx *ctx, + const struct dsync_mailbox_node *node, const char *name) +{ + struct dsync_mailbox_tree_sync_change *change; + + i_assert(ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL); + + change = array_append_space(&ctx->changes); + change->type = DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_BOX; + change->ns = node->ns; + change->full_name = p_strdup(ctx->pool, name); + memcpy(change->mailbox_guid, node->mailbox_guid, + sizeof(change->mailbox_guid)); + change->uid_validity = node->uid_validity; +} + +static void sort_siblings(ARRAY_TYPE(dsync_mailbox_node) *siblings) +{ + struct dsync_mailbox_node *const *nodes; + unsigned int i, count; + + array_sort(siblings, dsync_mailbox_node_name_cmp); + + nodes = array_get(siblings, &count); + if (count == 0) + return; + + nodes[0]->parent->first_child = nodes[0]; + for (i = 1; i < count; i++) + nodes[i-1]->next = nodes[i]; + nodes[count-1]->next = NULL; +} + +static void +sync_set_node_deleted(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node) +{ + const uint8_t *guid_p; + + /* for the rest of this sync assume that the mailbox has + already been deleted */ + guid_p = node->mailbox_guid; + hash_table_remove(tree->guid_hash, guid_p); + + node->existence = DSYNC_MAILBOX_NODE_DELETED; + memset(node->mailbox_guid, 0, sizeof(node->mailbox_guid)); + node->uid_validity = 0; +} + +static void +sync_delete_mailbox_node(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node, const char *reason) +{ + struct dsync_mailbox_tree_sync_change *change; + const char *name; + + if ((ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG) != 0 && + tree == ctx->local_tree) { + i_debug("brain %c: Deleting mailbox '%s' (GUID %s): %s", + (ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN) != 0 ? 'M' : 'S', + dsync_mailbox_node_get_full_name(tree, node), + guid_128_to_string(node->mailbox_guid), reason); + } + + if (tree == ctx->local_tree && + node->existence != DSYNC_MAILBOX_NODE_DELETED) { + /* delete this mailbox locally */ + i_assert(ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL); + change = array_append_space(&ctx->changes); + change->type = DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_BOX; + change->ns = node->ns; + name = dsync_mailbox_node_get_full_name(tree, node); + change->full_name = p_strdup(ctx->pool, name); + memcpy(change->mailbox_guid, node->mailbox_guid, + sizeof(change->mailbox_guid)); + } + sync_set_node_deleted(tree, node); +} + +static void +sync_delete_mailbox(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node, const char *reason) +{ + struct dsync_mailbox_tree *other_tree; + struct dsync_mailbox_node *other_node; + const uint8_t *guid_p; + + other_tree = tree == ctx->local_tree ? + ctx->remote_tree : ctx->local_tree; + guid_p = node->mailbox_guid; + other_node = hash_table_lookup(other_tree->guid_hash, guid_p); + if (other_node == NULL) { + /* doesn't exist / already deleted */ + } else { + sync_delete_mailbox_node(ctx, other_tree, other_node, reason); + } + sync_delete_mailbox_node(ctx, tree, node, reason); +} + +static void +sync_tree_sort_and_delete_mailboxes(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_tree *tree, + bool twoway_sync) +{ + struct dsync_mailbox_tree_bfs_iter *iter; + struct dsync_mailbox_node *node, *parent = NULL; + ARRAY_TYPE(dsync_mailbox_node) siblings; + + t_array_init(&siblings, 64); + + iter = dsync_mailbox_tree_bfs_iter_init(tree); + while (dsync_mailbox_tree_bfs_iter_next(iter, &node)) { + if (node->parent != parent) { + sort_siblings(&siblings); + array_clear(&siblings); + parent = node->parent; + } + if (node->existence == DSYNC_MAILBOX_NODE_DELETED && + !dsync_mailbox_node_is_dir(node)) { + if (twoway_sync) { + /* this mailbox was deleted. delete it from the + other side as well */ + sync_delete_mailbox(ctx, tree, node, + "Mailbox has been deleted"); + } else { + /* treat the node as if it didn't exist. it'll + get either recreated or deleted later. in + any case this function must handle all + existence=DELETED mailbox nodes by changing + them into directories (setting GUID=0) or + we'll assert-crash later */ + sync_set_node_deleted(tree, node); + } + } + ctx->combined_mailboxes_count++; + array_push_back(&siblings, &node); + } + sort_siblings(&siblings); + dsync_mailbox_tree_bfs_iter_deinit(&iter); +} + +static bool node_names_equal(const struct dsync_mailbox_node *n1, + const struct dsync_mailbox_node *n2) +{ + while (n1 != NULL && n2 != NULL) { + if (strcmp(n1->name, n2->name) != 0) + return FALSE; + n1 = n1->parent; + n2 = n2->parent; + } + return n1 == NULL && n2 == NULL; +} + +static void +dsync_mailbox_tree_node_attach_sorted(struct dsync_mailbox_node *node, + struct dsync_mailbox_node *parent) +{ + struct dsync_mailbox_node **p; + + node->parent = parent; + for (p = &parent->first_child; *p != NULL; p = &(*p)->next) { + if (dsync_mailbox_node_name_cmp(p, &node) > 0) + break; + } + node->next = *p; + *p = node; +} + +static void +dsync_mailbox_tree_node_move_sorted(struct dsync_mailbox_node *node, + struct dsync_mailbox_node *parent) +{ + /* detach from old parent */ + dsync_mailbox_tree_node_detach(node); + /* attach to new parent */ + dsync_mailbox_tree_node_attach_sorted(node, parent); +} + +static struct dsync_mailbox_node * +sorted_tree_get(struct dsync_mailbox_tree *tree, const char *name) +{ + struct dsync_mailbox_node *node, *parent, *ret; + + node = ret = dsync_mailbox_tree_get(tree, name); + while (node->parent != NULL && + node->existence == DSYNC_MAILBOX_NODE_NONEXISTENT) { + parent = node->parent; + dsync_mailbox_tree_node_detach(node); + dsync_mailbox_tree_node_attach_sorted(node, parent); + node = parent; + } + return ret; +} + +static struct dsync_mailbox_node * +sync_node_new(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node **pos, + struct dsync_mailbox_node *parent, + const struct dsync_mailbox_node *src) +{ + struct dsync_mailbox_node *node; + + node = p_new(tree->pool, struct dsync_mailbox_node, 1); + node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT; + node->name = p_strdup(tree->pool, src->name); + node->sync_temporary_name = src->sync_temporary_name; + node->ns = src->ns; + node->parent = parent; + node->next = *pos; + *pos = node; + return node; +} + +static struct dsync_mailbox_node * +sorted_tree_get_by_node_name(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_tree *other_tree, + struct dsync_mailbox_node *other_node) +{ + const char *parent_name; + + if (other_node == &other_tree->root) + return &tree->root; + + parent_name = dsync_mailbox_node_get_full_name(other_tree, other_node); + return sorted_tree_get(tree, parent_name); +} + +static bool node_has_child(struct dsync_mailbox_node *parent, const char *name) +{ + struct dsync_mailbox_node *node; + + for (node = parent->first_child; node != NULL; node = node->next) { + if (strcmp(node->name, name) == 0) + return TRUE; + } + return FALSE; +} + +static bool +node_has_existent_children(struct dsync_mailbox_node *node, bool dirs_ok) +{ + for (node = node->first_child; node != NULL; node = node->next) { + if (node->existence == DSYNC_MAILBOX_NODE_EXISTS && + (dirs_ok || !dsync_mailbox_node_is_dir(node))) + return TRUE; + if (node_has_existent_children(node, dirs_ok)) + return TRUE; + } + return FALSE; +} + +static bool node_is_existent(struct dsync_mailbox_node *node) +{ + if (node->existence == DSYNC_MAILBOX_NODE_EXISTS) + return TRUE; + return node_has_existent_children(node, TRUE); +} + +static bool sync_node_is_namespace_prefix(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node) +{ + const char *full_name; + size_t prefix_len = node->ns == NULL ? 0 : node->ns->prefix_len; + + if (strcmp(node->name, "INBOX") == 0 && node->parent == &tree->root) + return TRUE; + + if (prefix_len == 0) + return FALSE; + + full_name = dsync_mailbox_node_get_full_name(tree, node); + if (node->ns->prefix[prefix_len-1] == mail_namespace_get_sep(node->ns)) + prefix_len--; + return strncmp(full_name, node->ns->prefix, prefix_len) == 0 && + full_name[prefix_len] == '\0'; +} + +static void +sync_rename_node_to_temp(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node, + struct dsync_mailbox_node *new_parent, + const char **reason_r) +{ + struct dsync_mailbox_tree_sync_change *change; + const char *old_name, *new_name, *p; + char name[TEMP_MAX_NAME_LEN+1]; + buffer_t buf; + size_t prefix_len, max_prefix_len; + unsigned int counter = 1; + + i_assert(!sync_node_is_namespace_prefix(tree, node)); + + buffer_create_from_data(&buf, name, sizeof(name)); + max_prefix_len = TEMP_MAX_NAME_LEN - TEMP_SUFFIX_MAX_LEN - 1; + if (node->sync_temporary_name) { + /* the source name was also a temporary name. drop the + -<suffix> from it */ + p = strrchr(node->name, '-'); + i_assert(p != NULL); + if (max_prefix_len > (size_t)(p - node->name)) + max_prefix_len = p - node->name; + } + str_append_max(&buf, node->name, max_prefix_len); + str_append_c(&buf, '-'); + prefix_len = buf.used; + + do { + str_truncate(&buf, prefix_len); + str_printfa(&buf, TEMP_SUFFIX_FORMAT, counter++); + /* the generated name is quite unlikely to exist, + but check anyway.. */ + } while (node_has_child(node->parent, str_c(&buf))); + + old_name = tree != ctx->local_tree ? NULL : + dsync_mailbox_node_get_full_name(tree, node); + + *reason_r = t_strdup_printf("Renamed '%s' to '%s'", node->name, str_c(&buf)); + node->name = p_strdup(tree->pool, str_c(&buf)); + node->sync_temporary_name = TRUE; + node->last_renamed_or_created = 0; + dsync_mailbox_tree_node_move_sorted(node, new_parent); + + if (tree == ctx->local_tree && node_is_existent(node)) { + /* we're modifying a local tree. remember this change. */ + new_name = dsync_mailbox_node_get_full_name(tree, node); + + i_assert(ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL); + i_assert(strcmp(old_name, "INBOX") != 0); + change = array_append_space(&ctx->changes); + change->type = DSYNC_MAILBOX_TREE_SYNC_TYPE_RENAME; + change->ns = node->ns; + change->full_name = p_strdup(ctx->pool, old_name); + change->rename_dest_name = p_strdup(ctx->pool, new_name); + } +} + +static bool node_has_parent(struct dsync_mailbox_node *node, + struct dsync_mailbox_node *parent) +{ + for (; node != NULL; node = node->parent) { + if (node == parent) + return TRUE; + } + return FALSE; +} + +static void +sync_rename_node(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *temp_node, + struct dsync_mailbox_node *node, + const struct dsync_mailbox_node *other_node, + const char **reason_r) +{ + struct dsync_mailbox_tree_sync_change *change; + struct dsync_mailbox_tree *other_tree; + struct dsync_mailbox_node *parent; + const char *name, *other_name; + + i_assert(node != NULL); + i_assert(other_node != NULL); + + /* move/rename node in the tree, so that its position/name is identical + to other_node (in other_tree). temp_node's name is changed to + temporary name (i.e. it assumes that node's name becomes temp_node's + original name) */ + other_tree = tree == ctx->local_tree ? + ctx->remote_tree : ctx->local_tree; + + parent = sorted_tree_get_by_node_name(tree, other_tree, + other_node->parent); + if (node_has_parent(parent, node)) { + /* don't introduce a loop. temporarily rename node + under root. */ + sync_rename_node_to_temp(ctx, tree, node, &tree->root, reason_r); + *reason_r = t_strconcat(*reason_r, " (Don't introduce loop)", NULL); + return; + } + sync_rename_node_to_temp(ctx, tree, temp_node, temp_node->parent, reason_r); + + /* get the old name before it's modified */ + name = dsync_mailbox_node_get_full_name(tree, node); + + /* set the new name */ + *reason_r = t_strdup_printf("%s + Renamed '%s' to '%s'", + *reason_r, name, other_node->name); + node->name = p_strdup(tree->pool, other_node->name); + node->sync_temporary_name = other_node->sync_temporary_name; + node->last_renamed_or_created = other_node->last_renamed_or_created; + /* change node's parent if necessary. in any case detach+reattach it + sorted, because the nodes must be sorted by name, and the node's + name (or its parent) changed. */ + dsync_mailbox_tree_node_move_sorted(node, parent); + + if (tree == ctx->local_tree && node_is_existent(node)) { + /* we're modifying a local tree. remember this change. */ + other_name = dsync_mailbox_node_get_full_name(other_tree, other_node); + + i_assert(ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL); + i_assert(strcmp(name, "INBOX") != 0); + change = array_append_space(&ctx->changes); + change->type = DSYNC_MAILBOX_TREE_SYNC_TYPE_RENAME; + change->ns = node->ns; + change->full_name = p_strdup(ctx->pool, name); + change->rename_dest_name = p_strdup(ctx->pool, other_name); + } +} + +static int node_mailbox_guids_cmp(struct dsync_mailbox_node *node1, + struct dsync_mailbox_node *node2) +{ + int ret; + + while (node1 != NULL && node2 != NULL) { + if (node1->existence == DSYNC_MAILBOX_NODE_EXISTS && + node2->existence != DSYNC_MAILBOX_NODE_EXISTS) + return -1; + if (node2->existence == DSYNC_MAILBOX_NODE_EXISTS && + node1->existence != DSYNC_MAILBOX_NODE_EXISTS) + return 1; + + ret = memcmp(node1->mailbox_guid, node2->mailbox_guid, + sizeof(node1->mailbox_guid)); + if (ret != 0) + return ret; + + ret = node_mailbox_guids_cmp(node1->first_child, + node2->first_child); + if (ret != 0) + return ret; + node1 = node1->next; + node2 = node2->next; + } + if (node1 == NULL && node2 == NULL) + return 0; + return node1 != NULL ? -1 : 1; +} + +static int node_mailbox_names_cmp(struct dsync_mailbox_node *node1, + struct dsync_mailbox_node *node2) +{ + int ret; + + while (node1 != NULL && node2 != NULL) { + ret = strcmp(node1->name, node2->name); + if (ret != 0) + return ret; + + ret = node_mailbox_names_cmp(node1->first_child, + node2->first_child); + if (ret != 0) + return ret; + node1 = node1->next; + node2 = node2->next; + } + if (node1 == NULL && node2 == NULL) + return 0; + return node1 != NULL ? -1 : 1; +} + +static int node_mailbox_trees_cmp(struct dsync_mailbox_node *node1, + struct dsync_mailbox_node *node2) +{ + int ret; + + ret = node_mailbox_guids_cmp(node1, node2); + if (ret == 0) { + /* only a directory name changed and all the timestamps + are equal. just pick the alphabetically smaller. */ + ret = node_mailbox_names_cmp(node1, node2); + } + i_assert(ret != 0); + return ret; +} + +static time_t nodes_get_timestamp(struct dsync_mailbox_node *node1, + struct dsync_mailbox_node *node2) +{ + time_t ts = 0; + + /* avoid using temporary names in case all the timestamps are 0 */ + if (node1 != NULL && !node1->sync_temporary_name) + ts = node1->last_renamed_or_created + 1; + if (node2 != NULL && !node2->sync_temporary_name && + ts <= node2->last_renamed_or_created) + ts = node2->last_renamed_or_created + 1; + return ts; +} + +static bool sync_node_is_namespace_root(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node) +{ + if (node == NULL) + return FALSE; + if (node == &tree->root) + return TRUE; + return sync_node_is_namespace_prefix(tree, node); +} + +static bool ATTR_NULL(3, 4) +sync_rename_lower_ts(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_node *local_node1, + struct dsync_mailbox_node *remote_node1, + struct dsync_mailbox_node *local_node2, + struct dsync_mailbox_node *remote_node2, + const char **reason_r) +{ + time_t local_ts, remote_ts; + + /* We're scanning the tree at the position of local_node1 + and remote_node2. They have identical names. We also know that + local_node1&remote_node1 and local_node2&remote_node2 are "the same" + either because their GUIDs or (in case of one being a directory) + their childrens' GUIDs match. We don't know where local_node2 or + remote_node1 are located in the mailbox tree, or if they exist + at all. Note that node1 and node2 may be the same node pointers. */ + i_assert(strcmp(local_node1->name, remote_node2->name) == 0); + + if (sync_node_is_namespace_root(ctx->remote_tree, remote_node1) || + sync_node_is_namespace_root(ctx->remote_tree, remote_node2) || + sync_node_is_namespace_root(ctx->local_tree, local_node1) || + sync_node_is_namespace_root(ctx->local_tree, local_node2)) { + local_node1->sync_delayed_guid_change = TRUE; + remote_node2->sync_delayed_guid_change = TRUE; + *reason_r = "Can't rename namespace prefixes - will be merged later"; + return FALSE; + } + + local_ts = nodes_get_timestamp(local_node1, local_node2); + remote_ts = nodes_get_timestamp(remote_node1, remote_node2); + + if (ctx->sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) + local_ts = remote_ts+1; + else if (ctx->sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) + remote_ts = local_ts+1; + + /* The algorithm must be deterministic regardless of the sync direction, + so in case the timestamps are equal we need to resort to looking at + the other data. We'll start by looking at the nodes' mailbox GUIDs, + but if both of them don't exist continue looking into their + children. */ + if (local_ts > remote_ts || + (local_ts == remote_ts && + node_mailbox_trees_cmp(local_node1, remote_node2) < 0)) { + /* local nodes have a higher timestamp. we only want to do + renames where the destination parent is the current node's + (local_node1/remote_node2) parent. */ + + /* Numbers are GUIDs, letters are mailbox names: + + local 1A <-name conflict-> remote 2A + local 2B <- potentially -> remote 1[BC] + + Here we want to preserve the local 1A & 2B names: */ + if (local_node2 == NULL) { + /* local : 1A + remote: 1B, 2A -> 2A-temp, 1A */ + sync_rename_node(ctx, ctx->remote_tree, remote_node2, + remote_node1, local_node1, reason_r); + *reason_r = t_strconcat(*reason_r, "(local: local_node2=NULL)", NULL); + return TRUE; + } else if (remote_node1 == remote_node2) { + /* FIXME: this fixes an infinite loop when in + rename2 test, think it through why :) */ + *reason_r = "local: remote_node1=remote_node2"; + } else if (remote_node1 != NULL) { + /* a) local_node1->parent == local_node2->parent + + local : 1A, 2B + remote: 1B, 2A -> 2A-temp, 1A(, 2B) + remote: 1C, 2A -> 2B, 1A + remote: 1C, 2A, 3B -> 2A-temp, 1A(, 3B-temp, 2B) + + b) local_node1->parent != local_node2->parent + + local : 1X/A, 2Y/B + remote: 1Y/B, 2X/A -> 2X/A-temp, 1X/A(, 2Y/B) + remote: 1Z/C, 2X/A -> 2X/A-temp, 1X/A + remote: 1Z/C, 2X/A, 3Y/B -> 2X/A-temp, 1X/A + + We can handle all of these more easily by simply + always renaming 2 to a temporary name and handling + it when we reach B handling. */ + sync_rename_node(ctx, ctx->remote_tree, remote_node2, + remote_node1, local_node1, reason_r); + *reason_r = t_strconcat(*reason_r, "(local: remote_node1=NULL)", NULL); + return TRUE; + } else if (node_has_parent(local_node1, local_node2) && + ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) { + /* node2 is a parent of node1, but it should be + vice versa */ + sync_rename_node_to_temp(ctx, ctx->local_tree, + local_node1, local_node2->parent, reason_r); + *reason_r = t_strconcat(*reason_r, "(local: node2 parent of node1)", NULL); + return TRUE; + } else if (node_has_parent(local_node2, local_node1) && + ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) { + /* node1 is a parent of node2, but it should be + vice versa */ + sync_rename_node_to_temp(ctx, ctx->local_tree, + local_node2, local_node1->parent, reason_r); + *reason_r = t_strconcat(*reason_r, "(local: node1 parent of node2)", NULL); + return TRUE; + } else if (local_node1->existence == DSYNC_MAILBOX_NODE_EXISTS) { + sync_rename_node_to_temp(ctx, ctx->remote_tree, + remote_node2, remote_node2->parent, reason_r); + *reason_r = t_strconcat(*reason_r, "(local: local_node1 exists)", NULL); + return TRUE; + } else { + /* local : 1A, 2B + remote: 2A -> (2B) + remote: 2A, 3B -> (3B-temp, 2B) */ + *reason_r = "local: unchanged"; + } + } else { + /* remote nodes have a higher timestamp */ + if (remote_node1 == NULL) { + sync_rename_node(ctx, ctx->local_tree, local_node1, + local_node2, remote_node2, reason_r); + *reason_r = t_strconcat(*reason_r, "(remote: remote_node1=NULL)", NULL); + return TRUE; + } else if (local_node2 == local_node1) { + *reason_r = "remote: remote_node2=remote_node1"; + } else if (local_node2 != NULL) { + sync_rename_node(ctx, ctx->local_tree, local_node1, + local_node2, remote_node2, reason_r); + *reason_r = t_strconcat(*reason_r, "(remote: local_node2=NULL)", NULL); + return TRUE; + } else if (node_has_parent(remote_node1, remote_node2) && + ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) { + sync_rename_node_to_temp(ctx, ctx->remote_tree, + remote_node1, remote_node2->parent, reason_r); + *reason_r = t_strconcat(*reason_r, "(remote: node2 parent of node1)", NULL); + return TRUE; + } else if (node_has_parent(remote_node2, remote_node1) && + ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) { + sync_rename_node_to_temp(ctx, ctx->remote_tree, + remote_node2, remote_node1->parent, reason_r); + *reason_r = t_strconcat(*reason_r, "(remote: node1 parent of node2)", NULL); + return TRUE; + } else if (remote_node2->existence == DSYNC_MAILBOX_NODE_EXISTS) { + sync_rename_node_to_temp(ctx, ctx->local_tree, + local_node1, local_node1->parent, reason_r); + *reason_r = t_strconcat(*reason_r, "(remote: remote_node2 exists)", NULL); + return TRUE; + } else { + *reason_r = "remote: unchanged"; + } + } + return FALSE; +} + +static bool sync_rename_conflict(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_node *local_node1, + struct dsync_mailbox_node *remote_node2, + const char **reason_r) +{ + struct dsync_mailbox_node *local_node2, *remote_node1; + const uint8_t *guid_p; + bool ret; + + guid_p = local_node1->mailbox_guid; + remote_node1 = hash_table_lookup(ctx->remote_tree->guid_hash, guid_p); + guid_p = remote_node2->mailbox_guid; + local_node2 = hash_table_lookup(ctx->local_tree->guid_hash, guid_p); + + if ((remote_node1 != NULL && remote_node1->existence == DSYNC_MAILBOX_NODE_EXISTS) || + (local_node2 != NULL && local_node2->existence == DSYNC_MAILBOX_NODE_EXISTS)) { + /* conflicting name, rename the one with lower timestamp */ + ret = sync_rename_lower_ts(ctx, local_node1, remote_node1, + local_node2, remote_node2, reason_r); + *reason_r = t_strconcat("conflicting name: ", *reason_r, NULL); + return ret; + } else if (dsync_mailbox_node_is_dir(local_node1) || + dsync_mailbox_node_is_dir(remote_node2)) { + /* one of the nodes is a directory, and the other is a mailbox + that doesn't exist on the other side. there is no conflict, + we'll just need to create the mailbox later. */ + *reason_r = "mailbox not selectable yet"; + return FALSE; + } else { + /* both nodes are mailboxes that don't exist on the other side. + we'll merge these mailboxes together later and change their + GUIDs and UIDVALIDITYs to be the same */ + local_node1->sync_delayed_guid_change = TRUE; + remote_node2->sync_delayed_guid_change = TRUE; + *reason_r = "GUIDs conflict - will be merged later"; + return FALSE; + } +} + +static struct dsync_mailbox_node * +sync_find_branch(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_tree *other_tree, + struct dsync_mailbox_node *dir_node) +{ + struct dsync_mailbox_node *node, *other_node; + const uint8_t *guid_p; + + for (node = dir_node->first_child; node != NULL; node = node->next) { + if (dsync_mailbox_node_is_dir(node)) { + other_node = sync_find_branch(tree, other_tree, node); + if (other_node != NULL) + return other_node; + } else { + guid_p = node->mailbox_guid; + other_node = hash_table_lookup(other_tree->guid_hash, + guid_p); + if (other_node != NULL) + return other_node->parent; + } + } + return NULL; +} + +static bool sync_rename_directory(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_node *local_node1, + struct dsync_mailbox_node *remote_node2, + const char **reason_r) +{ + struct dsync_mailbox_node *remote_node1, *local_node2; + + /* see if we can find matching mailbox branches based on the nodes' + child mailboxes (with GUIDs). we can then rename the entire branch. + don't try to do this for namespace prefixes though. */ + remote_node1 = sync_find_branch(ctx->local_tree, + ctx->remote_tree, local_node1); + local_node2 = sync_find_branch(ctx->remote_tree, ctx->local_tree, + remote_node2); + if (remote_node1 == NULL || local_node2 == NULL) { + *reason_r = "Directory rename branch not found"; + return FALSE; + } + if (node_names_equal(remote_node1, local_node2)) { + *reason_r = "Directory name paths are equal"; + return FALSE; + } + + return sync_rename_lower_ts(ctx, local_node1, remote_node1, + local_node2, remote_node2, reason_r); +} + +static bool sync_rename_mailboxes(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_node *local_parent, + struct dsync_mailbox_node *remote_parent) +{ + struct dsync_mailbox_node **local_nodep = &local_parent->first_child; + struct dsync_mailbox_node **remote_nodep = &remote_parent->first_child; + struct dsync_mailbox_node *local_node, *remote_node; + const char *reason; + string_t *debug = NULL; + bool changed; + + if ((ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG) != 0) + debug = t_str_new(128); + + /* the nodes are sorted by their names. */ + while (*local_nodep != NULL || *remote_nodep != NULL) { + local_node = *local_nodep; + remote_node = *remote_nodep; + + if (local_node == NULL || + (remote_node != NULL && + strcmp(local_node->name, remote_node->name) > 0)) { + /* add a missing local node */ + local_node = sync_node_new(ctx->local_tree, local_nodep, + local_parent, remote_node); + } + if (remote_node == NULL || + strcmp(remote_node->name, local_node->name) > 0) { + /* add a missing remote node */ + remote_node = sync_node_new(ctx->remote_tree, remote_nodep, + remote_parent, local_node); + } + i_assert(strcmp(local_node->name, remote_node->name) == 0); + if (debug != NULL) { + str_truncate(debug, 0); + str_append(debug, "Mailbox "); + dsync_mailbox_node_append_full_name(debug, ctx->local_tree, local_node); + str_printfa(debug, ": local=%s/%ld/%d, remote=%s/%ld/%d", + guid_128_to_string(local_node->mailbox_guid), + (long)local_node->last_renamed_or_created, + local_node->existence, + guid_128_to_string(remote_node->mailbox_guid), + (long)remote_node->last_renamed_or_created, + remote_node->existence); + } + + if (dsync_mailbox_node_is_dir(local_node) && + dsync_mailbox_node_is_dir(remote_node)) { + /* both nodes are directories (or other side is + nonexistent). see if we can match them by their + child mailboxes */ + changed = sync_rename_directory(ctx, local_node, remote_node, &reason); + } else if (dsync_mailbox_node_guids_equal(local_node, + remote_node)) { + /* mailboxes are equal, no need to rename */ + reason = "Mailboxes are equal"; + changed = FALSE; + } else { + /* mailbox naming conflict */ + changed = sync_rename_conflict(ctx, local_node, + remote_node, &reason); + } + /* handle children, if there are any */ + if (debug != NULL) { + i_debug("brain %c: %s: %s", + (ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN) != 0 ? 'M' : 'S', + str_c(debug), reason); + } + + if (!changed) T_BEGIN { + changed = sync_rename_mailboxes(ctx, local_node, + remote_node); + } T_END; + if (changed) + return TRUE; + + local_nodep = &local_node->next; + remote_nodep = &remote_node->next; + } + return FALSE; +} + +static bool mailbox_node_hash_first_child(struct dsync_mailbox_node *node, + struct md5_context *md5) +{ + for (node = node->first_child; node != NULL; node = node->next) { + if (node->existence == DSYNC_MAILBOX_NODE_EXISTS) { + md5_update(md5, node->mailbox_guid, + sizeof(node->mailbox_guid)); + md5_update(md5, node->name, strlen(node->name)); + return TRUE; + } + if (node->first_child != NULL) { + if (mailbox_node_hash_first_child(node, md5)) + return TRUE; + } + } + return FALSE; +} + +static const char * +mailbox_node_generate_suffix(struct dsync_mailbox_node *node) +{ + struct md5_context md5; + unsigned char digest[MD5_RESULTLEN]; + + if (!dsync_mailbox_node_is_dir(node)) + return guid_128_to_string(node->mailbox_guid); + + md5_init(&md5); + if (!mailbox_node_hash_first_child(node, &md5)) + i_unreached(); /* we would have deleted it */ + md5_final(&md5, digest); + return binary_to_hex(digest, sizeof(digest)); +} + +static void suffix_inc(string_t *str) +{ + char *data; + size_t i; + + data = str_c_modifiable(str) + str_len(str)-1; + for (i = str_len(str); i > 0; i--, data--) { + if ((*data >= '0' && *data < '9') || + (*data >= 'a' && *data < 'f')) { + *data += 1; + return; + } else if (*data == '9') { + *data = 'a'; + return; + } else if (*data != 'f') { + i_unreached(); + } + } + i_unreached(); +} + +static void +sync_rename_temp_mailbox_node(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node, + const char **reason_r) +{ + const char *p, *new_suffix; + string_t *str = t_str_new(256); + size_t max_prefix_len; + + /* The name is currently <oldname>-<temp>. Both sides need to + use equivalent names, so we'll replace the <temp> if possible + with a) mailbox GUID, b) sha1 of childrens' (GUID|name)s. In the + very unlikely case of such name already existing, just increase + the last letters until it's not found. */ + new_suffix = mailbox_node_generate_suffix(node); + + p = strrchr(node->name, '-'); + i_assert(p != NULL); + p++; + max_prefix_len = TEMP_MAX_NAME_LEN - strlen(new_suffix) - 1; + if (max_prefix_len > (size_t)(p-node->name)) + max_prefix_len = p-node->name; + str_append_max(str, node->name, max_prefix_len); + str_append(str, new_suffix); + while (node_has_child(node->parent, str_c(str))) + suffix_inc(str); + + *reason_r = t_strdup_printf("Renamed '%s' to '%s'", + dsync_mailbox_node_get_full_name(tree, node), str_c(str)); + node->name = p_strdup(tree->pool, str_c(str)); + + dsync_mailbox_tree_node_move_sorted(node, node->parent); + node->sync_temporary_name = FALSE; +} + +static void +sync_rename_delete_node_dirs(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node) +{ + struct dsync_mailbox_node *child; + + for (child = node->first_child; child != NULL; child = child->next) + sync_rename_delete_node_dirs(ctx, tree, child); + + if (tree == ctx->local_tree && + ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL && + node->existence != DSYNC_MAILBOX_NODE_NONEXISTENT) { + sync_add_dir_change(ctx, node, + DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_DIR); + } + node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT; + node->sync_temporary_name = FALSE; +} + +static bool +sync_rename_temp_mailboxes(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node, bool *renames_r) +{ + const char *reason; + + for (; node != NULL; node = node->next) { + while (sync_rename_temp_mailboxes(ctx, tree, node->first_child, renames_r)) ; + + if (!node->sync_temporary_name) { + } else if (dsync_mailbox_node_is_dir(node) && + (node->first_child == NULL || + !node_has_existent_children(node, FALSE))) { + /* we can just delete this directory and + any child directories it may have */ + if ((ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG) != 0) { + i_debug("brain %c: %s mailbox %s: Delete directory-only tree", + (ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN) != 0 ? 'M' : 'S', + tree == ctx->local_tree ? "local" : "remote", + dsync_mailbox_node_get_full_name(tree, node)); + } + sync_rename_delete_node_dirs(ctx, tree, node); + } else { + T_BEGIN { + *renames_r = TRUE; + sync_rename_temp_mailbox_node(tree, node, &reason); + if ((ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG) != 0) { + i_debug("brain %c: %s mailbox %s: %s", + (ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN) != 0 ? 'M' : 'S', + tree == ctx->local_tree ? "local" : "remote", + dsync_mailbox_node_get_full_name(tree, node), reason); + } + } T_END; + return TRUE; + } + } + return FALSE; +} + +static int +dsync_mailbox_tree_handle_renames(struct dsync_mailbox_tree_sync_ctx *ctx, + bool *renames_r) +{ + unsigned int max_renames, count = 0; + bool changed; + + *renames_r = FALSE; + + max_renames = ctx->combined_mailboxes_count * 3; + do { + T_BEGIN { + changed = sync_rename_mailboxes(ctx, &ctx->local_tree->root, + &ctx->remote_tree->root); + } T_END; + if ((ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG) != 0 && + changed) { + i_debug("brain %c: -- Mailbox renamed, restart sync --", + (ctx->sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN) != 0 ? 'M' : 'S'); + } + } while (changed && ++count <= max_renames); + + if (changed) { + i_error("BUG: Mailbox renaming algorithm got into a potentially infinite loop, aborting"); + return -1; + } + + while (sync_rename_temp_mailboxes(ctx, ctx->local_tree, &ctx->local_tree->root, renames_r)) ; + while (sync_rename_temp_mailboxes(ctx, ctx->remote_tree, &ctx->remote_tree->root, renames_r)) ; + return 0; +} + +static bool sync_is_wrong_mailbox(struct dsync_mailbox_node *node, + const struct dsync_mailbox_node *wanted_node, + const char **reason_r) +{ + if (wanted_node->existence != DSYNC_MAILBOX_NODE_EXISTS) { + *reason_r = wanted_node->existence == DSYNC_MAILBOX_NODE_DELETED ? + "Mailbox has been deleted" : "Mailbox doesn't exist"; + return TRUE; + } + if (node->uid_validity != wanted_node->uid_validity) { + *reason_r = t_strdup_printf("UIDVALIDITY changed (%u -> %u)", + wanted_node->uid_validity, + node->uid_validity); + return TRUE; + } + if (node->uid_next > wanted_node->uid_next) { + /* we can't lower the UIDNEXT */ + *reason_r = t_strdup_printf("UIDNEXT is too high (%u > %u)", + node->uid_next, + wanted_node->uid_next); + return TRUE; + } + if (memcmp(node->mailbox_guid, wanted_node->mailbox_guid, + sizeof(node->mailbox_guid)) != 0) { + *reason_r = "GUID changed"; + return TRUE; + } + return FALSE; +} + +static void +sync_delete_wrong_mailboxes_branch(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_tree *tree, + const struct dsync_mailbox_tree *wanted_tree, + struct dsync_mailbox_node *node, + const struct dsync_mailbox_node *wanted_node) +{ + const char *reason; + int ret; + + while (node != NULL && wanted_node != NULL) { + if (node->first_child != NULL) { + sync_delete_wrong_mailboxes_branch(ctx, + tree, wanted_tree, + node->first_child, wanted_node->first_child); + } + ret = strcmp(node->name, wanted_node->name); + if (ret < 0) { + /* node shouldn't exist */ + if (node->existence == DSYNC_MAILBOX_NODE_EXISTS && + !dsync_mailbox_node_is_dir(node)) { + sync_delete_mailbox_node(ctx, tree, node, + "Mailbox doesn't exist"); + } + node = node->next; + } else if (ret > 0) { + /* wanted_node doesn't exist. it's created later. */ + wanted_node = wanted_node->next; + } else T_BEGIN { + if (sync_is_wrong_mailbox(node, wanted_node, &reason) && + node->existence == DSYNC_MAILBOX_NODE_EXISTS && + !dsync_mailbox_node_is_dir(node)) + sync_delete_mailbox_node(ctx, tree, node, reason); + node = node->next; + wanted_node = wanted_node->next; + } T_END; + } + for (; node != NULL; node = node->next) { + /* node and its children shouldn't exist */ + if (node->first_child != NULL) { + sync_delete_wrong_mailboxes_branch(ctx, + tree, wanted_tree, + node->first_child, NULL); + } + if (node->existence == DSYNC_MAILBOX_NODE_EXISTS && + !dsync_mailbox_node_is_dir(node)) + sync_delete_mailbox_node(ctx, tree, node, "Mailbox doesn't exist"); + } +} + +static void +sync_delete_wrong_mailboxes(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_tree *tree, + const struct dsync_mailbox_tree *wanted_tree) +{ + sync_delete_wrong_mailboxes_branch(ctx, tree, wanted_tree, + tree->root.first_child, + wanted_tree->root.first_child); +} + +static void sync_create_mailboxes(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_tree *tree) +{ + struct dsync_mailbox_tree *other_tree; + struct dsync_mailbox_tree_iter *iter; + struct dsync_mailbox_node *node, *other_node; + const char *name; + const uint8_t *guid_p; + + other_tree = tree == ctx->local_tree ? + ctx->remote_tree : ctx->local_tree; + + iter = dsync_mailbox_tree_iter_init(tree); + while (dsync_mailbox_tree_iter_next(iter, &name, &node)) { + /* make sure the renaming handled everything */ + i_assert(!node->sync_temporary_name); + if (dsync_mailbox_node_is_dir(node)) + continue; + + i_assert(node->existence == DSYNC_MAILBOX_NODE_EXISTS); + + guid_p = node->mailbox_guid; + other_node = hash_table_lookup(other_tree->guid_hash, guid_p); + if (other_node == NULL) + other_node = sorted_tree_get(other_tree, name); + if (dsync_mailbox_node_is_dir(other_node)) { + /* create a missing mailbox */ + other_node->existence = DSYNC_MAILBOX_NODE_EXISTS; + other_node->ns = node->ns; + other_node->uid_validity = node->uid_validity; + memcpy(other_node->mailbox_guid, node->mailbox_guid, + sizeof(other_node->mailbox_guid)); + if (other_tree == ctx->local_tree) + sync_add_create_change(ctx, other_node, name); + } else if (!guid_128_equals(node->mailbox_guid, + other_node->mailbox_guid)) { + /* mailbox with same name exists both locally and + remotely, but they have different GUIDs and neither + side has the other's GUID. typically this means that + both sides had autocreated some mailboxes (e.g. + INBOX). we'll just change the GUID for one of + them. */ + i_assert(node->existence == DSYNC_MAILBOX_NODE_EXISTS); + i_assert(node->ns == other_node->ns); + + if (other_tree == ctx->local_tree) + sync_add_create_change(ctx, node, name); + } else { + /* existing mailbox. mismatching UIDVALIDITY is handled + later while syncing the mailbox. */ + i_assert(node->existence == DSYNC_MAILBOX_NODE_EXISTS); + i_assert(node->ns == other_node->ns); + } + } + dsync_mailbox_tree_iter_deinit(&iter); +} + +static void +sync_subscription(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_node *local_node, + struct dsync_mailbox_node *remote_node) +{ + bool use_local; + + if (ctx->sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) + use_local = TRUE; + else if (ctx->sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) + use_local = FALSE; + else if (local_node->last_subscription_change > remote_node->last_subscription_change) + use_local = TRUE; + else if (local_node->last_subscription_change < remote_node->last_subscription_change) + use_local = FALSE; + else { + /* local and remote have equal timestamps. prefer to subscribe + rather than unsubscribe. */ + use_local = local_node->subscribed; + } + if (use_local) { + /* use local subscription state */ + remote_node->subscribed = local_node->subscribed; + } else { + /* use remote subscription state */ + local_node->subscribed = remote_node->subscribed; + sync_add_dir_change(ctx, local_node, local_node->subscribed ? + DSYNC_MAILBOX_TREE_SYNC_TYPE_SUBSCRIBE : + DSYNC_MAILBOX_TREE_SYNC_TYPE_UNSUBSCRIBE); + } +} + +static void sync_mailbox_child_dirs(struct dsync_mailbox_tree_sync_ctx *ctx, + struct dsync_mailbox_node *local_parent, + struct dsync_mailbox_node *remote_parent) +{ + struct dsync_mailbox_node **local_nodep = &local_parent->first_child; + struct dsync_mailbox_node **remote_nodep = &remote_parent->first_child; + struct dsync_mailbox_node *local_node, *remote_node; + int ret; + + /* NOTE: the nodes are always sorted. renaming created all of the + interesting nodes, but it may have left some extra nonexistent nodes + lying around, which we will delete. */ + while (*local_nodep != NULL && *remote_nodep != NULL) { + local_node = *local_nodep; + remote_node = *remote_nodep; + + ret = strcmp(local_node->name, remote_node->name); + if (ret < 0) { + i_assert(!node_is_existent(local_node)); + *local_nodep = local_node->next; + continue; + } + if (ret > 0) { + i_assert(!node_is_existent(remote_node)); + *remote_nodep = remote_node->next; + continue; + } + + if (local_node->existence == DSYNC_MAILBOX_NODE_EXISTS && + remote_node->existence == DSYNC_MAILBOX_NODE_NONEXISTENT && + ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) { + /* create to remote */ + remote_node->existence = DSYNC_MAILBOX_NODE_EXISTS; + } + if (remote_node->existence == DSYNC_MAILBOX_NODE_EXISTS && + local_node->existence == DSYNC_MAILBOX_NODE_NONEXISTENT && + ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) { + /* create to local */ + local_node->existence = DSYNC_MAILBOX_NODE_EXISTS; + sync_add_dir_change(ctx, local_node, + DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_DIR); + } + + /* create/delete child dirs */ + sync_mailbox_child_dirs(ctx, local_node, remote_node); + + if (local_node->subscribed != remote_node->subscribed) + sync_subscription(ctx, local_node, remote_node); + + if (local_node->existence == DSYNC_MAILBOX_NODE_DELETED && + !node_has_existent_children(local_node, TRUE) && + remote_node->existence == DSYNC_MAILBOX_NODE_EXISTS && + ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) { + /* delete from remote */ + i_assert(!node_has_existent_children(remote_node, TRUE)); + remote_node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT; + } + if (remote_node->existence == DSYNC_MAILBOX_NODE_DELETED && + !node_has_existent_children(remote_node, TRUE) && + local_node->existence == DSYNC_MAILBOX_NODE_EXISTS && + ctx->sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) { + /* delete from local */ + i_assert(!node_has_existent_children(local_node, TRUE)); + local_node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT; + sync_add_dir_change(ctx, local_node, + DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_DIR); + } + + local_nodep = &local_node->next; + remote_nodep = &remote_node->next; + } + while (*local_nodep != NULL) { + i_assert(!node_is_existent(*local_nodep)); + *local_nodep = (*local_nodep)->next; + } + while (*remote_nodep != NULL) { + i_assert(!node_is_existent(*remote_nodep)); + *remote_nodep = (*remote_nodep)->next; + } +} + +static void sync_mailbox_dirs(struct dsync_mailbox_tree_sync_ctx *ctx) +{ + sync_mailbox_child_dirs(ctx, &ctx->local_tree->root, + &ctx->remote_tree->root); +} + +static void +dsync_mailbox_tree_update_child_timestamps(struct dsync_mailbox_node *node, + time_t parent_timestamp) +{ + time_t ts; + + if (node->last_renamed_or_created < parent_timestamp) + node->last_renamed_or_created = parent_timestamp; + ts = node->last_renamed_or_created; + + for (node = node->first_child; node != NULL; node = node->next) + dsync_mailbox_tree_update_child_timestamps(node, ts); +} + +struct dsync_mailbox_tree_sync_ctx * +dsync_mailbox_trees_sync_init(struct dsync_mailbox_tree *local_tree, + struct dsync_mailbox_tree *remote_tree, + enum dsync_mailbox_trees_sync_type sync_type, + enum dsync_mailbox_trees_sync_flags sync_flags) +{ + struct dsync_mailbox_tree_sync_ctx *ctx; + unsigned int rename_counter = 0; + bool renames; + pool_t pool; + + i_assert(hash_table_is_created(local_tree->guid_hash)); + i_assert(hash_table_is_created(remote_tree->guid_hash)); + + pool = pool_alloconly_create(MEMPOOL_GROWING"dsync mailbox trees sync", + 1024*64); + ctx = p_new(pool, struct dsync_mailbox_tree_sync_ctx, 1); + ctx->pool = pool; + ctx->local_tree = local_tree; + ctx->remote_tree = remote_tree; + ctx->sync_type = sync_type; + ctx->sync_flags = sync_flags; + i_array_init(&ctx->changes, 128); + +again: + renames = FALSE; + ctx->combined_mailboxes_count = 0; + sync_tree_sort_and_delete_mailboxes(ctx, remote_tree, + sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY); + sync_tree_sort_and_delete_mailboxes(ctx, local_tree, + sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY); + + dsync_mailbox_tree_update_child_timestamps(&local_tree->root, 0); + dsync_mailbox_tree_update_child_timestamps(&remote_tree->root, 0); + if ((sync_flags & DSYNC_MAILBOX_TREES_SYNC_FLAG_NO_RENAMES) == 0) { + if (dsync_mailbox_tree_handle_renames(ctx, &renames) < 0) { + ctx->failed = TRUE; + return ctx; + } + } + + /* if we're not doing a two-way sync, delete now any mailboxes, which + a) shouldn't exist, b) doesn't have a matching GUID/UIDVALIDITY, + c) has a too high UIDNEXT */ + if (sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) + sync_delete_wrong_mailboxes(ctx, remote_tree, local_tree); + else if (sync_type == DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) + sync_delete_wrong_mailboxes(ctx, local_tree, remote_tree); + + if (sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL) + sync_create_mailboxes(ctx, remote_tree); + if (sync_type != DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE) + sync_create_mailboxes(ctx, local_tree); + if (renames && rename_counter++ <= ctx->combined_mailboxes_count*3) { + /* this rename algorithm is just horrible. we're retrying this + because the final sync_rename_temp_mailbox_node() calls + give different names to local & remote mailbox trees. + something's not right here, but this looping is better than + a crash in sync_mailbox_dirs() due to trees not matching. */ + goto again; + } + sync_mailbox_dirs(ctx); + return ctx; +} + +const struct dsync_mailbox_tree_sync_change * +dsync_mailbox_trees_sync_next(struct dsync_mailbox_tree_sync_ctx *ctx) +{ + if (ctx->change_idx == array_count(&ctx->changes)) + return NULL; + return array_idx(&ctx->changes, ctx->change_idx++); +} + +int dsync_mailbox_trees_sync_deinit(struct dsync_mailbox_tree_sync_ctx **_ctx) +{ + struct dsync_mailbox_tree_sync_ctx *ctx = *_ctx; + int ret = ctx->failed ? -1 : 0; + + *_ctx = NULL; + + array_free(&ctx->changes); + pool_unref(&ctx->pool); + return ret; +} diff --git a/src/doveadm/dsync/dsync-mailbox-tree.c b/src/doveadm/dsync/dsync-mailbox-tree.c new file mode 100644 index 0000000..e34f335 --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-tree.c @@ -0,0 +1,554 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "str.h" +#include "sort.h" +#include "mailbox-list-private.h" +#include "dsync-mailbox-tree-private.h" + + +struct dsync_mailbox_tree_iter { + struct dsync_mailbox_tree *tree; + + struct dsync_mailbox_node *cur; + string_t *name; +}; + +struct dsync_mailbox_tree * +dsync_mailbox_tree_init(char sep, char escape_char, char alt_char) +{ + struct dsync_mailbox_tree *tree; + pool_t pool; + + i_assert(sep != '\0'); + i_assert(alt_char != '\0'); + + pool = pool_alloconly_create(MEMPOOL_GROWING"dsync mailbox tree", 4096); + tree = p_new(pool, struct dsync_mailbox_tree, 1); + tree->pool = pool; + tree->sep = tree->sep_str[0] = sep; + tree->escape_char = escape_char; + tree->alt_char = alt_char; + tree->root.name = ""; + i_array_init(&tree->deletes, 32); + return tree; +} + +void dsync_mailbox_tree_deinit(struct dsync_mailbox_tree **_tree) +{ + struct dsync_mailbox_tree *tree = *_tree; + + *_tree = NULL; + hash_table_destroy(&tree->name128_hash); + hash_table_destroy(&tree->guid_hash); + array_free(&tree->deletes); + pool_unref(&tree->pool); +} + +static struct dsync_mailbox_node * +dsync_mailbox_node_find(struct dsync_mailbox_node *nodes, const char *name) +{ + for (; nodes != NULL; nodes = nodes->next) { + if (strcmp(name, nodes->name) == 0) + return nodes; + } + return NULL; +} + +struct dsync_mailbox_node * +dsync_mailbox_tree_lookup(struct dsync_mailbox_tree *tree, + const char *full_name) +{ + struct dsync_mailbox_node *node = &tree->root; + + T_BEGIN { + const char *const *path; + + path = t_strsplit(full_name, tree->sep_str); + for (; *path != NULL && node != NULL; path++) + node = dsync_mailbox_node_find(node->first_child, *path); + } T_END; + return node; +} + +void dsync_mailbox_tree_node_attach(struct dsync_mailbox_node *node, + struct dsync_mailbox_node *parent) +{ + node->parent = parent; + node->next = parent->first_child; + parent->first_child = node; +} + +void dsync_mailbox_tree_node_detach(struct dsync_mailbox_node *node) +{ + struct dsync_mailbox_node **p; + + for (p = &node->parent->first_child;; p = &(*p)->next) { + if (*p == node) { + *p = node->next; + break; + } + } + node->parent = NULL; +} + +struct dsync_mailbox_node * +dsync_mailbox_tree_get(struct dsync_mailbox_tree *tree, const char *full_name) +{ + struct dsync_mailbox_node *parent = NULL, *node = &tree->root; + + i_assert(tree->iter_count == 0); + + T_BEGIN { + const char *const *path; + + /* find the existing part */ + path = t_strsplit(full_name, tree->sep_str); + for (; *path != NULL; path++) { + parent = node; + node = dsync_mailbox_node_find(node->first_child, *path); + if (node == NULL) + break; + } + /* create the rest */ + for (; *path != NULL; path++) { + node = p_new(tree->pool, struct dsync_mailbox_node, 1); + node->name = p_strdup(tree->pool, *path); + node->ns = parent->ns; + dsync_mailbox_tree_node_attach(node, parent); + parent = node; + } + } T_END; + return node; +} + +static void +node_get_full_name_recurse(const struct dsync_mailbox_tree *tree, + const struct dsync_mailbox_node *node, string_t *str) +{ + if (node->parent != &tree->root) + node_get_full_name_recurse(tree, node->parent, str); + + str_append(str, node->name); + str_append_c(str, tree->sep); +} + +const char *dsync_mailbox_node_get_full_name(const struct dsync_mailbox_tree *tree, + const struct dsync_mailbox_node *node) +{ + string_t *str = t_str_new(128); + dsync_mailbox_node_append_full_name(str, tree, node); + return str_c(str); +} + +void dsync_mailbox_node_append_full_name(string_t *str, + const struct dsync_mailbox_tree *tree, + const struct dsync_mailbox_node *node) +{ + i_assert(node->parent != NULL); + + node_get_full_name_recurse(tree, node, str); + /* remove the trailing separator */ + str_truncate(str, str_len(str)-1); +} + +void dsync_mailbox_node_copy_data(struct dsync_mailbox_node *dest, + const struct dsync_mailbox_node *src) +{ + memcpy(dest->mailbox_guid, src->mailbox_guid, + sizeof(dest->mailbox_guid)); + dest->uid_validity = src->uid_validity; + dest->uid_next = src->uid_next; + dest->existence = src->existence; + dest->last_renamed_or_created = src->last_renamed_or_created; + dest->subscribed = src->subscribed; + dest->last_subscription_change = src->last_subscription_change; +} + +struct dsync_mailbox_tree_iter * +dsync_mailbox_tree_iter_init(struct dsync_mailbox_tree *tree) +{ + struct dsync_mailbox_tree_iter *iter; + + iter = i_new(struct dsync_mailbox_tree_iter, 1); + iter->tree = tree; + iter->name = str_new(default_pool, 128); + iter->cur = &tree->root; + + tree->iter_count++; + return iter; +} + +static size_t node_get_full_name_length(struct dsync_mailbox_node *node) +{ + if (node->parent->parent == NULL) + return strlen(node->name); + else { + return strlen(node->name) + 1 + + node_get_full_name_length(node->parent); + } +} + +bool dsync_mailbox_tree_iter_next(struct dsync_mailbox_tree_iter *iter, + const char **full_name_r, + struct dsync_mailbox_node **node_r) +{ + size_t len; + + if (iter->cur->first_child != NULL) + iter->cur = iter->cur->first_child; + else { + while (iter->cur->next == NULL) { + if (iter->cur == &iter->tree->root) + return FALSE; + iter->cur = iter->cur->parent; + } + iter->cur = iter->cur->next; + len = iter->cur->parent == &iter->tree->root ? 0 : + node_get_full_name_length(iter->cur->parent); + str_truncate(iter->name, len); + } + if (str_len(iter->name) > 0) + str_append_c(iter->name, iter->tree->sep); + str_append(iter->name, iter->cur->name); + *full_name_r = str_c(iter->name); + *node_r = iter->cur; + return TRUE; +} + +void dsync_mailbox_tree_iter_deinit(struct dsync_mailbox_tree_iter **_iter) +{ + struct dsync_mailbox_tree_iter *iter = *_iter; + + *_iter = NULL; + + i_assert(iter->tree->iter_count > 0); + iter->tree->iter_count--; + + str_free(&iter->name); + i_free(iter); +} + +void dsync_mailbox_tree_build_name128_hash(struct dsync_mailbox_tree *tree) +{ + struct dsync_mailbox_tree_iter *iter; + struct dsync_mailbox_node *node; + const char *name; + guid_128_t *sha128; + uint8_t *guid_p; + + i_assert(!hash_table_is_created(tree->name128_hash)); + + hash_table_create(&tree->name128_hash, + tree->pool, 0, guid_128_hash, guid_128_cmp); + iter = dsync_mailbox_tree_iter_init(tree); + while (dsync_mailbox_tree_iter_next(iter, &name, &node)) { + sha128 = p_new(tree->pool, guid_128_t, 1); + mailbox_name_get_sha128(name, *sha128); + guid_p = *sha128; + hash_table_insert(tree->name128_hash, guid_p, node); + } + dsync_mailbox_tree_iter_deinit(&iter); +} + +static const char * +convert_name_to_remote_sep(struct dsync_mailbox_tree *tree, const char *name) +{ + string_t *str = t_str_new(128); + + char remote_escape_chars[3] = { + tree->remote_escape_char, + tree->remote_sep, + '\0' + }; + + for (;;) { + const char *end = strchr(name, tree->sep); + const char *name_part = end == NULL ? name : + t_strdup_until(name, end++); + + if (tree->escape_char != '\0') + mailbox_list_name_unescape(&name_part, tree->escape_char); + if (remote_escape_chars[0] != '\0') { + /* The local name can be fully escaped to remote + name and back. */ + mailbox_list_name_escape(name_part, remote_escape_chars, + str); + } else { + /* There is no remote escape char, so for conflicting + separator use the alt_char. */ + for (; *name_part != '\0'; name_part++) { + if (*name_part == tree->remote_sep) + str_append_c(str, tree->alt_char); + else + str_append_c(str, *name_part); + } + } + if (end == NULL) + break; + str_append_c(str, tree->remote_sep); + } + return str_c(str); +} + +static void +dsync_mailbox_tree_build_name128_remotesep_hash(struct dsync_mailbox_tree *tree) +{ + struct dsync_mailbox_tree_iter *iter; + struct dsync_mailbox_node *node; + const char *name; + guid_128_t *sha128; + uint8_t *guid_p; + + i_assert(tree->sep != tree->remote_sep); + i_assert(!hash_table_is_created(tree->name128_remotesep_hash)); + + hash_table_create(&tree->name128_remotesep_hash, tree->pool, 0, + guid_128_hash, guid_128_cmp); + iter = dsync_mailbox_tree_iter_init(tree); + while (dsync_mailbox_tree_iter_next(iter, &name, &node)) { + sha128 = p_new(tree->pool, guid_128_t, 1); + T_BEGIN { + const char *remote_name = + convert_name_to_remote_sep(tree, name); + mailbox_name_get_sha128(remote_name, *sha128); + } T_END; + guid_p = *sha128; + hash_table_insert(tree->name128_remotesep_hash, guid_p, node); + } + dsync_mailbox_tree_iter_deinit(&iter); +} + +int dsync_mailbox_tree_guid_hash_add(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node, + struct dsync_mailbox_node **old_node_r) +{ + struct dsync_mailbox_node *old_node; + uint8_t *guid = node->mailbox_guid; + + if (guid_128_is_empty(node->mailbox_guid)) + return 0; + + *old_node_r = old_node = hash_table_lookup(tree->guid_hash, guid); + if (old_node == NULL) + hash_table_insert(tree->guid_hash, guid, node); + else if (old_node != node) + return -1; + return 0; +} + +int dsync_mailbox_tree_build_guid_hash(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node **dup_node1_r, + struct dsync_mailbox_node **dup_node2_r) +{ + struct dsync_mailbox_tree_iter *iter; + struct dsync_mailbox_node *node, *old_node; + const char *name; + int ret = 0; + + if (!hash_table_is_created(tree->guid_hash)) { + hash_table_create(&tree->guid_hash, tree->pool, 0, + guid_128_hash, guid_128_cmp); + } + iter = dsync_mailbox_tree_iter_init(tree); + while (dsync_mailbox_tree_iter_next(iter, &name, &node)) { + if (dsync_mailbox_tree_guid_hash_add(tree, node, &old_node) < 0) { + *dup_node1_r = node; + *dup_node2_r = old_node; + ret = -1; + } + } + dsync_mailbox_tree_iter_deinit(&iter); + return ret; +} + +struct dsync_mailbox_node * +dsync_mailbox_tree_lookup_guid(struct dsync_mailbox_tree *tree, + const guid_128_t guid) +{ + const uint8_t *guid_p = guid; + + return hash_table_lookup(tree->guid_hash, guid_p); +} + +const struct dsync_mailbox_delete * +dsync_mailbox_tree_get_deletes(struct dsync_mailbox_tree *tree, + unsigned int *count_r) +{ + return array_get(&tree->deletes, count_r); +} + +struct dsync_mailbox_node * +dsync_mailbox_tree_find_delete(struct dsync_mailbox_tree *tree, + const struct dsync_mailbox_delete *del) +{ + const uint8_t *guid_p = del->guid; + + i_assert(hash_table_is_created(tree->guid_hash)); + i_assert(tree->remote_sep != '\0'); + + if (del->type == DSYNC_MAILBOX_DELETE_TYPE_MAILBOX) { + /* find node by GUID */ + return hash_table_lookup(tree->guid_hash, guid_p); + } + + /* find node by name. this is a bit tricky, since the hierarchy + separator may differ from ours. */ + if (tree->sep == tree->remote_sep) { + if (!hash_table_is_created(tree->name128_hash)) + dsync_mailbox_tree_build_name128_hash(tree); + return hash_table_lookup(tree->name128_hash, guid_p); + } else { + if (!hash_table_is_created(tree->name128_remotesep_hash)) + dsync_mailbox_tree_build_name128_remotesep_hash(tree); + return hash_table_lookup(tree->name128_remotesep_hash, guid_p); + } +} + +void dsync_mailbox_tree_set_remote_chars(struct dsync_mailbox_tree *tree, + char remote_sep, char escape_char) +{ + i_assert(tree->remote_sep == '\0'); + i_assert(tree->remote_escape_char == '\0'); + + tree->remote_sep = remote_sep; + tree->remote_escape_char = escape_char; +} + +static void +dsync_mailbox_tree_dup_nodes(struct dsync_mailbox_tree *dest_tree, + const struct dsync_mailbox_node *src, + string_t *path) +{ + size_t prefix_len = str_len(path); + struct dsync_mailbox_node *node; + + if (prefix_len > 0) { + str_append_c(path, dest_tree->sep); + prefix_len++; + } + for (; src != NULL; src = src->next) { + str_truncate(path, prefix_len); + str_append(path, src->name); + node = dsync_mailbox_tree_get(dest_tree, str_c(path)); + + node->ns = src->ns; + memcpy(node->mailbox_guid, src->mailbox_guid, + sizeof(node->mailbox_guid)); + node->uid_validity = src->uid_validity; + node->uid_next = src->uid_next; + node->existence = src->existence; + node->last_renamed_or_created = src->last_renamed_or_created; + node->subscribed = src->subscribed; + node->last_subscription_change = src->last_subscription_change; + + if (src->first_child != NULL) { + dsync_mailbox_tree_dup_nodes(dest_tree, + src->first_child, path); + } + } +} + +struct dsync_mailbox_tree * +dsync_mailbox_tree_dup(const struct dsync_mailbox_tree *src) +{ + struct dsync_mailbox_tree *dest; + string_t *str = t_str_new(128); + + dest = dsync_mailbox_tree_init(src->sep, src->escape_char, + src->alt_char); + dsync_mailbox_tree_dup_nodes(dest, &src->root, str); + return dest; +} + +int dsync_mailbox_node_name_cmp(struct dsync_mailbox_node *const *n1, + struct dsync_mailbox_node *const *n2) +{ + return strcmp((*n1)->name, (*n2)->name); +} + +static bool +dsync_mailbox_nodes_equal(const struct dsync_mailbox_node *node1, + const struct dsync_mailbox_node *node2) +{ + return strcmp(node1->name, node2->name) == 0 && + node1->ns == node2->ns && + memcmp(node1->mailbox_guid, node2->mailbox_guid, + sizeof(node1->mailbox_guid)) == 0 && + node1->uid_validity == node2->uid_validity && + node1->existence == node2->existence && + node1->subscribed == node2->subscribed; +} + +static bool +dsync_mailbox_branches_equal(struct dsync_mailbox_node *node1, + struct dsync_mailbox_node *node2) +{ + /* this function is used only for unit tests, so performance doesn't + really matter */ + struct dsync_mailbox_node *n, **snodes1, **snodes2; + unsigned int i, count; + + for (n = node1, count = 0; n != NULL; n = n->next) count++; + for (n = node2, i = 0; n != NULL; n = n->next) i++; + if (i != count) + return FALSE; + if (count == 0) + return TRUE; + + /* sort the trees by name */ + snodes1 = t_new(struct dsync_mailbox_node *, count); + snodes2 = t_new(struct dsync_mailbox_node *, count); + for (n = node1, i = 0; n != NULL; n = n->next, i++) + snodes1[i] = n; + for (n = node2, i = 0; n != NULL; n = n->next, i++) + snodes2[i] = n; + i_qsort(snodes1, count, sizeof(*snodes1), dsync_mailbox_node_name_cmp); + i_qsort(snodes2, count, sizeof(*snodes2), dsync_mailbox_node_name_cmp); + + for (i = 0; i < count; i++) { + if (!dsync_mailbox_nodes_equal(snodes1[i], snodes2[i])) + return FALSE; + if (!dsync_mailbox_branches_equal(snodes1[i]->first_child, + snodes2[i]->first_child)) + return FALSE; + } + return TRUE; +} + +bool dsync_mailbox_trees_equal(struct dsync_mailbox_tree *tree1, + struct dsync_mailbox_tree *tree2) +{ + bool ret; + + T_BEGIN { + ret = dsync_mailbox_branches_equal(&tree1->root, &tree2->root); + } T_END; + return ret; +} + +const char *dsync_mailbox_node_to_string(const struct dsync_mailbox_node *node) +{ + return t_strdup_printf("guid=%s uid_validity=%u uid_next=%u subs=%s last_change=%ld last_subs=%ld", + guid_128_to_string(node->mailbox_guid), + node->uid_validity, node->uid_next, + node->subscribed ? "yes" : "no", + (long)node->last_renamed_or_created, + (long)node->last_subscription_change); +} + +const char * +dsync_mailbox_delete_type_to_string(enum dsync_mailbox_delete_type type) +{ + switch (type) { + case DSYNC_MAILBOX_DELETE_TYPE_MAILBOX: + return "mailbox"; + case DSYNC_MAILBOX_DELETE_TYPE_DIR: + return "dir"; + case DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE: + return "unsubscribe"; + } + i_unreached(); +} diff --git a/src/doveadm/dsync/dsync-mailbox-tree.h b/src/doveadm/dsync/dsync-mailbox-tree.h new file mode 100644 index 0000000..6969813 --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-tree.h @@ -0,0 +1,205 @@ +#ifndef DSYNC_MAILBOX_TREE_H +#define DSYNC_MAILBOX_TREE_H + +#include "guid.h" +#include "mail-error.h" + +struct mail_namespace; +struct dsync_brain; + +enum dsync_mailbox_trees_sync_type { + /* two-way sync for both mailboxes */ + DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY, + /* make remote tree look exactly like the local tree */ + DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_LOCAL, + /* make local tree look exactly like the remote tree */ + DSYNC_MAILBOX_TREES_SYNC_TYPE_PRESERVE_REMOTE +}; + +enum dsync_mailbox_trees_sync_flags { + /* Enable debugging */ + DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG = 0x01, + /* Show ourself as "master brain" in the debug output */ + DSYNC_MAILBOX_TREES_SYNC_FLAG_MASTER_BRAIN = 0x02, + /* Disable mailbox renaming logic. This is just a kludge that should + be removed once the renaming logic has no more bugs.. */ + DSYNC_MAILBOX_TREES_SYNC_FLAG_NO_RENAMES = 0x04 +}; + +enum dsync_mailbox_node_existence { + /* this is just a filler node for children or for + subscription deletion */ + DSYNC_MAILBOX_NODE_NONEXISTENT = 0, + /* if mailbox GUID is set, the mailbox exists. + otherwise the directory exists. */ + DSYNC_MAILBOX_NODE_EXISTS, + /* if mailbox GUID is set, the mailbox has been deleted. + otherwise the directory has been deleted. */ + DSYNC_MAILBOX_NODE_DELETED +}; + +struct dsync_mailbox_node { + struct dsync_mailbox_node *parent, *next, *first_child; + + /* namespace where this node belongs to */ + struct mail_namespace *ns; + /* this node's name (not including parents) */ + const char *name; + /* mailbox GUID, or full of zeros if this is about a directory name */ + guid_128_t mailbox_guid; + /* mailbox's UIDVALIDITY/UIDNEXT (may be 0 if not assigned yet) */ + uint32_t uid_validity, uid_next; + + /* existence of this mailbox/directory. + doesn't affect subscription state. */ + enum dsync_mailbox_node_existence existence; + /* last time the mailbox/directory was created/renamed, + 0 if not known */ + time_t last_renamed_or_created; + + /* last time the subscription state was changed, 0 if not known */ + time_t last_subscription_change; + /* is this mailbox or directory subscribed? */ + bool subscribed:1; + + /* Internal syncing flags: */ + bool sync_delayed_guid_change:1; + bool sync_temporary_name:1; +}; +ARRAY_DEFINE_TYPE(dsync_mailbox_node, struct dsync_mailbox_node *); + +#define dsync_mailbox_node_guids_equal(node1, node2) \ + (memcmp((node1)->mailbox_guid, (node2)->mailbox_guid, \ + sizeof(guid_128_t)) == 0) + +#define dsync_mailbox_node_is_dir(node) \ + guid_128_is_empty((node)->mailbox_guid) + +enum dsync_mailbox_delete_type { + /* Delete mailbox by given GUID */ + DSYNC_MAILBOX_DELETE_TYPE_MAILBOX = 1, + /* Delete mailbox directory by given SHA1 name */ + DSYNC_MAILBOX_DELETE_TYPE_DIR, + /* Unsubscribe mailbox by given SHA1 name */ + DSYNC_MAILBOX_DELETE_TYPE_UNSUBSCRIBE, +}; + +struct dsync_mailbox_delete { + enum dsync_mailbox_delete_type type; + guid_128_t guid; + time_t timestamp; +}; + +enum dsync_mailbox_tree_sync_type { + DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_BOX, + DSYNC_MAILBOX_TREE_SYNC_TYPE_CREATE_DIR, + DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_BOX, + DSYNC_MAILBOX_TREE_SYNC_TYPE_DELETE_DIR, + /* Rename given mailbox name and its children */ + DSYNC_MAILBOX_TREE_SYNC_TYPE_RENAME, + DSYNC_MAILBOX_TREE_SYNC_TYPE_SUBSCRIBE, + DSYNC_MAILBOX_TREE_SYNC_TYPE_UNSUBSCRIBE +}; + +struct dsync_mailbox_tree_sync_change { + enum dsync_mailbox_tree_sync_type type; + + /* for all types: */ + struct mail_namespace *ns; + const char *full_name; + + /* for create_box and delete_box: */ + guid_128_t mailbox_guid; + /* for create_box: */ + uint32_t uid_validity; + /* for rename: */ + const char *rename_dest_name; +}; + +struct dsync_mailbox_tree * +dsync_mailbox_tree_init(char sep, char escape_char, char alt_char); +void dsync_mailbox_tree_deinit(struct dsync_mailbox_tree **tree); + +/* Lookup a mailbox node by name. Returns NULL if not known. */ +struct dsync_mailbox_node * +dsync_mailbox_tree_lookup(struct dsync_mailbox_tree *tree, + const char *full_name); +/* Lookup a mailbox node by GUID. Returns NULL if not known. + The mailbox GUID hash must have been build before calling this. */ +struct dsync_mailbox_node * +dsync_mailbox_tree_lookup_guid(struct dsync_mailbox_tree *tree, + const guid_128_t guid); +/* Lookup or create a mailbox node by name. */ +struct dsync_mailbox_node * +dsync_mailbox_tree_get(struct dsync_mailbox_tree *tree, const char *full_name); + +/* Returns full name for the given mailbox node. */ +const char *dsync_mailbox_node_get_full_name(const struct dsync_mailbox_tree *tree, + const struct dsync_mailbox_node *node); +void dsync_mailbox_node_append_full_name(string_t *str, + const struct dsync_mailbox_tree *tree, + const struct dsync_mailbox_node *node); + +/* Copy everything from src to dest, except name and hierarchy pointers */ +void dsync_mailbox_node_copy_data(struct dsync_mailbox_node *dest, + const struct dsync_mailbox_node *src); + +/* Add nodes to tree from the given namespace. If box_name or box_guid is + non-NULL, add only that mailbox to the tree. */ +int dsync_mailbox_tree_fill(struct dsync_mailbox_tree *tree, + struct mail_namespace *ns, const char *box_name, + const guid_128_t box_guid, + const char *const *exclude_mailboxes, + enum mail_error *error_r); + +/* Return all known deleted mailboxes and directories. */ +const struct dsync_mailbox_delete * +dsync_mailbox_tree_get_deletes(struct dsync_mailbox_tree *tree, + unsigned int *count_r); +/* Return mailbox node for a given delete record, or NULL if it doesn't exist. + The delete record is intended to come from another tree, possibly with + a different hierarchy separator. dsync_mailbox_tree_build_guid_hash() must + have been called before this. */ +struct dsync_mailbox_node * +dsync_mailbox_tree_find_delete(struct dsync_mailbox_tree *tree, + const struct dsync_mailbox_delete *del); +/* Build GUID lookup hash, if it's not already built. Returns 0 if ok, -1 if + there are duplicate GUIDs. The nodes with the duplicate GUIDs are + returned. */ +int dsync_mailbox_tree_build_guid_hash(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node **dup_node1_r, + struct dsync_mailbox_node **dup_node2_r); +/* Manually add a new node to hash. */ +int dsync_mailbox_tree_guid_hash_add(struct dsync_mailbox_tree *tree, + struct dsync_mailbox_node *node, + struct dsync_mailbox_node **old_node_r); +/* Set remote separator used for directory deletions in + dsync_mailbox_tree_find_delete() */ +void dsync_mailbox_tree_set_remote_chars(struct dsync_mailbox_tree *tree, + char remote_sep, + char remote_escape_char); + +/* Iterate through all nodes in a tree (depth-first) */ +struct dsync_mailbox_tree_iter * +dsync_mailbox_tree_iter_init(struct dsync_mailbox_tree *tree); +bool dsync_mailbox_tree_iter_next(struct dsync_mailbox_tree_iter *iter, + const char **full_name_r, + struct dsync_mailbox_node **node_r); +void dsync_mailbox_tree_iter_deinit(struct dsync_mailbox_tree_iter **iter); + +/* Sync local and remote trees so at the end they're exactly the same. + Return changes done to local tree. */ +struct dsync_mailbox_tree_sync_ctx * +dsync_mailbox_trees_sync_init(struct dsync_mailbox_tree *local_tree, + struct dsync_mailbox_tree *remote_tree, + enum dsync_mailbox_trees_sync_type sync_type, + enum dsync_mailbox_trees_sync_flags sync_flags); +const struct dsync_mailbox_tree_sync_change * +dsync_mailbox_trees_sync_next(struct dsync_mailbox_tree_sync_ctx *ctx); +int dsync_mailbox_trees_sync_deinit(struct dsync_mailbox_tree_sync_ctx **ctx); + +const char *dsync_mailbox_node_to_string(const struct dsync_mailbox_node *node); +const char * +dsync_mailbox_delete_type_to_string(enum dsync_mailbox_delete_type type); + +#endif diff --git a/src/doveadm/dsync/dsync-mailbox.c b/src/doveadm/dsync/dsync-mailbox.c new file mode 100644 index 0000000..d6d06fd --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox.c @@ -0,0 +1,61 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "istream.h" +#include "mail-storage-private.h" +#include "dsync-brain-private.h" +#include "dsync-mailbox.h" + +void dsync_mailbox_attribute_dup(pool_t pool, + const struct dsync_mailbox_attribute *src, + struct dsync_mailbox_attribute *dest_r) +{ + dest_r->type = src->type; + dest_r->key = p_strdup(pool, src->key); + dest_r->value = p_strdup(pool, src->value); + if (src->value_stream != NULL) { + dest_r->value_stream = src->value_stream; + i_stream_ref(dest_r->value_stream); + } + + dest_r->deleted = src->deleted; + dest_r->last_change = src->last_change; + dest_r->modseq = src->modseq; +} + +int dsync_mailbox_lock(struct dsync_brain *brain, struct mailbox *box, + struct file_lock **lock_r) +{ + const char *path, *error; + int ret; + + /* Make sure the mailbox is open - locking requires it */ + if (mailbox_open(box) < 0) { + i_error("Can't open mailbox %s: %s", mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &brain->mail_error)); + return -1; + } + + ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_INDEX, &path); + if (ret < 0) { + i_error("Can't get mailbox %s path: %s", mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &brain->mail_error)); + return -1; + } + if (ret == 0) { + /* No index files - don't do any locking. In theory we still + could, but this lock is mainly meant to prevent replication + problems, and replication wouldn't work without indexes. */ + *lock_r = NULL; + return 0; + } + + if (mailbox_lock_file_create(box, DSYNC_MAILBOX_LOCK_FILENAME, + brain->mailbox_lock_timeout_secs, + lock_r, &error) <= 0) { + i_error("Failed to lock mailbox %s for dsyncing: %s", + box->vname, error); + return -1; + } + return 0; +} diff --git a/src/doveadm/dsync/dsync-mailbox.h b/src/doveadm/dsync/dsync-mailbox.h new file mode 100644 index 0000000..7e81c0c --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox.h @@ -0,0 +1,44 @@ +#ifndef DSYNC_MAILBOX_H +#define DSYNC_MAILBOX_H + +#include "mail-storage.h" + +struct dsync_brain; + +/* Mailbox that is going to be synced. Its name was already sent in the + mailbox tree. */ +struct dsync_mailbox { + guid_128_t mailbox_guid; + bool mailbox_lost; + bool mailbox_ignore; + bool have_guids, have_save_guids, have_only_guid128; + + uint32_t uid_validity, uid_next, messages_count, first_recent_uid; + uint64_t highest_modseq, highest_pvt_modseq; + ARRAY_TYPE(mailbox_cache_field) cache_fields; +}; + +struct dsync_mailbox_attribute { + enum mail_attribute_type type; + const char *key; + /* if both values are NULL = not looked up yet / deleted */ + const char *value; + struct istream *value_stream; + + time_t last_change; /* 0 = unknown */ + uint64_t modseq; /* 0 = unknown */ + + bool deleted; /* attribute is known to have been deleted */ + bool exported; /* internally used by exporting */ +}; +#define DSYNC_ATTR_HAS_VALUE(attr) \ + ((attr)->value != NULL || (attr)->value_stream != NULL) + +void dsync_mailbox_attribute_dup(pool_t pool, + const struct dsync_mailbox_attribute *src, + struct dsync_mailbox_attribute *dest_r); + +int dsync_mailbox_lock(struct dsync_brain *brain, struct mailbox *box, + struct file_lock **lock_r); + +#endif diff --git a/src/doveadm/dsync/dsync-serializer.c b/src/doveadm/dsync/dsync-serializer.c new file mode 100644 index 0000000..7b369b8 --- /dev/null +++ b/src/doveadm/dsync/dsync-serializer.c @@ -0,0 +1,117 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "str.h" +#include "strescape.h" +#include "dsync-serializer.h" + +struct dsync_serializer { + pool_t pool; + const char *const *keys; + unsigned int keys_count; +}; + +struct dsync_serializer_encoder { + pool_t pool; + struct dsync_serializer *serializer; + ARRAY_TYPE(const_string) values; +}; + +struct dsync_serializer *dsync_serializer_init(const char *const keys[]) +{ + struct dsync_serializer *serializer; + pool_t pool; + const char **dup_keys; + unsigned int i, count; + + pool = pool_alloconly_create("dsync serializer", 512); + serializer = p_new(pool, struct dsync_serializer, 1); + serializer->pool = pool; + + count = str_array_length(keys); + dup_keys = p_new(pool, const char *, count + 1); + for (i = 0; i < count; i++) + dup_keys[i] = p_strdup(pool, keys[i]); + serializer->keys = dup_keys; + serializer->keys_count = count; + return serializer; +} + +void dsync_serializer_deinit(struct dsync_serializer **_serializer) +{ + struct dsync_serializer *serializer = *_serializer; + + *_serializer = NULL; + + pool_unref(&serializer->pool); +} + +const char * +dsync_serializer_encode_header_line(struct dsync_serializer *serializer) +{ + string_t *str = t_str_new(128); + unsigned int i; + + for (i = 0; serializer->keys[i] != NULL; i++) { + if (i > 0) + str_append_c(str, '\t'); + str_append_tabescaped(str, serializer->keys[i]); + } + str_append_c(str, '\n'); + return str_c(str); +} + +struct dsync_serializer_encoder * +dsync_serializer_encode_begin(struct dsync_serializer *serializer) +{ + struct dsync_serializer_encoder *encoder; + pool_t pool = pool_alloconly_create("dsync serializer encode", 1024); + + encoder = p_new(pool, struct dsync_serializer_encoder, 1); + encoder->pool = pool; + encoder->serializer = serializer; + p_array_init(&encoder->values, pool, serializer->keys_count); + return encoder; +} + +void dsync_serializer_encode_add(struct dsync_serializer_encoder *encoder, + const char *key, const char *value) +{ + unsigned int i; + + for (i = 0; encoder->serializer->keys[i] != NULL; i++) { + if (strcmp(encoder->serializer->keys[i], key) == 0) { + value = p_strdup(encoder->pool, value); + array_idx_set(&encoder->values, i, &value); + return; + } + } + i_panic("Unknown key: %s", key); +} + +void dsync_serializer_encode_finish(struct dsync_serializer_encoder **_encoder, + string_t *output) +{ + struct dsync_serializer_encoder *encoder = *_encoder; + const char *const *values; + unsigned int i, count; + + *_encoder = NULL; + + values = array_get(&encoder->values, &count); + for (i = 0; i < count; i++) { + if (i > 0) + str_append_c(output, '\t'); + if (values[i] == NULL) + str_append_c(output, NULL_CHR); + else { + if (values[i][0] == NULL_CHR) + str_append_c(output, NULL_CHR); + str_append_tabescaped(output, values[i]); + } + } + str_append_c(output, '\n'); + + pool_unref(&encoder->pool); +} diff --git a/src/doveadm/dsync/dsync-serializer.h b/src/doveadm/dsync/dsync-serializer.h new file mode 100644 index 0000000..1ed6c31 --- /dev/null +++ b/src/doveadm/dsync/dsync-serializer.h @@ -0,0 +1,18 @@ +#ifndef DSYNC_SERIALIZER_H +#define DSYNC_SERIALIZER_H + +#define NULL_CHR '\002' + +struct dsync_serializer *dsync_serializer_init(const char *const keys[]); +void dsync_serializer_deinit(struct dsync_serializer **serializer); + +const char * +dsync_serializer_encode_header_line(struct dsync_serializer *serializer); +struct dsync_serializer_encoder * +dsync_serializer_encode_begin(struct dsync_serializer *serializer); +void dsync_serializer_encode_add(struct dsync_serializer_encoder *encoder, + const char *key, const char *value); +void dsync_serializer_encode_finish(struct dsync_serializer_encoder **encoder, + string_t *output); + +#endif diff --git a/src/doveadm/dsync/dsync-transaction-log-scan.c b/src/doveadm/dsync/dsync-transaction-log-scan.c new file mode 100644 index 0000000..dd2834a --- /dev/null +++ b/src/doveadm/dsync/dsync-transaction-log-scan.c @@ -0,0 +1,608 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "hash.h" +#include "mail-index-modseq.h" +#include "mail-storage-private.h" +#include "dsync-mail.h" +#include "dsync-mailbox.h" +#include "dsync-transaction-log-scan.h" + +struct dsync_transaction_log_scan { + pool_t pool; + HASH_TABLE_TYPE(dsync_uid_mail_change) changes; + HASH_TABLE_TYPE(dsync_attr_change) attr_changes; + struct mail_index_view *view; + uint32_t highest_wanted_uid; + + uint32_t last_log_seq; + uoff_t last_log_offset; + + bool returned_all_changes; +}; + +static bool ATTR_NOWARN_UNUSED_RESULT +export_change_get(struct dsync_transaction_log_scan *ctx, uint32_t uid, + enum dsync_mail_change_type type, + struct dsync_mail_change **change_r) +{ + struct dsync_mail_change *change; + const char *orig_guid; + + i_assert(uid > 0); + i_assert(type != DSYNC_MAIL_CHANGE_TYPE_SAVE); + + *change_r = NULL; + + if (uid > ctx->highest_wanted_uid) + return FALSE; + + change = hash_table_lookup(ctx->changes, POINTER_CAST(uid)); + if (change == NULL) { + /* first change for this UID */ + change = p_new(ctx->pool, struct dsync_mail_change, 1); + change->uid = uid; + change->type = type; + hash_table_insert(ctx->changes, POINTER_CAST(uid), change); + } else if (type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + /* expunge overrides flag changes */ + orig_guid = change->guid; + i_zero(change); + change->type = type; + change->uid = uid; + change->guid = orig_guid; + } else if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + /* already expunged, this change doesn't matter */ + return FALSE; + } else { + /* another flag update */ + } + *change_r = change; + return TRUE; +} + +static void +log_add_expunge(struct dsync_transaction_log_scan *ctx, const void *data, + const struct mail_transaction_header *hdr) +{ + const struct mail_transaction_expunge *rec = data, *end; + struct dsync_mail_change *change; + uint32_t uid; + + if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) { + /* this is simply a request for expunge */ + return; + } + end = CONST_PTR_OFFSET(data, hdr->size); + for (; rec != end; rec++) { + for (uid = rec->uid1; uid <= rec->uid2; uid++) { + export_change_get(ctx, uid, + DSYNC_MAIL_CHANGE_TYPE_EXPUNGE, + &change); + } + } +} + +static bool +log_add_expunge_uid(struct dsync_transaction_log_scan *ctx, const void *data, + const struct mail_transaction_header *hdr, uint32_t uid) +{ + const struct mail_transaction_expunge *rec = data, *end; + struct dsync_mail_change *change; + + if ((hdr->type & MAIL_TRANSACTION_EXTERNAL) == 0) { + /* this is simply a request for expunge */ + return FALSE; + } + end = CONST_PTR_OFFSET(data, hdr->size); + for (; rec != end; rec++) { + if (uid >= rec->uid1 && uid <= rec->uid2) { + export_change_get(ctx, uid, + DSYNC_MAIL_CHANGE_TYPE_EXPUNGE, + &change); + return TRUE; + } + } + return FALSE; +} + +static void +log_add_expunge_guid(struct dsync_transaction_log_scan *ctx, + struct mail_index_view *view, const void *data, + const struct mail_transaction_header *hdr) +{ + const struct mail_transaction_expunge_guid *rec = data, *end; + struct dsync_mail_change *change; + uint32_t seq; + bool external; + + external = (hdr->type & MAIL_TRANSACTION_EXTERNAL) != 0; + + end = CONST_PTR_OFFSET(data, hdr->size); + for (; rec != end; rec++) { + if (!external && mail_index_lookup_seq(view, rec->uid, &seq)) { + /* expunge request that hasn't been actually done yet. + we check non-external ones because they might have + the GUID while external ones don't. */ + continue; + } + if (export_change_get(ctx, rec->uid, + DSYNC_MAIL_CHANGE_TYPE_EXPUNGE, + &change) && + !guid_128_is_empty(rec->guid_128)) T_BEGIN { + change->guid = p_strdup(ctx->pool, + guid_128_to_string(rec->guid_128)); + } T_END; + } +} + +static bool +log_add_expunge_guid_uid(struct dsync_transaction_log_scan *ctx, const void *data, + const struct mail_transaction_header *hdr, uint32_t uid) +{ + const struct mail_transaction_expunge_guid *rec = data, *end; + struct dsync_mail_change *change; + + /* we're assuming UID is already known to be expunged */ + end = CONST_PTR_OFFSET(data, hdr->size); + for (; rec != end; rec++) { + if (rec->uid != uid) + continue; + + if (!export_change_get(ctx, rec->uid, + DSYNC_MAIL_CHANGE_TYPE_EXPUNGE, + &change)) + i_unreached(); + if (!guid_128_is_empty(rec->guid_128)) T_BEGIN { + change->guid = p_strdup(ctx->pool, + guid_128_to_string(rec->guid_128)); + } T_END; + return TRUE; + } + return FALSE; +} + +static void +log_add_flag_update(struct dsync_transaction_log_scan *ctx, const void *data, + const struct mail_transaction_header *hdr) +{ + const struct mail_transaction_flag_update *rec = data, *end; + struct dsync_mail_change *change; + uint32_t uid; + + end = CONST_PTR_OFFSET(data, hdr->size); + for (; rec != end; rec++) { + for (uid = rec->uid1; uid <= rec->uid2; uid++) { + if (export_change_get(ctx, uid, + DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE, + &change)) { + change->add_flags |= rec->add_flags; + change->remove_flags &= ENUM_NEGATE(rec->add_flags); + change->remove_flags |= rec->remove_flags; + change->add_flags &= ENUM_NEGATE(rec->remove_flags); + } + } + } +} + +static void +log_add_keyword_reset(struct dsync_transaction_log_scan *ctx, const void *data, + const struct mail_transaction_header *hdr) +{ + const struct mail_transaction_keyword_reset *rec = data, *end; + struct dsync_mail_change *change; + uint32_t uid; + + end = CONST_PTR_OFFSET(data, hdr->size); + for (; rec != end; rec++) { + for (uid = rec->uid1; uid <= rec->uid2; uid++) { + if (!export_change_get(ctx, uid, + DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE, + &change)) + continue; + + change->keywords_reset = TRUE; + if (array_is_created(&change->keyword_changes)) + array_clear(&change->keyword_changes); + } + } +} + +static void +keywords_change_remove(struct dsync_mail_change *change, const char *name) +{ + const char *const *changes; + unsigned int i, count; + + changes = array_get(&change->keyword_changes, &count); + for (i = 0; i < count; i++) { + if (strcmp(changes[i]+1, name) == 0) { + array_delete(&change->keyword_changes, i, 1); + break; + } + } +} + +static void +log_add_keyword_update(struct dsync_transaction_log_scan *ctx, const void *data, + const struct mail_transaction_header *hdr) +{ + const struct mail_transaction_keyword_update *rec = data; + struct dsync_mail_change *change; + const char *kw_name, *change_str; + const uint32_t *uids, *end; + unsigned int uids_offset; + uint32_t uid; + + uids_offset = sizeof(*rec) + rec->name_size; + if ((uids_offset % 4) != 0) + uids_offset += 4 - (uids_offset % 4); + + kw_name = t_strndup((const void *)(rec+1), rec->name_size); + switch (rec->modify_type) { + case MODIFY_ADD: + change_str = p_strdup_printf(ctx->pool, "%c%s", + KEYWORD_CHANGE_ADD, kw_name); + break; + case MODIFY_REMOVE: + change_str = p_strdup_printf(ctx->pool, "%c%s", + KEYWORD_CHANGE_REMOVE, kw_name); + break; + default: + i_unreached(); + } + + uids = CONST_PTR_OFFSET(rec, uids_offset); + end = CONST_PTR_OFFSET(rec, hdr->size); + + for (; uids < end; uids += 2) { + for (uid = uids[0]; uid <= uids[1]; uid++) { + if (!export_change_get(ctx, uid, + DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE, + &change)) + continue; + if (!array_is_created(&change->keyword_changes)) { + p_array_init(&change->keyword_changes, + ctx->pool, 4); + } else { + keywords_change_remove(change, kw_name); + } + array_push_back(&change->keyword_changes, &change_str); + } + } +} + +static void +log_add_modseq_update(struct dsync_transaction_log_scan *ctx, const void *data, + const struct mail_transaction_header *hdr, bool pvt_scan) +{ + const struct mail_transaction_modseq_update *rec = data, *end; + struct dsync_mail_change *change; + uint64_t modseq; + + /* update message's modseq, possibly by creating an empty flag change */ + end = CONST_PTR_OFFSET(rec, hdr->size); + for (; rec != end; rec++) { + if (rec->uid == 0) { + /* highestmodseq update */ + continue; + } + + if (!export_change_get(ctx, rec->uid, + DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE, + &change)) + continue; + + modseq = rec->modseq_low32 | + ((uint64_t)rec->modseq_high32 << 32); + if (!pvt_scan) { + if (change->modseq < modseq) + change->modseq = modseq; + } else { + if (change->pvt_modseq < modseq) + change->pvt_modseq = modseq; + } + } +} + +static void +log_add_attribute_update_key(struct dsync_transaction_log_scan *ctx, + const char *attr_change, uint64_t modseq) +{ + struct dsync_mailbox_attribute lookup_attr, *attr; + + i_assert(strlen(attr_change) > 2); /* checked by lib-index */ + + lookup_attr.type = attr_change[1] == 'p' ? + MAIL_ATTRIBUTE_TYPE_PRIVATE : MAIL_ATTRIBUTE_TYPE_SHARED; + lookup_attr.key = attr_change+2; + + attr = hash_table_lookup(ctx->attr_changes, &lookup_attr); + if (attr == NULL) { + attr = p_new(ctx->pool, struct dsync_mailbox_attribute, 1); + attr->type = lookup_attr.type; + attr->key = p_strdup(ctx->pool, lookup_attr.key); + hash_table_insert(ctx->attr_changes, attr, attr); + } + attr->deleted = attr_change[0] == '-'; + attr->modseq = modseq; +} + +static void +log_add_attribute_update(struct dsync_transaction_log_scan *ctx, + const void *data, + const struct mail_transaction_header *hdr, + uint64_t modseq) +{ + const char *attr_changes = data; + unsigned int i; + + for (i = 0; i < hdr->size && attr_changes[i] != '\0'; ) { + log_add_attribute_update_key(ctx, attr_changes+i, modseq); + i += strlen(attr_changes+i) + 1; + } +} + +static int +dsync_log_set(struct dsync_transaction_log_scan *ctx, + struct mail_index_view *view, bool pvt_scan, + struct mail_transaction_log_view *log_view, uint64_t modseq) +{ + uint32_t log_seq, end_seq; + uoff_t log_offset, end_offset; + const char *reason; + bool reset; + int ret; + + end_seq = view->log_file_head_seq; + end_offset = view->log_file_head_offset; + + if (modseq != 0 && + mail_index_modseq_get_next_log_offset(view, modseq, + &log_seq, &log_offset)) { + /* scan the view only up to end of the current view. + if there are more changes, we don't care about them until + the next sync. */ + ret = mail_transaction_log_view_set(log_view, + log_seq, log_offset, + end_seq, end_offset, + &reset, &reason); + if (ret != 0) + return ret; + } + + /* return everything we've got (until the end of the view) */ + if (!pvt_scan) + ctx->returned_all_changes = TRUE; + if (mail_transaction_log_view_set_all(log_view) < 0) + return -1; + + mail_transaction_log_view_get_prev_pos(log_view, &log_seq, &log_offset); + if (log_seq > end_seq || + (log_seq == end_seq && log_offset > end_offset)) { + end_seq = log_seq; + end_offset = log_offset; + } + ret = mail_transaction_log_view_set(log_view, + log_seq, log_offset, + end_seq, end_offset, + &reset, &reason); + if (ret == 0) { + /* we shouldn't get here. _view_set_all() already + reserved all the log files, the _view_set() only + removed unwanted ones. */ + i_error("%s: Couldn't set transaction log view (seq %u..%u): %s", + view->index->filepath, log_seq, end_seq, reason); + ret = -1; + } + if (ret < 0) + return -1; + if (modseq != 0) { + /* we didn't see all the changes that we wanted to */ + return 0; + } + return 1; +} + +static int +dsync_log_scan(struct dsync_transaction_log_scan *ctx, + struct mail_index_view *view, uint64_t modseq, bool pvt_scan) +{ + struct mail_transaction_log_view *log_view; + const struct mail_transaction_header *hdr; + const void *data; + uint32_t file_seq, max_seq; + uoff_t file_offset, max_offset; + uint64_t cur_modseq; + int ret; + + log_view = mail_transaction_log_view_open(view->index->log); + if ((ret = dsync_log_set(ctx, view, pvt_scan, log_view, modseq)) < 0) { + mail_transaction_log_view_close(&log_view); + return -1; + } + + /* read the log only up to current position in view */ + max_seq = view->log_file_expunge_seq; + max_offset = view->log_file_expunge_offset; + + mail_transaction_log_view_get_prev_pos(log_view, &file_seq, + &file_offset); + + while (mail_transaction_log_view_next(log_view, &hdr, &data) > 0) { + mail_transaction_log_view_get_prev_pos(log_view, &file_seq, + &file_offset); + if (file_offset >= max_offset && file_seq == max_seq) + break; + + if ((hdr->type & MAIL_TRANSACTION_SYNC) != 0) { + /* ignore changes done by dsync, unless we can get + expunged message's GUID from it */ + if ((hdr->type & MAIL_TRANSACTION_TYPE_MASK) != + MAIL_TRANSACTION_EXPUNGE_GUID) + continue; + } + + switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) { + case MAIL_TRANSACTION_EXPUNGE: + if (!pvt_scan) + log_add_expunge(ctx, data, hdr); + break; + case MAIL_TRANSACTION_EXPUNGE_GUID: + if (!pvt_scan) + log_add_expunge_guid(ctx, view, data, hdr); + break; + case MAIL_TRANSACTION_FLAG_UPDATE: + log_add_flag_update(ctx, data, hdr); + break; + case MAIL_TRANSACTION_KEYWORD_RESET: + log_add_keyword_reset(ctx, data, hdr); + break; + case MAIL_TRANSACTION_KEYWORD_UPDATE: + T_BEGIN { + log_add_keyword_update(ctx, data, hdr); + } T_END; + break; + case MAIL_TRANSACTION_MODSEQ_UPDATE: + log_add_modseq_update(ctx, data, hdr, pvt_scan); + break; + case MAIL_TRANSACTION_ATTRIBUTE_UPDATE: + cur_modseq = mail_transaction_log_view_get_prev_modseq(log_view); + log_add_attribute_update(ctx, data, hdr, cur_modseq); + break; + } + } + + if (!pvt_scan) { + ctx->last_log_seq = file_seq; + ctx->last_log_offset = file_offset; + } + mail_transaction_log_view_close(&log_view); + return ret; +} + +static int +dsync_mailbox_attribute_cmp(const struct dsync_mailbox_attribute *attr1, + const struct dsync_mailbox_attribute *attr2) +{ + if (attr1->type < attr2->type) + return -1; + if (attr1->type > attr2->type) + return 1; + return strcmp(attr1->key, attr2->key); +} + +static unsigned int +dsync_mailbox_attribute_hash(const struct dsync_mailbox_attribute *attr) +{ + return str_hash(attr->key) ^ attr->type; +} + +int dsync_transaction_log_scan_init(struct mail_index_view *view, + struct mail_index_view *pvt_view, + uint32_t highest_wanted_uid, + uint64_t modseq, uint64_t pvt_modseq, + struct dsync_transaction_log_scan **scan_r, + bool *pvt_too_old_r) +{ + struct dsync_transaction_log_scan *ctx; + pool_t pool; + int ret, ret2; + + *pvt_too_old_r = FALSE; + + pool = pool_alloconly_create(MEMPOOL_GROWING"dsync transaction log scan", + 10240); + ctx = p_new(pool, struct dsync_transaction_log_scan, 1); + ctx->pool = pool; + hash_table_create_direct(&ctx->changes, pool, 0); + hash_table_create(&ctx->attr_changes, pool, 0, + dsync_mailbox_attribute_hash, + dsync_mailbox_attribute_cmp); + ctx->view = view; + ctx->highest_wanted_uid = highest_wanted_uid; + + if ((ret = dsync_log_scan(ctx, view, modseq, FALSE)) < 0) + return -1; + if (pvt_view != NULL) { + if ((ret2 = dsync_log_scan(ctx, pvt_view, pvt_modseq, TRUE)) < 0) + return -1; + if (ret2 == 0) { + ret = 0; + *pvt_too_old_r = TRUE; + } + } + + *scan_r = ctx; + return ret; +} + +HASH_TABLE_TYPE(dsync_uid_mail_change) +dsync_transaction_log_scan_get_hash(struct dsync_transaction_log_scan *scan) +{ + return scan->changes; +} + +HASH_TABLE_TYPE(dsync_attr_change) +dsync_transaction_log_scan_get_attr_hash(struct dsync_transaction_log_scan *scan) +{ + return scan->attr_changes; +} + +bool +dsync_transaction_log_scan_has_all_changes(struct dsync_transaction_log_scan *scan) +{ + return scan->returned_all_changes; +} + +struct dsync_mail_change * +dsync_transaction_log_scan_find_new_expunge(struct dsync_transaction_log_scan *scan, + uint32_t uid) +{ + struct mail_transaction_log_view *log_view; + const struct mail_transaction_header *hdr; + const void *data; + const char *reason; + bool reset, found = FALSE; + + i_assert(uid > 0); + + if (scan->highest_wanted_uid < uid) + scan->highest_wanted_uid = uid; + + log_view = mail_transaction_log_view_open(scan->view->index->log); + if (mail_transaction_log_view_set(log_view, + scan->last_log_seq, + scan->last_log_offset, + (uint32_t)-1, UOFF_T_MAX, + &reset, &reason) > 0) { + while (!found && + mail_transaction_log_view_next(log_view, &hdr, &data) > 0) { + switch (hdr->type & MAIL_TRANSACTION_TYPE_MASK) { + case MAIL_TRANSACTION_EXPUNGE: + if (log_add_expunge_uid(scan, data, hdr, uid)) + found = TRUE; + break; + case MAIL_TRANSACTION_EXPUNGE_GUID: + if (log_add_expunge_guid_uid(scan, data, hdr, uid)) + found = TRUE; + break; + } + } + } + mail_transaction_log_view_close(&log_view); + + return !found ? NULL : + hash_table_lookup(scan->changes, POINTER_CAST(uid)); +} + +void dsync_transaction_log_scan_deinit(struct dsync_transaction_log_scan **_scan) +{ + struct dsync_transaction_log_scan *scan = *_scan; + + *_scan = NULL; + + hash_table_destroy(&scan->changes); + hash_table_destroy(&scan->attr_changes); + pool_unref(&scan->pool); +} diff --git a/src/doveadm/dsync/dsync-transaction-log-scan.h b/src/doveadm/dsync/dsync-transaction-log-scan.h new file mode 100644 index 0000000..458b775 --- /dev/null +++ b/src/doveadm/dsync/dsync-transaction-log-scan.h @@ -0,0 +1,32 @@ +#ifndef DSYNC_TRANSACTION_LOG_SCAN_H +#define DSYNC_TRANSACTION_LOG_SCAN_H + +HASH_TABLE_DEFINE_TYPE(dsync_uid_mail_change, + void *, struct dsync_mail_change *); +HASH_TABLE_DEFINE_TYPE(dsync_attr_change, + struct dsync_mailbox_attribute *, + struct dsync_mailbox_attribute *); + +struct mail_index_view; +struct dsync_transaction_log_scan; + +int dsync_transaction_log_scan_init(struct mail_index_view *view, + struct mail_index_view *pvt_view, + uint32_t highest_wanted_uid, + uint64_t modseq, uint64_t pvt_modseq, + struct dsync_transaction_log_scan **scan_r, + bool *pvt_too_old_r); +HASH_TABLE_TYPE(dsync_uid_mail_change) +dsync_transaction_log_scan_get_hash(struct dsync_transaction_log_scan *scan); +HASH_TABLE_TYPE(dsync_attr_change) +dsync_transaction_log_scan_get_attr_hash(struct dsync_transaction_log_scan *scan); +/* Returns TRUE if the entire transaction log was scanned */ +bool dsync_transaction_log_scan_has_all_changes(struct dsync_transaction_log_scan *scan); +/* If the given UID has been expunged after the initial log scan, create/update + a change record for it and return it. */ +struct dsync_mail_change * +dsync_transaction_log_scan_find_new_expunge(struct dsync_transaction_log_scan *scan, + uint32_t uid); +void dsync_transaction_log_scan_deinit(struct dsync_transaction_log_scan **scan); + +#endif diff --git a/src/doveadm/dsync/test-dsync-mailbox-tree-sync.c b/src/doveadm/dsync/test-dsync-mailbox-tree-sync.c new file mode 100644 index 0000000..b068137 --- /dev/null +++ b/src/doveadm/dsync/test-dsync-mailbox-tree-sync.c @@ -0,0 +1,781 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "sha1.h" +#include "str.h" +#include "mailbox-list-private.h" +#include "dsync-mailbox-tree-private.h" +#include "test-common.h" + +#include <stdio.h> + +#define MAX_DEPTH 4 +#define TEST_NAMESPACE_NAME "INBOX" + +static struct mail_namespace inbox_namespace = { + .prefix = TEST_NAMESPACE_NAME"/", + .prefix_len = sizeof(TEST_NAMESPACE_NAME)-1 + 1 +}; + +char mail_namespace_get_sep(struct mail_namespace *ns ATTR_UNUSED) +{ + return '/'; +} + +void mailbox_name_get_sha128(const char *name, guid_128_t guid_128_r) +{ + unsigned char sha[SHA1_RESULTLEN]; + + sha1_get_digest(name, strlen(name), sha); + memcpy(guid_128_r, sha, I_MIN(GUID_128_SIZE, sizeof(sha))); +} + +void mailbox_list_name_unescape(const char **name ATTR_UNUSED, + char escape_char ATTR_UNUSED) +{ +} + +void mailbox_list_name_escape(const char *name, + const char *escape_chars ATTR_UNUSED, + string_t *dest) +{ + str_append(dest, name); +} + +static struct dsync_mailbox_node * +node_create(struct dsync_mailbox_tree *tree, unsigned int counter, + const char *name, unsigned int last_renamed_or_created) +{ + struct dsync_mailbox_node *node; + + node = dsync_mailbox_tree_get(tree, name); + memcpy(node->mailbox_guid, &counter, sizeof(counter)); + node->uid_validity = counter; + node->existence = DSYNC_MAILBOX_NODE_EXISTS; + node->last_renamed_or_created = last_renamed_or_created; + return node; +} + +static struct dsync_mailbox_node * +random_node_create(struct dsync_mailbox_tree *tree, unsigned int counter, + const char *name) +{ + return node_create(tree, counter, name, i_rand_limit(10)); +} + +static void nodes_create(struct dsync_mailbox_tree *tree, unsigned int *counter, + const char *const *names) +{ + for (; *names != NULL; names++) { + *counter += 1; + node_create(tree, *counter, *names, 0); + } +} + +static void nodes_delete(struct dsync_mailbox_tree *tree, unsigned int *counter, + const char *const *names) +{ + struct dsync_mailbox_node *node; + + for (; *names != NULL; names++) { + *counter += 1; + node = node_create(tree, *counter, *names, 0); + node->existence = DSYNC_MAILBOX_NODE_DELETED; + } +} + +static void +create_random_nodes(struct dsync_mailbox_tree *tree, const char *parent_name, + unsigned int depth, unsigned int *counter) +{ + unsigned int parent_len, i, nodes_count = i_rand_minmax(1, 3); + string_t *str; + + if (depth == MAX_DEPTH) + return; + + str = t_str_new(32); + if (*parent_name != '\0') + str_printfa(str, "%s/", parent_name); + parent_len = str_len(str); + + for (i = 0; i < nodes_count; i++) { + *counter += 1; + str_truncate(str, parent_len); + str_printfa(str, "%u.%u", depth, i); + random_node_create(tree, *counter, str_c(str)); + create_random_nodes(tree, str_c(str), depth+1, counter); + } +} + +static struct dsync_mailbox_tree *create_random_tree(void) +{ + struct dsync_mailbox_tree *tree; + unsigned int counter = 0; + + tree = dsync_mailbox_tree_init('/', '\0', '_'); + create_random_nodes(tree, "", 0, &counter); + return tree; +} + +static void test_tree_nodes_fixup(struct dsync_mailbox_node **pos, + unsigned int *newguid_counter) +{ + struct dsync_mailbox_node *node; + + for (node = *pos; node != NULL; node = node->next) { + if (node->sync_delayed_guid_change) { + /* the real code will pick one of the GUIDs. + we don't really care which one gets picked, so we'll + just change them to the same new one */ + memcpy(node->mailbox_guid, newguid_counter, + sizeof(*newguid_counter)); + node->uid_validity = *newguid_counter; + *newguid_counter += 1; + } + if (node->existence == DSYNC_MAILBOX_NODE_DELETED) + node->existence = DSYNC_MAILBOX_NODE_NONEXISTENT; + test_tree_nodes_fixup(&node->first_child, newguid_counter); + if (node->existence != DSYNC_MAILBOX_NODE_EXISTS && + node->first_child == NULL) { + /* nonexistent node, drop it */ + *pos = node->next; + } else { + pos = &node->next; + } + } +} + +static void test_tree_fixup(struct dsync_mailbox_tree *tree) +{ + unsigned int newguid_counter = INT_MAX; + + test_tree_nodes_fixup(&tree->root.first_child, &newguid_counter); +} + +static void nodes_dump(const struct dsync_mailbox_node *node, unsigned int depth) +{ + unsigned int i; + + for (; node != NULL; node = node->next) { + for (i = 0; i < depth; i++) printf(" "); + printf("%-*s guid:%.5s uidv:%u %d%d %ld\n", 40-depth, node->name, + guid_128_to_string(node->mailbox_guid), node->uid_validity, + node->existence, node->subscribed ? 1 : 0, + (long)node->last_renamed_or_created); + nodes_dump(node->first_child, depth+1); + } +} + +static void trees_dump(struct dsync_mailbox_tree *tree1, + struct dsync_mailbox_tree *tree2) +{ + printf("tree1:\n"); + nodes_dump(tree1->root.first_child, 1); + printf("tree2:\n"); + nodes_dump(tree2->root.first_child, 1); +} + +static void test_trees_nofree(struct dsync_mailbox_tree *tree1, + struct dsync_mailbox_tree **_tree2) +{ + struct dsync_mailbox_tree *tree2 = *_tree2; + struct dsync_mailbox_tree *orig_tree1, *orig_tree2; + struct dsync_mailbox_tree_sync_ctx *ctx; + struct dsync_mailbox_node *dup_node1, *dup_node2; + + orig_tree1 = dsync_mailbox_tree_dup(tree1); + orig_tree2 = dsync_mailbox_tree_dup(tree2); + + /* test tree1 -> tree2 */ + dsync_mailbox_tree_build_guid_hash(tree1, &dup_node1, &dup_node2); + dsync_mailbox_tree_build_guid_hash(tree2, &dup_node1, &dup_node2); + ctx = dsync_mailbox_trees_sync_init(tree1, tree2, + DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY, + DSYNC_MAILBOX_TREES_SYNC_FLAG_DEBUG); + while (dsync_mailbox_trees_sync_next(ctx) != NULL) { + } + dsync_mailbox_trees_sync_deinit(&ctx); + test_tree_fixup(tree1); + test_tree_fixup(tree2); + if (!dsync_mailbox_trees_equal(tree1, tree2)) { + test_assert(FALSE); + trees_dump(tree1, tree2); + } + + /* test tree2 -> tree1 */ + dsync_mailbox_tree_build_guid_hash(orig_tree1, &dup_node1, &dup_node2); + dsync_mailbox_tree_build_guid_hash(orig_tree2, &dup_node1, &dup_node2); + ctx = dsync_mailbox_trees_sync_init(orig_tree2, orig_tree1, + DSYNC_MAILBOX_TREES_SYNC_TYPE_TWOWAY, 0); + while (dsync_mailbox_trees_sync_next(ctx) != NULL) { + } + dsync_mailbox_trees_sync_deinit(&ctx); + test_tree_fixup(orig_tree1); + test_tree_fixup(orig_tree2); + if (!dsync_mailbox_trees_equal(orig_tree1, orig_tree2)) { + test_assert(FALSE); + trees_dump(orig_tree1, orig_tree2); + } + + /* make sure both directions produced equal trees */ + if (!dsync_mailbox_trees_equal(tree1, orig_tree1)) { + test_assert(FALSE); + trees_dump(tree1, orig_tree1); + } + + dsync_mailbox_tree_deinit(_tree2); + dsync_mailbox_tree_deinit(&orig_tree1); + dsync_mailbox_tree_deinit(&orig_tree2); +} + +static void +test_tree_nodes_add_namespace(struct dsync_mailbox_node *node, + struct mail_namespace *ns) +{ + for (; node != NULL; node = node->next) { + node->ns = ns; + test_tree_nodes_add_namespace(node->first_child, ns); + } +} + +static void +test_tree_add_namespace(struct dsync_mailbox_tree *tree, + struct mail_namespace *ns) +{ + struct dsync_mailbox_node *node, *n; + + node = dsync_mailbox_tree_get(tree, TEST_NAMESPACE_NAME); + node->existence = DSYNC_MAILBOX_NODE_EXISTS; + i_assert(tree->root.first_child == node); + i_assert(node->first_child == NULL); + node->first_child = node->next; + for (n = node->first_child; n != NULL; n = n->next) + n->parent = node; + node->next = NULL; + + test_tree_nodes_add_namespace(&tree->root, ns); +} + +static void test_trees(struct dsync_mailbox_tree *tree1, + struct dsync_mailbox_tree *tree2) +{ + struct dsync_mailbox_tree *tree1_dup, *tree2_dup; + + tree1_dup = dsync_mailbox_tree_dup(tree1); + tree2_dup = dsync_mailbox_tree_dup(tree2); + + /* test without namespace prefix */ + test_trees_nofree(tree1, &tree2); + dsync_mailbox_tree_deinit(&tree1); + + /* test with namespace prefix */ + test_tree_add_namespace(tree1_dup, &inbox_namespace); + test_tree_add_namespace(tree2_dup, &inbox_namespace); + test_trees_nofree(tree1_dup, &tree2_dup); + dsync_mailbox_tree_deinit(&tree1_dup); +} + +static void test_dsync_mailbox_tree_sync_creates(void) +{ + static const char *common_nodes[] = { "foo", "foo/bar", NULL }; + static const char *create1_nodes[] = { "bar", "foo/baz", NULL }; + static const char *create2_nodes[] = { "foo/xyz", "foo/bar/3", NULL }; + struct dsync_mailbox_tree *tree1, *tree2; + unsigned int counter = 0; + + test_begin("dsync mailbox tree sync creates"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + nodes_create(tree1, &counter, common_nodes); + tree2 = dsync_mailbox_tree_dup(tree1); + nodes_create(tree1, &counter, create1_nodes); + nodes_create(tree2, &counter, create2_nodes); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_deletes(void) +{ + static const char *common_nodes[] = { "1", "2", "3", "2/s1", "2/s2", "x/y", NULL }; + static const char *delete1_nodes[] = { "1", "2", NULL }; + static const char *delete2_nodes[] = { "2/s1", "x/y", NULL }; + struct dsync_mailbox_tree *tree1, *tree2; + unsigned int counter = 0; + + test_begin("dsync mailbox tree sync deletes"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + nodes_create(tree1, &counter, common_nodes); + tree2 = dsync_mailbox_tree_dup(tree1); + nodes_delete(tree1, &counter, delete1_nodes); + nodes_delete(tree2, &counter, delete2_nodes); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames1(void) +{ + static const char *common_nodes[] = { "1", "2", "3", "2/s1", "2/s2", "x/y", "3/s3", NULL }; + struct dsync_mailbox_tree *tree1, *tree2; + struct dsync_mailbox_node *node; + unsigned int counter = 0; + + test_begin("dsync mailbox tree sync renames 1"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + nodes_create(tree1, &counter, common_nodes); + tree2 = dsync_mailbox_tree_dup(tree1); + + node = dsync_mailbox_tree_get(tree1, "1"); + node->name = "a"; + node->last_renamed_or_created = 1000; + node = dsync_mailbox_tree_get(tree2, "2"); + node->name = "b"; + node->last_renamed_or_created = 1000; + + node = dsync_mailbox_tree_get(tree1, "3/s3"); + node->name = "z"; + node->last_renamed_or_created = 1000; + dsync_mailbox_tree_node_detach(node); + dsync_mailbox_tree_node_attach(node, &tree1->root); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames2(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 2"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/1", 1); + node_create(tree1, 2, "0/1/2", 3); + + node_create(tree2, 1, "0", 0); + node_create(tree2, 2, "0/1/2", 0); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames3(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 3"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/2", 1); + node_create(tree1, 2, "0/3", 1); + + node_create(tree2, 1, "0/4/5", 0); + node_create(tree2, 2, "1", 0); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames4(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 4"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/b", 0); + node_create(tree1, 2, "c", 2); + + node_create(tree2, 2, "0/a", 0); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames5(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 5"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "b", 0); + node_create(tree1, 2, "c", 2); + + node_create(tree2, 2, "0/a", 0); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames6(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 6"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/1", 0); + node_create(tree1, 2, "0/2", 1); + + node_create(tree2, 1, "0", 1); + node_create(tree2, 2, "0/3", 0); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames7(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 7"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/2", 0); + node_create(tree2, 1, "1/2", 0); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames8(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 8"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/1", 0); + node_create(tree1, 2, "0/2", 1); + + node_create(tree2, 1, "0", 1); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames9(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 9"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/1/2", 0); + node_create(tree1, 2, "0/3", 1); + + node_create(tree2, 1, "0", 1); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames10(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 10"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/1", 0); + node_create(tree1, 3, "0/2/3", 0); + + node_create(tree2, 1, "0", 1); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames11(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 11"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/1", 2); + node_create(tree1, 0, "0/1/2", 0); + + node_create(tree2, 1, "0", 1); + node_create(tree2, 0, "0/1/2", 0); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames12(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 12"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/2", 0); + node_create(tree1, 2, "1", 0); + node_create(tree1, 3, "1/4", 0); + node_create(tree1, 4, "1/4/5", 1); + + node_create(tree2, 1, "1", 2); + node_create(tree2, 2, "1/4", 3); + node_create(tree2, 3, "1/4/6", 4); + node_create(tree2, 4, "1/3", 0); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames13(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 13"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 4, "0.0/1.0/2.1", 0); + node_create(tree1, 5, "0.1", 2); + node_create(tree1, 6, "0.1/1.0", 2); + node_create(tree1, 7, "0.1/1.0/2.0", 8); + + node_create(tree2, 5, "0.1/1.0", 5); + node_create(tree2, 6, "0.1/1.0/2.0", 8); + node_create(tree2, 7, "0.1/1.1", 1); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames14(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 14"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "1", 0); + node_create(tree1, 2, "1/2", 0); + node_create(tree1, 3, "1/2/4", 1); + + node_create(tree2, 1, "1/2", 3); + node_create(tree2, 2, "1/2/5", 4); + node_create(tree2, 3, "1/2/4", 0); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames15(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 15"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "1", 0); + node_create(tree2, 2, "1", 1); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames16(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 16"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "1/2", 4); + node_create(tree1, 2, "1", 2); + + node_create(tree2, 1, "2", 1); + node_create(tree2, 2, "1/2", 3); + node_create(tree2, 3, "1", 5); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames17(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 17"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "1", 1); + + node_create(tree2, 1, "1/2", 0); + node_create(tree2, 2, "1", 2); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames18(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 18"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 2, "a", 5); + node_create(tree1, 4, "a/c", 2); + node_create(tree1, 5, "b", 6); + + node_create(tree2, 1, "a", 7); + node_create(tree2, 2, "b", 3); + node_create(tree2, 3, "b/c", 4); + node_create(tree2, 4, "d", 1); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames19(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 19"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "0/2/1", 1); + node_create(tree1, 2, "0/4", 3); + node_create(tree1, 3, "0/2", 2); + + node_create(tree2, 1, "1", 0); + node_create(tree2, 2, "1/3", 4); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames20(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 20"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "1", 0); + node_create(tree1, 2, "0", 0); + node_create(tree1, 3, "0/2", 0); + /* rename 0 -> 1/0 */ + node_create(tree2, 1, "1", 0); + node_create(tree2, 2, "1/0", 1); + node_create(tree2, 3, "1/0/2", 0); + + test_trees_nofree(tree1, &tree2); + test_assert(tree1->root.first_child != NULL && + tree1->root.first_child->next == NULL); + dsync_mailbox_tree_deinit(&tree1); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_renames21(void) +{ +#if 0 + /* FIXME: we can't currently test this without crashing */ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 21"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 1, "INBOX", 0); + node_create(tree1, 2, "foo", 0); + /* swap INBOX and foo - the INBOX name is important since it's + treated specially */ + node_create(tree2, 1, "foo", 0); + node_create(tree2, 2, "INBOX", 1); + + test_trees(tree1, tree2); + test_end(); +#endif +} + +static void test_dsync_mailbox_tree_sync_renames22(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync renames 22"); + tree1 = dsync_mailbox_tree_init('/', '\0', '_'); + tree2 = dsync_mailbox_tree_init('/', '\0', '_'); + + node_create(tree1, 3, "p/a", 0); + node_create(tree1, 0, "p/2", 0); + node_create(tree1, 5, "p/2/h", 0); + + node_create(tree2, 4, "p/1/z", 0); + node_create(tree2, 1, "p/2", 0); + node_create(tree2, 2, "p/2/a", 0); + node_create(tree2, 5, "p/2/y", 0); + node_create(tree2, 3, "p/3", 0); + + test_trees(tree1, tree2); + test_end(); +} + +static void test_dsync_mailbox_tree_sync_random(void) +{ + struct dsync_mailbox_tree *tree1, *tree2; + + test_begin("dsync mailbox tree sync random"); + tree1 = create_random_tree(); + tree2 = create_random_tree(); + test_trees(tree1, tree2); + test_end(); +} + +int main(void) +{ + static void (*const test_functions[])(void) = { + test_dsync_mailbox_tree_sync_creates, + test_dsync_mailbox_tree_sync_deletes, + test_dsync_mailbox_tree_sync_renames1, + test_dsync_mailbox_tree_sync_renames2, + test_dsync_mailbox_tree_sync_renames3, + test_dsync_mailbox_tree_sync_renames4, + test_dsync_mailbox_tree_sync_renames5, + test_dsync_mailbox_tree_sync_renames6, + test_dsync_mailbox_tree_sync_renames7, + test_dsync_mailbox_tree_sync_renames8, + test_dsync_mailbox_tree_sync_renames9, + test_dsync_mailbox_tree_sync_renames10, + test_dsync_mailbox_tree_sync_renames11, + test_dsync_mailbox_tree_sync_renames12, + test_dsync_mailbox_tree_sync_renames13, + test_dsync_mailbox_tree_sync_renames14, + test_dsync_mailbox_tree_sync_renames15, + test_dsync_mailbox_tree_sync_renames16, + test_dsync_mailbox_tree_sync_renames17, + test_dsync_mailbox_tree_sync_renames18, + test_dsync_mailbox_tree_sync_renames19, + test_dsync_mailbox_tree_sync_renames20, + test_dsync_mailbox_tree_sync_renames21, + test_dsync_mailbox_tree_sync_renames22, + test_dsync_mailbox_tree_sync_random, + NULL + }; + return test_run(test_functions); +} |