diff options
Diffstat (limited to 'src/lib-storage/index/imapc')
-rw-r--r-- | src/lib-storage/index/imapc/Makefile.am | 36 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/Makefile.in | 861 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-list.c | 1013 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-list.h | 41 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-mail-fetch.c | 911 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-mail.c | 675 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-mail.h | 51 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-mailbox.c | 1015 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-save.c | 829 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-search.c | 332 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-search.h | 18 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-settings.c | 173 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-settings.h | 63 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-storage.c | 1353 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-storage.h | 274 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-sync.c | 702 | ||||
-rw-r--r-- | src/lib-storage/index/imapc/imapc-sync.h | 39 |
17 files changed, 8386 insertions, 0 deletions
diff --git a/src/lib-storage/index/imapc/Makefile.am b/src/lib-storage/index/imapc/Makefile.am new file mode 100644 index 0000000..72ee102 --- /dev/null +++ b/src/lib-storage/index/imapc/Makefile.am @@ -0,0 +1,36 @@ +noinst_LTLIBRARIES = libstorage_imapc.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-mail \ + -I$(top_srcdir)/src/lib-imap \ + -I$(top_srcdir)/src/lib-imap-client \ + -I$(top_srcdir)/src/lib-index \ + -I$(top_srcdir)/src/lib-storage \ + -I$(top_srcdir)/src/lib-storage/list \ + -I$(top_srcdir)/src/lib-storage/index \ + -I$(top_srcdir)/src/lib-ssl-iostream + +libstorage_imapc_la_SOURCES = \ + imapc-list.c \ + imapc-mail.c \ + imapc-mail-fetch.c \ + imapc-mailbox.c \ + imapc-save.c \ + imapc-search.c \ + imapc-settings.c \ + imapc-sync.c \ + imapc-storage.c + +headers = \ + imapc-list.h \ + imapc-mail.h \ + imapc-search.h \ + imapc-settings.h \ + imapc-storage.h \ + imapc-sync.h + +pkginc_libdir=$(pkgincludedir) +pkginc_lib_HEADERS = $(headers) diff --git a/src/lib-storage/index/imapc/Makefile.in b/src/lib-storage/index/imapc/Makefile.in new file mode 100644 index 0000000..4d73707 --- /dev/null +++ b/src/lib-storage/index/imapc/Makefile.in @@ -0,0 +1,861 @@ +# Makefile.in generated by automake 1.16.3 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994-2020 Free Software Foundation, Inc. + +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + + +VPATH = @srcdir@ +am__is_gnu_make = { \ + if test -z '$(MAKELEVEL)'; then \ + false; \ + elif test -n '$(MAKE_HOST)'; then \ + true; \ + elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ + true; \ + else \ + false; \ + fi; \ +} +am__make_running_with_option = \ + case $${target_option-} in \ + ?) ;; \ + *) echo "am__make_running_with_option: internal error: invalid" \ + "target option '$${target_option-}' specified" >&2; \ + exit 1;; \ + esac; \ + has_opt=no; \ + sane_makeflags=$$MAKEFLAGS; \ + if $(am__is_gnu_make); then \ + sane_makeflags=$$MFLAGS; \ + else \ + case $$MAKEFLAGS in \ + *\\[\ \ ]*) \ + bs=\\; \ + sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ + | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ + esac; \ + fi; \ + skip_next=no; \ + strip_trailopt () \ + { \ + flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ + }; \ + for flg in $$sane_makeflags; do \ + test $$skip_next = yes && { skip_next=no; continue; }; \ + case $$flg in \ + *=*|--*) continue;; \ + -*I) strip_trailopt 'I'; skip_next=yes;; \ + -*I?*) strip_trailopt 'I';; \ + -*O) strip_trailopt 'O'; skip_next=yes;; \ + -*O?*) strip_trailopt 'O';; \ + -*l) strip_trailopt 'l'; skip_next=yes;; \ + -*l?*) strip_trailopt 'l';; \ + -[dEDm]) skip_next=yes;; \ + -[JT]) skip_next=yes;; \ + esac; \ + case $$flg in \ + *$$target_option*) has_opt=yes; break;; \ + esac; \ + done; \ + test $$has_opt = yes +am__make_dryrun = (target_option=n; $(am__make_running_with_option)) +am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +build_triplet = @build@ +host_triplet = @host@ +subdir = src/lib-storage/index/imapc +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 $(pkginc_lib_HEADERS) \ + $(am__DIST_COMMON) +mkinstalldirs = $(install_sh) -d +CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +LTLIBRARIES = $(noinst_LTLIBRARIES) +libstorage_imapc_la_LIBADD = +am_libstorage_imapc_la_OBJECTS = imapc-list.lo imapc-mail.lo \ + imapc-mail-fetch.lo imapc-mailbox.lo imapc-save.lo \ + imapc-search.lo imapc-settings.lo imapc-sync.lo \ + imapc-storage.lo +libstorage_imapc_la_OBJECTS = $(am_libstorage_imapc_la_OBJECTS) +AM_V_lt = $(am__v_lt_@AM_V@) +am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@) +am__v_lt_0 = --silent +am__v_lt_1 = +AM_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)/imapc-list.Plo \ + ./$(DEPDIR)/imapc-mail-fetch.Plo ./$(DEPDIR)/imapc-mail.Plo \ + ./$(DEPDIR)/imapc-mailbox.Plo ./$(DEPDIR)/imapc-save.Plo \ + ./$(DEPDIR)/imapc-search.Plo ./$(DEPDIR)/imapc-settings.Plo \ + ./$(DEPDIR)/imapc-storage.Plo ./$(DEPDIR)/imapc-sync.Plo +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 = $(libstorage_imapc_la_SOURCES) +DIST_SOURCES = $(libstorage_imapc_la_SOURCES) +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__uninstall_files_from_dir = { \ + test -z "$$files" \ + || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ + || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ + $(am__cd) "$$dir" && rm -f $$files; }; \ + } +am__installdirs = "$(DESTDIR)$(pkginc_libdir)" +HEADERS = $(pkginc_lib_HEADERS) +am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) +# Read a list of newline-separated strings from the standard input, +# and print each of them once, without duplicates. Input order is +# *not* preserved. +am__uniquify_input = $(AWK) '\ + BEGIN { nonempty = 0; } \ + { items[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in items) print i; }; } \ +' +# Make sure the list of sources is unique. This is necessary because, +# e.g., the same source file might be shared among _SOURCES variables +# for different programs/libraries. +am__define_uniq_tagged_files = \ + list='$(am__tagged_files)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | $(am__uniquify_input)` +ETAGS = etags +CTAGS = ctags +am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +ACLOCAL = @ACLOCAL@ +ACLOCAL_AMFLAGS = @ACLOCAL_AMFLAGS@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +APPARMOR_LIBS = @APPARMOR_LIBS@ +AR = @AR@ +AUTH_CFLAGS = @AUTH_CFLAGS@ +AUTH_LIBS = @AUTH_LIBS@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +BINARY_CFLAGS = @BINARY_CFLAGS@ +BINARY_LDFLAGS = @BINARY_LDFLAGS@ +BISON = @BISON@ +CASSANDRA_CFLAGS = @CASSANDRA_CFLAGS@ +CASSANDRA_LIBS = @CASSANDRA_LIBS@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CDB_LIBS = @CDB_LIBS@ +CFLAGS = @CFLAGS@ +CLUCENE_CFLAGS = @CLUCENE_CFLAGS@ +CLUCENE_LIBS = @CLUCENE_LIBS@ +COMPRESS_LIBS = @COMPRESS_LIBS@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CRYPT_LIBS = @CRYPT_LIBS@ +CXX = @CXX@ +CXXCPP = @CXXCPP@ +CXXDEPMODE = @CXXDEPMODE@ +CXXFLAGS = @CXXFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +DICT_LIBS = @DICT_LIBS@ +DLLIB = @DLLIB@ +DLLTOOL = @DLLTOOL@ +DSYMUTIL = @DSYMUTIL@ +DUMPBIN = @DUMPBIN@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +EXEEXT = @EXEEXT@ +FGREP = @FGREP@ +FLEX = @FLEX@ +FUZZER_CPPFLAGS = @FUZZER_CPPFLAGS@ +FUZZER_LDFLAGS = @FUZZER_LDFLAGS@ +GREP = @GREP@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +KRB5CONFIG = @KRB5CONFIG@ +KRB5_CFLAGS = @KRB5_CFLAGS@ +KRB5_LIBS = @KRB5_LIBS@ +LD = @LD@ +LDAP_LIBS = @LDAP_LIBS@ +LDFLAGS = @LDFLAGS@ +LD_NO_WHOLE_ARCHIVE = @LD_NO_WHOLE_ARCHIVE@ +LD_WHOLE_ARCHIVE = @LD_WHOLE_ARCHIVE@ +LIBCAP = @LIBCAP@ +LIBDOVECOT = @LIBDOVECOT@ +LIBDOVECOT_COMPRESS = @LIBDOVECOT_COMPRESS@ +LIBDOVECOT_DEPS = @LIBDOVECOT_DEPS@ +LIBDOVECOT_DSYNC = @LIBDOVECOT_DSYNC@ +LIBDOVECOT_LA_LIBS = @LIBDOVECOT_LA_LIBS@ +LIBDOVECOT_LDA = @LIBDOVECOT_LDA@ +LIBDOVECOT_LDAP = @LIBDOVECOT_LDAP@ +LIBDOVECOT_LIBFTS = @LIBDOVECOT_LIBFTS@ +LIBDOVECOT_LIBFTS_DEPS = @LIBDOVECOT_LIBFTS_DEPS@ +LIBDOVECOT_LOGIN = @LIBDOVECOT_LOGIN@ +LIBDOVECOT_LUA = @LIBDOVECOT_LUA@ +LIBDOVECOT_LUA_DEPS = @LIBDOVECOT_LUA_DEPS@ +LIBDOVECOT_SQL = @LIBDOVECOT_SQL@ +LIBDOVECOT_STORAGE = @LIBDOVECOT_STORAGE@ +LIBDOVECOT_STORAGE_DEPS = @LIBDOVECOT_STORAGE_DEPS@ +LIBEXTTEXTCAT_CFLAGS = @LIBEXTTEXTCAT_CFLAGS@ +LIBEXTTEXTCAT_LIBS = @LIBEXTTEXTCAT_LIBS@ +LIBICONV = @LIBICONV@ +LIBICU_CFLAGS = @LIBICU_CFLAGS@ +LIBICU_LIBS = @LIBICU_LIBS@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LIBSODIUM_CFLAGS = @LIBSODIUM_CFLAGS@ +LIBSODIUM_LIBS = @LIBSODIUM_LIBS@ +LIBTIRPC_CFLAGS = @LIBTIRPC_CFLAGS@ +LIBTIRPC_LIBS = @LIBTIRPC_LIBS@ +LIBTOOL = @LIBTOOL@ +LIBUNWIND_CFLAGS = @LIBUNWIND_CFLAGS@ +LIBUNWIND_LIBS = @LIBUNWIND_LIBS@ +LIBWRAP_LIBS = @LIBWRAP_LIBS@ +LINKED_STORAGE_LDADD = @LINKED_STORAGE_LDADD@ +LIPO = @LIPO@ +LN_S = @LN_S@ +LTLIBICONV = @LTLIBICONV@ +LTLIBOBJS = @LTLIBOBJS@ +LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@ +LUA_CFLAGS = @LUA_CFLAGS@ +LUA_LIBS = @LUA_LIBS@ +MAINT = @MAINT@ +MAKEINFO = @MAKEINFO@ +MANIFEST_TOOL = @MANIFEST_TOOL@ +MKDIR_P = @MKDIR_P@ +MODULE_LIBS = @MODULE_LIBS@ +MODULE_SUFFIX = @MODULE_SUFFIX@ +MYSQL_CFLAGS = @MYSQL_CFLAGS@ +MYSQL_CONFIG = @MYSQL_CONFIG@ +MYSQL_LIBS = @MYSQL_LIBS@ +NM = @NM@ +NMEDIT = @NMEDIT@ +NOPLUGIN_LDFLAGS = @NOPLUGIN_LDFLAGS@ +OBJDUMP = @OBJDUMP@ +OBJEXT = @OBJEXT@ +OTOOL = @OTOOL@ +OTOOL64 = @OTOOL64@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PANDOC = @PANDOC@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PGSQL_CFLAGS = @PGSQL_CFLAGS@ +PGSQL_LIBS = @PGSQL_LIBS@ +PG_CONFIG = @PG_CONFIG@ +PIE_CFLAGS = @PIE_CFLAGS@ +PIE_LDFLAGS = @PIE_LDFLAGS@ +PKG_CONFIG = @PKG_CONFIG@ +PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ +PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ +QUOTA_LIBS = @QUOTA_LIBS@ +RANLIB = @RANLIB@ +RELRO_LDFLAGS = @RELRO_LDFLAGS@ +RPCGEN = @RPCGEN@ +RUN_TEST = @RUN_TEST@ +SED = @SED@ +SETTING_FILES = @SETTING_FILES@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +SQLITE_CFLAGS = @SQLITE_CFLAGS@ +SQLITE_LIBS = @SQLITE_LIBS@ +SQL_CFLAGS = @SQL_CFLAGS@ +SQL_LIBS = @SQL_LIBS@ +SSL_CFLAGS = @SSL_CFLAGS@ +SSL_LIBS = @SSL_LIBS@ +STRIP = @STRIP@ +SYSTEMD_CFLAGS = @SYSTEMD_CFLAGS@ +SYSTEMD_LIBS = @SYSTEMD_LIBS@ +VALGRIND = @VALGRIND@ +VERSION = @VERSION@ +ZSTD_CFLAGS = @ZSTD_CFLAGS@ +ZSTD_LIBS = @ZSTD_LIBS@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_AR = @ac_ct_AR@ +ac_ct_CC = @ac_ct_CC@ +ac_ct_CXX = @ac_ct_CXX@ +ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build = @build@ +build_alias = @build_alias@ +build_cpu = @build_cpu@ +build_os = @build_os@ +build_vendor = @build_vendor@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +dict_drivers = @dict_drivers@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +host = @host@ +host_alias = @host_alias@ +host_cpu = @host_cpu@ +host_os = @host_os@ +host_vendor = @host_vendor@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +moduledir = @moduledir@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +rundir = @rundir@ +runstatedir = @runstatedir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +sql_drivers = @sql_drivers@ +srcdir = @srcdir@ +ssldir = @ssldir@ +statedir = @statedir@ +sysconfdir = @sysconfdir@ +systemdservicetype = @systemdservicetype@ +systemdsystemunitdir = @systemdsystemunitdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +noinst_LTLIBRARIES = libstorage_imapc.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-mail \ + -I$(top_srcdir)/src/lib-imap \ + -I$(top_srcdir)/src/lib-imap-client \ + -I$(top_srcdir)/src/lib-index \ + -I$(top_srcdir)/src/lib-storage \ + -I$(top_srcdir)/src/lib-storage/list \ + -I$(top_srcdir)/src/lib-storage/index \ + -I$(top_srcdir)/src/lib-ssl-iostream + +libstorage_imapc_la_SOURCES = \ + imapc-list.c \ + imapc-mail.c \ + imapc-mail-fetch.c \ + imapc-mailbox.c \ + imapc-save.c \ + imapc-search.c \ + imapc-settings.c \ + imapc-sync.c \ + imapc-storage.c + +headers = \ + imapc-list.h \ + imapc-mail.h \ + imapc-search.h \ + imapc-settings.h \ + imapc-storage.h \ + imapc-sync.h + +pkginc_libdir = $(pkgincludedir) +pkginc_lib_HEADERS = $(headers) +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/lib-storage/index/imapc/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/lib-storage/index/imapc/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-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}; \ + } + +libstorage_imapc.la: $(libstorage_imapc_la_OBJECTS) $(libstorage_imapc_la_DEPENDENCIES) $(EXTRA_libstorage_imapc_la_DEPENDENCIES) + $(AM_V_CCLD)$(LINK) $(libstorage_imapc_la_OBJECTS) $(libstorage_imapc_la_LIBADD) $(LIBS) + +mostlyclean-compile: + -rm -f *.$(OBJEXT) + +distclean-compile: + -rm -f *.tab.c + +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-list.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-mail-fetch.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-mail.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-mailbox.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-save.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-search.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-settings.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-storage.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imapc-sync.Plo@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 +check: check-am +all-am: Makefile $(LTLIBRARIES) $(HEADERS) +installdirs: + for dir in "$(DESTDIR)$(pkginc_libdir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +install: install-am +install-exec: install-exec-am +install-data: install-data-am +uninstall: uninstall-am + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-am +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-am + +clean-am: clean-generic clean-libtool clean-noinstLTLIBRARIES \ + mostlyclean-am + +distclean: distclean-am + -rm -f ./$(DEPDIR)/imapc-list.Plo + -rm -f ./$(DEPDIR)/imapc-mail-fetch.Plo + -rm -f ./$(DEPDIR)/imapc-mail.Plo + -rm -f ./$(DEPDIR)/imapc-mailbox.Plo + -rm -f ./$(DEPDIR)/imapc-save.Plo + -rm -f ./$(DEPDIR)/imapc-search.Plo + -rm -f ./$(DEPDIR)/imapc-settings.Plo + -rm -f ./$(DEPDIR)/imapc-storage.Plo + -rm -f ./$(DEPDIR)/imapc-sync.Plo + -rm -f Makefile +distclean-am: clean-am distclean-compile distclean-generic \ + distclean-tags + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: install-pkginc_libHEADERS + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: + +install-html: install-html-am + +install-html-am: + +install-info: install-info-am + +install-info-am: + +install-man: + +install-pdf: install-pdf-am + +install-pdf-am: + +install-ps: install-ps-am + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-am + -rm -f ./$(DEPDIR)/imapc-list.Plo + -rm -f ./$(DEPDIR)/imapc-mail-fetch.Plo + -rm -f ./$(DEPDIR)/imapc-mail.Plo + -rm -f ./$(DEPDIR)/imapc-mailbox.Plo + -rm -f ./$(DEPDIR)/imapc-save.Plo + -rm -f ./$(DEPDIR)/imapc-search.Plo + -rm -f ./$(DEPDIR)/imapc-settings.Plo + -rm -f ./$(DEPDIR)/imapc-storage.Plo + -rm -f ./$(DEPDIR)/imapc-sync.Plo + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-compile mostlyclean-generic \ + mostlyclean-libtool + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-pkginc_libHEADERS + +.MAKE: install-am install-strip + +.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am clean \ + clean-generic clean-libtool clean-noinstLTLIBRARIES \ + cscopelist-am ctags ctags-am distclean distclean-compile \ + distclean-generic distclean-libtool distclean-tags distdir dvi \ + dvi-am html html-am info info-am install install-am \ + install-data install-data-am install-dvi install-dvi-am \ + install-exec install-exec-am install-html install-html-am \ + install-info install-info-am install-man install-pdf \ + install-pdf-am install-pkginc_libHEADERS install-ps \ + install-ps-am install-strip installcheck installcheck-am \ + installdirs maintainer-clean maintainer-clean-generic \ + mostlyclean mostlyclean-compile mostlyclean-generic \ + mostlyclean-libtool pdf pdf-am ps ps-am tags tags-am uninstall \ + uninstall-am uninstall-pkginc_libHEADERS + +.PRECIOUS: Makefile + + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/src/lib-storage/index/imapc/imapc-list.c b/src/lib-storage/index/imapc/imapc-list.c new file mode 100644 index 0000000..d987538 --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-list.c @@ -0,0 +1,1013 @@ +/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */ + +/* + There are various different mailbox names here. Here's an example assuming + - imapc_list_prefix = "prefix" + - remote imapc server separator = '/' + - mailbox_list separator = '^' (actually this is currently always the same + as remote separator, but this clarifies the example) + - namespace separator = ':' + - fs_list separator = '.' + - mailbox_list storage_name_escape_char = '+' + - mailbox_list vname_escape_char = '~' + - fs_list storage_name_escape_char = '%' + + remote_name = "prefix/~foo/bar^baz+_%_." + storage_name = "prefix^~foo^bar+5ebaz+2b_%_." + - separator is changed from / to ^ + - conflicting ^ separator in remote_name is escaped as +5e + - storage_name_escape character + is escaped as +2b + vname = "~7efoo:bar.baz+_%_." + - imapc_list_prefix is dropped + - vname_escape_character ~ is escaped into ~7e + - separator is changed from ^ to : + - storage_name_escape_characters are unescaped + fs_name = "prefix.~foo.bar^baz+_%25_%2e" + - this is generated from remote_name + - separator is changed from / to . + - storage_name_escape_character=% and fs_list separator . are escaped +*/ + +#include "lib.h" +#include "ioloop.h" +#include "str.h" +#include "imap-arg.h" +#include "imap-match.h" +#include "imap-utf7.h" +#include "mailbox-tree.h" +#include "mailbox-list-subscriptions.h" +#include "imapc-storage.h" +#include "imapc-list.h" + +struct imapc_mailbox_list_iterate_context { + struct mailbox_list_iterate_context ctx; + struct mailbox_tree_context *tree; + struct mailbox_node *ns_root; + + struct mailbox_tree_iterate_context *iter; + struct mailbox_info info; + string_t *special_use; +}; + +static struct { + const char *str; + enum mailbox_info_flags flag; +} imap_list_flags[] = { + { "\\NoSelect", MAILBOX_NOSELECT }, + { "\\NonExistent", MAILBOX_NONEXISTENT }, + { "\\NoInferiors", MAILBOX_NOINFERIORS }, + { "\\Subscribed", MAILBOX_SUBSCRIBED }, + { "\\All", MAILBOX_SPECIALUSE_ALL }, + { "\\Archive", MAILBOX_SPECIALUSE_ARCHIVE }, + { "\\Drafts", MAILBOX_SPECIALUSE_DRAFTS }, + { "\\Flagged", MAILBOX_SPECIALUSE_FLAGGED }, + { "\\Junk", MAILBOX_SPECIALUSE_JUNK }, + { "\\Sent", MAILBOX_SPECIALUSE_SENT }, + { "\\Trash", MAILBOX_SPECIALUSE_TRASH }, + { "\\Important", MAILBOX_SPECIALUSE_IMPORTANT } +}; + +extern struct mailbox_list imapc_mailbox_list; + +static void imapc_list_send_hierarchy_sep_lookup(struct imapc_mailbox_list *list); +static void imapc_untagged_list(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client); +static void imapc_untagged_lsub(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client); + +static struct mailbox_list *imapc_list_alloc(void) +{ + struct imapc_mailbox_list *list; + pool_t pool; + + pool = pool_alloconly_create("imapc mailbox list", 1024); + list = p_new(pool, struct imapc_mailbox_list, 1); + list->list = imapc_mailbox_list; + list->list.pool = pool; + /* separator is set lazily */ + list->mailboxes = mailbox_tree_init('\0'); + mailbox_tree_set_parents_nonexistent(list->mailboxes); + return &list->list; +} + +static int imapc_list_init(struct mailbox_list *_list, const char **error_r) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + + list->set = mail_user_set_get_driver_settings(_list->ns->user->set_info, + _list->ns->user_set, + IMAPC_STORAGE_NAME); + if (imapc_storage_client_create(_list->ns, list->set, _list->mail_set, + &list->client, error_r) < 0) + return -1; + list->client->_list = list; + + imapc_storage_client_register_untagged(list->client, "LIST", + imapc_untagged_list); + imapc_storage_client_register_untagged(list->client, "LSUB", + imapc_untagged_lsub); + imapc_list_send_hierarchy_sep_lookup(list); + return 0; +} + +static void imapc_list_deinit(struct mailbox_list *_list) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + + /* make sure all pending commands are aborted before anything is + deinitialized */ + if (list->client != NULL) { + list->client->destroying = TRUE; + imapc_client_logout(list->client->client); + imapc_storage_client_unref(&list->client); + } + if (list->index_list != NULL) + mailbox_list_destroy(&list->index_list); + mailbox_tree_deinit(&list->mailboxes); + if (list->tmp_subscriptions != NULL) + mailbox_tree_deinit(&list->tmp_subscriptions); + pool_unref(&list->list.pool); +} + +static void +imapc_list_copy_error_from_reply(struct imapc_mailbox_list *list, + enum mail_error default_error, + const struct imapc_command_reply *reply) +{ + enum mail_error error; + + if (imapc_resp_text_code_parse(reply->resp_text_key, &error)) { + mailbox_list_set_error(&list->list, error, + reply->text_without_resp); + } else { + mailbox_list_set_error(&list->list, default_error, + reply->text_without_resp); + } +} + +static void imapc_list_simple_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_simple_context *ctx = context; + + if (reply->state == IMAPC_COMMAND_STATE_OK) + ctx->ret = 0; + else if (reply->state == IMAPC_COMMAND_STATE_NO) { + imapc_list_copy_error_from_reply(ctx->client->_list, + MAIL_ERROR_PARAMS, reply); + ctx->ret = -1; + } else if (imapc_storage_client_handle_auth_failure(ctx->client)) { + ctx->ret = -1; + } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) { + mailbox_list_set_internal_error(&ctx->client->_list->list); + ctx->ret = -1; + } else { + mailbox_list_set_critical(&ctx->client->_list->list, + "imapc: Command failed: %s", reply->text_full); + ctx->ret = -1; + } + imapc_client_stop(ctx->client->client); +} + +static bool +imap_list_flag_parse(const char *str, enum mailbox_info_flags *flag_r) +{ + unsigned int i; + + for (i = 0; i < N_ELEMENTS(imap_list_flags); i++) { + if (strcasecmp(str, imap_list_flags[i].str) == 0) { + *flag_r = imap_list_flags[i].flag; + return TRUE; + } + } + return FALSE; +} + +static const char * +imapc_list_remote_to_storage_name(struct imapc_mailbox_list *list, + const char *remote_name) +{ + /* typically mailbox_list_escape_name() is used to escape vname into + a list name. but we want to convert remote IMAP name to a list name, + so we need to use the remote IMAP separator. */ + return mailbox_list_escape_name_params(remote_name, "", + list->root_sep, + mailbox_list_get_hierarchy_sep(&list->list), + list->list.set.storage_name_escape_char, ""); +} + +static const char * +imapc_list_remote_to_vname(struct imapc_mailbox_list *list, + const char *remote_name) +{ + return mailbox_list_get_vname(&list->list, + imapc_list_remote_to_storage_name(list, remote_name)); +} + +const char * +imapc_list_storage_to_remote_name(struct imapc_mailbox_list *list, + const char *storage_name) +{ + return mailbox_list_unescape_name_params(storage_name, "", list->root_sep, + mailbox_list_get_hierarchy_sep(&list->list), + list->list.set.storage_name_escape_char); +} + +static struct mailbox_node * +imapc_list_update_tree(struct imapc_mailbox_list *list, + struct mailbox_tree_context *tree, + const struct imap_arg *args) +{ + struct mailbox_node *node; + const struct imap_arg *flags; + const char *remote_name, *flag; + enum mailbox_info_flags info_flag, info_flags = 0; + bool created; + + if (!imap_arg_get_list(&args[0], &flags) || + args[1].type == IMAP_ARG_EOL || + !imap_arg_get_astring(&args[2], &remote_name)) + return NULL; + + while (imap_arg_get_atom(flags, &flag)) { + if (imap_list_flag_parse(flag, &info_flag)) + info_flags |= info_flag; + flags++; + } + + T_BEGIN { + const char *vname = + imapc_list_remote_to_vname(list, remote_name); + + if ((info_flags & MAILBOX_NONEXISTENT) != 0) + node = mailbox_tree_lookup(tree, vname); + else + node = mailbox_tree_get(tree, vname, &created); + } T_END; + if (node != NULL) + node->flags = info_flags; + return node; +} + +static void imapc_untagged_list(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client) +{ + struct imapc_mailbox_list *list = client->_list; + const struct imap_arg *args = reply->args; + const char *sep, *remote_name; + + if (list->root_sep == '\0') { + /* we haven't asked for the separator yet. + lets see if this is the reply for its request. */ + if (args[0].type == IMAP_ARG_EOL || + !imap_arg_get_nstring(&args[1], &sep) || + !imap_arg_get_astring(&args[2], &remote_name)) + return; + + /* we can't handle NIL separator yet */ + list->root_sep = sep == NULL ? '/' : sep[0]; + mailbox_tree_set_separator(list->mailboxes, list->root_sep); + } else { + (void)imapc_list_update_tree(list, list->mailboxes, args); + } +} + +static void imapc_untagged_lsub(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client) +{ + struct imapc_mailbox_list *list = client->_list; + const struct imap_arg *args = reply->args; + struct mailbox_node *node; + + if (list->root_sep == '\0') { + /* we haven't asked for the separator yet */ + return; + } + node = imapc_list_update_tree(list, list->tmp_subscriptions != NULL ? + list->tmp_subscriptions : + list->list.subscriptions, args); + if (node != NULL) { + if ((node->flags & MAILBOX_NOSELECT) == 0) + node->flags |= MAILBOX_SUBSCRIBED; + else { + /* LSUB \Noselect means that the mailbox isn't + subscribed, but it has children that are */ + node->flags &= ENUM_NEGATE(MAILBOX_NOSELECT); + } + } +} + +static void imapc_list_sep_verify(struct imapc_mailbox_list *list) +{ + const char *imapc_list_prefix = list->set->imapc_list_prefix; + + if (list->root_sep == '\0') { + mailbox_list_set_critical(&list->list, + "imapc: LIST didn't return hierarchy separator"); + } else if (imapc_list_prefix[0] != '\0' && + imapc_list_prefix[strlen(imapc_list_prefix)-1] == list->root_sep) { + mailbox_list_set_critical(&list->list, + "imapc_list_prefix must not end with hierarchy separator"); + } +} + +static void imapc_storage_sep_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_mailbox_list *list = context; + + list->root_sep_pending = FALSE; + if (reply->state == IMAPC_COMMAND_STATE_OK) + imapc_list_sep_verify(list); + else if (reply->state == IMAPC_COMMAND_STATE_NO) + imapc_list_copy_error_from_reply(list, MAIL_ERROR_PARAMS, reply); + else if (imapc_storage_client_handle_auth_failure(list->client)) + ; + else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) + mailbox_list_set_internal_error(&list->list); + else if (!list->list.ns->user->deinitializing) { + mailbox_list_set_critical(&list->list, + "imapc: Command failed: %s", reply->text_full); + } + imapc_client_stop(list->client->client); +} + +static void imapc_list_send_hierarchy_sep_lookup(struct imapc_mailbox_list *list) +{ + struct imapc_command *cmd; + + if (list->root_sep_pending) + return; + list->root_sep_pending = TRUE; + + cmd = imapc_client_cmd(list->client->client, + imapc_storage_sep_callback, list); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + imapc_command_send(cmd, "LIST \"\" \"\""); +} + +int imapc_list_try_get_root_sep(struct imapc_mailbox_list *list, char *sep_r) +{ + if (list->root_sep == '\0') { + if (imapc_storage_client_handle_auth_failure(list->client)) + return -1; + imapc_list_send_hierarchy_sep_lookup(list); + while (list->root_sep_pending) + imapc_client_run(list->client->client); + if (list->root_sep == '\0') + return -1; + } + *sep_r = list->root_sep; + return 0; +} + +static char imapc_list_get_hierarchy_sep(struct mailbox_list *_list) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + char sep; + + if (imapc_list_try_get_root_sep(list, &sep) < 0) { + /* we can't really return a failure here. just return a common + separator and fail all the future list operations. */ + return '/'; + } + return sep; +} + +static const char * +imapc_list_get_storage_name(struct mailbox_list *_list, const char *vname) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + const char *prefix = list->set->imapc_list_prefix; + const char *storage_name; + + storage_name = mailbox_list_default_get_storage_name(_list, vname); + if (*prefix != '\0' && strcasecmp(storage_name, "INBOX") != 0) { + storage_name = storage_name[0] == '\0' ? prefix : + t_strdup_printf("%s%c%s", prefix, + mailbox_list_get_hierarchy_sep(_list), + storage_name); + } + return storage_name; +} + +static const char * +imapc_list_get_vname(struct mailbox_list *_list, const char *storage_name) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + const char *prefix = list->set->imapc_list_prefix; + size_t prefix_len; + + if (*storage_name == '\0') { + /* ACL plugin does these lookups */ + } else if (*prefix != '\0' && strcasecmp(storage_name, "INBOX") != 0) { + prefix_len = strlen(prefix); + i_assert(str_begins(storage_name, prefix)); + storage_name += prefix_len; + if (storage_name[0] == '\0') { + /* we're looking up the prefix itself */ + } else { + i_assert(storage_name[0] == + mailbox_list_get_hierarchy_sep(_list)); + storage_name++; + } + } + return mailbox_list_default_get_vname(_list, storage_name); +} + +static struct mailbox_list *imapc_list_get_fs(struct imapc_mailbox_list *list) +{ + struct mailbox_list_settings list_set; + const char *error, *dir; + + dir = list->list.set.index_dir; + if (dir == NULL) + dir = list->list.set.root_dir; + + if (dir == NULL || dir[0] == '\0') { + /* indexes disabled */ + } else if (list->index_list == NULL && !list->index_list_failed) { + mailbox_list_settings_init_defaults(&list_set); + list_set.layout = MAILBOX_LIST_NAME_MAILDIRPLUSPLUS; + list_set.root_dir = dir; + list_set.index_pvt_dir = p_strdup_empty(list->list.pool, list->list.set.index_pvt_dir); + /* Filesystem needs to be able to store any kind of a mailbox + name. */ + list_set.storage_name_escape_char = + IMAPC_LIST_FS_NAME_ESCAPE_CHAR; + + if (mailbox_list_create(list_set.layout, list->list.ns, + &list_set, MAILBOX_LIST_FLAG_SECONDARY, + &list->index_list, &error) < 0) { + i_error("imapc: Couldn't create %s mailbox list: %s", + list_set.layout, error); + list->index_list_failed = TRUE; + } + } + return list->index_list; +} + +static const char * +imapc_list_storage_to_fs_name(struct imapc_mailbox_list *list, + const char *storage_name) +{ + struct mailbox_list *fs_list = imapc_list_get_fs(list); + const char *remote_name; + + if (storage_name == NULL) + return NULL; + + remote_name = imapc_list_storage_to_remote_name(list, storage_name); + return mailbox_list_escape_name_params(remote_name, "", + list->root_sep, + mailbox_list_get_hierarchy_sep(fs_list), + fs_list->set.storage_name_escape_char, ""); +} + +static const char * +imapc_list_fs_to_storage_name(struct imapc_mailbox_list *list, + const char *fs_name) +{ + struct mailbox_list *fs_list = imapc_list_get_fs(list); + const char *remote_name; + + if (fs_name == NULL) + return NULL; + + remote_name = mailbox_list_unescape_name_params(fs_name, "", + list->root_sep, + mailbox_list_get_hierarchy_sep(fs_list), + fs_list->set.storage_name_escape_char); + return imapc_list_remote_to_storage_name(list, remote_name); +} + +static int +imapc_list_get_path(struct mailbox_list *_list, const char *name, + enum mailbox_list_path_type type, const char **path_r) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + struct mailbox_list *fs_list = imapc_list_get_fs(list); + const char *fs_name; + + if (fs_list != NULL) { + fs_name = imapc_list_storage_to_fs_name(list, name); + return mailbox_list_get_path(fs_list, fs_name, type, path_r); + } else { + *path_r = NULL; + return 0; + } +} + +static const char * +imapc_list_get_temp_prefix(struct mailbox_list *_list, bool global) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + struct mailbox_list *fs_list = imapc_list_get_fs(list); + + if (fs_list != NULL) { + return global ? + mailbox_list_get_global_temp_prefix(fs_list) : + mailbox_list_get_temp_prefix(fs_list); + } else { + i_panic("imapc: Can't return a temp prefix for '%s'", + _list->ns->prefix); + } +} + +static const char * +imapc_list_join_refpattern(struct mailbox_list *list ATTR_UNUSED, + const char *ref, const char *pattern) +{ + return t_strconcat(ref, pattern, NULL); +} + +static struct imapc_command * +imapc_list_simple_context_init(struct imapc_simple_context *ctx, + struct imapc_mailbox_list *list) +{ + imapc_simple_context_init(ctx, list->client); + return imapc_client_cmd(list->client->client, + imapc_list_simple_callback, ctx); +} + +static void imapc_list_delete_unused_indexes(struct imapc_mailbox_list *list) +{ + struct mailbox_list *fs_list = imapc_list_get_fs(list); + struct mailbox_list_iterate_context *iter; + const struct mailbox_info *info; + const char *fs_name, *storage_name, *vname; + + if (fs_list == NULL) + return; + + iter = mailbox_list_iter_init(fs_list, "*", + MAILBOX_LIST_ITER_RAW_LIST | + MAILBOX_LIST_ITER_NO_AUTO_BOXES | + MAILBOX_LIST_ITER_RETURN_NO_FLAGS); + while ((info = mailbox_list_iter_next(iter)) != NULL) T_BEGIN { + fs_name = mailbox_list_get_storage_name(fs_list, info->vname); + storage_name = imapc_list_fs_to_storage_name(list, fs_name); + vname = mailbox_list_get_vname(&list->list, storage_name); + + /* list->mailboxes contains proper vnames. fs_vname */ + if (mailbox_tree_lookup(list->mailboxes, vname) == NULL) + (void)fs_list->v.delete_mailbox(fs_list, fs_name); + } T_END; + (void)mailbox_list_iter_deinit(&iter); +} + +static int imapc_list_refresh(struct imapc_mailbox_list *list) +{ + struct imapc_command *cmd; + struct imapc_simple_context ctx; + struct mailbox_node *node; + const char *pattern; + char sep; + + if (imapc_list_try_get_root_sep(list, &sep) < 0) + return -1; + if (list->refreshed_mailboxes) + return 0; + + if (*list->set->imapc_list_prefix == '\0') + pattern = "*"; + else { + /* list "prefix*" instead of "prefix.*". this may return a bit + more than we want, but we're also interested in the flags + of the prefix itself. */ + pattern = t_strdup_printf("%s*", list->set->imapc_list_prefix); + } + + cmd = imapc_list_simple_context_init(&ctx, list); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + imapc_command_sendf(cmd, "LIST \"\" %s", pattern); + mailbox_tree_deinit(&list->mailboxes); + list->mailboxes = mailbox_tree_init(mail_namespace_get_sep(list->list.ns)); + mailbox_tree_set_parents_nonexistent(list->mailboxes); + imapc_simple_run(&ctx, &cmd); + + if ((list->list.ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) { + /* INBOX always exists in IMAP server. since this namespace is + marked with inbox=yes, show the INBOX even if + imapc_list_prefix doesn't match it */ + bool created; + node = mailbox_tree_get(list->mailboxes, "INBOX", &created); + if (*list->set->imapc_list_prefix != '\0') { + /* this listing didn't include the INBOX itself, but + might have included its children. make sure there + aren't any extra flags in it (especially + \NonExistent) */ + node->flags &= MAILBOX_CHILDREN; + } + } + + if (ctx.ret == 0) { + list->refreshed_mailboxes = TRUE; + list->refreshed_mailboxes_recently = TRUE; + list->last_refreshed_mailboxes = ioloop_time; + imapc_list_delete_unused_indexes(list); + } + return ctx.ret; +} + +static void +imapc_list_build_match_tree(struct imapc_mailbox_list_iterate_context *ctx) +{ + struct imapc_mailbox_list *list = + (struct imapc_mailbox_list *)ctx->ctx.list; + struct mailbox_list_iter_update_context update_ctx; + struct mailbox_tree_iterate_context *iter; + struct mailbox_node *node; + const char *vname; + + i_zero(&update_ctx); + update_ctx.iter_ctx = &ctx->ctx; + update_ctx.tree_ctx = ctx->tree; + update_ctx.glob = ctx->ctx.glob; + update_ctx.match_parents = TRUE; + + iter = mailbox_tree_iterate_init(list->mailboxes, NULL, 0); + while ((node = mailbox_tree_iterate_next(iter, &vname)) != NULL) { + update_ctx.leaf_flags = node->flags; + mailbox_list_iter_update(&update_ctx, vname); + } + mailbox_tree_iterate_deinit(&iter); +} + +static struct mailbox_list_iterate_context * +imapc_list_iter_init(struct mailbox_list *_list, const char *const *patterns, + enum mailbox_list_iter_flags flags) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + struct mailbox_list_iterate_context *_ctx; + struct imapc_mailbox_list_iterate_context *ctx; + pool_t pool; + const char *ns_root_name; + char ns_sep; + int ret = 0; + + if ((flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) == 0 || + (flags & MAILBOX_LIST_ITER_RETURN_NO_FLAGS) == 0) + ret = imapc_list_refresh(list); + + list->iter_count++; + + if ((flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) { + /* we're listing only subscriptions. just use the cached + subscriptions list. */ + _ctx = mailbox_list_subscriptions_iter_init(_list, patterns, + flags); + if (ret < 0) + _ctx->failed = TRUE; + return _ctx; + } + + /* if we've already failed, make sure we don't call + mailbox_list_get_hierarchy_sep(), since it clears the error */ + ns_sep = ret < 0 ? '/' : mail_namespace_get_sep(_list->ns); + + pool = pool_alloconly_create("mailbox list imapc iter", 1024); + ctx = p_new(pool, struct imapc_mailbox_list_iterate_context, 1); + ctx->ctx.pool = pool; + ctx->ctx.list = _list; + ctx->ctx.flags = flags; + ctx->ctx.glob = imap_match_init_multiple(pool, patterns, FALSE, ns_sep); + array_create(&ctx->ctx.module_contexts, pool, sizeof(void *), 5); + + ctx->info.ns = _list->ns; + + ctx->tree = mailbox_tree_init(ns_sep); + mailbox_tree_set_parents_nonexistent(ctx->tree); + if (ret == 0) + imapc_list_build_match_tree(ctx); + + if (list->list.ns->prefix_len > 0) { + ns_root_name = t_strndup(_list->ns->prefix, + _list->ns->prefix_len - 1); + ctx->ns_root = mailbox_tree_lookup(ctx->tree, ns_root_name); + } + + ctx->iter = mailbox_tree_iterate_init(ctx->tree, NULL, 0); + if (ret < 0) + ctx->ctx.failed = TRUE; + return &ctx->ctx; +} + +static void +imapc_list_write_special_use(struct imapc_mailbox_list_iterate_context *ctx, + struct mailbox_node *node) +{ + unsigned int i; + + if (ctx->special_use == NULL) + ctx->special_use = str_new(ctx->ctx.pool, 64); + str_truncate(ctx->special_use, 0); + + for (i = 0; i < N_ELEMENTS(imap_list_flags); i++) { + if ((node->flags & imap_list_flags[i].flag) != 0 && + (node->flags & MAILBOX_SPECIALUSE_MASK) != 0) { + str_append(ctx->special_use, imap_list_flags[i].str); + str_append_c(ctx->special_use, ' '); + } + } + + if (str_len(ctx->special_use) > 0) { + str_truncate(ctx->special_use, str_len(ctx->special_use) - 1); + ctx->info.special_use = str_c(ctx->special_use); + } else { + ctx->info.special_use = NULL; + } +} + +static bool +imapc_list_is_ns_root(struct imapc_mailbox_list_iterate_context *ctx, + struct mailbox_node *node) +{ + struct mailbox_node *root_node = ctx->ns_root; + + while (root_node != NULL) { + if (node == root_node) + return TRUE; + root_node = root_node->parent; + } + return FALSE; +} + +static const struct mailbox_info * +imapc_list_iter_next(struct mailbox_list_iterate_context *_ctx) +{ + struct imapc_mailbox_list_iterate_context *ctx = + (struct imapc_mailbox_list_iterate_context *)_ctx; + struct imapc_mailbox_list *list = + (struct imapc_mailbox_list *)_ctx->list; + struct mailbox_node *node; + const char *vname; + + if (_ctx->failed) + return NULL; + + if ((_ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) + return mailbox_list_subscriptions_iter_next(_ctx); + + do { + node = mailbox_tree_iterate_next(ctx->iter, &vname); + if (node == NULL) + return mailbox_list_iter_default_next(_ctx); + } while ((node->flags & MAILBOX_MATCHED) == 0 || + (imapc_list_is_ns_root(ctx, node) && + (strcasecmp(vname, "INBOX") != 0 || + (ctx->info.ns->flags & NAMESPACE_FLAG_INBOX_ANY) == 0))); + + if (ctx->info.ns->prefix_len > 0 && + strcasecmp(vname, "INBOX") != 0 && + strncmp(vname, ctx->info.ns->prefix, ctx->info.ns->prefix_len-1) == 0 && + vname[ctx->info.ns->prefix_len] == '\0' && + list->set->imapc_list_prefix[0] == '\0') { + /* don't return "" name */ + return imapc_list_iter_next(_ctx); + } + + ctx->info.vname = vname; + ctx->info.flags = node->flags; + if ((_ctx->list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) { + /* we're iterating the INBOX namespace. pass through the + SPECIAL-USE flags if they exist. */ + imapc_list_write_special_use(ctx, node); + } else { + ctx->info.special_use = NULL; + } + return &ctx->info; +} + +static int imapc_list_iter_deinit(struct mailbox_list_iterate_context *_ctx) +{ + struct imapc_mailbox_list_iterate_context *ctx = + (struct imapc_mailbox_list_iterate_context *)_ctx; + struct imapc_mailbox_list *list = + (struct imapc_mailbox_list *)_ctx->list; + int ret = _ctx->failed ? -1 : 0; + + i_assert(list->iter_count > 0); + + if (--list->iter_count == 0) { + list->refreshed_mailboxes = FALSE; + list->refreshed_subscriptions = FALSE; + } + + if ((_ctx->flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) + return mailbox_list_subscriptions_iter_deinit(_ctx); + + mailbox_tree_iterate_deinit(&ctx->iter); + mailbox_tree_deinit(&ctx->tree); + pool_unref(&_ctx->pool); + return ret; +} + +static int +imapc_list_subscriptions_refresh(struct mailbox_list *_src_list, + struct mailbox_list *dest_list) +{ + struct imapc_mailbox_list *src_list = + (struct imapc_mailbox_list *)_src_list; + struct imapc_simple_context ctx; + struct imapc_command *cmd; + const char *pattern; + char list_sep, dest_sep = mail_namespace_get_sep(dest_list->ns); + + i_assert(src_list->tmp_subscriptions == NULL); + + if (imapc_list_try_get_root_sep(src_list, &list_sep) < 0) + return -1; + + if (src_list->refreshed_subscriptions) { + if (dest_list->subscriptions == NULL) + dest_list->subscriptions = mailbox_tree_init(dest_sep); + return 0; + } + + src_list->tmp_subscriptions = + mailbox_tree_init(mail_namespace_get_sep(_src_list->ns)); + + cmd = imapc_list_simple_context_init(&ctx, src_list); + if (*src_list->set->imapc_list_prefix == '\0') + pattern = "*"; + else + pattern = t_strdup_printf("%s*", src_list->set->imapc_list_prefix); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + imapc_command_sendf(cmd, "LSUB \"\" %s", pattern); + imapc_simple_run(&ctx, &cmd); + + if (ctx.ret < 0) + return -1; + + /* replace subscriptions tree in destination */ + if (dest_list->subscriptions != NULL) + mailbox_tree_deinit(&dest_list->subscriptions); + dest_list->subscriptions = src_list->tmp_subscriptions; + src_list->tmp_subscriptions = NULL; + mailbox_tree_set_separator(dest_list->subscriptions, dest_sep); + + src_list->refreshed_subscriptions = TRUE; + return 0; +} + +static int imapc_list_set_subscribed(struct mailbox_list *_list, + const char *name, bool set) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + struct imapc_command *cmd; + struct imapc_simple_context ctx; + + cmd = imapc_list_simple_context_init(&ctx, list); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + imapc_command_sendf(cmd, set ? "SUBSCRIBE %s" : "UNSUBSCRIBE %s", + imapc_list_storage_to_remote_name(list, name)); + imapc_simple_run(&ctx, &cmd); + return ctx.ret; +} + +static int +imapc_list_delete_mailbox(struct mailbox_list *_list, const char *name) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + struct mailbox_list *fs_list = imapc_list_get_fs(list); + enum imapc_capability capa; + struct imapc_command *cmd; + struct imapc_simple_context ctx; + + if (imapc_storage_client_handle_auth_failure(list->client)) + return -1; + if (imapc_client_get_capabilities(list->client->client, &capa) < 0) + return -1; + + cmd = imapc_list_simple_context_init(&ctx, list); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + if (!imapc_command_connection_is_selected(cmd)) + imapc_command_abort(&cmd); + else { + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT); + if ((capa & IMAPC_CAPABILITY_UNSELECT) != 0) + imapc_command_sendf(cmd, "UNSELECT"); + else + imapc_command_sendf(cmd, "SELECT \"~~~\""); + imapc_simple_run(&ctx, &cmd); + } + + cmd = imapc_list_simple_context_init(&ctx, list); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + imapc_command_sendf(cmd, "DELETE %s", imapc_list_storage_to_remote_name(list, name)); + imapc_simple_run(&ctx, &cmd); + + if (fs_list != NULL && ctx.ret == 0) { + const char *fs_name = imapc_list_storage_to_fs_name(list, name); + (void)fs_list->v.delete_mailbox(fs_list, fs_name); + } + return ctx.ret; +} + +static int +imapc_list_delete_dir(struct mailbox_list *_list, const char *name) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + struct mailbox_list *fs_list = imapc_list_get_fs(list); + + if (fs_list != NULL) { + const char *fs_name = imapc_list_storage_to_fs_name(list, name); + (void)mailbox_list_delete_dir(fs_list, fs_name); + } + return 0; +} + +static int +imapc_list_delete_symlink(struct mailbox_list *list, + const char *name ATTR_UNUSED) +{ + mailbox_list_set_error(list, MAIL_ERROR_NOTPOSSIBLE, "Not supported"); + return -1; +} + +static int +imapc_list_rename_mailbox(struct mailbox_list *oldlist, const char *oldname, + struct mailbox_list *newlist, const char *newname) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)oldlist; + struct mailbox_list *fs_list = imapc_list_get_fs(list); + struct imapc_command *cmd; + struct imapc_simple_context ctx; + + if (oldlist != newlist) { + mailbox_list_set_error(oldlist, MAIL_ERROR_NOTPOSSIBLE, + "Can't rename mailboxes across storages."); + return -1; + } + + cmd = imapc_list_simple_context_init(&ctx, list); + imapc_command_sendf(cmd, "RENAME %s %s", + imapc_list_storage_to_remote_name(list, oldname), + imapc_list_storage_to_remote_name(list, newname)); + imapc_simple_run(&ctx, &cmd); + if (ctx.ret == 0 && fs_list != NULL && oldlist == newlist) { + const char *old_fs_name = + imapc_list_storage_to_fs_name(list, oldname); + const char *new_fs_name = + imapc_list_storage_to_fs_name(list, newname); + (void)fs_list->v.rename_mailbox(fs_list, old_fs_name, + fs_list, new_fs_name); + } + return ctx.ret; +} + +int imapc_list_get_mailbox_flags(struct mailbox_list *_list, const char *name, + enum mailbox_info_flags *flags_r) +{ + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)_list; + struct mailbox_node *node; + const char *vname; + + vname = mailbox_list_get_vname(_list, name); + if (!list->refreshed_mailboxes_recently) { + if (imapc_list_refresh(list) < 0) + return -1; + i_assert(list->refreshed_mailboxes_recently); + } + + if (list->mailboxes == NULL) { + /* imapc list isn't used, but e.g. LAYOUT=none */ + *flags_r = 0; + return 0; + } + node = mailbox_tree_lookup(list->mailboxes, vname); + if (node == NULL) + *flags_r = MAILBOX_NONEXISTENT; + else + *flags_r = node->flags; + return 0; +} + +struct mailbox_list imapc_mailbox_list = { + .name = MAILBOX_LIST_NAME_IMAPC, + .props = MAILBOX_LIST_PROP_NO_ROOT | MAILBOX_LIST_PROP_AUTOCREATE_DIRS | + MAILBOX_LIST_PROP_NO_LIST_INDEX, + .mailbox_name_max_length = MAILBOX_LIST_NAME_MAX_LENGTH, + + .v = { + .alloc = imapc_list_alloc, + .init = imapc_list_init, + .deinit = imapc_list_deinit, + .get_hierarchy_sep = imapc_list_get_hierarchy_sep, + .get_vname = imapc_list_get_vname, + .get_storage_name = imapc_list_get_storage_name, + .get_path = imapc_list_get_path, + .get_temp_prefix = imapc_list_get_temp_prefix, + .join_refpattern = imapc_list_join_refpattern, + .iter_init = imapc_list_iter_init, + .iter_next = imapc_list_iter_next, + .iter_deinit = imapc_list_iter_deinit, + .subscriptions_refresh = imapc_list_subscriptions_refresh, + .set_subscribed = imapc_list_set_subscribed, + .delete_mailbox = imapc_list_delete_mailbox, + .delete_dir = imapc_list_delete_dir, + .delete_symlink = imapc_list_delete_symlink, + .rename_mailbox = imapc_list_rename_mailbox, + } +}; diff --git a/src/lib-storage/index/imapc/imapc-list.h b/src/lib-storage/index/imapc/imapc-list.h new file mode 100644 index 0000000..11df6b0 --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-list.h @@ -0,0 +1,41 @@ +#ifndef IMAPC_LIST_H +#define IMAPC_LIST_H + +struct imap_arg; + +#include "mailbox-list-private.h" + +#define MAILBOX_LIST_NAME_IMAPC "imapc" + +struct imapc_mailbox_list { + struct mailbox_list list; + const struct imapc_settings *set; + struct imapc_storage_client *client; + struct mailbox_list *index_list; + + /* mailboxes are stored as vnames */ + struct mailbox_tree_context *mailboxes, *tmp_subscriptions; + char root_sep; + time_t last_refreshed_mailboxes; + + unsigned int iter_count; + + /* mailboxes/subscriptions are fully refreshed only during + mailbox list iteration. */ + bool refreshed_subscriptions:1; + bool refreshed_mailboxes:1; + /* mailbox list's "recently refreshed" state is reset by syncing a + mailbox. mainly we use this to cache mailboxes' existence to avoid + issuing a LIST command every time. */ + bool refreshed_mailboxes_recently:1; + bool index_list_failed:1; + bool root_sep_pending:1; +}; + +int imapc_list_get_mailbox_flags(struct mailbox_list *list, const char *name, + enum mailbox_info_flags *flags_r); +int imapc_list_try_get_root_sep(struct imapc_mailbox_list *list, char *sep_r); +const char *imapc_list_storage_to_remote_name(struct imapc_mailbox_list *list, + const char *storage_name); + +#endif diff --git a/src/lib-storage/index/imapc/imapc-mail-fetch.c b/src/lib-storage/index/imapc/imapc-mail-fetch.c new file mode 100644 index 0000000..1b0eee7 --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-mail-fetch.c @@ -0,0 +1,911 @@ +/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "ioloop.h" +#include "istream.h" +#include "istream-concat.h" +#include "istream-header-filter.h" +#include "message-header-parser.h" +#include "imap-arg.h" +#include "imap-util.h" +#include "imap-date.h" +#include "imap-quote.h" +#include "imap-bodystructure.h" +#include "imap-resp-code.h" +#include "imapc-mail.h" +#include "imapc-storage.h" + +static void imapc_mail_set_failure(struct imapc_mail *mail, + const struct imapc_command_reply *reply) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(mail->imail.mail.mail.box); + + mail->last_fetch_reply = p_strdup(mail->imail.mail.pool, reply->text_full); + + switch (reply->state) { + case IMAPC_COMMAND_STATE_OK: + break; + case IMAPC_COMMAND_STATE_NO: + if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_FIX_BROKEN_MAILS)) { + /* fetch-fix-broken-mails feature disabled - + fail any mails with missing replies */ + break; + } + if (reply->resp_text_key != NULL && + (strcasecmp(reply->resp_text_key, IMAP_RESP_CODE_SERVERBUG) == 0 || + strcasecmp(reply->resp_text_key, IMAP_RESP_CODE_LIMIT) == 0)) { + /* this is a temporary error, retrying should work. + Yahoo sends * BYE + + NO [LIMIT] UID FETCH Rate limit hit. */ + } else { + /* hopefully this is a permanent failure */ + mail->fetch_ignore_if_missing = TRUE; + } + break; + case IMAPC_COMMAND_STATE_BAD: + case IMAPC_COMMAND_STATE_DISCONNECTED: + case IMAPC_COMMAND_STATE_AUTH_FAILED: + mail->fetch_failed = TRUE; + break; + } +} + +static void +imapc_mail_fetch_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_fetch_request *request = context; + struct imapc_fetch_request *const *requests; + struct imapc_mail *mail; + struct imapc_mailbox *mbox = NULL; + unsigned int i, count; + + array_foreach_elem(&request->mails, mail) { + i_assert(mail->fetch_count > 0); + imapc_mail_set_failure(mail, reply); + if (--mail->fetch_count == 0) + mail->fetching_fields = 0; + mbox = IMAPC_MAILBOX(mail->imail.mail.mail.box); + } + i_assert(mbox != NULL); + + requests = array_get(&mbox->fetch_requests, &count); + for (i = 0; i < count; i++) { + if (requests[i] == request) { + array_delete(&mbox->fetch_requests, i, 1); + break; + } + } + i_assert(i < count); + + array_free(&request->mails); + i_free(request); + + if (reply->state == IMAPC_COMMAND_STATE_OK) + ; + else if (reply->state == IMAPC_COMMAND_STATE_NO) { + imapc_copy_error_from_reply(mbox->storage, MAIL_ERROR_PARAMS, + reply); + } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) { + /* The disconnection message was already logged */ + mail_storage_set_internal_error(&mbox->storage->storage); + } else { + mailbox_set_critical(&mbox->box, + "imapc: Mail FETCH failed: %s", reply->text_full); + } + imapc_client_stop(mbox->storage->client->client); +} + +static bool +headers_have_subset(const char *const *superset, const char *const *subset) +{ + unsigned int i; + + if (superset == NULL) + return FALSE; + if (subset != NULL) { + for (i = 0; subset[i] != NULL; i++) { + if (!str_array_icase_find(superset, subset[i])) + return FALSE; + } + } + return TRUE; +} + +static const char *const * +headers_merge(pool_t pool, const char *const *h1, const char *const *h2) +{ + ARRAY_TYPE(const_string) headers; + const char *value; + unsigned int i; + + p_array_init(&headers, pool, 16); + if (h1 != NULL) { + for (i = 0; h1[i] != NULL; i++) { + value = p_strdup(pool, h1[i]); + array_push_back(&headers, &value); + } + } + if (h2 != NULL) { + for (i = 0; h2[i] != NULL; i++) { + if (h1 == NULL || !str_array_icase_find(h1, h2[i])) { + value = p_strdup(pool, h2[i]); + array_push_back(&headers, &value); + } + } + } + array_append_zero(&headers); + return array_front(&headers); +} + +static bool +imapc_mail_try_merge_fetch(struct imapc_mailbox *mbox, string_t *str) +{ + const char *s1 = str_c(str); + const char *s2 = str_c(mbox->pending_fetch_cmd); + const char *p1, *p2; + + i_assert(str_begins(s1, "UID FETCH ")); + i_assert(str_begins(s2, "UID FETCH ")); + + /* skip over UID range */ + p1 = strchr(s1+10, ' '); + p2 = strchr(s2+10, ' '); + + if (null_strcmp(p1, p2) != 0) + return FALSE; + /* append the new UID to the pending FETCH UID range */ + str_truncate(str, p1-s1); + str_insert(mbox->pending_fetch_cmd, p2-s2, ","); + str_insert(mbox->pending_fetch_cmd, p2-s2+1, str_c(str) + 10); + return TRUE; +} + +static void +imapc_mail_delayed_send_or_merge(struct imapc_mail *mail, string_t *str) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(mail->imail.mail.mail.box); + + if (mbox->pending_fetch_request != NULL && + !imapc_mail_try_merge_fetch(mbox, str)) { + /* send the previous FETCH and create a new one */ + imapc_mail_fetch_flush(mbox); + } + if (mbox->pending_fetch_request == NULL) { + mbox->pending_fetch_request = + i_new(struct imapc_fetch_request, 1); + i_array_init(&mbox->pending_fetch_request->mails, 4); + i_assert(mbox->pending_fetch_cmd->used == 0); + str_append_str(mbox->pending_fetch_cmd, str); + } + array_push_back(&mbox->pending_fetch_request->mails, &mail); + + if (mbox->to_pending_fetch_send == NULL && + array_count(&mbox->pending_fetch_request->mails) > + mbox->box.storage->set->mail_prefetch_count) { + /* we're now prefetching the maximum number of mails. this + most likely means that we need to flush out the command now + before sending anything else. delay it a little bit though + in case the sending code doesn't actually use + mail_prefetch_count and wants to fetch more. + + note that we don't want to add this timeout too early, + because we want to optimize the maximum number of messages + placed into a single FETCH. even without timeout the command + gets flushed by imapc_mail_fetch() call. */ + mbox->to_pending_fetch_send = + timeout_add_short(0, imapc_mail_fetch_flush, mbox); + } +} + +static int +imapc_mail_send_fetch(struct mail *_mail, enum mail_fetch_field fields, + const char *const *headers) +{ + struct imapc_mail *mail = IMAPC_MAIL(_mail); + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + struct mail_index_view *view; + string_t *str; + uint32_t seq; + unsigned int i; + + i_assert(headers == NULL || + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_HEADERS)); + + if (!mbox->selected) { + mail_storage_set_error(_mail->box->storage, + MAIL_ERROR_NOTPOSSIBLE, "Can't fetch mails before selecting mailbox"); + return -1; + } + + if (!mail_stream_access_start(_mail)) + return -1; + + /* drop any fields that we may already be fetching currently */ + fields &= ENUM_NEGATE(mail->fetching_fields); + if (headers_have_subset(mail->fetching_headers, headers)) + headers = NULL; + if (fields == 0 && headers == NULL) + return mail->fetch_sent ? 0 : 1; + + if (!_mail->saving) { + /* if we already know that the mail is expunged, + don't try to FETCH it */ + view = mbox->delayed_sync_view != NULL ? + mbox->delayed_sync_view : mbox->box.view; + if (!mail_index_lookup_seq(view, _mail->uid, &seq) || + mail_index_is_expunged(view, seq)) { + mail_set_expunged(_mail); + return -1; + } + } else if (mbox->client_box == NULL) { + /* opened as save-only. we'll need to fetch the mail, + so actually SELECT/EXAMINE the mailbox */ + i_assert(mbox->box.opened); + + if (imapc_mailbox_select(mbox) < 0) + return -1; + } + + if ((fields & MAIL_FETCH_STREAM_BODY) != 0) + fields |= MAIL_FETCH_STREAM_HEADER; + + str = t_str_new(64); + str_printfa(str, "UID FETCH %u (", _mail->uid); + if ((fields & MAIL_FETCH_RECEIVED_DATE) != 0) + str_append(str, "INTERNALDATE "); + if ((fields & MAIL_FETCH_SAVE_DATE) != 0) { + i_assert(HAS_ALL_BITS(mbox->capabilities, + IMAPC_CAPABILITY_SAVEDATE)); + str_append(str, "SAVEDATE "); + } + if ((fields & (MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE)) != 0) + str_append(str, "RFC822.SIZE "); + if ((fields & MAIL_FETCH_GUID) != 0) { + str_append(str, mbox->guid_fetch_field_name); + str_append_c(str, ' '); + } + if ((fields & MAIL_FETCH_IMAP_BODY) != 0) + str_append(str, "BODY "); + if ((fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) != 0) + str_append(str, "BODYSTRUCTURE "); + + if ((fields & MAIL_FETCH_STREAM_BODY) != 0) { + if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_ZIMBRA_WORKAROUNDS)) + str_append(str, "BODY.PEEK[] "); + else { + /* BODY.PEEK[] can return different headers than + BODY.PEEK[HEADER] (e.g. invalid 8bit chars replaced + with '?' in HEADER) - this violates IMAP protocol + and messes up dsync since it sometimes fetches the + full body and sometimes only the headers. */ + str_append(str, "BODY.PEEK[HEADER] BODY.PEEK[TEXT] "); + } + fields |= MAIL_FETCH_STREAM_HEADER; + } else if ((fields & MAIL_FETCH_STREAM_HEADER) != 0) + str_append(str, "BODY.PEEK[HEADER] "); + else if (headers != NULL) { + mail->fetching_headers = + headers_merge(mail->imail.mail.data_pool, headers, + mail->fetching_headers); + str_append(str, "BODY.PEEK[HEADER.FIELDS ("); + for (i = 0; mail->fetching_headers[i] != NULL; i++) { + if (i > 0) + str_append_c(str, ' '); + imap_append_astring(str, mail->fetching_headers[i]); + } + str_append(str, ")] "); + mail->header_list_fetched = FALSE; + } + str_truncate(str, str_len(str)-1); + str_append_c(str, ')'); + + mail->fetching_fields |= fields; + mail->fetch_count++; + mail->fetch_sent = FALSE; + mail->fetch_failed = FALSE; + + imapc_mail_delayed_send_or_merge(mail, str); + return 1; +} + +static void imapc_mail_cache_get(struct imapc_mail *mail, + struct imapc_mail_cache *cache) +{ + if (mail->body_fetched) + return; + + if (cache->fd != -1) { + mail->fd = cache->fd; + mail->imail.data.stream = i_stream_create_fd(mail->fd, 0); + cache->fd = -1; + } else if (cache->buf != NULL) { + mail->body = cache->buf; + mail->imail.data.stream = + i_stream_create_from_data(mail->body->data, + mail->body->used); + cache->buf = NULL; + } else { + return; + } + mail->header_fetched = TRUE; + mail->body_fetched = TRUE; + /* The stream was already accessed and now it's cached. + It still needs to be set accessed to avoid assert-crash. */ + mail->imail.mail.mail.mail_stream_accessed = TRUE; + imapc_mail_init_stream(mail); +} + +static enum mail_fetch_field +imapc_mail_get_wanted_fetch_fields(struct imapc_mail *mail) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(mail->imail.mail.mail.box); + struct index_mail_data *data = &mail->imail.data; + enum mail_fetch_field fields = 0; + + if ((data->wanted_fields & MAIL_FETCH_RECEIVED_DATE) != 0 && + data->received_date == (time_t)-1) + fields |= MAIL_FETCH_RECEIVED_DATE; + if ((data->wanted_fields & MAIL_FETCH_SAVE_DATE) != 0 && + data->save_date == (time_t)-1) { + if (HAS_ALL_BITS(mbox->capabilities, IMAPC_CAPABILITY_SAVEDATE)) + fields |= MAIL_FETCH_SAVE_DATE; + else + fields |= MAIL_FETCH_RECEIVED_DATE; + } + if ((data->wanted_fields & (MAIL_FETCH_PHYSICAL_SIZE | + MAIL_FETCH_VIRTUAL_SIZE)) != 0 && + data->physical_size == UOFF_T_MAX && + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE)) + fields |= MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE; + if ((data->wanted_fields & MAIL_FETCH_IMAP_BODY) != 0 && + data->body == NULL && + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE)) + fields |= MAIL_FETCH_IMAP_BODY; + if ((data->wanted_fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) != 0 && + data->bodystructure == NULL && + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE)) + fields |= MAIL_FETCH_IMAP_BODYSTRUCTURE; + if ((data->wanted_fields & MAIL_FETCH_GUID) != 0 && + data->guid == NULL && mbox->guid_fetch_field_name != NULL) + fields |= MAIL_FETCH_GUID; + + if (data->stream == NULL && data->access_part != 0) { + if ((data->access_part & (READ_BODY | PARSE_BODY)) != 0) + fields |= MAIL_FETCH_STREAM_BODY; + fields |= MAIL_FETCH_STREAM_HEADER; + } + return fields; +} + +void imapc_mail_try_init_stream_from_cache(struct imapc_mail *mail) +{ + struct mail *_mail = &mail->imail.mail.mail; + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + + if (mbox->prev_mail_cache.uid == _mail->uid) + imapc_mail_cache_get(mail, &mbox->prev_mail_cache); +} + +bool imapc_mail_prefetch(struct mail *_mail) +{ + struct imapc_mail *mail = IMAPC_MAIL(_mail); + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + struct index_mail_data *data = &mail->imail.data; + enum mail_fetch_field fields; + const char *const *headers = NULL; + + /* try to get as much from cache as possible */ + imapc_mail_update_access_parts(&mail->imail); + /* If mail is already cached we can avoid re-FETCHing the mail. + However, don't initialize the stream if we don't actually want to + access the mail. */ + if (mail->imail.data.access_part != 0) + imapc_mail_try_init_stream_from_cache(mail); + + fields = imapc_mail_get_wanted_fetch_fields(mail); + if (data->wanted_headers != NULL && data->stream == NULL && + (fields & MAIL_FETCH_STREAM_HEADER) == 0 && + !imapc_mail_has_headers_in_cache(&mail->imail, data->wanted_headers)) { + /* fetch specific headers */ + if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_HEADERS)) + headers = data->wanted_headers->name; + else + fields |= MAIL_FETCH_STREAM_HEADER; + } + if (fields != 0 || headers != NULL) T_BEGIN { + if (imapc_mail_send_fetch(_mail, fields, headers) > 0) + mail->imail.data.prefetch_sent = TRUE; + } T_END; + return !mail->imail.data.prefetch_sent; +} + +static bool +imapc_mail_have_fields(struct imapc_mail *imail, enum mail_fetch_field fields) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(imail->imail.mail.mail.box); + + if ((fields & MAIL_FETCH_RECEIVED_DATE) != 0) { + if (imail->imail.data.received_date == (time_t)-1) + return FALSE; + fields &= ENUM_NEGATE(MAIL_FETCH_RECEIVED_DATE); + } + if ((fields & MAIL_FETCH_SAVE_DATE) != 0) { + i_assert(HAS_ALL_BITS(mbox->capabilities, + IMAPC_CAPABILITY_SAVEDATE)); + if (imail->imail.data.save_date == (time_t)-1) + return FALSE; + fields &= ENUM_NEGATE(MAIL_FETCH_SAVE_DATE); + } + if ((fields & (MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE)) != 0) { + if (imail->imail.data.physical_size == UOFF_T_MAX) + return FALSE; + fields &= ENUM_NEGATE(MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE); + } + if ((fields & MAIL_FETCH_GUID) != 0) { + if (imail->imail.data.guid == NULL) + return FALSE; + fields &= ENUM_NEGATE(MAIL_FETCH_GUID); + } + if ((fields & MAIL_FETCH_IMAP_BODY) != 0) { + if (imail->imail.data.body == NULL) + return FALSE; + fields &= ENUM_NEGATE(MAIL_FETCH_IMAP_BODY); + } + if ((fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) != 0) { + if (imail->imail.data.bodystructure == NULL) + return FALSE; + fields &= ENUM_NEGATE(MAIL_FETCH_IMAP_BODYSTRUCTURE); + } + if ((fields & (MAIL_FETCH_STREAM_HEADER | + MAIL_FETCH_STREAM_BODY)) != 0) { + if (imail->imail.data.stream == NULL) + return FALSE; + fields &= ENUM_NEGATE(MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY); + } + i_assert(fields == 0); + return TRUE; +} + +int imapc_mail_fetch(struct mail *_mail, enum mail_fetch_field fields, + const char *const *headers) +{ + struct imapc_mail *imail = IMAPC_MAIL(_mail); + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + int ret; + + if ((fields & MAIL_FETCH_GUID) != 0 && + mbox->guid_fetch_field_name == NULL) { + mail_storage_set_error(_mail->box->storage, + MAIL_ERROR_NOTPOSSIBLE, + "Message GUID not available in this server"); + return -1; + } + if (_mail->saving) { + mail_storage_set_error(_mail->box->storage, + MAIL_ERROR_NOTPOSSIBLE, + "Attempting to issue FETCH for a mail not yet committed"); + return -1; + } + + fields |= imapc_mail_get_wanted_fetch_fields(imail); + T_BEGIN { + ret = imapc_mail_send_fetch(_mail, fields, headers); + } T_END; + if (ret < 0) + return -1; + + /* we'll continue waiting until we've got all the fields we wanted, + or until all FETCH replies have been received (i.e. some FETCHes + failed) */ + if (ret > 0) + imapc_mail_fetch_flush(mbox); + while (imail->fetch_count > 0 && + (!imapc_mail_have_fields(imail, fields) || + !imail->header_list_fetched)) { + imapc_mailbox_run_nofetch(mbox); + } + if (imail->fetch_failed) { + mail_storage_set_internal_error(&mbox->storage->storage); + return -1; + } + return 0; +} + +void imapc_mail_fetch_flush(struct imapc_mailbox *mbox) +{ + struct imapc_command *cmd; + struct imapc_mail *mail; + + if (mbox->pending_fetch_request == NULL) { + i_assert(mbox->to_pending_fetch_send == NULL); + return; + } + + array_foreach_elem(&mbox->pending_fetch_request->mails, mail) + mail->fetch_sent = TRUE; + + cmd = imapc_client_mailbox_cmd(mbox->client_box, + imapc_mail_fetch_callback, + mbox->pending_fetch_request); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + array_push_back(&mbox->fetch_requests, &mbox->pending_fetch_request); + + imapc_command_send(cmd, str_c(mbox->pending_fetch_cmd)); + + mbox->pending_fetch_request = NULL; + timeout_remove(&mbox->to_pending_fetch_send); + str_truncate(mbox->pending_fetch_cmd, 0); +} + +static bool imapc_find_lfile_arg(const struct imapc_untagged_reply *reply, + const struct imap_arg *arg, int *fd_r) +{ + const struct imap_arg *list; + unsigned int i, count; + + for (i = 0; i < reply->file_args_count; i++) { + const struct imapc_arg_file *farg = &reply->file_args[i]; + + if (farg->parent_arg == arg->parent && + imap_arg_get_list_full(arg->parent, &list, &count) && + farg->list_idx < count && &list[farg->list_idx] == arg) { + *fd_r = farg->fd; + return TRUE; + } + } + return FALSE; +} + +static void imapc_stream_filter(struct istream **input) +{ + static const char *imapc_hide_headers[] = { + /* Added by MS Exchange 2010 when \Flagged flag is set. + This violates IMAP guarantee of messages being immutable. */ + "X-Message-Flag" + }; + struct istream *filter_input; + + filter_input = i_stream_create_header_filter(*input, + HEADER_FILTER_EXCLUDE, + imapc_hide_headers, N_ELEMENTS(imapc_hide_headers), + *null_header_filter_callback, NULL); + i_stream_unref(input); + *input = filter_input; +} + +void imapc_mail_init_stream(struct imapc_mail *mail) +{ + struct index_mail *imail = &mail->imail; + struct mail *_mail = &imail->mail.mail; + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + struct istream *input; + uoff_t size; + int ret; + + i_stream_set_name(imail->data.stream, + t_strdup_printf("imapc mail uid=%u", _mail->uid)); + index_mail_set_read_buffer_size(_mail, imail->data.stream); + + if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE)) { + /* enable filtering only when we're not passing through + RFC822.SIZE. otherwise we'll get size mismatches. */ + imapc_stream_filter(&imail->data.stream); + } + if (imail->mail.v.istream_opened != NULL) { + if (imail->mail.v.istream_opened(_mail, + &imail->data.stream) < 0) { + index_mail_close_streams(imail); + return; + } + } + ret = i_stream_get_size(imail->data.stream, TRUE, &size); + if (ret < 0) { + index_mail_close_streams(imail); + return; + } + i_assert(ret != 0); + /* Once message body is fetched, we can be sure of what its size is. + If we had already received RFC822.SIZE, overwrite it here in case + it's wrong. Also in more special cases the RFC822.SIZE may be + smaller than the fetched message header. In this case change the + size as well, otherwise reading via istream-mail will fail. */ + if (mail->body_fetched || imail->data.physical_size < size) { + if (mail->body_fetched) { + imail->data.inexact_total_sizes = FALSE; + /* Don't trust any existing virtual_size. Also don't + set it to size, because there's no guarantees about + the content having proper CRLF newlines, especially + not if istream_opened() has changed the stream. */ + imail->data.virtual_size = UOFF_T_MAX; + } + imail->data.physical_size = size; + } + + imail->data.stream_has_only_header = !mail->body_fetched; + if (index_mail_init_stream(imail, NULL, NULL, &input) < 0) + index_mail_close_streams(imail); +} + +static void +imapc_fetch_stream(struct imapc_mail *mail, + const struct imapc_untagged_reply *reply, + const struct imap_arg *arg, + bool have_header, bool have_body) +{ + struct index_mail *imail = &mail->imail; + struct imapc_mailbox *mbox = IMAPC_MAILBOX(imail->mail.mail.box); + struct istream *hdr_stream = NULL; + const char *value; + int fd; + + if (imail->data.stream != NULL) { + i_assert(mail->header_fetched); + if (mail->body_fetched || !have_body) + return; + if (have_header) { + /* replace the existing stream */ + } else if (mail->fd == -1) { + /* append this body stream to the existing + header stream */ + hdr_stream = imail->data.stream; + i_stream_ref(hdr_stream); + } else { + /* append this body stream to the existing + header stream. we'll need to recreate the stream + with autoclosed fd. */ + if (lseek(mail->fd, 0, SEEK_SET) < 0) + i_error("lseek(imapc) failed: %m"); + hdr_stream = i_stream_create_fd_autoclose(&mail->fd, 0); + } + index_mail_close_streams(imail); + i_close_fd(&mail->fd); + } else { + if (!have_header) { + /* BODY.PEEK[TEXT] received - we can't currently handle + this before receiving BODY.PEEK[HEADER] reply */ + return; + } + } + + if (arg->type == IMAP_ARG_LITERAL_SIZE) { + if (!imapc_find_lfile_arg(reply, arg, &fd)) { + i_stream_unref(&hdr_stream); + return; + } + if ((fd = dup(fd)) == -1) { + i_error("dup() failed: %m"); + i_stream_unref(&hdr_stream); + return; + } + mail->fd = fd; + imail->data.stream = i_stream_create_fd(fd, 0); + } else { + if (!imap_arg_get_nstring(arg, &value)) + value = NULL; + if (value == NULL || + (value[0] == '\0' && + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_EMPTY_IS_EXPUNGED))) { + mail_set_expunged(&imail->mail.mail); + i_stream_unref(&hdr_stream); + return; + } + if (mail->body == NULL) { + mail->body = buffer_create_dynamic(default_pool, + arg->str_len + 1); + } else if (!have_header && hdr_stream != NULL) { + /* header is already in the buffer - add body now + without destroying the existing header data */ + i_stream_unref(&hdr_stream); + } else { + buffer_set_used_size(mail->body, 0); + } + buffer_append(mail->body, value, arg->str_len); + imail->data.stream = i_stream_create_from_data(mail->body->data, + mail->body->used); + } + if (have_header) + mail->header_fetched = TRUE; + mail->body_fetched = have_body; + + if (hdr_stream != NULL) { + struct istream *inputs[3]; + + inputs[0] = hdr_stream; + inputs[1] = imail->data.stream; + inputs[2] = NULL; + imail->data.stream = i_stream_create_concat(inputs); + i_stream_unref(&inputs[0]); + i_stream_unref(&inputs[1]); + } + + imapc_mail_init_stream(mail); +} + +static void +imapc_fetch_header_stream(struct imapc_mail *mail, + const struct imapc_untagged_reply *reply, + const struct imap_arg *args) +{ + const enum message_header_parser_flags hdr_parser_flags = + MESSAGE_HEADER_PARSER_FLAG_SKIP_INITIAL_LWSP | + MESSAGE_HEADER_PARSER_FLAG_DROP_CR; + const struct imap_arg *hdr_list; + struct mailbox_header_lookup_ctx *headers_ctx; + struct message_header_parser_ctx *parser; + struct message_header_line *hdr; + struct istream *input; + ARRAY_TYPE(const_string) hdr_arr; + const char *value; + int ret, fd; + + if (!imap_arg_get_list(args, &hdr_list)) + return; + if (!imap_arg_atom_equals(args+1, "]")) + return; + args += 2; + + /* see if this is reply to the latest headers list request + (parse it even if it's not) */ + t_array_init(&hdr_arr, 16); + while (imap_arg_get_astring(hdr_list, &value)) { + array_push_back(&hdr_arr, &value); + hdr_list++; + } + if (hdr_list->type != IMAP_ARG_EOL) + return; + array_append_zero(&hdr_arr); + + if (headers_have_subset(array_front(&hdr_arr), mail->fetching_headers)) + mail->header_list_fetched = TRUE; + + if (args->type == IMAP_ARG_LITERAL_SIZE) { + if (!imapc_find_lfile_arg(reply, args, &fd)) + return; + input = i_stream_create_fd(fd, 0); + } else { + if (!imap_arg_get_nstring(args, &value)) + return; + if (value == NULL) { + mail_set_expunged(&mail->imail.mail.mail); + return; + } + input = i_stream_create_from_data(value, args->str_len); + } + + headers_ctx = mailbox_header_lookup_init(mail->imail.mail.mail.box, + array_front(&hdr_arr)); + index_mail_parse_header_init(&mail->imail, headers_ctx); + + parser = message_parse_header_init(input, NULL, hdr_parser_flags); + while ((ret = message_parse_header_next(parser, &hdr)) > 0) + index_mail_parse_header(NULL, hdr, &mail->imail); + i_assert(ret != 0); + index_mail_parse_header(NULL, NULL, &mail->imail); + message_parse_header_deinit(&parser); + + mailbox_header_lookup_unref(&headers_ctx); + i_stream_destroy(&input); +} + +static const char * +imapc_args_to_bodystructure(struct imapc_mail *mail, + const struct imap_arg *list_arg, bool extended) +{ + const struct imap_arg *args; + struct message_part *parts = NULL; + const char *ret, *error; + pool_t pool; + + if (!imap_arg_get_list(list_arg, &args)) { + mail_set_critical(&mail->imail.mail.mail, + "imapc: Server sent invalid BODYSTRUCTURE parameters"); + return NULL; + } + + pool = pool_alloconly_create("imap bodystructure", 1024); + if (imap_bodystructure_parse_args(args, pool, &parts, &error) < 0) { + mail_set_critical(&mail->imail.mail.mail, + "imapc: Server sent invalid BODYSTRUCTURE: %s", error); + ret = NULL; + } else { + string_t *str = t_str_new(128); + if (imap_bodystructure_write(parts, str, extended, &error) < 0) { + /* All the input to imap_bodystructure_write() came + from imap_bodystructure_parse_args(). We should never + get here. Instead, if something is wrong the + parsing should have returned an error already. */ + str_truncate(str, 0); + imap_write_args(str, args); + i_panic("Failed to write parsed BODYSTRUCTURE: %s " + "(original string: '%s')", error, str_c(str)); + } + ret = p_strdup(mail->imail.mail.data_pool, str_c(str)); + } + pool_unref(&pool); + return ret; +} + +void imapc_mail_fetch_update(struct imapc_mail *mail, + const struct imapc_untagged_reply *reply, + const struct imap_arg *args) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(mail->imail.mail.mail.box); + const char *key, *value; + unsigned int i; + uoff_t size; + time_t t; + int tz; + bool match = FALSE; + + for (i = 0; args[i].type != IMAP_ARG_EOL; i += 2) { + if (!imap_arg_get_atom(&args[i], &key) || + args[i+1].type == IMAP_ARG_EOL) + break; + + if (strcasecmp(key, "BODY[]") == 0) { + imapc_fetch_stream(mail, reply, &args[i+1], TRUE, TRUE); + match = TRUE; + } else if (strcasecmp(key, "BODY[HEADER]") == 0) { + imapc_fetch_stream(mail, reply, &args[i+1], TRUE, FALSE); + match = TRUE; + } else if (strcasecmp(key, "BODY[TEXT]") == 0) { + imapc_fetch_stream(mail, reply, &args[i+1], FALSE, TRUE); + match = TRUE; + } else if (strcasecmp(key, "BODY[HEADER.FIELDS") == 0) { + imapc_fetch_header_stream(mail, reply, &args[i+1]); + match = TRUE; + } else if (strcasecmp(key, "INTERNALDATE") == 0) { + if (imap_arg_get_astring(&args[i+1], &value) && + imap_parse_datetime(value, &t, &tz)) { + mail->imail.data.received_date = t; + if (HAS_NO_BITS(mbox->capabilities, + IMAPC_CAPABILITY_SAVEDATE)) + mail->imail.data.save_date = t; + } + match = TRUE; + } else if (strcasecmp(key, "SAVEDATE") == 0) { + if (imap_arg_get_astring(&args[i+1], &value)) { + if (strcasecmp(value, "NIL") == 0) + mail->imail.data.save_date = 0; + else if (imap_parse_datetime(value, &t, &tz)) + mail->imail.data.save_date = t; + } + match = TRUE; + } else if (strcasecmp(key, "BODY") == 0) { + if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE)) { + mail->imail.data.body = + imapc_args_to_bodystructure(mail, &args[i+1], FALSE); + } + match = TRUE; + } else if (strcasecmp(key, "BODYSTRUCTURE") == 0) { + if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE)) { + mail->imail.data.bodystructure = + imapc_args_to_bodystructure(mail, &args[i+1], TRUE); + } + match = TRUE; + } else if (strcasecmp(key, "RFC822.SIZE") == 0) { + if (imap_arg_get_atom(&args[i+1], &value) && + str_to_uoff(value, &size) == 0 && + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE)) { + mail->imail.data.physical_size = size; + mail->imail.data.virtual_size = size; + mail->imail.data.inexact_total_sizes = TRUE; + } + match = TRUE; + } else if (strcasecmp(key, "X-GM-MSGID") == 0 || + strcasecmp(key, "X-GUID") == 0) { + if (imap_arg_get_astring(&args[i+1], &value)) { + mail->imail.data.guid = + p_strdup(mail->imail.mail.pool, value); + } + match = TRUE; + } + } + if (!match) { + /* this is only a FETCH FLAGS update for the wanted mail */ + } else { + imapc_client_stop(mbox->storage->client->client); + } +} diff --git a/src/lib-storage/index/imapc/imapc-mail.c b/src/lib-storage/index/imapc/imapc-mail.c new file mode 100644 index 0000000..1ecf03e --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-mail.c @@ -0,0 +1,675 @@ +/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "hex-binary.h" +#include "sha1.h" +#include "istream.h" +#include "message-part-data.h" +#include "imap-envelope.h" +#include "imapc-msgmap.h" +#include "imapc-mail.h" +#include "imapc-storage.h" + +static bool imapc_mail_get_cached_guid(struct mail *_mail); + +struct mail * +imapc_mail_alloc(struct mailbox_transaction_context *t, + enum mail_fetch_field wanted_fields, + struct mailbox_header_lookup_ctx *wanted_headers) +{ + struct imapc_mail *mail; + pool_t pool; + + pool = pool_alloconly_create("mail", 2048); + mail = p_new(pool, struct imapc_mail, 1); + mail->fd = -1; + + index_mail_init(&mail->imail, t, wanted_fields, wanted_headers, pool, NULL); + return &mail->imail.mail.mail; +} + +static bool imapc_mail_is_expunged(struct mail *_mail) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + struct imapc_msgmap *msgmap; + uint32_t lseq, rseq; + + if (!mbox->initial_sync_done) { + /* unknown at this point */ + return FALSE; + } + + if (mbox->sync_view != NULL) { + /* check if another session has already expunged it */ + if (!mail_index_lookup_seq(mbox->sync_view, _mail->uid, &lseq)) + return TRUE; + } + + /* check if we've received EXPUNGE for it */ + msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box); + if (!imapc_msgmap_uid_to_rseq(msgmap, _mail->uid, &rseq)) + return TRUE; + + /* we may be running against a server that hasn't bothered sending + us an EXPUNGE. see if NOOP sends it. */ + imapc_mailbox_noop(mbox); + if (!mbox->initial_sync_done) { + /* NOOP caused a reconnection and desync */ + return FALSE; + } + + return !imapc_msgmap_uid_to_rseq(msgmap, _mail->uid, &rseq); +} + +static int imapc_mail_failed(struct mail *mail, const char *field) +{ + struct imapc_mail *imail = IMAPC_MAIL(mail); + struct imapc_mailbox *mbox = IMAPC_MAILBOX(mail->box); + bool fix_broken_mail = FALSE; + + if (mail->expunged || imapc_mail_is_expunged(mail)) { + mail_set_expunged(mail); + } else if (!imapc_client_mailbox_is_opened(mbox->client_box)) { + /* we've already logged a disconnection error */ + mail_storage_set_internal_error(mail->box->storage); + } else { + /* By default we'll assume that this is a critical failure, + because we don't want to lose any data. We can be here + either because it's a temporary failure on the server or + it's a permanent failure. Unfortunately we can't know + which case it is, so permanent failures need to be worked + around by setting imapc_features=fetch-fix-broken-mails. + + One reason for permanent failures was that earlier Exchange + versions failed to return any data for messages in Calendars + mailbox. This seems to be fixed in newer versions. + */ + fix_broken_mail = imail->fetch_ignore_if_missing; + mail_set_critical(mail, + "imapc: Remote server didn't send %s%s (FETCH replied: %s)", + field, fix_broken_mail ? " - treating it as empty" : "", + imail->last_fetch_reply); + } + return fix_broken_mail ? 0 : -1; +} + +static uint64_t imapc_mail_get_modseq(struct mail *_mail) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + struct imapc_msgmap *msgmap; + const uint64_t *modseqs; + unsigned int count; + uint32_t rseq; + + if (!imapc_mailbox_has_modseqs(mbox)) + return index_mail_get_modseq(_mail); + + msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box); + if (imapc_msgmap_uid_to_rseq(msgmap, _mail->uid, &rseq)) { + modseqs = array_get(&mbox->rseq_modseqs, &count); + if (rseq <= count) + return modseqs[rseq-1]; + } + return 1; /* unknown modseq */ +} + +static int imapc_mail_get_received_date(struct mail *_mail, time_t *date_r) +{ + struct index_mail *mail = INDEX_MAIL(_mail); + struct index_mail_data *data = &mail->data; + + if (index_mail_get_received_date(_mail, date_r) == 0) + return 0; + + if (data->received_date == (time_t)-1) { + if (imapc_mail_fetch(_mail, MAIL_FETCH_RECEIVED_DATE, NULL) < 0) + return -1; + if (data->received_date == (time_t)-1) { + if (imapc_mail_failed(_mail, "INTERNALDATE") < 0) + return -1; + /* assume that the server never returns INTERNALDATE + for this mail (see BODY[] failure handling) */ + data->received_date = 0; + } + } + *date_r = data->received_date; + return 0; +} + +static int imapc_mail_get_save_date(struct mail *_mail, time_t *date_r) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + struct index_mail *mail = INDEX_MAIL(_mail); + struct index_mail_data *data = &mail->data; + + if (data->save_date != 0 && index_mail_get_save_date(_mail, date_r) > 0) + return 1; + + if (HAS_NO_BITS(mbox->capabilities, IMAPC_CAPABILITY_SAVEDATE)) { + data->save_date = 0; + } else if (data->save_date == (time_t)-1) { + if (imapc_mail_fetch(_mail, MAIL_FETCH_SAVE_DATE, NULL) < 0) + return -1; + if (data->save_date == (time_t)-1 && + imapc_mail_failed(_mail, "SAVEDATE") < 0) + return -1; + } + if (data->save_date == (time_t)-1 || data->save_date == 0) { + if (imapc_mail_get_received_date(_mail, date_r) < 0) + return -1; + return 0; + } + *date_r = data->save_date; + return 1; +} + +static int imapc_mail_get_physical_size(struct mail *_mail, uoff_t *size_r) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + struct index_mail *mail = INDEX_MAIL(_mail); + struct index_mail_data *data = &mail->data; + struct istream *input; + uoff_t old_offset; + int ret; + + if (data->physical_size == UOFF_T_MAX) + (void)index_mail_get_physical_size(_mail, size_r); + if (data->physical_size != UOFF_T_MAX) { + *size_r = data->physical_size; + return 0; + } + + if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE) && + data->stream == NULL) { + /* Trust RFC822.SIZE to be correct enough to present to the + IMAP client. However, it can be wrong in some implementation + so try not to trust it too much. */ + if (imapc_mail_fetch(_mail, MAIL_FETCH_PHYSICAL_SIZE, NULL) < 0) + return -1; + if (data->physical_size == UOFF_T_MAX) { + if (imapc_mail_failed(_mail, "RFC822.SIZE") < 0) + return -1; + /* assume that the server never returns RFC822.SIZE + for this mail (see BODY[] failure handling) */ + data->physical_size = 0; + } + *size_r = data->physical_size; + return 0; + } + + old_offset = data->stream == NULL ? 0 : data->stream->v_offset; + if (mail_get_stream(_mail, NULL, NULL, &input) < 0) + return -1; + i_assert(data->stream != NULL); + i_stream_seek(data->stream, old_offset); + + ret = i_stream_get_size(data->stream, TRUE, + &data->physical_size); + if (ret <= 0) { + i_assert(ret != 0); + mail_set_critical(_mail, "imapc: stat(%s) failed: %m", + i_stream_get_name(data->stream)); + return -1; + } + *size_r = data->physical_size; + return 0; +} + +static int imapc_mail_get_virtual_size(struct mail *_mail, uoff_t *size_r) +{ + struct index_mail *mail = INDEX_MAIL(_mail); + struct index_mail_data *data = &mail->data; + + if (imapc_mail_get_physical_size(_mail, size_r) < 0) + return -1; + data->virtual_size = data->physical_size; + return 0; +} + +static int +imapc_mail_get_header_stream(struct mail *_mail, + struct mailbox_header_lookup_ctx *headers, + struct istream **stream_r) +{ + struct imapc_mail *mail = IMAPC_MAIL(_mail); + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + enum mail_lookup_abort old_abort = _mail->lookup_abort; + int ret; + + if (mail->imail.data.access_part != 0 || + !IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_HEADERS)) { + /* we're going to be reading the header/body anyway */ + return index_mail_get_header_stream(_mail, headers, stream_r); + } + + /* see if the wanted headers are already in cache */ + _mail->lookup_abort = MAIL_LOOKUP_ABORT_READ_MAIL; + ret = index_mail_get_header_stream(_mail, headers, stream_r); + _mail->lookup_abort = old_abort; + if (ret == 0) + return 0; + + /* fetch only the wanted headers */ + if (imapc_mail_fetch(_mail, 0, headers->name) < 0) + return -1; + /* the headers should cached now. */ + return index_mail_get_header_stream(_mail, headers, stream_r); +} + +static int +imapc_mail_get_headers(struct mail *_mail, const char *field, + bool decode_to_utf8, const char *const **value_r) +{ + struct mailbox_header_lookup_ctx *headers; + const char *header_names[2]; + const unsigned char *data; + size_t size; + struct istream *input; + int ret; + + header_names[0] = field; + header_names[1] = NULL; + headers = mailbox_header_lookup_init(_mail->box, header_names); + ret = mail_get_header_stream(_mail, headers, &input); + mailbox_header_lookup_unref(&headers); + if (ret < 0) + return -1; + + while (i_stream_read_more(input, &data, &size) > 0) + i_stream_skip(input, size); + /* the header should cached now. */ + return index_mail_get_headers(_mail, field, decode_to_utf8, value_r); +} + +static int +imapc_mail_get_first_header(struct mail *_mail, const char *field, + bool decode_to_utf8, const char **value_r) +{ + const char *const *values; + int ret; + + ret = imapc_mail_get_headers(_mail, field, decode_to_utf8, &values); + if (ret <= 0) + return ret; + *value_r = values[0]; + return 1; +} + +static int +imapc_mail_get_stream(struct mail *_mail, bool get_body, + struct message_size *hdr_size, + struct message_size *body_size, struct istream **stream_r) +{ + struct imapc_mail *mail = IMAPC_MAIL(_mail); + struct index_mail_data *data = &mail->imail.data; + enum mail_fetch_field fetch_field; + + if (get_body && !mail->body_fetched && + mail->imail.data.stream != NULL) { + /* we've fetched the header, but we need the body now too */ + index_mail_close_streams(&mail->imail); + /* don't re-use any cached header sizes. we may be + intentionally downloading the full body because the header + wasn't returned correctly (e.g. pop3-migration does this) */ + data->hdr_size_set = FALSE; + } + + /* See if we can get it from cache. If the wanted_fields/headers are + set properly, this is usually already done by prefetching. */ + imapc_mail_try_init_stream_from_cache(mail); + + if (data->stream == NULL) { + if (!data->initialized) { + /* coming here from mail_set_seq() */ + mail_set_aborted(_mail); + return -1; + } + if (_mail->expunged) { + /* We already detected that the mail is expunged. + Don't spend time trying to FETCH it again. */ + mail_set_expunged(_mail); + return -1; + } + fetch_field = get_body || + (data->access_part & READ_BODY) != 0 ? + MAIL_FETCH_STREAM_BODY : MAIL_FETCH_STREAM_HEADER; + if (imapc_mail_fetch(_mail, fetch_field, NULL) < 0) + return -1; + + if (data->stream == NULL) { + if (imapc_mail_failed(_mail, "BODY[]") < 0) + return -1; + i_assert(data->stream == NULL); + + /* return the broken email as empty */ + mail->body_fetched = TRUE; + data->stream = i_stream_create_from_data(NULL, 0); + imapc_mail_init_stream(mail); + } + } + + return index_mail_init_stream(&mail->imail, hdr_size, body_size, + stream_r); +} + +bool imapc_mail_has_headers_in_cache(struct index_mail *mail, + struct mailbox_header_lookup_ctx *headers) +{ + struct mail *_mail = &mail->mail.mail; + unsigned int i; + + for (i = 0; i < headers->count; i++) { + if (mail_cache_field_exists(_mail->transaction->cache_view, + _mail->seq, headers->idx[i]) <= 0) + return FALSE; + } + return TRUE; +} + +void imapc_mail_update_access_parts(struct index_mail *mail) +{ + struct mail *_mail = &mail->mail.mail; + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + struct index_mail_data *data = &mail->data; + struct mailbox_header_lookup_ctx *header_ctx; + const char *str; + time_t date; + uoff_t size; + + if ((data->wanted_fields & MAIL_FETCH_RECEIVED_DATE) != 0) + (void)index_mail_get_received_date(_mail, &date); + if ((data->wanted_fields & MAIL_FETCH_SAVE_DATE) != 0) { + if (index_mail_get_save_date(_mail, &date) < 0 && + HAS_NO_BITS(mbox->capabilities, + IMAPC_CAPABILITY_SAVEDATE)) { + (void)index_mail_get_received_date(_mail, &date); + data->save_date = data->received_date; + } + } + if ((data->wanted_fields & (MAIL_FETCH_PHYSICAL_SIZE | + MAIL_FETCH_VIRTUAL_SIZE)) != 0) { + if (index_mail_get_physical_size(_mail, &size) < 0 && + !IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE)) + data->access_part |= READ_HDR | READ_BODY; + } + if ((data->wanted_fields & MAIL_FETCH_GUID) != 0) + (void)imapc_mail_get_cached_guid(_mail); + if ((data->wanted_fields & MAIL_FETCH_IMAP_BODY) != 0) + (void)index_mail_get_cached_body(mail, &str); + if ((data->wanted_fields & MAIL_FETCH_IMAP_BODYSTRUCTURE) != 0) + (void)index_mail_get_cached_bodystructure(mail, &str); + + if (data->access_part == 0 && data->wanted_headers != NULL && + !IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_HEADERS)) { + /* see if all wanted headers exist in cache */ + if (!imapc_mail_has_headers_in_cache(mail, data->wanted_headers)) + data->access_part |= PARSE_HDR; + } + if (data->access_part == 0 && + (data->wanted_fields & MAIL_FETCH_IMAP_ENVELOPE) != 0 && + !IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_HEADERS)) { + /* the common code already checked this partially, + but we need a guaranteed correct answer */ + header_ctx = mailbox_header_lookup_init(_mail->box, + message_part_envelope_headers); + if (!imapc_mail_has_headers_in_cache(mail, header_ctx)) + data->access_part |= PARSE_HDR; + mailbox_header_lookup_unref(&header_ctx); + } +} + +static void imapc_mail_set_seq(struct mail *_mail, uint32_t seq, bool saving) +{ + struct imapc_mail *imail = IMAPC_MAIL(_mail); + struct index_mail *mail = &imail->imail; + struct imapc_mailbox *mbox = (struct imapc_mailbox *)_mail->box; + + index_mail_set_seq(_mail, seq, saving); + if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_RFC822_SIZE)) { + /* RFC822.SIZE may be read from vsize record or cache. It may + not be exactly correct. */ + mail->data.inexact_total_sizes = TRUE; + } + + /* searching code handles prefetching internally, + elsewhere we want to do it immediately */ + if (!mail->mail.search_mail && !_mail->saving) + (void)imapc_mail_prefetch(_mail); +} + +static void +imapc_mail_add_temp_wanted_fields(struct mail *_mail, + enum mail_fetch_field fields, + struct mailbox_header_lookup_ctx *headers) +{ + struct index_mail *mail = INDEX_MAIL(_mail); + + index_mail_add_temp_wanted_fields(_mail, fields, headers); + if (_mail->seq != 0) + imapc_mail_update_access_parts(mail); +} + +static void imapc_mail_close(struct mail *_mail) +{ + struct imapc_mail *mail = IMAPC_MAIL(_mail); + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + struct imapc_mail_cache *cache = &mbox->prev_mail_cache; + + if (mail->fetch_count > 0) { + imapc_mail_fetch_flush(mbox); + while (mail->fetch_count > 0) + imapc_mailbox_run_nofetch(mbox); + } + + index_mail_close(_mail); + + mail->fetching_headers = NULL; + if (mail->body_fetched) { + imapc_mail_cache_free(cache); + cache->uid = _mail->uid; + if (mail->fd != -1) { + cache->fd = mail->fd; + mail->fd = -1; + } else { + cache->buf = mail->body; + mail->body = NULL; + } + } + i_close_fd(&mail->fd); + buffer_free(&mail->body); + mail->header_fetched = FALSE; + mail->body_fetched = FALSE; + + i_assert(mail->fetch_count == 0); +} + +static int imapc_mail_get_hdr_hash(struct index_mail *imail) +{ + struct istream *input; + const unsigned char *data; + size_t size; + uoff_t old_offset; + struct sha1_ctxt sha1_ctx; + unsigned char sha1_output[SHA1_RESULTLEN]; + const char *sha1_str; + + sha1_init(&sha1_ctx); + old_offset = imail->data.stream == NULL ? 0 : + imail->data.stream->v_offset; + if (mail_get_hdr_stream(&imail->mail.mail, NULL, &input) < 0) + return -1; + i_assert(imail->data.stream != NULL); + while (i_stream_read_more(input, &data, &size) > 0) { + sha1_loop(&sha1_ctx, data, size); + i_stream_skip(input, size); + } + i_stream_seek(imail->data.stream, old_offset); + sha1_result(&sha1_ctx, sha1_output); + + sha1_str = binary_to_hex(sha1_output, sizeof(sha1_output)); + imail->data.guid = p_strdup(imail->mail.data_pool, sha1_str); + return 0; +} + +static bool imapc_mail_get_cached_guid(struct mail *_mail) +{ + struct index_mail *imail = INDEX_MAIL(_mail); + const enum index_cache_field cache_idx = + imail->ibox->cache_fields[MAIL_CACHE_GUID].idx; + string_t *str; + + if (imail->data.guid != NULL) { + if (mail_cache_field_can_add(_mail->transaction->cache_trans, + _mail->seq, cache_idx)) { + /* GUID was prefetched - add to cache */ + index_mail_cache_add_idx(imail, cache_idx, + imail->data.guid, strlen(imail->data.guid)); + } + return TRUE; + } + + str = str_new(imail->mail.data_pool, 64); + if (mail_cache_lookup_field(_mail->transaction->cache_view, + str, imail->mail.mail.seq, cache_idx) > 0) { + imail->data.guid = str_c(str); + return TRUE; + } + return FALSE; +} + +static int imapc_mail_get_guid(struct mail *_mail, const char **value_r) +{ + struct index_mail *imail = INDEX_MAIL(_mail); + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + const enum index_cache_field cache_idx = + imail->ibox->cache_fields[MAIL_CACHE_GUID].idx; + + if (imapc_mail_get_cached_guid(_mail)) { + *value_r = imail->data.guid; + return 0; + } + + /* GUID not in cache, fetch it */ + if (mbox->guid_fetch_field_name != NULL) { + if (imapc_mail_fetch(_mail, MAIL_FETCH_GUID, NULL) < 0) + return -1; + if (imail->data.guid == NULL) { + (void)imapc_mail_failed(_mail, mbox->guid_fetch_field_name); + return -1; + } + } else { + /* use hash of message headers as the GUID */ + if (imapc_mail_get_hdr_hash(imail) < 0) + return -1; + } + + index_mail_cache_add_idx(imail, cache_idx, + imail->data.guid, strlen(imail->data.guid)); + *value_r = imail->data.guid; + return 0; +} + +static int +imapc_mail_get_special(struct mail *_mail, enum mail_fetch_field field, + const char **value_r) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(_mail->box); + struct index_mail *imail = INDEX_MAIL(_mail); + uint64_t num; + + switch (field) { + case MAIL_FETCH_GUID: + if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_GUID_FORCED) && + mbox->guid_fetch_field_name == NULL) { + /* GUIDs not supported by server */ + break; + } + *value_r = ""; + return imapc_mail_get_guid(_mail, value_r); + case MAIL_FETCH_UIDL_BACKEND: + if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_GMAIL_MIGRATION)) + break; + if (imapc_mail_get_guid(_mail, value_r) < 0) + return -1; + if (str_to_uint64(*value_r, &num) < 0) { + mail_set_critical(_mail, + "X-GM-MSGID not 64bit integer as expected for POP3 UIDL generation: %s", *value_r); + return -1; + } + + *value_r = p_strdup_printf(imail->mail.data_pool, + "GmailId%"PRIx64, num); + return 0; + case MAIL_FETCH_IMAP_BODY: + if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE)) + break; + + if (index_mail_get_cached_body(imail, value_r)) + return 0; + if (imapc_mail_fetch(_mail, field, NULL) < 0) + return -1; + if (imail->data.body == NULL) { + (void)imapc_mail_failed(_mail, "BODY"); + return -1; + } + *value_r = imail->data.body; + return 0; + case MAIL_FETCH_IMAP_BODYSTRUCTURE: + if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_BODYSTRUCTURE)) + break; + + if (index_mail_get_cached_bodystructure(imail, value_r)) + return 0; + if (imapc_mail_fetch(_mail, field, NULL) < 0) + return -1; + if (imail->data.bodystructure == NULL) { + (void)imapc_mail_failed(_mail, "BODYSTRUCTURE"); + return -1; + } + *value_r = imail->data.bodystructure; + return 0; + default: + break; + } + + return index_mail_get_special(_mail, field, value_r); +} + +struct mail_vfuncs imapc_mail_vfuncs = { + imapc_mail_close, + index_mail_free, + imapc_mail_set_seq, + index_mail_set_uid, + index_mail_set_uid_cache_updates, + imapc_mail_prefetch, + index_mail_precache, + imapc_mail_add_temp_wanted_fields, + + index_mail_get_flags, + index_mail_get_keywords, + index_mail_get_keyword_indexes, + imapc_mail_get_modseq, + index_mail_get_pvt_modseq, + index_mail_get_parts, + index_mail_get_date, + imapc_mail_get_received_date, + imapc_mail_get_save_date, + imapc_mail_get_virtual_size, + imapc_mail_get_physical_size, + imapc_mail_get_first_header, + imapc_mail_get_headers, + imapc_mail_get_header_stream, + imapc_mail_get_stream, + index_mail_get_binary_stream, + imapc_mail_get_special, + index_mail_get_backend_mail, + index_mail_update_flags, + index_mail_update_keywords, + index_mail_update_modseq, + index_mail_update_pvt_modseq, + NULL, + index_mail_expunge, + index_mail_set_cache_corrupted, + index_mail_opened, +}; diff --git a/src/lib-storage/index/imapc/imapc-mail.h b/src/lib-storage/index/imapc/imapc-mail.h new file mode 100644 index 0000000..52dfe5e --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-mail.h @@ -0,0 +1,51 @@ +#ifndef IMAPC_MAIL_H +#define IMAPC_MAIL_H + +#include "index-mail.h" + +struct imap_arg; +struct imapc_untagged_reply; +struct imapc_mailbox; + +struct imapc_mail { + struct index_mail imail; + + enum mail_fetch_field fetching_fields; + const char *const *fetching_headers; + unsigned int fetch_count; + bool fetch_sent; + const char *last_fetch_reply; + + int fd; + buffer_t *body; + bool header_fetched; + bool body_fetched; + bool header_list_fetched; + bool fetch_ignore_if_missing; + bool fetch_failed; +}; + +#define IMAPC_MAIL(s) container_of(s, struct imapc_mail, imail.mail.mail) + +extern struct mail_vfuncs imapc_mail_vfuncs; + +struct mail * +imapc_mail_alloc(struct mailbox_transaction_context *t, + enum mail_fetch_field wanted_fields, + struct mailbox_header_lookup_ctx *wanted_headers); +int imapc_mail_fetch(struct mail *mail, enum mail_fetch_field fields, + const char *const *headers); +void imapc_mail_try_init_stream_from_cache(struct imapc_mail *mail); +bool imapc_mail_prefetch(struct mail *mail); +void imapc_mail_fetch_flush(struct imapc_mailbox *mbox); +void imapc_mail_init_stream(struct imapc_mail *mail); +bool imapc_mail_has_headers_in_cache(struct index_mail *mail, + struct mailbox_header_lookup_ctx *headers); + +void imapc_mail_fetch_update(struct imapc_mail *mail, + const struct imapc_untagged_reply *reply, + const struct imap_arg *args); +void imapc_mail_update_access_parts(struct index_mail *mail); +void imapc_mail_command_flush(struct imapc_mailbox *mbox); + +#endif diff --git a/src/lib-storage/index/imapc/imapc-mailbox.c b/src/lib-storage/index/imapc/imapc-mailbox.c new file mode 100644 index 0000000..73a38e2 --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-mailbox.c @@ -0,0 +1,1015 @@ +/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "str.h" +#include "mail-index-modseq.h" +#include "imap-arg.h" +#include "imap-seqset.h" +#include "imap-util.h" +#include "imapc-mail.h" +#include "imapc-msgmap.h" +#include "imapc-list.h" +#include "imapc-search.h" +#include "imapc-sync.h" +#include "imapc-storage.h" + +#define NOTIFY_DELAY_MSECS 500 + +void imapc_mailbox_set_corrupted(struct imapc_mailbox *mbox, + const char *reason, ...) +{ + const char *errmsg; + va_list va; + + va_start(va, reason); + errmsg = t_strdup_printf("Mailbox '%s' state corrupted: %s", + mbox->box.name, t_strdup_vprintf(reason, va)); + va_end(va); + + mail_storage_set_internal_error(&mbox->storage->storage); + + if (!mbox->initial_sync_done) { + /* we failed during initial sync. need to rebuild indexes if + we want to get this fixed */ + mail_index_mark_corrupted(mbox->box.index); + } else { + /* maybe the remote server is buggy and has become confused. + try reconnecting. */ + } + imapc_client_mailbox_reconnect(mbox->client_box, errmsg); +} + +struct mail_index_view * +imapc_mailbox_get_sync_view(struct imapc_mailbox *mbox) +{ + if (mbox->sync_view == NULL) + mbox->sync_view = mail_index_view_open(mbox->box.index); + return mbox->sync_view; +} + +static void imapc_mailbox_init_delayed_trans(struct imapc_mailbox *mbox) +{ + if (mbox->delayed_sync_trans != NULL) + return; + + i_assert(mbox->delayed_sync_cache_view == NULL); + i_assert(mbox->delayed_sync_cache_trans == NULL); + + mbox->delayed_sync_trans = + mail_index_transaction_begin(imapc_mailbox_get_sync_view(mbox), + MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL); + mbox->delayed_sync_view = + mail_index_transaction_open_updated_view(mbox->delayed_sync_trans); + + mbox->delayed_sync_cache_view = + mail_cache_view_open(mbox->box.cache, mbox->delayed_sync_view); + mbox->delayed_sync_cache_trans = + mail_cache_get_transaction(mbox->delayed_sync_cache_view, + mbox->delayed_sync_trans); +} + +static int imapc_mailbox_commit_delayed_expunges(struct imapc_mailbox *mbox) +{ + struct mail_index_view *view = imapc_mailbox_get_sync_view(mbox); + struct mail_index_transaction *trans; + struct seq_range_iter iter; + unsigned int n; + uint32_t lseq, uid; + int ret; + + trans = mail_index_transaction_begin(view, + MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL); + + seq_range_array_iter_init(&iter, &mbox->delayed_expunged_uids); n = 0; + while (seq_range_array_iter_nth(&iter, n++, &uid)) { + if (mail_index_lookup_seq(view, uid, &lseq)) + mail_index_expunge(trans, lseq); + } + array_clear(&mbox->delayed_expunged_uids); + ret = mail_index_transaction_commit(&trans); + if (ret < 0) + mailbox_set_index_error(&mbox->box); + return ret; +} + +int imapc_mailbox_commit_delayed_trans(struct imapc_mailbox *mbox, + bool force, bool *changes_r) +{ + int ret = 0; + + *changes_r = FALSE; + + if (mbox->delayed_sync_view != NULL) + mail_index_view_close(&mbox->delayed_sync_view); + if (mbox->delayed_sync_trans == NULL) + ; + else if (!mbox->selected && !force) { + /* ignore any changes done during SELECT */ + mail_index_transaction_rollback(&mbox->delayed_sync_trans); + } else { + if (mail_index_transaction_commit(&mbox->delayed_sync_trans) < 0) { + mailbox_set_index_error(&mbox->box); + ret = -1; + } + *changes_r = TRUE; + } + mbox->delayed_sync_cache_trans = NULL; + if (mbox->delayed_sync_cache_view != NULL) + mail_cache_view_close(&mbox->delayed_sync_cache_view); + + if (array_count(&mbox->delayed_expunged_uids) > 0) { + /* delayed expunges - commit them now in a separate + transaction. Reopen mbox->sync_view to see changes + committed in delayed_sync_trans. */ + if (mbox->sync_view != NULL) + mail_index_view_close(&mbox->sync_view); + if (imapc_mailbox_commit_delayed_expunges(mbox) < 0) + ret = -1; + } + + if (mbox->sync_view != NULL) + mail_index_view_close(&mbox->sync_view); + i_assert(mbox->delayed_sync_trans == NULL); + i_assert(mbox->delayed_sync_view == NULL); + i_assert(mbox->delayed_sync_cache_trans == NULL); + return ret; +} + +static void imapc_mailbox_idle_timeout(struct imapc_mailbox *mbox) +{ + timeout_remove(&mbox->to_idle_delay); + if (mbox->box.notify_callback != NULL) + mbox->box.notify_callback(&mbox->box, mbox->box.notify_context); +} + +static void imapc_mailbox_idle_notify(struct imapc_mailbox *mbox) +{ + struct ioloop *old_ioloop = current_ioloop; + + if (mbox->box.notify_callback != NULL && + mbox->to_idle_delay == NULL) { + io_loop_set_current(mbox->storage->root_ioloop); + mbox->to_idle_delay = + timeout_add_short(NOTIFY_DELAY_MSECS, + imapc_mailbox_idle_timeout, mbox); + io_loop_set_current(old_ioloop); + } +} + +static void +imapc_mailbox_index_expunge(struct imapc_mailbox *mbox, uint32_t uid) +{ + uint32_t lseq; + + if (mail_index_lookup_seq(mbox->sync_view, uid, &lseq)) + mail_index_expunge(mbox->delayed_sync_trans, lseq); + else if (mail_index_lookup_seq(mbox->delayed_sync_view, uid, &lseq)) { + /* this message exists only in this transaction. lib-index + can't currently handle expunging anything except the last + appended message in a transaction, and fixing it would be + quite a lot of trouble. so instead we'll just delay doing + this expunge until after the current transaction has been + committed. */ + seq_range_array_add(&mbox->delayed_expunged_uids, uid); + } else { + /* already expunged by another session */ + } +} + +static void +imapc_mailbox_fetch_state_finish(struct imapc_mailbox *mbox) +{ + uint32_t lseq, uid, msg_count; + + if (mbox->sync_next_lseq == 0) { + /* FETCH n:*, not 1:* */ + i_assert(mbox->state_fetched_success || + (mbox->box.flags & MAILBOX_FLAG_SAVEONLY) != 0); + return; + } + + /* if we haven't seen FETCH reply for some messages at the end of + mailbox they've been externally expunged. */ + msg_count = mail_index_view_get_messages_count(mbox->delayed_sync_view); + for (lseq = mbox->sync_next_lseq; lseq <= msg_count; lseq++) { + mail_index_lookup_uid(mbox->delayed_sync_view, lseq, &uid); + if (uid >= mbox->sync_uid_next) { + /* another process already added new messages to index + that our IMAP connection hasn't seen yet */ + break; + } + imapc_mailbox_index_expunge(mbox, uid); + } + + mbox->sync_next_lseq = 0; + mbox->sync_next_rseq = 0; + mbox->state_fetched_success = TRUE; +} + +static void +imapc_mailbox_fetch_state_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_mailbox *mbox = context; + + mbox->state_fetching_uid1 = FALSE; + mbox->delayed_untagged_exists = FALSE; + imapc_client_stop(mbox->storage->client->client); + + switch (reply->state) { + case IMAPC_COMMAND_STATE_OK: + imapc_mailbox_fetch_state_finish(mbox); + break; + case IMAPC_COMMAND_STATE_NO: + imapc_copy_error_from_reply(mbox->storage, MAIL_ERROR_PARAMS, reply); + break; + case IMAPC_COMMAND_STATE_DISCONNECTED: + mail_storage_set_internal_error(mbox->box.storage); + + break; + default: + mail_storage_set_critical(mbox->box.storage, + "imapc: state FETCH failed: %s", reply->text_full); + break; + } +} + +void imap_mailbox_select_finish(struct imapc_mailbox *mbox) +{ + if (mbox->exists_count == 0) { + /* no mails. expunge everything. */ + mbox->sync_next_lseq = 1; + imapc_mailbox_init_delayed_trans(mbox); + imapc_mailbox_fetch_state_finish(mbox); + } else { + /* We don't know the latest flags, refresh them. */ + (void)imapc_mailbox_fetch_state(mbox, 1); + } + mbox->selected = TRUE; +} + +bool +imapc_mailbox_fetch_state(struct imapc_mailbox *mbox, uint32_t first_uid) +{ + struct imapc_command *cmd; + + if (mbox->exists_count == 0) { + /* empty mailbox - no point in fetching anything. + just make sure everything is expunged in local index. + Delay calling imapc_mailbox_fetch_state_finish() until + SELECT finishes, so we see the updated UIDNEXT. */ + return FALSE; + } + if (mbox->state_fetching_uid1) { + /* retrying after reconnection - don't send duplicate */ + return FALSE; + } + + string_t *str = t_str_new(64); + str_printfa(str, "UID FETCH %u:* (FLAGS", first_uid); + if (imapc_mailbox_has_modseqs(mbox)) { + str_append(str, " MODSEQ"); + mail_index_modseq_enable(mbox->box.index); + } + if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_GMAIL_MIGRATION)) { + enum mailbox_info_flags flags; + + if (!mail_index_is_in_memory(mbox->box.index)) { + /* these can be efficiently fetched among flags and + stored into cache */ + str_append(str, " X-GM-MSGID"); + } + /* do this only for the \All mailbox */ + if (imapc_list_get_mailbox_flags(mbox->box.list, + mbox->box.name, &flags) == 0 && + (flags & MAILBOX_SPECIALUSE_ALL) != 0) + str_append(str, " X-GM-LABELS"); + + } + str_append_c(str, ')'); + + cmd = imapc_client_mailbox_cmd(mbox->client_box, + imapc_mailbox_fetch_state_callback, mbox); + if (first_uid == 1) { + mbox->sync_next_lseq = 1; + mbox->sync_next_rseq = 1; + mbox->state_fetched_success = FALSE; + /* only the FETCH 1:* is retriable - others will be retried + by the 1:* after the reconnection */ + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + } + mbox->state_fetching_uid1 = first_uid == 1; + imapc_command_send(cmd, str_c(str)); + return TRUE; +} + +static void +imapc_untagged_exists(const struct imapc_untagged_reply *reply, + struct imapc_mailbox *mbox) +{ + struct mail_index_view *view; + uint32_t exists_count = reply->num; + + if (mbox == NULL) + return; + if (IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox) && + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_NO_MSN_UPDATES)) { + /* ignore all except the first EXISTS reply (returned by + SELECT) */ + return; + } + + mbox->exists_count = exists_count; + mbox->exists_received = TRUE; + + view = mbox->delayed_sync_view; + if (view == NULL) + view = imapc_mailbox_get_sync_view(mbox); + + if (!mbox->selecting && mbox->sync_fetch_first_uid != 1) { + const struct mail_index_header *hdr; + hdr = mail_index_get_header(view); + mbox->sync_fetch_first_uid = hdr->next_uid; + mbox->delayed_untagged_exists = TRUE; + } + imapc_mailbox_idle_notify(mbox); +} + +static bool keywords_are_equal(struct mail_keywords *kw, + const ARRAY_TYPE(keyword_indexes) *kw_arr) +{ + const unsigned int *kw_idx; + unsigned int i, j, count; + + kw_idx = array_get(kw_arr, &count); + if (count != kw->count) + return FALSE; + + /* there are normally only a few keywords, so O(n^2) is fine */ + for (i = 0; i < count; i++) { + for (j = 0; j < count; j++) { + if (kw->idx[i] == kw_idx[j]) + break; + } + if (j == count) + return FALSE; + } + return TRUE; +} + +static int +imapc_mailbox_msgmap_update(struct imapc_mailbox *mbox, + uint32_t rseq, uint32_t fetch_uid, + uint32_t *lseq_r, uint32_t *uid_r, + bool *new_message_r) +{ + struct imapc_msgmap *msgmap; + uint32_t uid, msg_count, rseq2; + + *lseq_r = 0; + *uid_r = uid = fetch_uid; + *new_message_r = FALSE; + + if (rseq > mbox->exists_count) { + /* Receiving a FETCH for a message that EXISTS hasn't + announced yet. MS Exchange has a bug where our UID FETCH + request sometimes sends replies where sequences are above + EXISTS value, but their UIDs are for existing messages. + We'll just ignore these replies. */ + return 0; + } + if (rseq < mbox->prev_skipped_rseq && + fetch_uid > mbox->prev_skipped_uid) { + /* This was the initial attempt at catching the above + MS Exchange bug, but the above one appears to catch all + these cases. But keep it here just in case. */ + imapc_mailbox_set_corrupted(mbox, + "FETCH sequence/UID order is mixed " + "(seq=%u,%u vs uid=%u,%u)", + mbox->prev_skipped_rseq, rseq, + mbox->prev_skipped_uid, fetch_uid); + return -1; + } + + msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box); + msg_count = imapc_msgmap_count(msgmap); + if (fetch_uid != 0 && mbox->state_fetched_success && + (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_FETCH_MSN_WORKAROUNDS) || + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_NO_MSN_UPDATES))) { + /* if we know the UID, use own own generated rseq instead of + the potentially broken rseq that the server sent. + Skip this during the initial FETCH 1:* (UID ..) handling, + or we can't detect duplicate UIDs and will instead + assert-crash later on. */ + uint32_t fixed_rseq; + + if (imapc_msgmap_uid_to_rseq(msgmap, fetch_uid, &fixed_rseq)) + rseq = fixed_rseq; + else if (fetch_uid >= imapc_msgmap_uidnext(msgmap) && + rseq <= msg_count) { + /* The current rseq is wrong. Lets hope that the + correct rseq is the next new one. This happens + especially with no-msn-updates when mails have been + expunged and new mails arrive in the same session. */ + rseq = msg_count+1; + } + } + + if (rseq <= msg_count) { + uid = imapc_msgmap_rseq_to_uid(msgmap, rseq); + if (uid != fetch_uid && fetch_uid != 0) { + imapc_mailbox_set_corrupted(mbox, + "FETCH UID mismatch (%u != %u)", + fetch_uid, uid); + return -1; + } + *uid_r = uid; + } else if (fetch_uid == 0 || rseq != msg_count+1) { + /* probably a flag update for a message we haven't yet + received our initial UID FETCH for. we should get + another one. */ + if (fetch_uid == 0) + return 0; + + if (imapc_msgmap_uid_to_rseq(msgmap, fetch_uid, &rseq2)) { + imapc_mailbox_set_corrupted(mbox, + "FETCH returned wrong sequence for UID %u " + "(%u != %u)", fetch_uid, rseq, rseq2); + return -1; + } + mbox->prev_skipped_rseq = rseq; + mbox->prev_skipped_uid = fetch_uid; + /* Check if this uid must be added later when syncing. */ + *new_message_r = TRUE; + } else if (fetch_uid < imapc_msgmap_uidnext(msgmap)) { + imapc_mailbox_set_corrupted(mbox, + "Expunged message reappeared in session " + "(uid=%u < next_uid=%u)", + fetch_uid, imapc_msgmap_uidnext(msgmap)); + return -1; + } else { + /* newly seen message */ + imapc_msgmap_append(msgmap, rseq, uid); + if (uid < mbox->min_append_uid || + uid < mail_index_get_header(mbox->delayed_sync_view)->next_uid) { + /* message is already added to index */ + } else if (mbox->state_fetching_uid1) { + /* Initial fetching, allow messages to be appened to + index directly */ + mail_index_append(mbox->delayed_sync_trans, + uid, lseq_r); + mbox->min_append_uid = uid + 1; + } else { + /* message is not yet added to index, in order to + prevent log synchronization errors add this + message later, when the mailbox is synced. */ + *new_message_r = TRUE; + } + } + return 0; +} + +bool imapc_mailbox_name_equals(struct imapc_mailbox *mbox, + const char *remote_name) +{ + const char *imapc_remote_name = + imapc_mailbox_get_remote_name(mbox); + + if (strcmp(imapc_remote_name, remote_name) == 0) { + /* match */ + return TRUE; + } else if (strcasecmp(mbox->box.name, "INBOX") == 0 && + strcasecmp(remote_name, "INBOX") == 0) { + /* case-insensitive INBOX */ + return TRUE; + } + return FALSE; +} + +static struct imapc_untagged_fetch_ctx * +imapc_untagged_fetch_ctx_create(void) +{ + pool_t pool = pool_alloconly_create("imapc untagged fetch ctx", 128); + struct imapc_untagged_fetch_ctx *ctx = + p_new(pool, struct imapc_untagged_fetch_ctx, 1); + ctx->pool = pool; + return ctx; +} + +void imapc_untagged_fetch_ctx_free(struct imapc_untagged_fetch_ctx **_ctx) +{ + struct imapc_untagged_fetch_ctx *ctx = *_ctx; + + *_ctx = NULL; + i_assert(ctx != NULL); + + pool_unref(&ctx->pool); +} + +void imapc_untagged_fetch_update_flags(struct imapc_mailbox *mbox, + struct imapc_untagged_fetch_ctx *ctx, + struct mail_index_view *view, + uint32_t lseq) +{ + ARRAY_TYPE(keyword_indexes) old_kws; + struct mail_keywords *kw; + const struct mail_index_record *rec = NULL; + const char *atom; + + if (!ctx->have_flags) + return; + + rec = mail_index_lookup(view, lseq); + if (rec->flags != ctx->flags) { + mail_index_update_flags(mbox->delayed_sync_trans, lseq, + MODIFY_REPLACE, ctx->flags); + } + + t_array_init(&old_kws, 8); + mail_index_lookup_keywords(view, lseq, &old_kws); + + if (ctx->have_gmail_labels) { + /* add keyword for mails that have GMail labels. + this can be used for "All Mail" mailbox migrations + with dsync */ + atom = "$GMailHaveLabels"; + array_push_back(&ctx->keywords, &atom); + } + + array_append_zero(&ctx->keywords); + kw = mail_index_keywords_create(mbox->box.index, + array_front(&ctx->keywords)); + if (!keywords_are_equal(kw, &old_kws)) { + mail_index_update_keywords(mbox->delayed_sync_trans, + lseq, MODIFY_REPLACE, kw); + } + mail_index_keywords_unref(&kw); +} + +static bool imapc_untagged_fetch_handle(struct imapc_mailbox *mbox, + struct imapc_untagged_fetch_ctx *ctx, + uint32_t rseq) +{ + uint32_t lseq; + bool new_message; + + imapc_mailbox_init_delayed_trans(mbox); + if (imapc_mailbox_msgmap_update(mbox, rseq, ctx->fetch_uid, + &lseq, &ctx->uid, + &new_message) < 0 || ctx->uid == 0) + return FALSE; + + if ((ctx->flags & MAIL_RECENT) == 0 && mbox->highest_nonrecent_uid < ctx->uid) { + /* remember for STATUS_FIRST_RECENT_UID */ + mbox->highest_nonrecent_uid = ctx->uid; + } + /* FIXME: we should ideally also pass these through so they show up + to clients. */ + ctx->flags &= ENUM_NEGATE(MAIL_RECENT); + + if (lseq == 0) { + if (!mail_index_lookup_seq(mbox->delayed_sync_view, + ctx->uid, &lseq)) { + /* already expunged by another session */ + if (rseq == mbox->sync_next_rseq) + mbox->sync_next_rseq++; + return new_message; + } + } + + if (rseq == mbox->sync_next_rseq) { + /* we're doing the initial full sync of mails. expunge any + mails that no longer exist. */ + while (mbox->sync_next_lseq < lseq) { + mail_index_lookup_uid(mbox->delayed_sync_view, + mbox->sync_next_lseq, &ctx->uid); + imapc_mailbox_index_expunge(mbox, ctx->uid); + mbox->sync_next_lseq++; + } + i_assert(lseq == mbox->sync_next_lseq); + mbox->sync_next_rseq++; + mbox->sync_next_lseq++; + } + + if (!new_message) { + /* Only update flags immediately for existing messages */ + imapc_untagged_fetch_update_flags(mbox, ctx, + mbox->delayed_sync_view, lseq); + } + + if (ctx->modseq != 0) { + if (mail_index_modseq_lookup(mbox->delayed_sync_view, lseq) < ctx->modseq) + mail_index_update_modseq(mbox->delayed_sync_trans, lseq, ctx->modseq); + array_idx_set(&mbox->rseq_modseqs, rseq-1, &ctx->modseq); + } + if (ctx->guid != NULL) { + struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(&mbox->box); + const enum index_cache_field guid_cache_idx = + ibox->cache_fields[MAIL_CACHE_GUID].idx; + + if (mail_cache_field_can_add(mbox->delayed_sync_cache_trans, + lseq, guid_cache_idx)) { + mail_cache_add(mbox->delayed_sync_cache_trans, lseq, + guid_cache_idx, ctx->guid, strlen(ctx->guid)); + } + } + return new_message; +} + +static bool imapc_untagged_fetch_parse(struct imapc_mailbox *mbox, + struct imapc_untagged_fetch_ctx *ctx, + const struct imap_arg *list) +{ + const struct imap_arg *flags_list, *modseq_list; + const char *atom, *patom; + unsigned int i, j; + + ctx->fetch_uid = 0; ctx->flags = 0; + for (i = 0; list[i].type != IMAP_ARG_EOL; i += 2) { + if (!imap_arg_get_atom(&list[i], &atom) || + list[i+1].type == IMAP_ARG_EOL) + return FALSE; + + if (strcasecmp(atom, "UID") == 0) { + if (!imap_arg_get_atom(&list[i+1], &atom) || + str_to_uint32(atom, &ctx->fetch_uid) < 0) + return FALSE; + } else if (strcasecmp(atom, "FLAGS") == 0) { + if (!imap_arg_get_list(&list[i+1], &flags_list)) + return FALSE; + + p_array_init(&ctx->keywords, ctx->pool, 8); + ctx->have_flags = TRUE; + for (j = 0; flags_list[j].type != IMAP_ARG_EOL; j++) { + if (!imap_arg_get_atom(&flags_list[j], &atom)) + return FALSE; + if (atom[0] == '\\') + ctx->flags |= imap_parse_system_flag(atom); + else { + patom = p_strdup(ctx->pool, atom); + /* keyword */ + array_push_back(&ctx->keywords, &patom); + } + } + } else if (strcasecmp(atom, "MODSEQ") == 0 && + imapc_mailbox_has_modseqs(mbox)) { + /* (modseq-number) */ + if (!imap_arg_get_list(&list[i+1], &modseq_list)) + return FALSE; + if (!imap_arg_get_atom(&modseq_list[0], &atom) || + str_to_uint64(atom, &ctx->modseq) < 0 || + modseq_list[1].type != IMAP_ARG_EOL) + return FALSE; + } else if (strcasecmp(atom, "X-GM-MSGID") == 0 && + !mbox->initial_sync_done) { + if (imap_arg_get_atom(&list[i+1], &atom)) + ctx->guid = atom; + } else if (strcasecmp(atom, "X-GM-LABELS") == 0 && + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_GMAIL_MIGRATION)) { + if (!imap_arg_get_list(&list[i+1], &flags_list)) + return FALSE; + for (j = 0; flags_list[j].type != IMAP_ARG_EOL; j++) { + if (!imap_arg_get_astring(&flags_list[j], &atom)) + return FALSE; + if (strcasecmp(atom, "\\Muted") != 0) + ctx->have_gmail_labels = TRUE; + } + } + } + if (ctx->fetch_uid == 0 && + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_NO_MSN_UPDATES)) { + /* UID missing and we're not tracking MSNs */ + return FALSE; + } + + return TRUE; +} + +static void imapc_untagged_fetch(const struct imapc_untagged_reply *reply, + struct imapc_mailbox *mbox) +{ + const struct imap_arg *list; + struct imapc_fetch_request *fetch_request; + struct imapc_mail *mail; + bool new_message = FALSE; + + if (mbox == NULL || reply->num == 0 || !imap_arg_get_list(reply->args, &list)) + return; + if (!IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox)) { + /* SELECTing another mailbox - this FETCH is still for the + previous selected mailbox. */ + return; + } + + struct imapc_untagged_fetch_ctx *ctx = + imapc_untagged_fetch_ctx_create(); + if (!imapc_untagged_fetch_parse(mbox, ctx, list)) { + imapc_untagged_fetch_ctx_free(&ctx); + return; + } + + new_message = imapc_untagged_fetch_handle(mbox, ctx, reply->num); + + /* if this is a reply to some FETCH request, update the mail's fields */ + array_foreach_elem(&mbox->fetch_requests, fetch_request) { + array_foreach_elem(&fetch_request->mails, mail) { + if (mail->imail.mail.mail.uid == ctx->uid) + imapc_mail_fetch_update(mail, reply, list); + } + } + + if (!new_message) { + /* Handling this context is finished if the mail was not new + to the local index. It has not been added to + mbox->untagged_fetch_contexts so no need to delete it from + the array. The context itself can be freed here. */ + imapc_untagged_fetch_ctx_free(&ctx); + } else { + /* If this is a new message store this context to be handled + when syncing */ + array_push_back(&mbox->untagged_fetch_contexts, &ctx); + } + imapc_mailbox_idle_notify(mbox); +} + +static void imapc_untagged_expunge(const struct imapc_untagged_reply *reply, + struct imapc_mailbox *mbox) +{ + struct imapc_msgmap *msgmap; + uint32_t uid, rseq = reply->num; + + if (mbox == NULL || rseq == 0 || + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_NO_MSN_UPDATES)) + return; + if (!IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox)) { + /* SELECTing another mailbox - this EXPUNGE is still for the + previous selected mailbox. */ + return; + } + + mbox->prev_skipped_rseq = 0; + mbox->prev_skipped_uid = 0; + + if (mbox->exists_count == 0) { + imapc_mailbox_set_corrupted(mbox, + "EXPUNGE received for empty mailbox"); + return; + } + mbox->exists_count--; + + msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box); + if (rseq > imapc_msgmap_count(msgmap)) { + /* we haven't even seen this message yet */ + return; + } + uid = imapc_msgmap_rseq_to_uid(msgmap, rseq); + imapc_msgmap_expunge(msgmap, rseq); + if (array_is_created(&mbox->rseq_modseqs)) + array_delete(&mbox->rseq_modseqs, rseq-1, 1); + + imapc_mailbox_init_delayed_trans(mbox); + imapc_mailbox_index_expunge(mbox, uid); + imapc_mailbox_idle_notify(mbox); +} + +static void +imapc_untagged_esearch_gmail_pop3(const struct imap_arg *args, + struct imapc_mailbox *mbox) +{ + struct imapc_msgmap *msgmap; + const char *atom; + struct seq_range_iter iter; + ARRAY_TYPE(seq_range) rseqs; + unsigned int n; + uint32_t rseq, lseq, uid; + ARRAY_TYPE(keyword_indexes) keywords; + struct mail_keywords *kw; + unsigned int pop3_deleted_kw_idx; + + i_free_and_null(mbox->sync_gmail_pop3_search_tag); + + /* It should contain ALL <seqset> or nonexistent if nothing matched */ + if (args[0].type == IMAP_ARG_EOL) + return; + t_array_init(&rseqs, 64); + if (!imap_arg_atom_equals(&args[0], "ALL") || + !imap_arg_get_atom(&args[1], &atom) || + imap_seq_set_nostar_parse(atom, &rseqs) < 0) { + i_error("Invalid gmail-pop3 ESEARCH reply"); + return; + } + + mail_index_keyword_lookup_or_create(mbox->box.index, + mbox->storage->set->pop3_deleted_flag, &pop3_deleted_kw_idx); + + t_array_init(&keywords, 1); + array_push_back(&keywords, &pop3_deleted_kw_idx); + kw = mail_index_keywords_create_from_indexes(mbox->box.index, &keywords); + + msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box); + seq_range_array_iter_init(&iter, &rseqs); n = 0; + while (seq_range_array_iter_nth(&iter, n++, &rseq)) { + if (rseq > imapc_msgmap_count(msgmap)) { + /* we haven't even seen this message yet */ + break; + } + uid = imapc_msgmap_rseq_to_uid(msgmap, rseq); + if (!mail_index_lookup_seq(mbox->delayed_sync_view, + uid, &lseq)) + continue; + + /* add the pop3_deleted_flag */ + mail_index_update_keywords(mbox->delayed_sync_trans, + lseq, MODIFY_ADD, kw); + } + mail_index_keywords_unref(&kw); +} + +static void imapc_untagged_search(const struct imapc_untagged_reply *reply, + struct imapc_mailbox *mbox) +{ + if (mbox == NULL) + return; + if (!IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox)) { + /* SELECTing another mailbox - this SEARCH is still for the + previous selected mailbox. */ + return; + } + imapc_search_reply_search(reply->args, mbox); +} + +static void imapc_untagged_esearch(const struct imapc_untagged_reply *reply, + struct imapc_mailbox *mbox) +{ + const struct imap_arg *tag_list; + const char *str; + + if (mbox == NULL || !imap_arg_get_list(reply->args, &tag_list)) + return; + if (!IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox)) { + /* SELECTing another mailbox - this ESEARCH is still for the + previous selected mailbox. */ + return; + } + + /* ESEARCH begins with (TAG <tag>) */ + if (!imap_arg_atom_equals(&tag_list[0], "TAG") || + !imap_arg_get_string(&tag_list[1], &str) || + tag_list[2].type != IMAP_ARG_EOL) + return; + + /* for now the only ESEARCH reply that we have is for getting GMail's + list of hidden POP3 messages. */ + if (mbox->sync_gmail_pop3_search_tag != NULL && + strcmp(mbox->sync_gmail_pop3_search_tag, str) == 0) + imapc_untagged_esearch_gmail_pop3(reply->args+1, mbox); + else + imapc_search_reply_esearch(reply->args+1, mbox); +} + +static void imapc_sync_uid_validity(struct imapc_mailbox *mbox) +{ + const struct mail_index_header *hdr; + + imapc_mailbox_init_delayed_trans(mbox); + hdr = mail_index_get_header(mbox->delayed_sync_view); + if (hdr->uid_validity != mbox->sync_uid_validity && + mbox->sync_uid_validity != 0) { + if (hdr->uid_validity != 0) { + /* uidvalidity changed, reset the entire mailbox */ + mail_index_reset(mbox->delayed_sync_trans); + mbox->sync_fetch_first_uid = 1; + /* The reset needs to be committed before FETCH 1:* + results are received. */ + bool changes; + if (imapc_mailbox_commit_delayed_trans(mbox, TRUE, &changes) < 0) + mail_index_mark_corrupted(mbox->box.index); + imapc_mailbox_init_delayed_trans(mbox); + } + mail_index_update_header(mbox->delayed_sync_trans, + offsetof(struct mail_index_header, uid_validity), + &mbox->sync_uid_validity, + sizeof(mbox->sync_uid_validity), TRUE); + } +} + +static void +imapc_resp_text_uidvalidity(const struct imapc_untagged_reply *reply, + struct imapc_mailbox *mbox) +{ + uint32_t uid_validity; + + if (mbox == NULL || + str_to_uint32(reply->resp_text_value, &uid_validity) < 0 || + uid_validity == 0) + return; + + if (mbox->sync_uid_validity != uid_validity) { + mbox->sync_uid_validity = uid_validity; + imapc_mail_cache_free(&mbox->prev_mail_cache); + imapc_sync_uid_validity(mbox); + } +} + +static void +imapc_resp_text_uidnext(const struct imapc_untagged_reply *reply, + struct imapc_mailbox *mbox) +{ + uint32_t uid_next; + + if (mbox == NULL || + str_to_uint32(reply->resp_text_value, &uid_next) < 0) + return; + + mbox->sync_uid_next = uid_next; +} + +static void +imapc_resp_text_highestmodseq(const struct imapc_untagged_reply *reply, + struct imapc_mailbox *mbox) +{ + uint64_t highestmodseq; + + if (mbox == NULL || + str_to_uint64(reply->resp_text_value, &highestmodseq) < 0) + return; + + mbox->sync_highestmodseq = highestmodseq; +} + +static void +imapc_resp_text_permanentflags(const struct imapc_untagged_reply *reply, + struct imapc_mailbox *mbox) +{ + const struct imap_arg *flags_args, *arg; + const char *flag; + unsigned int idx; + + i_assert(reply->args[0].type == IMAP_ARG_ATOM); + + if (mbox == NULL || !imap_arg_get_list(&reply->args[1], &flags_args)) + return; + + mbox->permanent_flags = 0; + mbox->box.disallow_new_keywords = TRUE; + + for (arg = flags_args; arg->type != IMAP_ARG_EOL; arg++) { + if (!imap_arg_get_atom(arg, &flag)) + continue; + + if (strcmp(flag, "\\*") == 0) + mbox->box.disallow_new_keywords = FALSE; + else if (*flag == '\\') + mbox->permanent_flags |= imap_parse_system_flag(flag); + else { + /* we'll simply make sure that it exists in the index */ + mail_index_keyword_lookup_or_create(mbox->box.index, + flag, &idx); + } + } +} + +void imapc_mailbox_register_untagged(struct imapc_mailbox *mbox, + const char *key, + imapc_mailbox_callback_t *callback) +{ + struct imapc_mailbox_event_callback *cb; + + cb = array_append_space(&mbox->untagged_callbacks); + cb->name = p_strdup(mbox->box.pool, key); + cb->callback = callback; +} + +void imapc_mailbox_register_resp_text(struct imapc_mailbox *mbox, + const char *key, + imapc_mailbox_callback_t *callback) +{ + struct imapc_mailbox_event_callback *cb; + + cb = array_append_space(&mbox->resp_text_callbacks); + cb->name = p_strdup(mbox->box.pool, key); + cb->callback = callback; +} + +void imapc_mailbox_register_callbacks(struct imapc_mailbox *mbox) +{ + imapc_mailbox_register_untagged(mbox, "EXISTS", + imapc_untagged_exists); + imapc_mailbox_register_untagged(mbox, "FETCH", + imapc_untagged_fetch); + imapc_mailbox_register_untagged(mbox, "EXPUNGE", + imapc_untagged_expunge); + imapc_mailbox_register_untagged(mbox, "SEARCH", + imapc_untagged_search); + imapc_mailbox_register_untagged(mbox, "ESEARCH", + imapc_untagged_esearch); + imapc_mailbox_register_resp_text(mbox, "UIDVALIDITY", + imapc_resp_text_uidvalidity); + imapc_mailbox_register_resp_text(mbox, "UIDNEXT", + imapc_resp_text_uidnext); + imapc_mailbox_register_resp_text(mbox, "HIGHESTMODSEQ", + imapc_resp_text_highestmodseq); + imapc_mailbox_register_resp_text(mbox, "PERMANENTFLAGS", + imapc_resp_text_permanentflags); +} diff --git a/src/lib-storage/index/imapc/imapc-save.c b/src/lib-storage/index/imapc/imapc-save.c new file mode 100644 index 0000000..c50f46b --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-save.c @@ -0,0 +1,829 @@ +/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "istream.h" +#include "istream-crlf.h" +#include "ostream.h" +#include "imap-date.h" +#include "imap-util.h" +#include "imap-seqset.h" +#include "imap-quote.h" +#include "index-mail.h" +#include "mail-copy.h" +#include "mailbox-list-private.h" +#include "imapc-msgmap.h" +#include "imapc-storage.h" +#include "imapc-sync.h" +#include "imapc-mail.h" +#include "seq-set-builder.h" + +struct imapc_save_context { + struct mail_save_context ctx; + + struct imapc_mailbox *mbox; + struct imapc_mailbox *src_mbox; + struct mail_index_transaction *trans; + + int fd; + char *temp_path; + struct istream *input; + + uint32_t dest_uid_validity; + ARRAY_TYPE(seq_range) dest_saved_uids; + unsigned int save_count; + + bool failed:1; + bool finished:1; +}; + +struct imapc_save_cmd_context { + struct imapc_save_context *ctx; + int ret; +}; + +#define IMAPC_SAVECTX(s) container_of(s, struct imapc_save_context, ctx) +#define IMAPC_SERVER_CMDLINE_MAX_LEN 8000 + +void imapc_transaction_save_rollback(struct mail_save_context *_ctx); +static void imapc_mail_copy_bulk_flush(struct imapc_mailbox *mbox); + +struct mail_save_context * +imapc_save_alloc(struct mailbox_transaction_context *t) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(t->box); + struct imapc_save_context *ctx; + + i_assert((t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0); + + if (t->save_ctx == NULL) { + ctx = i_new(struct imapc_save_context, 1); + ctx->ctx.transaction = t; + ctx->mbox = mbox; + ctx->src_mbox = NULL; + ctx->trans = t->itrans; + ctx->fd = -1; + t->save_ctx = &ctx->ctx; + } + return t->save_ctx; +} + +int imapc_save_begin(struct mail_save_context *_ctx, struct istream *input) +{ + struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx); + const char *path; + + i_assert(ctx->fd == -1); + + if (imapc_storage_client_handle_auth_failure(ctx->mbox->storage->client)) + return -1; + + ctx->fd = imapc_client_create_temp_fd(ctx->mbox->storage->client->client, + &path); + if (ctx->fd == -1) { + mail_set_critical(_ctx->dest_mail, + "Couldn't create temp file %s", path); + ctx->failed = TRUE; + return -1; + } + /* we may not know the size of the input, or be sure that it contains + only CRLFs. so we'll always first write the mail to a temp file and + upload it from there to remote server. */ + ctx->finished = FALSE; + ctx->temp_path = i_strdup(path); + ctx->input = i_stream_create_crlf(input); + _ctx->data.output = o_stream_create_fd_file(ctx->fd, 0, FALSE); + o_stream_cork(_ctx->data.output); + return 0; +} + +int imapc_save_continue(struct mail_save_context *_ctx) +{ + struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx); + + if (ctx->failed) + return -1; + + if (index_storage_save_continue(_ctx, ctx->input, NULL) < 0) { + ctx->failed = TRUE; + return -1; + } + return 0; +} + +static void imapc_save_appenduid(struct imapc_save_context *ctx, + const struct imapc_command_reply *reply, + uint32_t *uid_r) +{ + const char *const *args; + uint32_t uid_validity, dest_uid; + + *uid_r = 0; + + /* <uidvalidity> <dest uid-set> */ + args = t_strsplit(reply->resp_text_value, " "); + if (str_array_length(args) != 2) + return; + + if (str_to_uint32(args[0], &uid_validity) < 0) + return; + if (ctx->dest_uid_validity == 0) + ctx->dest_uid_validity = uid_validity; + else if (ctx->dest_uid_validity != uid_validity) + return; + + if (str_to_uint32(args[1], &dest_uid) == 0) { + seq_range_array_add_with_init(&ctx->dest_saved_uids, + 32, dest_uid); + *uid_r = dest_uid; + } +} + +static void +imapc_save_add_to_index(struct imapc_save_context *ctx, uint32_t uid) +{ + struct mail *_mail = ctx->ctx.dest_mail; + struct index_mail *imail = INDEX_MAIL(_mail); + uint32_t seq; + + /* we'll temporarily append messages and at commit time expunge + them all, since we can't guarantee that no one else has saved + messages to remote server during our transaction */ + mail_index_append(ctx->trans, uid, &seq); + mail_set_seq_saving(_mail, seq); + imail->data.no_caching = TRUE; + imail->data.forced_no_caching = TRUE; + + if (ctx->fd != -1) { + struct imapc_mail *imapc_mail = IMAPC_MAIL(_mail); + imail->data.stream = i_stream_create_fd_autoclose(&ctx->fd, 0); + imapc_mail->header_fetched = TRUE; + imapc_mail->body_fetched = TRUE; + /* The saved stream wasn't actually read, but it needs to be + set accessed to avoid assert-crash. */ + _mail->mail_stream_accessed = TRUE; + imapc_mail_init_stream(imapc_mail); + } + + ctx->save_count++; +} + +static void imapc_save_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_save_cmd_context *ctx = context; + uint32_t uid = 0; + + if (reply->state == IMAPC_COMMAND_STATE_OK) { + if (reply->resp_text_key != NULL && + strcasecmp(reply->resp_text_key, "APPENDUID") == 0) + imapc_save_appenduid(ctx->ctx, reply, &uid); + imapc_save_add_to_index(ctx->ctx, uid); + ctx->ret = 0; + } else if (imapc_storage_client_handle_auth_failure(ctx->ctx->mbox->storage->client)) { + ctx->ret = -1; + } else if (reply->state == IMAPC_COMMAND_STATE_NO) { + imapc_copy_error_from_reply(ctx->ctx->mbox->storage, + MAIL_ERROR_PARAMS, reply); + ctx->ret = -1; + } else { + mailbox_set_critical(&ctx->ctx->mbox->box, + "imapc: APPEND failed: %s", reply->text_full); + ctx->ret = -1; + } + imapc_client_stop(ctx->ctx->mbox->storage->client->client); +} + +static void +imapc_save_noop_callback(const struct imapc_command_reply *reply ATTR_UNUSED, + void *context) +{ + struct imapc_save_cmd_context *ctx = context; + + /* we don't really care about the reply */ + ctx->ret = 0; + imapc_client_stop(ctx->ctx->mbox->storage->client->client); +} + +static void +imapc_copy_rollback_store_callback(const struct imapc_command_reply *reply ATTR_UNUSED, + void *context) +{ + struct imapc_save_context *ctx = context; + /* Can't do much about a non successful STORE here */ + if (reply->state != IMAPC_COMMAND_STATE_OK) { + e_error(ctx->src_mbox->box.event, + "imapc: Failed to set \\Deleted flag for rolling back " + "failed copy: %s", reply->text_full); + ctx->src_mbox->rollback_pending = FALSE; + ctx->finished = TRUE; + ctx->failed = TRUE; + } else { + i_assert(ctx->src_mbox->rollback_pending); + } + /* No need stop the imapc client here there is always an additional + expunge callback after this. */ +} + +static void +imapc_copy_rollback_expunge_callback(const struct imapc_command_reply *reply ATTR_UNUSED, + void *context) +{ + struct imapc_save_context *ctx = context; + + /* Can't do much about a non successful EXPUNGE here */ + if (reply->state != IMAPC_COMMAND_STATE_OK) { + e_error(ctx->src_mbox->box.event, + "imapc: Failed to expunge messages for rolling back " + "failed copy: %s", reply->text_full); + ctx->src_mbox->rollback_pending = FALSE; + ctx->finished = TRUE; + ctx->failed = TRUE; + } else { + ctx->finished = TRUE; + ctx->src_mbox->rollback_pending = FALSE; + } + imapc_client_stop(ctx->src_mbox->storage->client->client); +} + +static void +imapc_append_keywords(string_t *str, struct mail_keywords *kw) +{ + const ARRAY_TYPE(keywords) *kw_arr; + const char *kw_str; + unsigned int i; + + kw_arr = mail_index_get_keywords(kw->index); + for (i = 0; i < kw->count; i++) { + kw_str = array_idx_elem(kw_arr, kw->idx[i]); + if (str_len(str) > 1) + str_append_c(str, ' '); + str_append(str, kw_str); + } +} + +static int imapc_save_append(struct imapc_save_context *ctx) +{ + struct mail_save_context *_ctx = &ctx->ctx; + struct mail_save_data *mdata = &_ctx->data; + struct imapc_command *cmd; + struct imapc_save_cmd_context sctx; + struct istream *input; + const char *flags = "", *internaldate = ""; + + if (mdata->flags != 0 || mdata->keywords != NULL) { + string_t *str = t_str_new(64); + + str_append(str, " ("); + imap_write_flags(str, mdata->flags & ENUM_NEGATE(MAIL_RECENT), + NULL); + if (mdata->keywords != NULL) + imapc_append_keywords(str, mdata->keywords); + str_append_c(str, ')'); + flags = str_c(str); + } + if (mdata->received_date != (time_t)-1) { + internaldate = t_strdup_printf(" \"%s\"", + imap_to_datetime(mdata->received_date)); + } + + ctx->mbox->exists_received = FALSE; + + input = i_stream_create_fd(ctx->fd, IO_BLOCK_SIZE); + sctx.ctx = ctx; + sctx.ret = -2; + cmd = imapc_client_cmd(ctx->mbox->storage->client->client, + imapc_save_callback, &sctx); + imapc_command_sendf(cmd, "APPEND %s%1s%1s %p", + imapc_mailbox_get_remote_name(ctx->mbox), + flags, internaldate, input); + i_stream_unref(&input); + while (sctx.ret == -2) + imapc_mailbox_run(ctx->mbox); + + if (sctx.ret == 0 && ctx->mbox->selected && + !ctx->mbox->exists_received) { + /* e.g. Courier doesn't send EXISTS reply before the tagged + APPEND reply. That isn't exactly required by the IMAP RFC, + but it makes the behavior better. See if NOOP finds + the mail. */ + sctx.ret = -2; + cmd = imapc_client_cmd(ctx->mbox->storage->client->client, + imapc_save_noop_callback, &sctx); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + imapc_command_send(cmd, "NOOP"); + while (sctx.ret == -2) + imapc_mailbox_run(ctx->mbox); + } + return sctx.ret; +} + +int imapc_save_finish(struct mail_save_context *_ctx) +{ + struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx); + struct mail_storage *storage = _ctx->transaction->box->storage; + + ctx->finished = TRUE; + + if (!ctx->failed) { + if (o_stream_finish(_ctx->data.output) < 0) { + if (!mail_storage_set_error_from_errno(storage)) { + mail_set_critical(_ctx->dest_mail, + "write(%s) failed: %s", ctx->temp_path, + o_stream_get_error(_ctx->data.output)); + } + ctx->failed = TRUE; + } + } + + if (!ctx->failed) { + if (imapc_save_append(ctx) < 0) + ctx->failed = TRUE; + } + + o_stream_unref(&_ctx->data.output); + i_stream_unref(&ctx->input); + i_close_fd_path(&ctx->fd, ctx->temp_path); + i_free(ctx->temp_path); + index_save_context_free(_ctx); + return ctx->failed ? -1 : 0; +} + +void imapc_save_cancel(struct mail_save_context *_ctx) +{ + struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx); + + ctx->failed = TRUE; + (void)imapc_transaction_save_commit_pre(_ctx); + (void)imapc_save_finish(_ctx); +} + +static void imapc_copy_bulk_finish(struct imapc_save_context *ctx) +{ + while (ctx->src_mbox != NULL && ctx->src_mbox->pending_copy_request != NULL) + imapc_mailbox_run_nofetch(ctx->src_mbox); +} + +int imapc_transaction_save_commit_pre(struct mail_save_context *_ctx) +{ + struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx); + struct mail_transaction_commit_changes *changes = + _ctx->transaction->changes; + uint32_t i, last_seq; + + i_assert(ctx->finished || ctx->failed); + + /* expunge all added messages from index before commit */ + last_seq = mail_index_view_get_messages_count(_ctx->transaction->view); + if (last_seq == 0) + return -1; + for (i = 0; i < ctx->save_count; i++) + mail_index_expunge(ctx->trans, last_seq - i); + + if (!ctx->failed && array_is_created(&ctx->dest_saved_uids)) { + changes->uid_validity = ctx->dest_uid_validity; + array_append_array(&changes->saved_uids, &ctx->dest_saved_uids); + } + return 0; +} + +int imapc_transaction_save_commit(struct mailbox_transaction_context *t) +{ + struct imapc_save_context *ctx = NULL; + struct imapc_mailbox *src_mbox = NULL; + + if (t->save_ctx != NULL) { + ctx = IMAPC_SAVECTX(t->save_ctx); + src_mbox = ctx->src_mbox; + } + + if (src_mbox != NULL && src_mbox->pending_copy_request != NULL) { + /* If there is still a copy command to send flush it now */ + imapc_mail_copy_bulk_flush(src_mbox); + imapc_copy_bulk_finish(ctx); + } + + if (ctx != NULL) + return ctx->failed ? -1 : 0; + return 0; +} + +void imapc_transaction_save_commit_post(struct mail_save_context *_ctx, + struct mail_index_transaction_commit_result *result ATTR_UNUSED) +{ + imapc_transaction_save_rollback(_ctx); +} + +static void +imapc_expunge_construct_cmd_str(string_t *store_cmd, + string_t *expunge_cmd, + string_t *uids) +{ + str_append(store_cmd, "UID STORE "); + str_append_str(store_cmd, uids); + str_append(store_cmd, " +FLAGS (\\Deleted)"); + str_append(expunge_cmd, "UID EXPUNGE "); + str_append_str(expunge_cmd, uids); + /* Clear already appened uids */ + str_truncate(uids, 0); +} + +static void +imapc_expunge_send_cmd_str(struct imapc_save_context *ctx, + string_t *uids) +{ + struct imapc_command *store_cmd, *expunge_cmd; + + string_t *store_cmd_str, *expunge_cmd_str; + store_cmd_str = t_str_new(128); + expunge_cmd_str = t_str_new(128); + + imapc_expunge_construct_cmd_str(store_cmd_str, expunge_cmd_str, uids); + /* Make sure line length is less than 8k */ + i_assert(str_len(store_cmd_str) < IMAPC_SERVER_CMDLINE_MAX_LEN); + i_assert(str_len(expunge_cmd_str) < IMAPC_SERVER_CMDLINE_MAX_LEN); + + store_cmd = imapc_client_mailbox_cmd(ctx->src_mbox->client_box, + imapc_copy_rollback_store_callback, + ctx); + expunge_cmd = imapc_client_mailbox_cmd(ctx->src_mbox->client_box, + imapc_copy_rollback_expunge_callback, + ctx); + ctx->src_mbox->rollback_pending = TRUE; + imapc_command_send(store_cmd, str_c(store_cmd_str)); + imapc_command_send(expunge_cmd, str_c(expunge_cmd_str)); +} + +static void +imapc_rollback_send_expunge(struct imapc_save_context *ctx) +{ + string_t *uids_str; + struct seqset_builder *seqset_builder; + struct seq_range_iter iter; + unsigned int i = 0; + uint32_t uid; + + if (!array_not_empty(&ctx->src_mbox->copy_rollback_expunge_uids)) + return; + + uids_str = t_str_new(128); + seqset_builder = seqset_builder_init(uids_str); + seq_range_array_iter_init(&iter, &ctx->src_mbox->copy_rollback_expunge_uids); + + /* Iterate over all uids that must be rolled back */ + while (seq_range_array_iter_nth(&iter, i++, &uid)) { + /* Try to add the to the seqset builder while respecting + the maximum length of IMAPC_SERVER_CMDLINE_MAX_LEN. */ + if (!seqset_builder_try_add(seqset_builder, + IMAPC_SERVER_CMDLINE_MAX_LEN - + strlen("UID STORE +FLAGS (\\Deleted)"), + uid)) { + /* Maximum length is reached send the rollback + and wait for it to be finished. */ + imapc_expunge_send_cmd_str(ctx, uids_str); + while (ctx->src_mbox->rollback_pending) + imapc_mailbox_run_nofetch(ctx->src_mbox); + + /* Truncate the uids_str and create a new + seqset_builder for the next command */ + seqset_builder_deinit(&seqset_builder); + str_truncate(uids_str, 0); + seqset_builder = seqset_builder_init(uids_str); + /* Make sure the current uid which is part of + the next uid_str */ + seqset_builder_add(seqset_builder, uid); + } + } + if (str_len(uids_str) > 0) + imapc_expunge_send_cmd_str(ctx, uids_str); + while (ctx->src_mbox->rollback_pending) + imapc_mailbox_run_nofetch(ctx->src_mbox); +} + +static void imapc_copy_bulk_ctx_deinit(struct imapc_save_context *ctx) +{ + /* Clean up the pending copy and the context attached to it */ + str_truncate(ctx->src_mbox->pending_copy_cmd, 0); + i_free(ctx->src_mbox->copy_dest_box); +} + +void imapc_transaction_save_rollback(struct mail_save_context *_ctx) +{ + struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx); + + if ((ctx->src_mbox != NULL && ctx->src_mbox->pending_copy_request != NULL) || + !ctx->finished) { + /* There is still a pending copy which should not be send + as rollback() is called or the transaction has not yet + finished and rollback is called */ + ctx->failed = TRUE; + (void)imapc_transaction_save_commit_pre(_ctx); + + i_assert(ctx->finished || ctx->src_mbox != NULL); + /* Clean up the pending copy and the context attached to it */ + if (ctx->src_mbox != NULL) { + if (ctx->src_mbox->pending_copy_request != NULL) { + seqset_builder_deinit(&ctx->src_mbox->pending_copy_request->uidset_builder); + i_free(ctx->src_mbox->pending_copy_request); + } + imapc_copy_bulk_ctx_deinit(ctx); + imapc_client_stop(ctx->src_mbox->storage->client->client); + } + } + + /* Expunge all added messages from index */ + if (ctx->failed && array_is_created(&ctx->dest_saved_uids)) { + i_assert(ctx->src_mbox != NULL); + seq_range_array_merge(&ctx->src_mbox->copy_rollback_expunge_uids, &ctx->dest_saved_uids); + /* Make sure context is not finished already */ + ctx->finished = FALSE; + imapc_rollback_send_expunge(ctx); + array_free(&ctx->dest_saved_uids); + } + + if (ctx->finished || ctx->failed) { + array_free(&ctx->dest_saved_uids); + i_free(ctx); + } +} + +static bool imapc_save_copyuid(struct imapc_save_context *ctx, + const struct imapc_command_reply *reply, + uint32_t *uid_r) +{ + ARRAY_TYPE(seq_range) dest_uidset, source_uidset; + struct seq_range_iter iter; + const char *const *args; + uint32_t uid_validity; + + *uid_r = 0; + + /* <uidvalidity> <source uid-set> <dest uid-set> */ + args = t_strsplit(reply->resp_text_value, " "); + if (str_array_length(args) != 3) + return FALSE; + + if (str_to_uint32(args[0], &uid_validity) < 0) + return FALSE; + if (ctx->dest_uid_validity == 0) + ctx->dest_uid_validity = uid_validity; + else if (ctx->dest_uid_validity != uid_validity) + return FALSE; + + t_array_init(&source_uidset, 8); + t_array_init(&dest_uidset, 8); + + if (imap_seq_set_nostar_parse(args[1], &source_uidset) < 0) + return FALSE; + if (imap_seq_set_nostar_parse(args[2], &dest_uidset) < 0) + return FALSE; + + if (!array_is_created(&ctx->dest_saved_uids)) + i_array_init(&ctx->dest_saved_uids, 8); + + seq_range_array_merge(&ctx->dest_saved_uids, &dest_uidset); + + seq_range_array_iter_init(&iter, &dest_uidset); + (void)seq_range_array_iter_nth(&iter, 0, uid_r); + return TRUE; +} + +static void imapc_copy_set_error(struct imapc_save_context *sctx, + const struct imapc_command_reply *reply) +{ + sctx->failed = TRUE; + + if (reply->state != IMAPC_COMMAND_STATE_BAD) + imapc_copy_error_from_reply(sctx->mbox->storage, + MAIL_ERROR_PARAMS, reply); + else + mailbox_set_critical(&sctx->mbox->box, + "imapc: COPY failed: %s", + reply->text_full); +} + +static void +imapc_copy_simple_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_save_cmd_context *ctx = context; + uint32_t uid = 0; + + if (reply->state == IMAPC_COMMAND_STATE_OK) { + if (reply->resp_text_key != NULL && + strcasecmp(reply->resp_text_key, "COPYUID") == 0) + imapc_save_copyuid(ctx->ctx, reply, &uid); + imapc_save_add_to_index(ctx->ctx, uid); + ctx->ret = 0; + } else if (reply->state == IMAPC_COMMAND_STATE_NO) { + imapc_copy_error_from_reply(ctx->ctx->mbox->storage, + MAIL_ERROR_PARAMS, reply); + ctx->ret = -1; + } else { + mailbox_set_critical(&ctx->ctx->mbox->box, + "imapc: COPY failed: %s", reply->text_full); + ctx->ret = -1; + } + imapc_client_stop(ctx->ctx->mbox->storage->client->client); +} + +static int +imapc_copy_simple(struct mail_save_context *_ctx, struct mail *mail) +{ + struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx); + struct mailbox_transaction_context *_t = _ctx->transaction; + struct imapc_save_cmd_context sctx; + struct imapc_command *cmd; + + sctx.ret = -2; + sctx.ctx = ctx; + cmd = imapc_client_mailbox_cmd(ctx->src_mbox->client_box, + imapc_copy_simple_callback, + &sctx); + imapc_command_sendf(cmd, "UID COPY %u %s", mail->uid, _t->box->name); + while (sctx.ret == -2) + imapc_mailbox_run(ctx->src_mbox); + ctx->finished = TRUE; + return sctx.ret; +} + +static void imapc_copy_bulk_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_copy_request *request = context; + struct imapc_save_context *ctx = request->sctx; + struct imapc_mailbox *mbox = ctx->src_mbox; + unsigned int uid; + + i_assert(mbox != NULL); + i_assert(request == mbox->pending_copy_request); + + /* Check the reply state and add uid's to index and + dest_saved_uids. */ + if (ctx->failed) { + /* If the saving already failed try to find UIDs already + copied from the reply so that rollback can expunge + them */ + if (null_strcasecmp(reply->resp_text_key, "COPYUID") == 0) { + (void)imapc_save_copyuid(ctx, reply, &uid); + imapc_transaction_save_rollback(&ctx->ctx); + } + } else if (reply->state == IMAPC_COMMAND_STATE_OK) { + if (reply->resp_text_key != NULL && + strcasecmp(reply->resp_text_key, "COPYUID") == 0 && + imapc_save_copyuid(ctx, reply, &uid)) { + ctx->finished = TRUE; + } + } else { + imapc_copy_set_error(ctx, reply); + } + + ctx->src_mbox->pending_copy_request = NULL; + i_free(request); + imapc_client_stop(mbox->storage->client->client); +} + +static void imapc_mail_copy_bulk_flush(struct imapc_mailbox *mbox) +{ + struct imapc_command *cmd; + + i_assert(mbox != NULL); + i_assert(mbox->pending_copy_request != NULL); + i_assert(mbox->client_box != NULL); + + cmd = imapc_client_mailbox_cmd(mbox->client_box, + imapc_copy_bulk_callback, + mbox->pending_copy_request); + + seqset_builder_deinit(&mbox->pending_copy_request->uidset_builder); + + str_append(mbox->pending_copy_cmd, " "); + imap_append_astring(mbox->pending_copy_cmd, mbox->copy_dest_box); + + imapc_command_send(cmd, str_c(mbox->pending_copy_cmd)); + + imapc_copy_bulk_ctx_deinit(mbox->pending_copy_request->sctx); +} + +static bool +imapc_mail_copy_bulk_try_merge(struct imapc_mailbox *mbox, uint32_t uid, + const char *box) +{ + i_assert(str_begins(str_c(mbox->pending_copy_cmd), "UID COPY ")); + + if (strcmp(box, mbox->copy_dest_box) != 0) { + /* Not the same mailbox merging not possible */ + return FALSE; + } + return seqset_builder_try_add(mbox->pending_copy_request->uidset_builder, + IMAPC_SERVER_CMDLINE_MAX_LEN, uid); +} + +static void +imapc_mail_copy_bulk_delayed_send_or_merge(struct imapc_save_context *ctx, + uint32_t uid, + const char *box) +{ + struct imapc_mailbox *mbox = ctx->src_mbox; + + if (mbox->pending_copy_request != NULL && + !imapc_mail_copy_bulk_try_merge(mbox, uid, box)) { + /* send the previous COPY and create new one after + waiting for this one to be finished. */ + imapc_mail_copy_bulk_flush(mbox); + imapc_copy_bulk_finish(mbox->pending_copy_request->sctx); + } + if (mbox->pending_copy_request == NULL) { + mbox->pending_copy_request = + i_new(struct imapc_copy_request, 1); + str_printfa(mbox->pending_copy_cmd, "UID COPY "); + mbox->pending_copy_request->uidset_builder = + seqset_builder_init(mbox->pending_copy_cmd); + seqset_builder_add(mbox->pending_copy_request->uidset_builder, + uid); + mbox->copy_dest_box = i_strdup(box); + } else { + i_assert(mbox->pending_copy_request->sctx == ctx); + } + mbox->pending_copy_request->sctx = ctx; +} + +static int +imapc_copy_bulk(struct imapc_save_context *ctx, struct mail *mail) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(ctx->ctx.transaction->box); + + imapc_mail_copy_bulk_delayed_send_or_merge(ctx, mail->uid, + imapc_mailbox_get_remote_name(mbox)); + imapc_save_add_to_index(ctx, 0); + + return ctx->failed ? -1 : 0; +} + +static bool imapc_is_mail_expunged(struct imapc_mailbox *mbox, uint32_t uid) +{ + if (array_is_created(&mbox->delayed_expunged_uids) && + seq_range_exists(&mbox->delayed_expunged_uids, uid)) + return TRUE; + if (mbox->delayed_sync_trans == NULL) + return FALSE; + + struct mail_index_view *view = + mail_index_transaction_get_view(mbox->delayed_sync_trans); + uint32_t seq; + return mail_index_lookup_seq(view, uid, &seq) && + mail_index_transaction_is_expunged(mbox->delayed_sync_trans, seq); +} + +int imapc_copy(struct mail_save_context *_ctx, struct mail *mail) +{ + struct imapc_save_context *ctx = IMAPC_SAVECTX(_ctx); + struct mailbox_transaction_context *_t = _ctx->transaction; + struct imapc_msgmap *src_msgmap; + uint32_t rseq; + int ret; + + i_assert((_t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0); + + if (_t->box->storage == mail->box->storage) { + /* Currently we don't support copying mails from multiple + different source mailboxes within the same transaction. */ + i_assert(ctx->src_mbox == NULL || &ctx->src_mbox->box == mail->box); + ctx->src_mbox = IMAPC_MAILBOX(mail->box); + if (!mail->expunged && imapc_is_mail_expunged(ctx->mbox, mail->uid)) + mail_set_expunged(mail); + /* same server, we can use COPY for the mail */ + src_msgmap = + imapc_client_mailbox_get_msgmap(ctx->src_mbox->client_box); + if (mail->expunged || + !imapc_msgmap_uid_to_rseq(src_msgmap, mail->uid, &rseq)) { + mail_storage_set_error(mail->box->storage, + MAIL_ERROR_EXPUNGED, + "Some of the requested messages no longer exist."); + ctx->finished = TRUE; + index_save_context_free(_ctx); + return -1; + } + /* Mail has not been expunged and can be copied. */ + if (ctx->mbox->capabilities == 0) { + /* The destination mailbox has not yet been selected + so the capabilities are unknown */ + if (imapc_client_get_capabilities(ctx->mbox->storage->client->client, + &ctx->mbox->capabilities) < 0) { + mail_storage_set_error(mail->box->storage, + MAIL_ERROR_UNAVAILABLE, + "Failed to determine capabilities for mailbox."); + ctx->finished = TRUE; + index_save_context_free(_ctx); + return -1; + } + } + if ((ctx->mbox->capabilities & IMAPC_CAPABILITY_UIDPLUS) != 0) + ret = imapc_copy_bulk(ctx, mail); + else + ret = imapc_copy_simple(_ctx, mail); + index_save_context_free(_ctx); + return ret; + } + return mail_storage_copy(_ctx, mail); +} diff --git a/src/lib-storage/index/imapc/imapc-search.c b/src/lib-storage/index/imapc/imapc-search.c new file mode 100644 index 0000000..98fa8b5 --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-search.c @@ -0,0 +1,332 @@ +/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "imap-arg.h" +#include "imap-seqset.h" +#include "imap-util.h" +#include "mail-search.h" +#include "imapc-msgmap.h" +#include "imapc-storage.h" +#include "imapc-search.h" + +#define IMAPC_SEARCHCTX(obj) \ + MODULE_CONTEXT(obj, imapc_storage_module) + +struct imapc_search_context { + union mail_search_module_context module_ctx; + + ARRAY_TYPE(seq_range) rseqs; + struct seq_range_iter iter; + unsigned int n; + bool finished; + bool success; +}; + +static MODULE_CONTEXT_DEFINE_INIT(imapc_storage_module, + &mail_storage_module_register); + +static bool +imapc_build_search_query_args(struct imapc_mailbox *mbox, + const struct mail_search_arg *args, + bool parent_or, string_t *str); + +static bool imapc_search_is_fast_local(const struct mail_search_arg *args) +{ + const struct mail_search_arg *arg; + + for (arg = args; arg != NULL; arg = arg->next) { + switch (arg->type) { + case SEARCH_OR: + case SEARCH_SUB: + if (!imapc_search_is_fast_local(arg->value.subargs)) + return FALSE; + break; + case SEARCH_ALL: + case SEARCH_SEQSET: + case SEARCH_UIDSET: + case SEARCH_FLAGS: + case SEARCH_KEYWORDS: + case SEARCH_MODSEQ: + case SEARCH_MAILBOX: + case SEARCH_MAILBOX_GUID: + case SEARCH_MAILBOX_GLOB: + case SEARCH_REAL_UID: + break; + default: + return FALSE; + } + } + return TRUE; +} + +static bool +imapc_build_search_query_arg(struct imapc_mailbox *mbox, + const struct mail_search_arg *arg, + string_t *str) +{ + struct mail_search_arg arg2 = *arg; + const char *error; + + if (arg->match_not) + str_append(str, "NOT "); + arg2.match_not = FALSE; + arg = &arg2; + + switch (arg->type) { + case SEARCH_OR: + imapc_build_search_query_args(mbox, arg->value.subargs, TRUE, str); + return TRUE; + case SEARCH_SUB: + str_append_c(str, '('); + imapc_build_search_query_args(mbox, arg->value.subargs, FALSE, str); + str_append_c(str, ')'); + return TRUE; + case SEARCH_SEQSET: + /* translate to UIDs */ + T_BEGIN { + ARRAY_TYPE(seq_range) uids; + + t_array_init(&uids, 64); + mailbox_get_uid_range(&mbox->box, &arg->value.seqset, + &uids); + str_append(str, "UID "); + imap_write_seq_range(str, &uids); + } T_END; + return TRUE; + case SEARCH_BEFORE: + case SEARCH_SINCE: + case SEARCH_ON: + if (arg->type != SEARCH_ON && + (mbox->capabilities & IMAPC_CAPABILITY_WITHIN) == 0) { + /* a bit kludgy way to check this.. */ + size_t pos = str_len(str); + if (!mail_search_arg_to_imap(str, arg, &error)) + return FALSE; + if (strncasecmp(str_c(str) + pos, "OLDER", 5) == 0 || + strncasecmp(str_c(str) + pos, "YOUNGER", 7) == 0) + return FALSE; + return TRUE; + } + if (arg->value.date_type == MAIL_SEARCH_DATE_TYPE_SAVED && + (mbox->capabilities & IMAPC_CAPABILITY_SAVEDATE) == 0) { + /* Fall back to internal date if save date is not + supported. */ + arg2.value.date_type = MAIL_SEARCH_DATE_TYPE_RECEIVED; + } + /* fall through */ + case SEARCH_ALL: + case SEARCH_UIDSET: + case SEARCH_FLAGS: + case SEARCH_KEYWORDS: + case SEARCH_SMALLER: + case SEARCH_LARGER: + case SEARCH_HEADER: + case SEARCH_HEADER_ADDRESS: + case SEARCH_HEADER_COMPRESS_LWSP: + case SEARCH_BODY: + case SEARCH_TEXT: + return mail_search_arg_to_imap(str, arg, &error); + /* extensions */ + case SEARCH_MODSEQ: + if ((mbox->capabilities & IMAPC_CAPABILITY_CONDSTORE) == 0) + return FALSE; + return mail_search_arg_to_imap(str, arg, &error); + case SEARCH_SAVEDATESUPPORTED: + if ((mbox->capabilities & IMAPC_CAPABILITY_SAVEDATE) == 0) + return FALSE; + return mail_search_arg_to_imap(str, arg, &error); + case SEARCH_INTHREAD: + case SEARCH_GUID: + case SEARCH_MAILBOX: + case SEARCH_MAILBOX_GUID: + case SEARCH_MAILBOX_GLOB: + case SEARCH_REAL_UID: + case SEARCH_MIMEPART: + /* not supported for now */ + break; + case SEARCH_NIL: + i_unreached(); + } + return FALSE; +} + +static bool +imapc_build_search_query_args(struct imapc_mailbox *mbox, + const struct mail_search_arg *args, + bool parent_or, string_t *str) +{ + const struct mail_search_arg *arg; + + for (arg = args; arg != NULL; arg = arg->next) { + if (parent_or && arg->next != NULL) + str_append(str, "OR "); + if (!imapc_build_search_query_arg(mbox, arg, str)) + return FALSE; + str_append_c(str, ' '); + } + str_truncate(str, str_len(str)-1); + return TRUE; +} + +static bool imapc_build_search_query(struct imapc_mailbox *mbox, + const struct mail_search_args *args, + const char **query_r) +{ + string_t *str = t_str_new(128); + + if (!IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_SEARCH)) { + /* SEARCH command passthrough not enabled */ + return FALSE; + } + if (imapc_search_is_fast_local(args->args)) + return FALSE; + + if ((mbox->capabilities & IMAPC_CAPABILITY_ESEARCH) != 0) + str_append(str, "SEARCH RETURN (ALL) "); + else + str_append(str, "UID SEARCH "); + if (!imapc_build_search_query_args(mbox, args->args, FALSE, str)) + return FALSE; + *query_r = str_c(str); + return TRUE; +} + +static void imapc_search_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct mail_search_context *ctx = context; + struct imapc_mailbox *mbox = IMAPC_MAILBOX(ctx->transaction->box); + struct imapc_search_context *ictx = IMAPC_SEARCHCTX(ctx); + i_assert(ictx != NULL); + + ictx->finished = TRUE; + if (reply->state == IMAPC_COMMAND_STATE_OK) { + seq_range_array_iter_init(&ictx->iter, &ictx->rseqs); + ictx->success = TRUE; + } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) { + mail_storage_set_internal_error(mbox->box.storage); + } else { + mailbox_set_critical(&mbox->box, + "imapc: Command failed: %s", reply->text_full); + } + imapc_client_stop(mbox->storage->client->client); +} + +struct mail_search_context * +imapc_search_init(struct mailbox_transaction_context *t, + struct mail_search_args *args, + const enum mail_sort_type *sort_program, + enum mail_fetch_field wanted_fields, + struct mailbox_header_lookup_ctx *wanted_headers) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(t->box); + struct mail_search_context *ctx; + struct imapc_search_context *ictx; + struct imapc_command *cmd; + const char *search_query; + + ctx = index_storage_search_init(t, args, sort_program, + wanted_fields, wanted_headers); + + if (!imapc_build_search_query(mbox, args, &search_query)) { + /* can't optimize this with SEARCH */ + return ctx; + } + + ictx = i_new(struct imapc_search_context, 1); + i_array_init(&ictx->rseqs, 64); + MODULE_CONTEXT_SET(ctx, imapc_storage_module, ictx); + + cmd = imapc_client_mailbox_cmd(mbox->client_box, + imapc_search_callback, ctx); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + imapc_command_send(cmd, search_query); + + i_assert(mbox->search_ctx == NULL); + mbox->search_ctx = ictx; + while (!ictx->finished) + imapc_client_run(mbox->storage->client->client); + mbox->search_ctx = NULL; + return ctx; +} + +static void imapc_search_set_matches(struct mail_search_arg *args) +{ + for (; args != NULL; args = args->next) { + if (args->type == SEARCH_OR || + args->type == SEARCH_SUB) + imapc_search_set_matches(args->value.subargs); + args->match_always = TRUE; + args->result = 1; + } +} + +bool imapc_search_next_update_seq(struct mail_search_context *ctx) +{ + struct imapc_search_context *ictx = IMAPC_SEARCHCTX(ctx); + + if (ictx == NULL || !ictx->success) + return index_storage_search_next_update_seq(ctx); + + if (!seq_range_array_iter_nth(&ictx->iter, ictx->n++, &ctx->seq)) + return FALSE; + ctx->progress_cur = ctx->seq; + + imapc_search_set_matches(ctx->args->args); + return TRUE; +} + +int imapc_search_deinit(struct mail_search_context *ctx) +{ + struct imapc_search_context *ictx = IMAPC_SEARCHCTX(ctx); + + if (ictx != NULL) { + array_free(&ictx->rseqs); + i_free(ictx); + } + return index_storage_search_deinit(ctx); +} + +void imapc_search_reply_search(const struct imap_arg *args, + struct imapc_mailbox *mbox) +{ + struct imapc_msgmap *msgmap = + imapc_client_mailbox_get_msgmap(mbox->client_box); + const char *atom; + uint32_t uid, rseq; + + if (mbox->search_ctx == NULL) { + i_error("Unexpected SEARCH reply"); + return; + } + + /* we're doing UID SEARCH, so need to convert UIDs to sequences */ + for (unsigned int i = 0; args[i].type != IMAP_ARG_EOL; i++) { + if (!imap_arg_get_atom(&args[i], &atom) || + str_to_uint32(atom, &uid) < 0 || uid == 0) { + i_error("Invalid SEARCH reply"); + break; + } + if (imapc_msgmap_uid_to_rseq(msgmap, uid, &rseq)) + seq_range_array_add(&mbox->search_ctx->rseqs, rseq); + } +} + +void imapc_search_reply_esearch(const struct imap_arg *args, + struct imapc_mailbox *mbox) +{ + const char *atom; + + if (mbox->search_ctx == NULL) { + i_error("Unexpected ESEARCH reply"); + return; + } + + /* It should contain ALL <seqset> or nonexistent if nothing matched */ + if (args[0].type != IMAP_ARG_EOL && + (!imap_arg_atom_equals(&args[0], "ALL") || + !imap_arg_get_atom(&args[1], &atom) || + imap_seq_set_nostar_parse(atom, &mbox->search_ctx->rseqs) < 0)) + i_error("Invalid ESEARCH reply"); +} diff --git a/src/lib-storage/index/imapc/imapc-search.h b/src/lib-storage/index/imapc/imapc-search.h new file mode 100644 index 0000000..0966569 --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-search.h @@ -0,0 +1,18 @@ +#ifndef IMAPC_SEARCH_H +#define IMAPC_SEARCH_H + +struct mail_search_context * +imapc_search_init(struct mailbox_transaction_context *t, + struct mail_search_args *args, + const enum mail_sort_type *sort_program, + enum mail_fetch_field wanted_fields, + struct mailbox_header_lookup_ctx *wanted_headers); +bool imapc_search_next_update_seq(struct mail_search_context *ctx); +int imapc_search_deinit(struct mail_search_context *ctx); + +void imapc_search_reply_search(const struct imap_arg *args, + struct imapc_mailbox *mbox); +void imapc_search_reply_esearch(const struct imap_arg *args, + struct imapc_mailbox *mbox); + +#endif diff --git a/src/lib-storage/index/imapc/imapc-settings.c b/src/lib-storage/index/imapc/imapc-settings.c new file mode 100644 index 0000000..6183f8f --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-settings.c @@ -0,0 +1,173 @@ +/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "settings-parser.h" +#include "mail-storage-settings.h" +#include "imapc-settings.h" + +#include <stddef.h> + +#undef DEF +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct imapc_settings) + +static bool imapc_settings_check(void *_set, pool_t pool, const char **error_r); + +static const struct setting_define imapc_setting_defines[] = { + DEF(STR, imapc_host), + DEF(IN_PORT, imapc_port), + + DEF(STR_VARS, imapc_user), + DEF(STR_VARS, imapc_master_user), + DEF(STR, imapc_password), + DEF(STR, imapc_sasl_mechanisms), + + DEF(ENUM, imapc_ssl), + DEF(BOOL, imapc_ssl_verify), + + DEF(STR, imapc_features), + DEF(STR, imapc_rawlog_dir), + DEF(STR, imapc_list_prefix), + DEF(TIME, imapc_cmd_timeout), + DEF(TIME, imapc_max_idle_time), + DEF(UINT, imapc_connection_retry_count), + DEF(TIME_MSECS, imapc_connection_retry_interval), + DEF(SIZE, imapc_max_line_length), + + DEF(STR, pop3_deleted_flag), + + SETTING_DEFINE_LIST_END +}; + +static const struct imapc_settings imapc_default_settings = { + .imapc_host = "", + .imapc_port = 143, + + .imapc_user = "", + .imapc_master_user = "", + .imapc_password = "", + .imapc_sasl_mechanisms = "", + + .imapc_ssl = "no:imaps:starttls", + .imapc_ssl_verify = TRUE, + + .imapc_features = "", + .imapc_rawlog_dir = "", + .imapc_list_prefix = "", + .imapc_cmd_timeout = 5*60, + .imapc_max_idle_time = 60*29, + .imapc_connection_retry_count = 1, + .imapc_connection_retry_interval = 1000, + .imapc_max_line_length = 0, + + .pop3_deleted_flag = "" +}; + +static const struct setting_parser_info imapc_setting_parser_info = { + .module_name = "imapc", + .defines = imapc_setting_defines, + .defaults = &imapc_default_settings, + + .type_offset = SIZE_MAX, + .struct_size = sizeof(struct imapc_settings), + + .parent_offset = SIZE_MAX, + .parent = &mail_user_setting_parser_info, + + .check_func = imapc_settings_check +}; + +const struct setting_parser_info *imapc_get_setting_parser_info(void) +{ + return &imapc_setting_parser_info; +} + +/* <settings checks> */ +struct imapc_feature_list { + const char *name; + enum imapc_features num; +}; + +static const struct imapc_feature_list imapc_feature_list[] = { + { "rfc822.size", IMAPC_FEATURE_RFC822_SIZE }, + { "guid-forced", IMAPC_FEATURE_GUID_FORCED }, + { "fetch-headers", IMAPC_FEATURE_FETCH_HEADERS }, + { "gmail-migration", IMAPC_FEATURE_GMAIL_MIGRATION }, + { "search", IMAPC_FEATURE_SEARCH }, + { "zimbra-workarounds", IMAPC_FEATURE_ZIMBRA_WORKAROUNDS }, + { "no-examine", IMAPC_FEATURE_NO_EXAMINE }, + { "proxyauth", IMAPC_FEATURE_PROXYAUTH }, + { "fetch-msn-workarounds", IMAPC_FEATURE_FETCH_MSN_WORKAROUNDS }, + { "fetch-fix-broken-mails", IMAPC_FEATURE_FETCH_FIX_BROKEN_MAILS }, + { "modseq", IMAPC_FEATURE_MODSEQ }, + { "delay-login", IMAPC_FEATURE_DELAY_LOGIN }, + { "fetch-bodystructure", IMAPC_FEATURE_FETCH_BODYSTRUCTURE }, + { "send-id", IMAPC_FEATURE_SEND_ID }, + { "fetch-empty-is-expunged", IMAPC_FEATURE_FETCH_EMPTY_IS_EXPUNGED }, + { "no-msn-updates", IMAPC_FEATURE_NO_MSN_UPDATES }, + { "acl", IMAPC_FEATURE_ACL }, + { NULL, 0 } +}; + +static int +imapc_settings_parse_throttle(struct imapc_settings *set, + const char *throttle_str, const char **error_r) +{ + const char *const *tmp; + + tmp = t_strsplit(throttle_str, ":"); + if (str_array_length(tmp) != 3 || + str_to_uint(tmp[0], &set->throttle_init_msecs) < 0 || + str_to_uint(tmp[1], &set->throttle_max_msecs) < 0 || + str_to_uint(tmp[2], &set->throttle_shrink_min_msecs) < 0) { + *error_r = "imapc_features: Invalid throttle settings"; + return -1; + } + return 0; +} + +static int +imapc_settings_parse_features(struct imapc_settings *set, + const char **error_r) +{ + enum imapc_features features = 0; + const struct imapc_feature_list *list; + const char *const *str; + + str = t_strsplit_spaces(set->imapc_features, " ,"); + for (; *str != NULL; str++) { + list = imapc_feature_list; + for (; list->name != NULL; list++) { + if (strcasecmp(*str, list->name) == 0) { + features |= list->num; + break; + } + } + if (strncasecmp(*str, "throttle:", 9) == 0) { + if (imapc_settings_parse_throttle(set, *str + 9, error_r) < 0) + return -1; + continue; + } + if (list->name == NULL) { + *error_r = t_strdup_printf("imapc_features: " + "Unknown feature: %s", *str); + return -1; + } + } + set->parsed_features = features; + return 0; +} + +static bool imapc_settings_check(void *_set, pool_t pool ATTR_UNUSED, + const char **error_r) +{ + struct imapc_settings *set = _set; + + if (set->imapc_max_idle_time == 0) { + *error_r = "imapc_max_idle_time must not be 0"; + return FALSE; + } + if (imapc_settings_parse_features(set, error_r) < 0) + return FALSE; + return TRUE; +} diff --git a/src/lib-storage/index/imapc/imapc-settings.h b/src/lib-storage/index/imapc/imapc-settings.h new file mode 100644 index 0000000..e4c1da1 --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-settings.h @@ -0,0 +1,63 @@ +#ifndef IMAPC_SETTINGS_H +#define IMAPC_SETTINGS_H + +#include "net.h" + +/* <settings checks> */ +enum imapc_features { + IMAPC_FEATURE_RFC822_SIZE = 0x01, + IMAPC_FEATURE_GUID_FORCED = 0x02, + IMAPC_FEATURE_FETCH_HEADERS = 0x04, + IMAPC_FEATURE_GMAIL_MIGRATION = 0x08, + IMAPC_FEATURE_SEARCH = 0x10, + IMAPC_FEATURE_ZIMBRA_WORKAROUNDS = 0x20, + IMAPC_FEATURE_NO_EXAMINE = 0x40, + IMAPC_FEATURE_PROXYAUTH = 0x80, + IMAPC_FEATURE_FETCH_MSN_WORKAROUNDS = 0x100, + IMAPC_FEATURE_FETCH_FIX_BROKEN_MAILS = 0x200, + IMAPC_FEATURE_MODSEQ = 0x400, + IMAPC_FEATURE_DELAY_LOGIN = 0x800, + IMAPC_FEATURE_FETCH_BODYSTRUCTURE = 0x1000, + IMAPC_FEATURE_SEND_ID = 0x2000, + IMAPC_FEATURE_FETCH_EMPTY_IS_EXPUNGED = 0x4000, + IMAPC_FEATURE_NO_MSN_UPDATES = 0x8000, + IMAPC_FEATURE_ACL = 0x10000, +}; +/* </settings checks> */ + +/* + * NOTE: Any additions here should be reflected in imapc_storage_create's + * serialization of settings. + */ +struct imapc_settings { + const char *imapc_host; + in_port_t imapc_port; + + const char *imapc_user; + const char *imapc_master_user; + const char *imapc_password; + const char *imapc_sasl_mechanisms; + + const char *imapc_ssl; + bool imapc_ssl_verify; + + const char *imapc_features; + const char *imapc_rawlog_dir; + const char *imapc_list_prefix; + unsigned int imapc_cmd_timeout; + unsigned int imapc_max_idle_time; + unsigned int imapc_connection_retry_count; + unsigned int imapc_connection_retry_interval; + uoff_t imapc_max_line_length; + + const char *pop3_deleted_flag; + + enum imapc_features parsed_features; + unsigned int throttle_init_msecs; + unsigned int throttle_max_msecs; + unsigned int throttle_shrink_min_msecs; +}; + +const struct setting_parser_info *imapc_get_setting_parser_info(void); + +#endif diff --git a/src/lib-storage/index/imapc/imapc-storage.c b/src/lib-storage/index/imapc/imapc-storage.c new file mode 100644 index 0000000..2c9fcdf --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-storage.c @@ -0,0 +1,1353 @@ +/* Copyright (c) 2011-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "str.h" +#include "imap-arg.h" +#include "imap-resp-code.h" +#include "mailbox-tree.h" +#include "imapc-connection.h" +#include "imapc-msgmap.h" +#include "imapc-mail.h" +#include "imapc-list.h" +#include "imapc-search.h" +#include "imapc-sync.h" +#include "imapc-settings.h" +#include "imapc-storage.h" + +#define DNS_CLIENT_SOCKET_NAME "dns-client" + +struct imapc_open_context { + struct imapc_mailbox *mbox; + int ret; +}; + +struct imapc_resp_code_map { + const char *code; + enum mail_error error; +}; + +extern struct mail_storage imapc_storage; +extern struct mailbox imapc_mailbox; + +static struct event_category event_category_imapc = { + .name = "imapc", + .parent = &event_category_storage, +}; + +static struct imapc_resp_code_map imapc_resp_code_map[] = { + { IMAP_RESP_CODE_UNAVAILABLE, MAIL_ERROR_TEMP }, + { IMAP_RESP_CODE_AUTHFAILED, MAIL_ERROR_PERM }, + { IMAP_RESP_CODE_AUTHZFAILED, MAIL_ERROR_PERM }, + { IMAP_RESP_CODE_EXPIRED, MAIL_ERROR_PERM }, + { IMAP_RESP_CODE_PRIVACYREQUIRED, MAIL_ERROR_PERM }, + { IMAP_RESP_CODE_CONTACTADMIN, MAIL_ERROR_PERM }, + { IMAP_RESP_CODE_NOPERM, MAIL_ERROR_PERM }, + { IMAP_RESP_CODE_INUSE, MAIL_ERROR_INUSE }, + { IMAP_RESP_CODE_EXPUNGEISSUED, MAIL_ERROR_EXPUNGED }, + { IMAP_RESP_CODE_CORRUPTION, MAIL_ERROR_TEMP }, + { IMAP_RESP_CODE_SERVERBUG, MAIL_ERROR_TEMP }, + /* { IMAP_RESP_CODE_CLIENTBUG, 0 }, */ + { IMAP_RESP_CODE_CANNOT, MAIL_ERROR_NOTPOSSIBLE }, + { IMAP_RESP_CODE_LIMIT, MAIL_ERROR_LIMIT }, + { IMAP_RESP_CODE_OVERQUOTA, MAIL_ERROR_NOQUOTA }, + { IMAP_RESP_CODE_ALREADYEXISTS, MAIL_ERROR_EXISTS }, + { IMAP_RESP_CODE_NONEXISTENT, MAIL_ERROR_NOTFOUND } +}; + +static void imapc_untagged_status(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client); +static void imapc_untagged_namespace(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client); +static int imapc_mailbox_run_status(struct mailbox *box, + enum mailbox_status_items items, + struct mailbox_status *status_r); + +bool imapc_resp_text_code_parse(const char *str, enum mail_error *error_r) +{ + unsigned int i; + + if (str == NULL) + return FALSE; + + for (i = 0; i < N_ELEMENTS(imapc_resp_code_map); i++) { + if (strcmp(imapc_resp_code_map[i].code, str) == 0) { + *error_r = imapc_resp_code_map[i].error; + return TRUE; + } + } + return FALSE; +} + +bool imapc_mail_error_to_resp_text_code(enum mail_error error, const char **str_r) +{ + unsigned int i; + + for (i = 0; i < N_ELEMENTS(imapc_resp_code_map); i++) { + if (imapc_resp_code_map[i].error == error) { + *str_r = imapc_resp_code_map[i].code; + return TRUE; + } + } + return FALSE; +} + +bool imapc_mailbox_has_modseqs(struct imapc_mailbox *mbox) +{ + return (mbox->capabilities & (IMAPC_CAPABILITY_CONDSTORE | + IMAPC_CAPABILITY_QRESYNC)) != 0 && + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_MODSEQ); +} + +static struct mail_storage *imapc_storage_alloc(void) +{ + struct imapc_storage *storage; + pool_t pool; + + pool = pool_alloconly_create("imapc storage", 2048); + storage = p_new(pool, struct imapc_storage, 1); + storage->storage = imapc_storage; + storage->storage.pool = pool; + storage->root_ioloop = current_ioloop; + return &storage->storage; +} + +void imapc_copy_error_from_reply(struct imapc_storage *storage, + enum mail_error default_error, + const struct imapc_command_reply *reply) +{ + enum mail_error error; + + if (imapc_resp_text_code_parse(reply->resp_text_key, &error)) { + mail_storage_set_error(&storage->storage, error, + reply->text_without_resp); + } else { + mail_storage_set_error(&storage->storage, default_error, + reply->text_without_resp); + } +} + +void imapc_simple_context_init(struct imapc_simple_context *sctx, + struct imapc_storage_client *client) +{ + i_zero(sctx); + sctx->client = client; + sctx->ret = -2; +} + +void imapc_simple_run(struct imapc_simple_context *sctx, + struct imapc_command **cmd) +{ + if (imapc_storage_client_handle_auth_failure(sctx->client)) { + imapc_command_abort(cmd); + imapc_client_logout(sctx->client->client); + sctx->ret = -1; + } + *cmd = NULL; + while (sctx->ret == -2) + imapc_client_run(sctx->client->client); +} + +void imapc_mailbox_run(struct imapc_mailbox *mbox) +{ + imapc_mail_fetch_flush(mbox); + imapc_mailbox_run_nofetch(mbox); +} + +void imapc_mailbox_run_nofetch(struct imapc_mailbox *mbox) +{ + do { + imapc_client_run(mbox->storage->client->client); + } while (mbox->storage->reopen_count > 0 || + mbox->state_fetching_uid1); +} + +void imapc_simple_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_simple_context *ctx = context; + + if (reply->state == IMAPC_COMMAND_STATE_OK) + ctx->ret = 0; + else if (reply->state == IMAPC_COMMAND_STATE_NO) { + imapc_copy_error_from_reply(ctx->client->_storage, + MAIL_ERROR_PARAMS, reply); + ctx->ret = -1; + } else if (imapc_storage_client_handle_auth_failure(ctx->client)) { + ctx->ret = -1; + } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) { + mail_storage_set_internal_error(&ctx->client->_storage->storage); + ctx->ret = -1; + } else { + mail_storage_set_critical(&ctx->client->_storage->storage, + "imapc: Command failed: %s", reply->text_full); + ctx->ret = -1; + } + imapc_client_stop(ctx->client->client); +} + +void imapc_mailbox_noop(struct imapc_mailbox *mbox) +{ + struct imapc_command *cmd; + struct imapc_simple_context sctx; + + if (mbox->client_box == NULL) { + /* mailbox opening hasn't finished yet */ + return; + } + + imapc_simple_context_init(&sctx, mbox->storage->client); + cmd = imapc_client_mailbox_cmd(mbox->client_box, + imapc_simple_callback, &sctx); + imapc_command_send(cmd, "NOOP"); + imapc_simple_run(&sctx, &cmd); +} + +static void +imapc_storage_client_untagged_cb(const struct imapc_untagged_reply *reply, + void *context) +{ + struct imapc_storage_client *client = context; + struct imapc_mailbox *mbox = reply->untagged_box_context; + const struct imapc_storage_event_callback *cb; + const struct imapc_mailbox_event_callback *mcb; + + array_foreach(&client->untagged_callbacks, cb) { + if (strcasecmp(reply->name, cb->name) == 0) + cb->callback(reply, client); + } + + if (mbox == NULL) + return; + + array_foreach(&mbox->untagged_callbacks, mcb) { + if (strcasecmp(reply->name, mcb->name) == 0) + mcb->callback(reply, mbox); + } + + if (reply->resp_text_key != NULL) { + array_foreach(&mbox->resp_text_callbacks, mcb) { + if (strcasecmp(reply->resp_text_key, mcb->name) == 0) + mcb->callback(reply, mbox); + } + } +} + +static void +imapc_storage_client_login_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_storage_client *client = context; + + client->auth_returned = TRUE; + imapc_client_stop(client->client); + + if (reply->state == IMAPC_COMMAND_STATE_OK) + return; + if (client->destroying && + reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) { + /* user's work was finished before imapc login finished - + it's not an error */ + return; + } + + client->auth_failed_state = reply->state; + client->auth_failed_reason = i_strdup(reply->text_full); + if (!imapc_storage_client_handle_auth_failure(client)) + i_unreached(); +} + +bool imapc_storage_client_handle_auth_failure(struct imapc_storage_client *client) +{ + if (client->auth_failed_state == IMAPC_COMMAND_STATE_OK) + return FALSE; + + /* We need to set the error to either storage or to list, depending on + whether the caller is from mail-storage.h API or mailbox-list.h API. + We don't know here what the caller is though, so just set the error + to both of them. */ + if (client->_storage != NULL) { + if (client->auth_failed_state == IMAPC_COMMAND_STATE_DISCONNECTED) + mail_storage_set_internal_error(&client->_storage->storage); + else { + mail_storage_set_error(&client->_storage->storage, + MAIL_ERROR_PERM, client->auth_failed_reason); + } + } + if (client->_list != NULL) { + if (client->auth_failed_state == IMAPC_COMMAND_STATE_DISCONNECTED) + mailbox_list_set_internal_error(&client->_list->list); + else { + mailbox_list_set_error(&client->_list->list, + MAIL_ERROR_PERM, client->auth_failed_reason); + } + } + return TRUE; +} + +static void imapc_storage_client_login(struct imapc_storage_client *client, + struct mail_user *user, const char *host) +{ + imapc_client_login(client->client); + if (!user->namespaces_created) { + /* we're still initializing the user. wait for the + login to finish, so we can fail the user creation + if it fails. */ + while (!client->auth_returned) + imapc_client_run(client->client); + if (imapc_storage_client_handle_auth_failure(client)) { + user->error = p_strdup_printf(user->pool, + "imapc: Login to %s failed: %s", + host, client->auth_failed_reason); + } + } +} + +int imapc_storage_client_create(struct mail_namespace *ns, + const struct imapc_settings *imapc_set, + const struct mail_storage_settings *mail_set, + struct imapc_storage_client **client_r, + const char **error_r) +{ + struct imapc_storage_client *client; + struct imapc_client_settings set; + string_t *str; + + i_zero(&set); + set.host = imapc_set->imapc_host; + if (*set.host == '\0') { + *error_r = "missing imapc_host"; + return -1; + } + set.port = imapc_set->imapc_port; + if (imapc_set->imapc_user[0] != '\0') + set.username = imapc_set->imapc_user; + else if (ns->owner != NULL) + set.username = ns->owner->username; + else + set.username = ns->user->username; + set.master_user = imapc_set->imapc_master_user; + set.password = imapc_set->imapc_password; + if (*set.password == '\0') { + *error_r = "missing imapc_password"; + return -1; + } + set.sasl_mechanisms = imapc_set->imapc_sasl_mechanisms; + set.use_proxyauth = (imapc_set->parsed_features & IMAPC_FEATURE_PROXYAUTH) != 0; + set.cmd_timeout_msecs = imapc_set->imapc_cmd_timeout * 1000; + set.connect_retry_count = imapc_set->imapc_connection_retry_count; + set.connect_retry_interval_msecs = imapc_set->imapc_connection_retry_interval; + set.max_idle_time = imapc_set->imapc_max_idle_time; + set.max_line_length = imapc_set->imapc_max_line_length; + set.dns_client_socket_path = *ns->user->set->base_dir == '\0' ? "" : + t_strconcat(ns->user->set->base_dir, "/", + DNS_CLIENT_SOCKET_NAME, NULL); + set.debug = mail_set->mail_debug; + set.rawlog_dir = mail_user_home_expand(ns->user, + imapc_set->imapc_rawlog_dir); + if ((imapc_set->parsed_features & IMAPC_FEATURE_SEND_ID) != 0) + set.session_id_prefix = ns->user->session_id; + + str = t_str_new(128); + mail_user_set_get_temp_prefix(str, ns->user->set); + set.temp_path_prefix = str_c(str); + + mail_user_init_ssl_client_settings(ns->user, &set.ssl_set); + if (!imapc_set->imapc_ssl_verify) + set.ssl_set.allow_invalid_cert = TRUE; + + if (strcmp(imapc_set->imapc_ssl, "imaps") == 0) + set.ssl_mode = IMAPC_CLIENT_SSL_MODE_IMMEDIATE; + else if (strcmp(imapc_set->imapc_ssl, "starttls") == 0) + set.ssl_mode = IMAPC_CLIENT_SSL_MODE_STARTTLS; + else + set.ssl_mode = IMAPC_CLIENT_SSL_MODE_NONE; + + set.throttle_set.init_msecs = imapc_set->throttle_init_msecs; + set.throttle_set.max_msecs = imapc_set->throttle_max_msecs; + set.throttle_set.shrink_min_msecs = imapc_set->throttle_shrink_min_msecs; + + client = i_new(struct imapc_storage_client, 1); + client->refcount = 1; + i_array_init(&client->untagged_callbacks, 16); + /* FIXME: storage->event would be better, but we first get here when + creating mailbox_list, and storage doesn't even exist yet. */ + client->client = imapc_client_init(&set, ns->user->event); + imapc_client_register_untagged(client->client, + imapc_storage_client_untagged_cb, client); + + imapc_client_set_login_callback(client->client, imapc_storage_client_login_callback, client); + + if ((ns->flags & NAMESPACE_FLAG_LIST_PREFIX) != 0 && + (imapc_set->parsed_features & IMAPC_FEATURE_DELAY_LOGIN) == 0) { + /* start logging in immediately */ + imapc_storage_client_login(client, ns->user, set.host); + } + + *client_r = client; + return 0; +} + +void imapc_storage_client_unref(struct imapc_storage_client **_client) +{ + struct imapc_storage_client *client = *_client; + struct imapc_storage_event_callback *cb; + + *_client = NULL; + + i_assert(client->refcount > 0); + if (--client->refcount > 0) + return; + imapc_client_deinit(&client->client); + array_foreach_modifiable(&client->untagged_callbacks, cb) + i_free(cb->name); + array_free(&client->untagged_callbacks); + i_free(client->auth_failed_reason); + i_free(client); +} + +static int +imapc_storage_create(struct mail_storage *_storage, + struct mail_namespace *ns, + const char **error_r) +{ + struct imapc_storage *storage = IMAPC_STORAGE(_storage); + struct imapc_mailbox_list *imapc_list = NULL; + + storage->set = mail_namespace_get_driver_settings(ns, _storage); + + /* serialize all the settings */ + _storage->unique_root_dir = p_strdup_printf(_storage->pool, + "%s%s://(%s|%s):%s@%s:%u/%s mechs:%s features:%s " + "rawlog:%s cmd_timeout:%u maxidle:%u maxline:%zuu " + "pop3delflg:%s root_dir:%s", + storage->set->imapc_ssl, + storage->set->imapc_ssl_verify ? "(verify)" : "", + storage->set->imapc_user, + storage->set->imapc_master_user, + storage->set->imapc_password, + storage->set->imapc_host, + storage->set->imapc_port, + storage->set->imapc_list_prefix, + storage->set->imapc_sasl_mechanisms, + storage->set->imapc_features, + storage->set->imapc_rawlog_dir, + storage->set->imapc_cmd_timeout, + storage->set->imapc_max_idle_time, + (size_t) storage->set->imapc_max_line_length, + storage->set->pop3_deleted_flag, + ns->list->set.root_dir); + + if (strcmp(ns->list->name, MAILBOX_LIST_NAME_IMAPC) == 0) { + imapc_list = (struct imapc_mailbox_list *)ns->list; + storage->client = imapc_list->client; + storage->client->refcount++; + } else { + if (imapc_storage_client_create(ns, storage->set, _storage->set, + &storage->client, error_r) < 0) + return -1; + } + storage->client->_storage = storage; + p_array_init(&storage->remote_namespaces, _storage->pool, 4); + if (IMAPC_HAS_FEATURE(storage, IMAPC_FEATURE_FETCH_BODYSTRUCTURE)) { + _storage->nonbody_access_fields |= + MAIL_FETCH_IMAP_BODY | MAIL_FETCH_IMAP_BODYSTRUCTURE; + } + + imapc_storage_client_register_untagged(storage->client, "STATUS", + imapc_untagged_status); + imapc_storage_client_register_untagged(storage->client, "NAMESPACE", + imapc_untagged_namespace); + + return 0; +} + +static void imapc_storage_destroy(struct mail_storage *_storage) +{ + struct imapc_storage *storage = IMAPC_STORAGE(_storage); + + storage->client->destroying = TRUE; + + /* make sure all pending commands are aborted before anything is + deinitialized */ + imapc_client_logout(storage->client->client); + + imapc_storage_client_unref(&storage->client); + index_storage_destroy(_storage); +} + +void imapc_storage_client_register_untagged(struct imapc_storage_client *client, + const char *name, + imapc_storage_callback_t *callback) +{ + struct imapc_storage_event_callback *cb; + + cb = array_append_space(&client->untagged_callbacks); + cb->name = i_strdup(name); + cb->callback = callback; +} + +void imapc_storage_client_unregister_untagged(struct imapc_storage_client *client, + const char *name) +{ + struct imapc_storage_event_callback *cb; + unsigned int idx; + array_foreach_modifiable(&client->untagged_callbacks, cb) { + if (strcmp(cb->name, name) == 0) { + idx = array_foreach_idx(&client->untagged_callbacks, cb); + i_free(cb->name); + array_delete(&client->untagged_callbacks, idx, 1); + return; + } + } + i_unreached(); +} + +static void +imapc_storage_get_list_settings(const struct mail_namespace *ns ATTR_UNUSED, + struct mailbox_list_settings *set) +{ + if (set->layout == NULL) + set->layout = MAILBOX_LIST_NAME_IMAPC; + set->storage_name_escape_char = IMAPC_LIST_STORAGE_NAME_ESCAPE_CHAR; + /* We want to have all imapc mailboxes accessible, so escape them if + necessary. */ + if (set->vname_escape_char == '\0') + set->vname_escape_char = IMAPC_LIST_VNAME_ESCAPE_CHAR; +} + +static struct mailbox * +imapc_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list, + const char *vname, enum mailbox_flags flags) +{ + struct imapc_mailbox *mbox; + pool_t pool; + + pool = pool_alloconly_create("imapc mailbox", 1024*4); + mbox = p_new(pool, struct imapc_mailbox, 1); + mbox->box = imapc_mailbox; + mbox->box.pool = pool; + mbox->box.storage = storage; + mbox->box.list = list; + mbox->box.mail_vfuncs = &imapc_mail_vfuncs; + + index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX); + + mbox->storage = IMAPC_STORAGE(storage); + + p_array_init(&mbox->untagged_callbacks, pool, 16); + p_array_init(&mbox->resp_text_callbacks, pool, 16); + p_array_init(&mbox->fetch_requests, pool, 16); + p_array_init(&mbox->untagged_fetch_contexts, pool, 16); + p_array_init(&mbox->delayed_expunged_uids, pool, 16); + p_array_init(&mbox->copy_rollback_expunge_uids, pool, 16); + mbox->pending_fetch_cmd = str_new(pool, 128); + mbox->pending_copy_cmd = str_new(pool, 128); + mbox->prev_mail_cache.fd = -1; + imapc_mailbox_register_callbacks(mbox); + return &mbox->box; +} + +const char *imapc_mailbox_get_remote_name(struct imapc_mailbox *mbox) +{ + struct imapc_mailbox_list *list = + container_of(mbox->box.list, struct imapc_mailbox_list, list); + + if (strcmp(mbox->box.list->name, MAILBOX_LIST_NAME_IMAPC) != 0) + return mbox->box.name; + return imapc_list_storage_to_remote_name(list, mbox->box.name); +} + +static int +imapc_mailbox_exists(struct mailbox *box, bool auto_boxes, + enum mailbox_existence *existence_r) +{ + if (auto_boxes && mailbox_is_autocreated(box)) { + *existence_r = MAILBOX_EXISTENCE_SELECT; + return 0; + } + + if (strcmp(box->list->name, MAILBOX_LIST_NAME_IMAPC) != 0) { + if (box->inbox_any) + *existence_r = MAILBOX_EXISTENCE_SELECT; + else + *existence_r = MAILBOX_EXISTENCE_NONE; + return 0; + } + + enum mailbox_info_flags flags; + + struct imapc_mailbox_list *list = (struct imapc_mailbox_list *)box->list; + + if (imapc_storage_client_handle_auth_failure(list->client)) { + mail_storage_copy_list_error(box->storage, box->list); + return -1; + } + if (imapc_list_get_mailbox_flags(box->list, box->name, &flags) < 0) { + mail_storage_copy_list_error(box->storage, box->list); + return -1; + } + if ((flags & MAILBOX_NONEXISTENT) != 0) + *existence_r = MAILBOX_EXISTENCE_NONE; + else if ((flags & MAILBOX_NOSELECT) != 0) + *existence_r = MAILBOX_EXISTENCE_NOSELECT; + else + *existence_r = MAILBOX_EXISTENCE_SELECT; + return 0; +} + +static bool imapc_mailbox_want_examine(struct imapc_mailbox *mbox) +{ + if (IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_NO_EXAMINE)) { + /* mainly a Courier-workaround: With POP3-only Maildir that + doesn't have UIDVALIDITY set, EXAMINE won't generate a + permanent UIDVALIDITY while SELECT will. */ + return FALSE; + } + return (mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) == 0 && + ((mbox->box.flags & MAILBOX_FLAG_READONLY) != 0 || + (mbox->box.flags & MAILBOX_FLAG_SAVEONLY) != 0); +} + +static bool +imapc_mailbox_verify_select(struct imapc_mailbox *mbox, const char **error_r) +{ + if (!mbox->exists_received) + *error_r = "EXISTS not received"; + else if (mbox->sync_uid_validity == 0) + *error_r = "UIDVALIDITY not received"; + else + return TRUE; + return FALSE; +} + +static void +imapc_mailbox_reopen_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_mailbox *mbox = context; + const char *errmsg; + + i_assert(mbox->storage->reopen_count > 0); + mbox->storage->reopen_count--; + mbox->selecting = FALSE; + if (reply->state != IMAPC_COMMAND_STATE_OK) + errmsg = reply->text_full; + else if (imapc_mailbox_verify_select(mbox, &errmsg)) { + imap_mailbox_select_finish(mbox); + errmsg = NULL; + } + + if (errmsg != NULL) { + imapc_client_mailbox_reconnect(mbox->client_box, + t_strdup_printf("Reopening mailbox '%s' failed: %s", + mbox->box.name, errmsg)); + } + + imapc_client_stop(mbox->storage->client->client); +} + +static void imapc_mailbox_reopen(void *context) +{ + struct imapc_mailbox *mbox = context; + struct imapc_command *cmd; + + /* we're reconnecting and need to reopen the mailbox */ + mbox->prev_skipped_rseq = 0; + mbox->prev_skipped_uid = 0; + imapc_msgmap_reset(imapc_client_mailbox_get_msgmap(mbox->client_box)); + + if (mbox->selecting) { + /* We reconnected during the initial SELECT/EXAMINE. It'll be + automatically resent by lib-imap-client, so we don't need to + send it again here. */ + i_assert(!mbox->initial_sync_done); + return; + } + if (!mbox->initial_sync_done) { + /* Initial FETCH 1:* didn't fully succeed. We're reconnecting + and lib-imap-client is automatically resending it. But we + need to reset the sync_next_* state so that if any of the + mails are now expunged we won't get confused and crash. */ + mbox->sync_next_lseq = 1; + mbox->sync_next_rseq = 1; + } + + mbox->state_fetched_success = FALSE; + mbox->initial_sync_done = FALSE; + mbox->selecting = TRUE; + mbox->selected = FALSE; + mbox->exists_received = FALSE; + + cmd = imapc_client_mailbox_cmd(mbox->client_box, + imapc_mailbox_reopen_callback, mbox); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT); + if (imapc_mailbox_want_examine(mbox)) { + imapc_command_sendf(cmd, "EXAMINE %s", + imapc_mailbox_get_remote_name(mbox)); + } else { + imapc_command_sendf(cmd, "SELECT %s", + imapc_mailbox_get_remote_name(mbox)); + } + mbox->storage->reopen_count++; +} + +static void +imapc_mailbox_open_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_open_context *ctx = context; + const char *error; + + ctx->mbox->selecting = FALSE; + if (reply->state == IMAPC_COMMAND_STATE_OK) { + if (!imapc_mailbox_verify_select(ctx->mbox, &error)) { + mailbox_set_critical(&ctx->mbox->box, + "imapc: Opening mailbox failed: %s", error); + ctx->ret = -1; + } else { + imap_mailbox_select_finish(ctx->mbox); + ctx->ret = 0; + } + } else if (reply->state == IMAPC_COMMAND_STATE_NO) { + /* Unless the remote IMAP server supports sending + resp-text-code, we don't know if the NO reply is because + the mailbox doesn't exist or because of some internal error. + We'll default to assuming it doesn't exist, so e.g. + mailbox { auto=create } will auto-create missing mailboxes. + However, INBOX is a special mailbox, which is always + autocreated if it doesn't exist. This is true in both the + local Dovecot and the remote IMAP server. This means that + there's no point in trying to send CREATE INBOX to the + remote server. We'll avoid that by defaulting to temporary + failure with INBOX. */ + enum mail_error default_error = + ctx->mbox->box.inbox_any ? + MAIL_ERROR_TEMP : MAIL_ERROR_NOTFOUND; + imapc_copy_error_from_reply(ctx->mbox->storage, + default_error, reply); + ctx->ret = -1; + } else if (imapc_storage_client_handle_auth_failure(ctx->mbox->storage->client)) { + ctx->ret = -1; + } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) { + ctx->ret = -1; + mail_storage_set_internal_error(ctx->mbox->box.storage); + } else { + mailbox_set_critical(&ctx->mbox->box, + "imapc: Opening mailbox failed: %s", reply->text_full); + ctx->ret = -1; + } + imapc_client_stop(ctx->mbox->storage->client->client); +} + +static int imapc_mailbox_get_capabilities(struct imapc_mailbox *mbox) +{ + /* If authentication failed, don't check again. */ + if (imapc_storage_client_handle_auth_failure(mbox->storage->client)) + return -1; + + return imapc_client_get_capabilities(mbox->storage->client->client, + &mbox->capabilities); + +} + +static void imapc_mailbox_get_extensions(struct imapc_mailbox *mbox) +{ + if (mbox->guid_fetch_field_name == NULL) { + /* see if we can get message GUIDs somehow */ + if ((mbox->capabilities & IMAPC_CAPABILITY_X_GM_EXT_1) != 0) { + /* GMail */ + mbox->guid_fetch_field_name = "X-GM-MSGID"; + } + } +} + +int imapc_mailbox_select(struct imapc_mailbox *mbox) +{ + struct imapc_command *cmd; + struct imapc_open_context ctx; + + i_assert(mbox->client_box == NULL); + + if (imapc_mailbox_get_capabilities(mbox) < 0) + return -1; + + if (imapc_mailbox_has_modseqs(mbox)) { + if (!array_is_created(&mbox->rseq_modseqs)) + i_array_init(&mbox->rseq_modseqs, 32); + else + array_clear(&mbox->rseq_modseqs); + } + + mbox->client_box = + imapc_client_mailbox_open(mbox->storage->client->client, mbox); + imapc_client_mailbox_set_reopen_cb(mbox->client_box, + imapc_mailbox_reopen, mbox); + + imapc_mailbox_get_extensions(mbox); + + mbox->selecting = TRUE; + mbox->exists_received = FALSE; + ctx.mbox = mbox; + ctx.ret = -2; + cmd = imapc_client_mailbox_cmd(mbox->client_box, + imapc_mailbox_open_callback, &ctx); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_SELECT | + IMAPC_COMMAND_FLAG_RETRIABLE); + if (imapc_mailbox_want_examine(mbox)) { + imapc_command_sendf(cmd, "EXAMINE %s", + imapc_mailbox_get_remote_name(mbox)); + } else { + imapc_command_sendf(cmd, "SELECT %s", + imapc_mailbox_get_remote_name(mbox)); + } + + while (ctx.ret == -2 || mbox->state_fetching_uid1) + imapc_mailbox_run(mbox); + if (!mbox->state_fetched_success) + ctx.ret = -1; + return ctx.ret; +} + +static int imapc_mailbox_open(struct mailbox *box) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(box); + + if (index_storage_mailbox_open(box, FALSE) < 0) + return -1; + + if (box->deleting || (box->flags & MAILBOX_FLAG_SAVEONLY) != 0) { + /* We don't actually want to SELECT the mailbox. */ + return 0; + } + + if (*box->name == '\0' && + (box->list->ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0) { + /* trying to open INBOX as the namespace prefix. + Don't allow this. */ + mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND, + "Mailbox isn't selectable"); + mailbox_close(box); + return -1; + } + + if (imapc_mailbox_select(mbox) < 0) { + mailbox_close(box); + return -1; + } + return 0; +} + +void imapc_mail_cache_free(struct imapc_mail_cache *cache) +{ + i_close_fd(&cache->fd); + buffer_free(&cache->buf); + cache->uid = 0; +} + +static void imapc_mailbox_close(struct mailbox *box) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(box); + bool changes; + + (void)imapc_mailbox_commit_delayed_trans(mbox, FALSE, &changes); + imapc_mail_fetch_flush(mbox); + + /* Arriving here we may have fetch contexts still unprocessed, + if there have been no mailbox_sync() after receiving the untagged replies. + Losing these changes isn't a problem, since the same changes will be found + out after connecting to the server the next time. */ + struct imapc_untagged_fetch_ctx *untagged_fetch_context; + array_foreach_elem(&mbox->untagged_fetch_contexts, untagged_fetch_context) + imapc_untagged_fetch_ctx_free(&untagged_fetch_context); + array_clear(&mbox->untagged_fetch_contexts); + + if (mbox->client_box != NULL) + imapc_client_mailbox_close(&mbox->client_box); + if (array_is_created(&mbox->rseq_modseqs)) + array_free(&mbox->rseq_modseqs); + if (mbox->sync_view != NULL) + mail_index_view_close(&mbox->sync_view); + timeout_remove(&mbox->to_idle_delay); + timeout_remove(&mbox->to_idle_check); + imapc_mail_cache_free(&mbox->prev_mail_cache); + index_storage_mailbox_close(box); +} + +static int +imapc_mailbox_create(struct mailbox *box, + const struct mailbox_update *update ATTR_UNUSED, + bool directory) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(box); + struct imapc_command *cmd; + struct imapc_simple_context sctx; + const char *remote_name = imapc_mailbox_get_remote_name(mbox); + + if (!directory) + ; + else if (strcmp(box->list->name, MAILBOX_LIST_NAME_IMAPC) == 0) { + struct imapc_mailbox_list *imapc_list = + (struct imapc_mailbox_list *)box->list; + remote_name = t_strdup_printf("%s%c", remote_name, + imapc_list->root_sep); + } else { + remote_name = t_strdup_printf("%s%c", remote_name, + mailbox_list_get_hierarchy_sep(box->list)); + } + imapc_simple_context_init(&sctx, mbox->storage->client); + cmd = imapc_client_cmd(mbox->storage->client->client, + imapc_simple_callback, &sctx); + imapc_command_sendf(cmd, "CREATE %s", remote_name); + imapc_simple_run(&sctx, &cmd); + return sctx.ret; +} + +static int imapc_mailbox_update(struct mailbox *box, + const struct mailbox_update *update) +{ + if (!guid_128_is_empty(update->mailbox_guid) || + update->uid_validity != 0 || update->min_next_uid != 0 || + update->min_first_recent_uid != 0) { + mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE, + "Not supported"); + } + return index_storage_mailbox_update(box, update); +} + +static void imapc_untagged_status(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client) +{ + struct imapc_storage *storage = client->_storage; + struct mailbox_status *status; + const struct imap_arg *list; + const char *remote_name, *key, *value; + uint32_t num; + unsigned int i; + + if (!imap_arg_get_astring(&reply->args[0], &remote_name) || + !imap_arg_get_list(&reply->args[1], &list)) + return; + + if (storage->cur_status_box == NULL) + return; + + if (!imapc_mailbox_name_equals(storage->cur_status_box, + remote_name)) + return; + + status = storage->cur_status; + for (i = 0; list[i].type != IMAP_ARG_EOL; i += 2) { + if (!imap_arg_get_atom(&list[i], &key) || + !imap_arg_get_atom(&list[i+1], &value) || + str_to_uint32(value, &num) < 0) + return; + + if (strcasecmp(key, "MESSAGES") == 0) + status->messages = num; + else if (strcasecmp(key, "RECENT") == 0) + status->recent = num; + else if (strcasecmp(key, "UIDNEXT") == 0) + status->uidnext = num; + else if (strcasecmp(key, "UIDVALIDITY") == 0) + status->uidvalidity = num; + else if (strcasecmp(key, "UNSEEN") == 0) + status->unseen = num; + else if (strcasecmp(key, "HIGHESTMODSEQ") == 0 && + imapc_mailbox_has_modseqs(storage->cur_status_box)) + status->highest_modseq = num; + } +} + +static void imapc_untagged_namespace(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client) +{ + struct imapc_storage *storage = client->_storage; + static enum mail_namespace_type ns_types[] = { + MAIL_NAMESPACE_TYPE_PRIVATE, + MAIL_NAMESPACE_TYPE_SHARED, + MAIL_NAMESPACE_TYPE_PUBLIC + }; + struct imapc_namespace *ns; + const struct imap_arg *list, *list2; + const char *prefix, *sep; + unsigned int i; + + array_clear(&storage->remote_namespaces); + for (i = 0; i < N_ELEMENTS(ns_types); i++) { + if (reply->args[i].type == IMAP_ARG_NIL) + continue; + if (!imap_arg_get_list(&reply->args[i], &list)) + break; + + for (; list->type != IMAP_ARG_EOL; list++) { + if (!imap_arg_get_list(list, &list2) || + !imap_arg_get_astring(&list2[0], &prefix) || + !imap_arg_get_nstring(&list2[1], &sep)) + break; + + ns = array_append_space(&storage->remote_namespaces); + ns->prefix = p_strdup(storage->storage.pool, prefix); + ns->separator = sep == NULL ? '\0' : sep[0]; + ns->type = ns_types[i]; + } + } +} + +static void imapc_mailbox_get_selected_status(struct imapc_mailbox *mbox, + enum mailbox_status_items items, + struct mailbox_status *status_r) +{ + index_storage_get_open_status(&mbox->box, items, status_r); + if ((items & STATUS_PERMANENT_FLAGS) != 0) + status_r->permanent_flags = mbox->permanent_flags; + if ((items & STATUS_FIRST_RECENT_UID) != 0) + status_r->first_recent_uid = mbox->highest_nonrecent_uid + 1; + if ((items & STATUS_HIGHESTMODSEQ) != 0) { + /* FIXME: this doesn't work perfectly. we're now just returning + the HIGHESTMODSEQ from the current index, which may or may + not be correct. with QRESYNC enabled we could be returning + sync_highestmodseq, but that would require implementing + VANISHED replies. and without QRESYNC we'd have to issue + STATUS (HIGHESTMODSEQ), which isn't efficient since we get + here constantly (after every IMAP command). */ + } + if (imapc_mailbox_has_modseqs(mbox)) { + /* even if local indexes are only in memory, we still + have modseqs on the IMAP server itself. */ + status_r->nonpermanent_modseqs = FALSE; + } +} + +static int imapc_mailbox_delete(struct mailbox *box) +{ + box->delete_skip_empty_check = TRUE; + return index_storage_mailbox_delete(box); +} + +static int imapc_mailbox_run_status(struct mailbox *box, + enum mailbox_status_items items, + struct mailbox_status *status_r) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(box); + struct imapc_command *cmd; + struct imapc_simple_context sctx; + string_t *str; + + if (imapc_mailbox_get_capabilities(mbox) < 0) + return -1; + + str = t_str_new(256); + if ((items & STATUS_MESSAGES) != 0) + str_append(str, " MESSAGES"); + if ((items & STATUS_RECENT) != 0) + str_append(str, " RECENT"); + if ((items & STATUS_UIDNEXT) != 0) + str_append(str, " UIDNEXT"); + if ((items & STATUS_UIDVALIDITY) != 0) + str_append(str, " UIDVALIDITY"); + if ((items & STATUS_UNSEEN) != 0) + str_append(str, " UNSEEN"); + if ((items & STATUS_HIGHESTMODSEQ) != 0 && + imapc_mailbox_has_modseqs(mbox)) + str_append(str, " HIGHESTMODSEQ"); + + if (str_len(str) == 0) { + /* nothing requested */ + return 0; + } + + imapc_simple_context_init(&sctx, mbox->storage->client); + mbox->storage->cur_status_box = mbox; + mbox->storage->cur_status = status_r; + cmd = imapc_client_cmd(mbox->storage->client->client, + imapc_simple_callback, &sctx); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + imapc_command_sendf(cmd, "STATUS %s (%1s)", + imapc_mailbox_get_remote_name(mbox), str_c(str)+1); + imapc_simple_run(&sctx, &cmd); + mbox->storage->cur_status_box = NULL; + mbox->storage->cur_status = NULL; + return sctx.ret; +} + +static int imapc_mailbox_get_status(struct mailbox *box, + enum mailbox_status_items items, + struct mailbox_status *status_r) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(box); + + if (mbox->guid_fetch_field_name != NULL || + IMAPC_BOX_HAS_FEATURE(mbox, IMAPC_FEATURE_GUID_FORCED)) + status_r->have_guids = TRUE; + + if (box->opened) { + imapc_mailbox_get_selected_status(mbox, items, status_r); + } else if ((items & (STATUS_FIRST_UNSEEN_SEQ | STATUS_KEYWORDS | + STATUS_PERMANENT_FLAGS | + STATUS_FIRST_RECENT_UID)) != 0) { + /* getting these requires opening the mailbox */ + if (mailbox_open(box) < 0) + return -1; + imapc_mailbox_get_selected_status(mbox, items, status_r); + } else { + if (imapc_mailbox_run_status(box, items, status_r) < 0) + return -1; + /* If this mailbox has private indexes make sure to check + STATUS_UNSEEN from there. */ + if (box->list->set.index_pvt_dir != NULL && + (items & (STATUS_UNSEEN)) != 0) { + struct mailbox_status pvt_idx_status; + index_storage_get_status(box, STATUS_UNSEEN, + &pvt_idx_status); + status_r->unseen = pvt_idx_status.unseen; + } + } + + if (box->opened && !box->deleting && (items & STATUS_UIDNEXT) != 0 && + mbox->sync_uid_next == 0) { + /* Courier-workaround, it doesn't send UIDNEXT on SELECT */ + if (imapc_mailbox_run_status(box, STATUS_UIDNEXT, status_r) < 0) + return -1; + } + return 0; +} + +static int imapc_mailbox_get_namespaces(struct imapc_mailbox *mbox) +{ + struct imapc_storage *storage = mbox->storage; + struct imapc_command *cmd; + struct imapc_simple_context sctx; + + if (storage->namespaces_requested) + return 0; + + if (imapc_mailbox_get_capabilities(mbox) < 0) + return -1; + if ((mbox->capabilities & IMAPC_CAPABILITY_NAMESPACE) == 0) { + /* NAMESPACE capability not supported */ + return 0; + } + + imapc_simple_context_init(&sctx, storage->client); + cmd = imapc_client_cmd(storage->client->client, + imapc_simple_callback, &sctx); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + imapc_command_send(cmd, "NAMESPACE"); + imapc_simple_run(&sctx, &cmd); + + if (sctx.ret < 0) + return -1; + storage->namespaces_requested = TRUE; + return 0; +} + +static const struct imapc_namespace * +imapc_namespace_find_mailbox(struct imapc_storage *storage, + const char *remote_name) +{ + const struct imapc_namespace *ns, *best_ns = NULL; + size_t best_len = UINT_MAX, len; + + array_foreach(&storage->remote_namespaces, ns) { + len = strlen(ns->prefix); + if (str_begins(remote_name, ns->prefix)) { + if (best_len > len) { + best_ns = ns; + best_len = len; + } + } + } + return best_ns; +} + +static int imapc_mailbox_get_metadata(struct mailbox *box, + enum mailbox_metadata_items items, + struct mailbox_metadata *metadata_r) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(box); + const struct imapc_namespace *ns; + + if ((items & MAILBOX_METADATA_GUID) != 0) { + /* a bit ugly way to do this, but better than nothing for now. + FIXME: if indexes are enabled, keep this there. */ + mail_generate_guid_128_hash(box->name, metadata_r->guid); + items &= ENUM_NEGATE(MAILBOX_METADATA_GUID); + } + if ((items & MAILBOX_METADATA_BACKEND_NAMESPACE) != 0) { + if (imapc_mailbox_get_namespaces(mbox) < 0) + return -1; + + const char *remote_name = imapc_mailbox_get_remote_name(mbox); + ns = imapc_namespace_find_mailbox(mbox->storage, remote_name); + if (ns != NULL) { + metadata_r->backend_ns_prefix = ns->prefix; + metadata_r->backend_ns_type = ns->type; + } + items &= ENUM_NEGATE(MAILBOX_METADATA_BACKEND_NAMESPACE); + } + if (items != 0) { + if (index_mailbox_get_metadata(box, items, metadata_r) < 0) + return -1; + } + return 0; +} + +static void imapc_noop_callback(const struct imapc_command_reply *reply, + void *context) + +{ + struct imapc_storage *storage = context; + + if (reply->state == IMAPC_COMMAND_STATE_OK) + ; + else if (reply->state == IMAPC_COMMAND_STATE_NO) + imapc_copy_error_from_reply(storage, MAIL_ERROR_PARAMS, reply); + else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) + mail_storage_set_internal_error(&storage->storage); + else { + mail_storage_set_critical(&storage->storage, + "imapc: NOOP failed: %s", reply->text_full); + } +} + +static void imapc_idle_timeout(struct imapc_mailbox *mbox) +{ + struct imapc_command *cmd; + + cmd = imapc_client_mailbox_cmd(mbox->client_box, + imapc_noop_callback, mbox->storage); + imapc_command_send(cmd, "NOOP"); +} + +static void imapc_idle_noop_callback(const struct imapc_command_reply *reply, + void *context) + +{ + struct imapc_mailbox *mbox = context; + + imapc_noop_callback(reply, mbox->box.storage); + if (mbox->client_box != NULL) + imapc_client_mailbox_idle(mbox->client_box); +} + +static void imapc_notify_changes(struct mailbox *box) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(box); + const struct mail_storage_settings *set = box->storage->set; + struct imapc_command *cmd; + + if (box->notify_callback == NULL) { + timeout_remove(&mbox->to_idle_check); + return; + } + + if ((mbox->capabilities & IMAPC_CAPABILITY_IDLE) != 0) { + /* remote server is already in IDLE. but since some servers + don't notice changes immediately, we'll force them to check + here by sending a NOOP. this helps with clients that break + IDLE when clicking "get mail". */ + cmd = imapc_client_mailbox_cmd(mbox->client_box, + imapc_idle_noop_callback, mbox); + imapc_command_send(cmd, "NOOP"); + } else { + /* remote server doesn't support IDLE. + check for changes with NOOP every once in a while. */ + i_assert(!imapc_client_is_running(mbox->storage->client->client)); + mbox->to_idle_check = + timeout_add(set->mailbox_idle_check_interval * 1000, + imapc_idle_timeout, mbox); + } +} + +static bool imapc_is_inconsistent(struct mailbox *box) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(box); + + if (box->view != NULL && + mail_index_view_is_inconsistent(box->view)) + return TRUE; + + return mbox->client_box == NULL ? FALSE : + !imapc_client_mailbox_is_opened(mbox->client_box); +} + +struct mail_storage imapc_storage = { + .name = IMAPC_STORAGE_NAME, + .class_flags = MAIL_STORAGE_CLASS_FLAG_NO_ROOT | + MAIL_STORAGE_CLASS_FLAG_UNIQUE_ROOT | + MAIL_STORAGE_CLASS_FLAG_SECONDARY_INDEX, + .event_category = &event_category_imapc, + + .v = { + imapc_get_setting_parser_info, + imapc_storage_alloc, + imapc_storage_create, + imapc_storage_destroy, + NULL, + imapc_storage_get_list_settings, + NULL, + imapc_mailbox_alloc, + NULL, + NULL, + } +}; + +static int +imapc_mailbox_transaction_commit(struct mailbox_transaction_context *t, + struct mail_transaction_commit_changes *changes_r) +{ + int ret = imapc_transaction_save_commit(t); + int ret2 = index_transaction_commit(t, changes_r); + return ret >= 0 && ret2 >= 0 ? 0 : -1; +} + +struct mailbox imapc_mailbox = { + .v = { + index_storage_is_readonly, + index_storage_mailbox_enable, + imapc_mailbox_exists, + imapc_mailbox_open, + imapc_mailbox_close, + index_storage_mailbox_free, + imapc_mailbox_create, + imapc_mailbox_update, + imapc_mailbox_delete, + index_storage_mailbox_rename, + imapc_mailbox_get_status, + imapc_mailbox_get_metadata, + index_storage_set_subscribed, + index_storage_attribute_set, + index_storage_attribute_get, + index_storage_attribute_iter_init, + index_storage_attribute_iter_next, + index_storage_attribute_iter_deinit, + NULL, + NULL, + imapc_mailbox_sync_init, + index_mailbox_sync_next, + imapc_mailbox_sync_deinit, + NULL, + imapc_notify_changes, + index_transaction_begin, + imapc_mailbox_transaction_commit, + index_transaction_rollback, + NULL, + imapc_mail_alloc, + imapc_search_init, + imapc_search_deinit, + index_storage_search_next_nonblock, + imapc_search_next_update_seq, + index_storage_search_next_match_mail, + imapc_save_alloc, + imapc_save_begin, + imapc_save_continue, + imapc_save_finish, + imapc_save_cancel, + imapc_copy, + imapc_transaction_save_commit_pre, + imapc_transaction_save_commit_post, + imapc_transaction_save_rollback, + imapc_is_inconsistent + } +}; diff --git a/src/lib-storage/index/imapc/imapc-storage.h b/src/lib-storage/index/imapc/imapc-storage.h new file mode 100644 index 0000000..6624a0d --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-storage.h @@ -0,0 +1,274 @@ +#ifndef IMAPC_STORAGE_H +#define IMAPC_STORAGE_H + +#include "index-storage.h" +#include "imapc-settings.h" +#include "imapc-client.h" + +#define IMAPC_STORAGE_NAME "imapc" +/* storage_name separator */ +#define IMAPC_LIST_STORAGE_NAME_ESCAPE_CHAR '%' +/* fs_name separator */ +#define IMAPC_LIST_FS_NAME_ESCAPE_CHAR '%' +/* vname separator */ +#define IMAPC_LIST_VNAME_ESCAPE_CHAR '~' + +struct imap_arg; +struct imapc_untagged_reply; +struct imapc_command_reply; +struct imapc_mailbox; +struct imapc_storage_client; + +typedef void imapc_storage_callback_t(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client); +typedef void imapc_mailbox_callback_t(const struct imapc_untagged_reply *reply, + struct imapc_mailbox *mbox); + +struct imapc_storage_event_callback { + char *name; + imapc_storage_callback_t *callback; +}; + +struct imapc_mailbox_event_callback { + const char *name; + imapc_mailbox_callback_t *callback; +}; + +#define IMAPC_HAS_FEATURE(mstorage, feature) \ + (((mstorage)->set->parsed_features & feature) != 0) +#define IMAPC_BOX_HAS_FEATURE(mbox, feature) \ + (((mbox)->storage->set->parsed_features & feature) != 0) + +/* Returns TRUE if we can assume from now on that untagged EXPUNGE, FETCH, etc. + replies belong to this mailbox instead of to the previously selected + mailbox. */ +#define IMAPC_MAILBOX_IS_FULLY_SELECTED(mbox) \ + ((mbox)->sync_uid_validity != 0) + +struct imapc_namespace { + const char *prefix; + char separator; + enum mail_namespace_type type; +}; + +struct imapc_storage_client { + int refcount; + + /* either one of these may not be available: */ + struct imapc_storage *_storage; + struct imapc_mailbox_list *_list; + + struct imapc_client *client; + + ARRAY(struct imapc_storage_event_callback) untagged_callbacks; + + /* IMAPC_COMMAND_STATE_OK if no auth failure (yet), otherwise result to + the LOGIN/AUTHENTICATE command. */ + enum imapc_command_state auth_failed_state; + char *auth_failed_reason; + + /* Authentication reply was received (success or failure) */ + bool auth_returned:1; + bool destroying:1; +}; + +struct imapc_storage { + struct mail_storage storage; + const struct imapc_settings *set; + + struct ioloop *root_ioloop; + struct imapc_storage_client *client; + + struct imapc_mailbox *cur_status_box; + struct mailbox_status *cur_status; + unsigned int reopen_count; + + ARRAY(struct imapc_namespace) remote_namespaces; + + bool namespaces_requested:1; +}; + +struct imapc_mail_cache { + uint32_t uid; + + /* either fd != -1 or buf != NULL */ + int fd; + buffer_t *buf; +}; + +struct imapc_fetch_request { + ARRAY(struct imapc_mail *) mails; +}; + +struct imapc_untagged_fetch_ctx { + pool_t pool; + + /* keywords, flags, guid, modseq and fetch_uid may or may not be + received with an untagged fetch response */ + ARRAY_TYPE(const_string) keywords; + /* Is set if have_flags is TRUE */ + enum mail_flags flags; + const char *guid; + uint64_t modseq; + uint32_t fetch_uid; + + /* uid is generated locally based on the remote MSN or fetch_uid */ + uint32_t uid; + + bool have_gmail_labels:1; + bool have_flags:1; +}; + +struct imapc_copy_request { + struct imapc_save_context *sctx; + struct seqset_builder *uidset_builder; +}; + +struct imapc_mailbox { + struct mailbox box; + struct imapc_storage *storage; + struct imapc_client_mailbox *client_box; + enum imapc_capability capabilities; + + struct mail_index_transaction *delayed_sync_trans; + struct mail_index_view *sync_view, *delayed_sync_view; + struct mail_cache_view *delayed_sync_cache_view; + struct mail_cache_transaction_ctx *delayed_sync_cache_trans; + struct timeout *to_idle_check, *to_idle_delay; + + ARRAY(struct imapc_fetch_request *) fetch_requests; + ARRAY(struct imapc_untagged_fetch_ctx *) untagged_fetch_contexts; + /* if non-empty, contains the latest FETCH command we're going to be + sending soon (but still waiting to see if we can increase its + UID range) */ + string_t *pending_fetch_cmd; + /* if non-empty, contains the latest COPY command we're going to be + sending soon. */ + string_t *pending_copy_cmd; + char *copy_dest_box; + struct imapc_fetch_request *pending_fetch_request; + struct imapc_copy_request *pending_copy_request; + struct timeout *to_pending_fetch_send; + + ARRAY(struct imapc_mailbox_event_callback) untagged_callbacks; + ARRAY(struct imapc_mailbox_event_callback) resp_text_callbacks; + + enum mail_flags permanent_flags; + uint32_t highest_nonrecent_uid; + + ARRAY(uint64_t) rseq_modseqs; + ARRAY_TYPE(seq_range) delayed_expunged_uids; + ARRAY_TYPE(seq_range) copy_rollback_expunge_uids; + uint32_t sync_uid_validity; + uint32_t sync_uid_next; + uint64_t sync_highestmodseq; + uint32_t sync_fetch_first_uid; + uint32_t sync_next_lseq; + uint32_t sync_next_rseq; + uint32_t exists_count; + uint32_t min_append_uid; + char *sync_gmail_pop3_search_tag; + + /* keep the previous fetched message body cached, + mainly for partial IMAP fetches */ + struct imapc_mail_cache prev_mail_cache; + + uint32_t prev_skipped_rseq, prev_skipped_uid; + struct imapc_sync_context *sync_ctx; + + const char *guid_fetch_field_name; + struct imapc_search_context *search_ctx; + + bool selecting:1; + bool syncing:1; + bool initial_sync_done:1; + bool selected:1; + bool exists_received:1; + bool state_fetching_uid1:1; + bool state_fetched_success:1; + bool rollback_pending:1; + bool delayed_untagged_exists:1; +}; + +struct imapc_simple_context { + struct imapc_storage_client *client; + int ret; +}; + +#define IMAPC_STORAGE(s) container_of(s, struct imapc_storage, storage) +#define IMAPC_MAILBOX(s) container_of(s, struct imapc_mailbox, box) + +int imapc_storage_client_create(struct mail_namespace *ns, + const struct imapc_settings *imapc_set, + const struct mail_storage_settings *mail_set, + struct imapc_storage_client **client_r, + const char **error_r); +void imapc_storage_client_unref(struct imapc_storage_client **client); +bool imapc_storage_client_handle_auth_failure(struct imapc_storage_client *client); + +struct mail_save_context * +imapc_save_alloc(struct mailbox_transaction_context *_t); +int imapc_save_begin(struct mail_save_context *ctx, struct istream *input); +int imapc_save_continue(struct mail_save_context *ctx); +int imapc_save_finish(struct mail_save_context *ctx); +void imapc_save_cancel(struct mail_save_context *ctx); +int imapc_copy(struct mail_save_context *ctx, struct mail *mail); + +int imapc_transaction_save_commit(struct mailbox_transaction_context *t); +int imapc_transaction_save_commit_pre(struct mail_save_context *ctx); +void imapc_transaction_save_commit_post(struct mail_save_context *ctx, + struct mail_index_transaction_commit_result *result); +void imapc_transaction_save_rollback(struct mail_save_context *ctx); + +void imapc_mailbox_run(struct imapc_mailbox *mbox); +void imapc_mailbox_run_nofetch(struct imapc_mailbox *mbox); +void imapc_mail_cache_free(struct imapc_mail_cache *cache); +int imapc_mailbox_select(struct imapc_mailbox *mbox); +void imap_mailbox_select_finish(struct imapc_mailbox *mbox); + +bool imapc_mailbox_has_modseqs(struct imapc_mailbox *mbox); +bool imapc_resp_text_code_parse(const char *str, enum mail_error *error_r); +bool imapc_mail_error_to_resp_text_code(enum mail_error error, const char **str_r); +void imapc_copy_error_from_reply(struct imapc_storage *storage, + enum mail_error default_error, + const struct imapc_command_reply *reply); +void imapc_simple_context_init(struct imapc_simple_context *sctx, + struct imapc_storage_client *client); +void imapc_simple_run(struct imapc_simple_context *sctx, + struct imapc_command **cmd); +void imapc_simple_callback(const struct imapc_command_reply *reply, + void *context); +int imapc_mailbox_commit_delayed_trans(struct imapc_mailbox *mbox, + bool force, bool *changes_r); +bool imapc_mailbox_name_equals(struct imapc_mailbox *mbox, + const char *remote_name); +void imapc_mailbox_noop(struct imapc_mailbox *mbox); +void imapc_mailbox_set_corrupted(struct imapc_mailbox *mbox, + const char *reason, ...) ATTR_FORMAT(2, 3); +const char *imapc_mailbox_get_remote_name(struct imapc_mailbox *mbox); + +void imapc_storage_client_register_untagged(struct imapc_storage_client *client, + const char *name, + imapc_storage_callback_t *callback); +void imapc_storage_client_unregister_untagged(struct imapc_storage_client *client, + const char *name); +void imapc_mailbox_register_untagged(struct imapc_mailbox *mbox, + const char *name, + imapc_mailbox_callback_t *callback); +void imapc_mailbox_register_resp_text(struct imapc_mailbox *mbox, + const char *key, + imapc_mailbox_callback_t *callback); + +void imapc_mailbox_register_callbacks(struct imapc_mailbox *mbox); + +struct mail_index_view * +imapc_mailbox_get_sync_view(struct imapc_mailbox *mbox); + +void imapc_untagged_fetch_ctx_free(struct imapc_untagged_fetch_ctx **_ctx); +void imapc_untagged_fetch_update_flags(struct imapc_mailbox *mbox, + struct imapc_untagged_fetch_ctx *ctx, + struct mail_index_view *view, + uint32_t lseq); +bool imapc_mailbox_fetch_state(struct imapc_mailbox *mbox, uint32_t first_uid); + +#endif diff --git a/src/lib-storage/index/imapc/imapc-sync.c b/src/lib-storage/index/imapc/imapc-sync.c new file mode 100644 index 0000000..3130121 --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-sync.c @@ -0,0 +1,702 @@ +/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "hash.h" +#include "str.h" +#include "sort.h" +#include "imap-util.h" +#include "mail-cache.h" +#include "mail-index-modseq.h" +#include "index-sync-private.h" +#include "imapc-msgmap.h" +#include "imapc-list.h" +#include "imapc-storage.h" +#include "imapc-sync.h" + +struct imapc_sync_command { + struct imapc_sync_context *ctx; + char *cmd_str; + bool ignore_no; +}; + +static void imapc_sync_callback(const struct imapc_command_reply *reply, + void *context) +{ + struct imapc_sync_command *cmd = context; + struct imapc_sync_context *ctx = cmd->ctx; + + i_assert(ctx->sync_command_count > 0); + + if (reply->state == IMAPC_COMMAND_STATE_OK) + ; + else if (reply->state == IMAPC_COMMAND_STATE_NO && cmd->ignore_no) { + /* maybe the message was expunged already. + some servers fail STOREs with NO in such situation. */ + } else if (reply->state == IMAPC_COMMAND_STATE_DISCONNECTED) { + /* the disconnection is already logged, don't flood + the logs unnecessarily */ + mail_storage_set_internal_error(&ctx->mbox->storage->storage); + ctx->failed = TRUE; + } else { + mailbox_set_critical(&ctx->mbox->box, + "imapc: Sync command '%s' failed: %s", + cmd->cmd_str, reply->text_full); + ctx->failed = TRUE; + } + + if (--ctx->sync_command_count == 0) + imapc_client_stop(ctx->mbox->storage->client->client); + i_free(cmd->cmd_str); + i_free(cmd); +} + +static struct imapc_command * +imapc_sync_cmd_full(struct imapc_sync_context *ctx, const char *cmd_str, + bool ignore_no) +{ + struct imapc_sync_command *sync_cmd; + struct imapc_command *cmd; + + sync_cmd = i_new(struct imapc_sync_command, 1); + sync_cmd->ctx = ctx; + sync_cmd->cmd_str = i_strdup(cmd_str); + sync_cmd->ignore_no = ignore_no; + + ctx->sync_command_count++; + cmd = imapc_client_mailbox_cmd(ctx->mbox->client_box, + imapc_sync_callback, sync_cmd); + imapc_command_set_flags(cmd, IMAPC_COMMAND_FLAG_RETRIABLE); + imapc_command_send(cmd, cmd_str); + return cmd; +} + +static struct imapc_command * +imapc_sync_cmd(struct imapc_sync_context *ctx, const char *cmd_str) +{ + return imapc_sync_cmd_full(ctx, cmd_str, FALSE); +} + +static unsigned int imapc_sync_store_hash(const struct imapc_sync_store *store) +{ + return str_hash(store->flags) ^ store->modify_type; +} + +static int imapc_sync_store_cmp(const struct imapc_sync_store *store1, + const struct imapc_sync_store *store2) +{ + if (store1->modify_type != store2->modify_type) + return 1; + return strcmp(store1->flags, store2->flags); +} + +static const char *imapc_sync_flags_sort(const char *flags) +{ + if (strchr(flags, ' ') == NULL) + return flags; + + const char **str = t_strsplit(flags, " "); + i_qsort(str, str_array_length(str), sizeof(const char *), + i_strcasecmp_p); + return t_strarray_join(str, " "); +} + +static void +imapc_sync_store_flush(struct imapc_sync_context *ctx) +{ + struct imapc_sync_store *store; + const char *sorted_flags; + + if (ctx->prev_uid1 == 0) + return; + + sorted_flags = imapc_sync_flags_sort(str_c(ctx->prev_flags)); + struct imapc_sync_store store_lookup = { + .modify_type = ctx->prev_modify_type, + .flags = sorted_flags, + }; + store = hash_table_lookup(ctx->stores, &store_lookup); + if (store == NULL) { + store = p_new(ctx->pool, struct imapc_sync_store, 1); + store->modify_type = ctx->prev_modify_type; + store->flags = p_strdup(ctx->pool, sorted_flags); + p_array_init(&store->uids, ctx->pool, 4); + hash_table_insert(ctx->stores, store, store); + } + seq_range_array_add_range(&store->uids, ctx->prev_uid1, ctx->prev_uid2); +} + +static void +imapc_sync_store(struct imapc_sync_context *ctx, + enum modify_type modify_type, uint32_t uid1, uint32_t uid2, + const char *flags) +{ + if (ctx->prev_flags == NULL) { + ctx->prev_flags = str_new(ctx->pool, 128); + hash_table_create(&ctx->stores, ctx->pool, 0, + imapc_sync_store_hash, imapc_sync_store_cmp); + } + + if (ctx->prev_uid1 != uid1 || ctx->prev_uid2 != uid2 || + ctx->prev_modify_type != modify_type) { + imapc_sync_store_flush(ctx); + ctx->prev_uid1 = uid1; + ctx->prev_uid2 = uid2; + ctx->prev_modify_type = modify_type; + str_truncate(ctx->prev_flags, 0); + } + if (str_len(ctx->prev_flags) > 0) + str_append_c(ctx->prev_flags, ' '); + str_append(ctx->prev_flags, flags); +} + +static void +imapc_sync_finish_store(struct imapc_sync_context *ctx) +{ + struct hash_iterate_context *iter; + struct imapc_sync_store *store; + string_t *cmd = t_str_new(128); + + imapc_sync_store_flush(ctx); + + if (!hash_table_is_created(ctx->stores)) + return; + + iter = hash_table_iterate_init(ctx->stores); + while (hash_table_iterate(iter, ctx->stores, &store, &store)) { + str_truncate(cmd, 0); + str_append(cmd, "UID STORE "); + imap_write_seq_range(cmd, &store->uids); + str_printfa(cmd, " %cFLAGS (%s)", + store->modify_type == MODIFY_ADD ? '+' : '-', + store->flags); + imapc_sync_cmd_full(ctx, str_c(cmd), TRUE); + } + hash_table_iterate_deinit(&iter); + hash_table_destroy(&ctx->stores); +} + +static void +imapc_sync_add_missing_deleted_flags(struct imapc_sync_context *ctx, + uint32_t seq1, uint32_t seq2) +{ + const struct mail_index_record *rec; + uint32_t seq, uid1, uid2; + + /* if any of them has a missing \Deleted flag, + just add it to all of them. */ + for (seq = seq1; seq <= seq2; seq++) { + rec = mail_index_lookup(ctx->sync_view, seq); + if ((rec->flags & MAIL_DELETED) == 0) + break; + } + + if (seq <= seq2) { + mail_index_lookup_uid(ctx->sync_view, seq1, &uid1); + mail_index_lookup_uid(ctx->sync_view, seq2, &uid2); + + imapc_sync_store(ctx, MODIFY_ADD, uid1, uid2, "\\Deleted"); + } +} + +static void imapc_sync_index_flags(struct imapc_sync_context *ctx, + const struct mail_index_sync_rec *sync_rec) +{ + string_t *str = t_str_new(128); + + i_assert(sync_rec->type == MAIL_INDEX_SYNC_TYPE_FLAGS); + + if (sync_rec->add_flags != 0) { + i_assert((sync_rec->add_flags & MAIL_RECENT) == 0); + + imap_write_flags(str, sync_rec->add_flags, NULL); + imapc_sync_store(ctx, MODIFY_ADD, sync_rec->uid1, + sync_rec->uid2, str_c(str)); + } + + if (sync_rec->remove_flags != 0) { + i_assert((sync_rec->remove_flags & MAIL_RECENT) == 0); + str_truncate(str, 0); + imap_write_flags(str, sync_rec->remove_flags, NULL); + imapc_sync_store(ctx, MODIFY_REMOVE, sync_rec->uid1, + sync_rec->uid2, str_c(str)); + } +} + +static void +imapc_sync_index_keyword(struct imapc_sync_context *ctx, + const struct mail_index_sync_rec *sync_rec) +{ + const char *kw_str; + enum modify_type modify_type; + + switch (sync_rec->type) { + case MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD: + modify_type = MODIFY_ADD; + break; + case MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE: + modify_type = MODIFY_REMOVE; + break; + default: + i_unreached(); + } + + kw_str = array_idx_elem(ctx->keywords, sync_rec->keyword_idx); + imapc_sync_store(ctx, modify_type, sync_rec->uid1, + sync_rec->uid2, kw_str); +} + +static void imapc_sync_expunge_finish(struct imapc_sync_context *ctx) +{ + string_t *str; + + if (array_count(&ctx->expunged_uids) == 0) + return; + + if ((ctx->mbox->capabilities & IMAPC_CAPABILITY_UIDPLUS) == 0) { + /* just expunge everything */ + imapc_sync_cmd(ctx, "EXPUNGE"); + return; + } + + /* build a list of UIDs to expunge */ + str = t_str_new(128); + str_append(str, "UID EXPUNGE "); + imap_write_seq_range(str, &ctx->expunged_uids); + imapc_sync_cmd(ctx, str_c(str)); +} + +static void imapc_sync_uid_next(struct imapc_sync_context *ctx) +{ + struct imapc_mailbox *mbox = ctx->mbox; + const struct mail_index_header *hdr; + uint32_t uid_next = mbox->sync_uid_next; + + if (uid_next < mbox->min_append_uid) + uid_next = mbox->min_append_uid; + + hdr = mail_index_get_header(ctx->sync_view); + if (hdr->next_uid < uid_next) { + mail_index_update_header(ctx->trans, + offsetof(struct mail_index_header, next_uid), + &uid_next, sizeof(uid_next), FALSE); + } +} + +static void imapc_sync_highestmodseq(struct imapc_sync_context *ctx) +{ + if (imapc_mailbox_has_modseqs(ctx->mbox) && + mail_index_modseq_get_highest(ctx->sync_view) < ctx->mbox->sync_highestmodseq) + mail_index_update_highest_modseq(ctx->trans, ctx->mbox->sync_highestmodseq); +} + +static void +imapc_initial_sync_check(struct imapc_sync_context *ctx, bool nooped) +{ + struct imapc_msgmap *msgmap = + imapc_client_mailbox_get_msgmap(ctx->mbox->client_box); + struct mail_index_view *view = ctx->mbox->delayed_sync_view; + const struct mail_index_header *hdr = mail_index_get_header(view); + uint32_t rseq, lseq, ruid, luid, rcount, lcount; + + rseq = lseq = 1; + rcount = imapc_msgmap_count(msgmap); + lcount = mail_index_view_get_messages_count(view); + while (rseq <= rcount || lseq <= lcount) { + if (rseq <= rcount) + ruid = imapc_msgmap_rseq_to_uid(msgmap, rseq); + else + ruid = (uint32_t)-1; + if (lseq <= lcount) + mail_index_lookup_uid(view, lseq, &luid); + else + luid = (uint32_t)-1; + + if (ruid == luid) { + /* message exists in index and in remote server */ + lseq++; rseq++; + } else if (luid < ruid) { + /* message exists in index but not in remote server */ + if (luid >= ctx->mbox->sync_uid_next) { + /* the message was added to index by another + imapc session, and it's not visible yet + in this session */ + break; + } + /* it's already expunged and we should have marked it */ + i_assert(mail_index_is_expunged(view, lseq) || + seq_range_exists(&ctx->mbox->delayed_expunged_uids, luid)); + lseq++; + } else { + /* message doesn't exist in index, but exists in + remote server */ + if (lseq > lcount && ruid >= hdr->next_uid) { + /* the message hasn't been yet added to index */ + break; + } + + /* another imapc session expunged it => + NOOP should send us an EXPUNGE event */ + if (!nooped) { + imapc_mailbox_noop(ctx->mbox); + imapc_initial_sync_check(ctx, TRUE); + return; + } + /* already nooped => index is corrupted */ + imapc_mailbox_set_corrupted(ctx->mbox, + "Expunged message uid=%u reappeared", ruid); + ctx->failed = TRUE; + return; + } + } +} + +static void +imapc_sync_send_commands(struct imapc_sync_context *ctx) +{ + if (ctx->mbox->exists_count == 0) { + /* empty mailbox - no point in fetching anything */ + return; + } + + if (IMAPC_BOX_HAS_FEATURE(ctx->mbox, IMAPC_FEATURE_GMAIL_MIGRATION) && + ctx->mbox->storage->set->pop3_deleted_flag[0] != '\0') { + struct imapc_command *cmd; + + cmd = imapc_sync_cmd(ctx, "SEARCH RETURN (ALL) X-GM-RAW \"in:^pop\""); + i_free(ctx->mbox->sync_gmail_pop3_search_tag); + ctx->mbox->sync_gmail_pop3_search_tag = + i_strdup(imapc_command_get_tag(cmd)); + } +} + +static void imapc_sync_index(struct imapc_sync_context *ctx) +{ + struct imapc_mailbox *mbox = ctx->mbox; + struct mail_index_sync_rec sync_rec; + uint32_t seq1, seq2; + + i_array_init(&ctx->expunged_uids, 64); + ctx->keywords = mail_index_get_keywords(mbox->box.index); + ctx->pool = pool_alloconly_create("imapc sync pool", 1024); + + while (mail_index_sync_next(ctx->index_sync_ctx, &sync_rec)) T_BEGIN { + if (!mail_index_lookup_seq_range(ctx->sync_view, + sync_rec.uid1, sync_rec.uid2, + &seq1, &seq2)) { + /* already expunged, nothing to do. */ + } else switch (sync_rec.type) { + case MAIL_INDEX_SYNC_TYPE_EXPUNGE: + imapc_sync_add_missing_deleted_flags(ctx, seq1, seq2); + seq_range_array_add_range(&ctx->expunged_uids, + sync_rec.uid1, sync_rec.uid2); + break; + case MAIL_INDEX_SYNC_TYPE_FLAGS: + imapc_sync_index_flags(ctx, &sync_rec); + break; + case MAIL_INDEX_SYNC_TYPE_KEYWORD_ADD: + case MAIL_INDEX_SYNC_TYPE_KEYWORD_REMOVE: + imapc_sync_index_keyword(ctx, &sync_rec); + break; + } + } T_END; + imapc_sync_finish_store(ctx); + pool_unref(&ctx->pool); + + if (!mbox->initial_sync_done) + imapc_sync_send_commands(ctx); + + imapc_sync_expunge_finish(ctx); + while (ctx->sync_command_count > 0) + imapc_mailbox_run(mbox); + array_free(&ctx->expunged_uids); + + if (!mbox->state_fetched_success) { + /* All the sync commands succeeded, but we got disconnected. + imapc_initial_sync_check() will crash if we go there. */ + ctx->failed = TRUE; + } + + /* add uidnext & highestmodseq after all appends */ + imapc_sync_uid_next(ctx); + imapc_sync_highestmodseq(ctx); + + mailbox_sync_notify(&mbox->box, 0, 0); + + if (!ctx->failed) { + /* reset only after a successful sync */ + mbox->sync_fetch_first_uid = 0; + } + if (!mbox->initial_sync_done && !ctx->failed) { + imapc_initial_sync_check(ctx, FALSE); + mbox->initial_sync_done = TRUE; + } +} + +static int +imapc_sync_begin(struct imapc_mailbox *mbox, + struct imapc_sync_context **ctx_r, bool force) +{ + struct imapc_sync_context *ctx; + enum mail_index_sync_flags sync_flags; + int ret; + + i_assert(!mbox->syncing); + + ctx = i_new(struct imapc_sync_context, 1); + ctx->mbox = mbox; + + sync_flags = index_storage_get_sync_flags(&mbox->box) | + MAIL_INDEX_SYNC_FLAG_FLUSH_DIRTY; + if (!force) + sync_flags |= MAIL_INDEX_SYNC_FLAG_REQUIRE_CHANGES; + + ret = mail_index_sync_begin(mbox->box.index, &ctx->index_sync_ctx, + &ctx->sync_view, &ctx->trans, + sync_flags); + if (ret <= 0) { + if (ret < 0) + mailbox_set_index_error(&mbox->box); + i_free(ctx); + *ctx_r = NULL; + return ret; + } + + i_assert(mbox->sync_view == NULL); + i_assert(mbox->delayed_sync_trans == NULL); + mbox->sync_view = ctx->sync_view; + mbox->delayed_sync_view = + mail_index_transaction_open_updated_view(ctx->trans); + mbox->delayed_sync_trans = ctx->trans; + mbox->delayed_sync_cache_view = + mail_cache_view_open(mbox->box.cache, mbox->delayed_sync_view); + mbox->delayed_sync_cache_trans = + mail_cache_get_transaction(mbox->delayed_sync_cache_view, + mbox->delayed_sync_trans); + mbox->min_append_uid = mail_index_get_header(ctx->sync_view)->next_uid; + + mbox->syncing = TRUE; + mbox->sync_ctx = ctx; + + if (mbox->delayed_untagged_exists) { + bool fetch_send = imapc_mailbox_fetch_state(mbox, + mbox->min_append_uid); + while (fetch_send && mbox->delayed_untagged_exists) + imapc_mailbox_run(mbox); + } + + if (!mbox->box.deleting) + imapc_sync_index(ctx); + + mail_index_view_close(&mbox->delayed_sync_view); + mbox->delayed_sync_trans = NULL; + mbox->sync_view = NULL; + + *ctx_r = ctx; + return 0; +} + +static int imapc_sync_finish(struct imapc_sync_context **_ctx) +{ + struct imapc_sync_context *ctx = *_ctx; + bool changes; + int ret = ctx->failed ? -1 : 0; + + *_ctx = NULL; + /* Commit the transaction even if we failed. This is important, because + during the sync delayed_sync_trans points to the sync transaction. + Even if the syncing doesn't fully succeed, we don't want to lose + changes in delayed_sync_trans. */ + if (mail_index_sync_commit(&ctx->index_sync_ctx) < 0) { + mailbox_set_index_error(&ctx->mbox->box); + ret = -1; + } + if (ctx->mbox->sync_gmail_pop3_search_tag != NULL) { + mailbox_set_critical(&ctx->mbox->box, + "gmail-pop3 search not successful"); + i_free_and_null(ctx->mbox->sync_gmail_pop3_search_tag); + ret = -1; + } + mail_cache_view_close(&ctx->mbox->delayed_sync_cache_view); + ctx->mbox->delayed_sync_cache_trans = NULL; + + ctx->mbox->syncing = FALSE; + ctx->mbox->sync_ctx = NULL; + + /* this is done simply to commit delayed expunges if there are any + (has to be done after sync is committed) */ + if (imapc_mailbox_commit_delayed_trans(ctx->mbox, FALSE, &changes) < 0) + ctx->failed = TRUE; + + i_free(ctx); + return ret; +} + +static int imapc_untagged_fetch_uid_cmp(struct imapc_untagged_fetch_ctx *const *ctx1, + struct imapc_untagged_fetch_ctx *const *ctx2) +{ + return (*ctx1)->uid < (*ctx2)->uid ? -1 : + (*ctx1)->uid > (*ctx2)->uid ? 1 : 0; +} + +static void imapc_sync_handle_untagged_fetches(struct imapc_mailbox *mbox) +{ + struct imapc_untagged_fetch_ctx *untagged_fetch_context; + struct mail_index_view *updated_view; + uint32_t lseq; + + i_assert(array_count(&mbox->untagged_fetch_contexts) > 0); + i_assert(mbox->delayed_sync_trans == NULL); + + array_sort(&mbox->untagged_fetch_contexts, imapc_untagged_fetch_uid_cmp); + + mbox->delayed_sync_trans = + mail_index_transaction_begin(imapc_mailbox_get_sync_view(mbox), + MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL); + + array_foreach_elem(&mbox->untagged_fetch_contexts, untagged_fetch_context) { + if (untagged_fetch_context->uid < mbox->min_append_uid || + untagged_fetch_context->uid < mail_index_get_header(mbox->sync_view)->next_uid) { + /* The message was already added */ + continue; + } + + mail_index_append(mbox->delayed_sync_trans, + untagged_fetch_context->uid, + &lseq); + mbox->min_append_uid = untagged_fetch_context->uid + 1; + } + + updated_view = mail_index_transaction_open_updated_view(mbox->delayed_sync_trans); + + array_foreach_elem(&mbox->untagged_fetch_contexts, untagged_fetch_context) { + if (!untagged_fetch_context->have_flags) { + imapc_untagged_fetch_ctx_free(&untagged_fetch_context); + continue; + } + + /* Lookup the mail belonging to this context using the + context->uid */ + if (!mail_index_lookup_seq(updated_view, + untagged_fetch_context->uid, + &lseq)) { + /* mail is expunged already */ + imapc_untagged_fetch_ctx_free(&untagged_fetch_context); + continue; + } + + imapc_untagged_fetch_update_flags(mbox, untagged_fetch_context, + updated_view, lseq); + imapc_untagged_fetch_ctx_free(&untagged_fetch_context); + } + + mail_index_view_close(&updated_view); + if (mail_index_transaction_commit(&mbox->delayed_sync_trans) < 0) + mailbox_set_index_error(&mbox->box); + array_clear(&mbox->untagged_fetch_contexts); +} + +static int imapc_sync(struct imapc_mailbox *mbox) +{ + struct imapc_sync_context *sync_ctx; + bool force = mbox->sync_fetch_first_uid != 0; + + if ((mbox->box.flags & MAILBOX_FLAG_SAVEONLY) != 0) { + /* we're only saving mails here - no syncing actually wanted */ + return 0; + } + + if (imapc_sync_begin(mbox, &sync_ctx, force) < 0) + return -1; + + if (!array_is_empty(&mbox->untagged_fetch_contexts)) + imapc_sync_handle_untagged_fetches(mbox); + + if (sync_ctx == NULL) + return 0; + if (imapc_sync_finish(&sync_ctx) < 0) + return -1; + return 0; +} + +static void +imapc_noop_if_needed(struct imapc_mailbox *mbox, enum mailbox_sync_flags flags) +{ + if (!mbox->initial_sync_done) { + /* we just SELECTed/EXAMINEd the mailbox, don't do another + NOOP. */ + } else if ((flags & MAILBOX_SYNC_FLAG_FAST) == 0 && + ((mbox->capabilities & IMAPC_CAPABILITY_IDLE) == 0 || + (flags & MAILBOX_SYNC_FLAG_FULL_READ) != 0)) { + /* do NOOP to make sure we have the latest changes before + starting sync. this is necessary either because se don't + support IDLE at all, or because we want to be sure that we + have the latest changes (IDLE is started with a small delay, + so we might not actually even be in IDLE right not) */ + imapc_mailbox_noop(mbox); + } +} + +static bool imapc_mailbox_need_initial_fetch(struct imapc_mailbox *mbox) +{ + if (mbox->box.deleting) { + /* If the mailbox is about to be deleted there is no need to + expect initial fetch to be done */ + return FALSE; + } + if ((mbox->box.flags & MAILBOX_FLAG_SAVEONLY) != 0) { + /* The mailbox is opened only for saving there is no need to + expect initial fetchting do be done. */ + return FALSE; + } + return TRUE; +} + +struct mailbox_sync_context * +imapc_mailbox_sync_init(struct mailbox *box, enum mailbox_sync_flags flags) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(box); + struct imapc_mailbox_list *list = mbox->storage->client->_list; + bool changes; + int ret = 0; + + if (list != NULL) { + if (!list->refreshed_mailboxes && + list->last_refreshed_mailboxes < ioloop_time) + list->refreshed_mailboxes_recently = FALSE; + } + + imapc_noop_if_needed(mbox, flags); + + if (imapc_storage_client_handle_auth_failure(mbox->storage->client)) + ret = -1; + else if (!mbox->state_fetched_success && !mbox->state_fetching_uid1 && + imapc_mailbox_need_initial_fetch(mbox)) { + /* initial FETCH failed already */ + ret = -1; + } + if (imapc_mailbox_commit_delayed_trans(mbox, FALSE, &changes) < 0) + ret = -1; + if ((changes || mbox->sync_fetch_first_uid != 0 || + index_mailbox_want_full_sync(&mbox->box, flags)) && + ret == 0) + ret = imapc_sync(mbox); + + return index_mailbox_sync_init(box, flags, ret < 0); +} + +int imapc_mailbox_sync_deinit(struct mailbox_sync_context *ctx, + struct mailbox_sync_status *status_r) +{ + struct imapc_mailbox *mbox = IMAPC_MAILBOX(ctx->box); + int ret; + + ret = index_mailbox_sync_deinit(ctx, status_r); + ctx = NULL; + + if (mbox->client_box == NULL) + return ret; + + imapc_client_mailbox_idle(mbox->client_box); + return ret; +} diff --git a/src/lib-storage/index/imapc/imapc-sync.h b/src/lib-storage/index/imapc/imapc-sync.h new file mode 100644 index 0000000..54a0368 --- /dev/null +++ b/src/lib-storage/index/imapc/imapc-sync.h @@ -0,0 +1,39 @@ +#ifndef IMAPC_SYNC_H +#define IMAPC_SYNC_H + +struct mailbox; +struct mailbox_sync_status; + +struct imapc_sync_store { + enum modify_type modify_type; + const char *flags; + + ARRAY_TYPE(seq_range) uids; +}; + +struct imapc_sync_context { + struct imapc_mailbox *mbox; + struct mail_index_sync_ctx *index_sync_ctx; + struct mail_index_view *sync_view; + struct mail_index_transaction *trans; + + const ARRAY_TYPE(keywords) *keywords; + ARRAY_TYPE(seq_range) expunged_uids; + unsigned int sync_command_count; + + pool_t pool; + HASH_TABLE(struct imapc_sync_store *, struct imapc_sync_store *) stores; + + uint32_t prev_uid1, prev_uid2; + enum modify_type prev_modify_type; + string_t *prev_flags; + + bool failed:1; +}; + +struct mailbox_sync_context * +imapc_mailbox_sync_init(struct mailbox *box, enum mailbox_sync_flags flags); +int imapc_mailbox_sync_deinit(struct mailbox_sync_context *ctx, + struct mailbox_sync_status *status_r); + +#endif |