diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:51:24 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:51:24 +0000 |
commit | f7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch) | |
tree | a3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/imap | |
parent | Initial commit. (diff) | |
download | dovecot-upstream.tar.xz dovecot-upstream.zip |
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/imap')
76 files changed, 18693 insertions, 0 deletions
diff --git a/src/imap/Makefile.am b/src/imap/Makefile.am new file mode 100644 index 0000000..0a45fd3 --- /dev/null +++ b/src/imap/Makefile.am @@ -0,0 +1,130 @@ +pkglibexecdir = $(libexecdir)/dovecot + +pkglibexec_PROGRAMS = imap + +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-test \ + -I$(top_srcdir)/src/lib-settings \ + -I$(top_srcdir)/src/lib-dict \ + -I$(top_srcdir)/src/lib-master \ + -I$(top_srcdir)/src/lib-mail \ + -I$(top_srcdir)/src/lib-smtp \ + -I$(top_srcdir)/src/lib-imap \ + -I$(top_srcdir)/src/lib-imap-urlauth \ + -I$(top_srcdir)/src/lib-imap-storage \ + -I$(top_srcdir)/src/lib-index \ + -I$(top_srcdir)/src/lib-storage \ + $(BINARY_CFLAGS) + +imap_LDFLAGS = -export-dynamic \ + $(BINARY_LDFLAGS) + +imap_LDADD = \ + ../lib-imap-urlauth/libimap-urlauth.la \ + $(LIBDOVECOT_STORAGE) \ + $(LIBDOVECOT) +imap_DEPENDENCIES = \ + ../lib-imap-urlauth/libimap-urlauth.la \ + $(LIBDOVECOT_STORAGE_DEPS) \ + $(LIBDOVECOT_DEPS) + +cmds = \ + cmd-append.c \ + cmd-capability.c \ + cmd-cancelupdate.c \ + cmd-check.c \ + cmd-close.c \ + cmd-copy.c \ + cmd-create.c \ + cmd-delete.c \ + cmd-enable.c \ + cmd-examine.c \ + cmd-expunge.c \ + cmd-fetch.c \ + cmd-genurlauth.c \ + cmd-getmetadata.c \ + cmd-id.c \ + cmd-idle.c \ + cmd-list.c \ + cmd-logout.c \ + cmd-lsub.c \ + cmd-namespace.c \ + cmd-noop.c \ + cmd-notify.c \ + cmd-rename.c \ + cmd-resetkey.c \ + cmd-search.c \ + cmd-select.c \ + cmd-setmetadata.c \ + cmd-sort.c \ + cmd-status.c \ + cmd-store.c \ + cmd-subscribe.c \ + cmd-thread.c \ + cmd-unselect.c \ + cmd-unsubscribe.c \ + cmd-urlfetch.c \ + cmd-x-cancel.c \ + cmd-x-state.c + +common_sources = \ + $(cmds) \ + imap-client.c \ + imap-client-hibernate.c \ + imap-commands.c \ + imap-commands-util.c \ + imap-expunge.c \ + imap-feature.c \ + imap-fetch.c \ + imap-fetch-body.c \ + imap-list.c \ + imap-master-client.c \ + imap-notify.c \ + imap-search.c \ + imap-search-args.c \ + imap-settings.c \ + imap-status.c \ + imap-state.c \ + imap-sync.c \ + mail-storage-callbacks.c + +imap_SOURCES = \ + $(common_sources) \ + main.c + +headers = \ + imap-client.h \ + imap-commands.h \ + imap-commands-util.h \ + imap-common.h \ + imap-expunge.h \ + imap-feature.h \ + imap-fetch.h \ + imap-list.h \ + imap-master-client.h \ + imap-notify.h \ + imap-search.h \ + imap-search-args.h \ + imap-settings.h \ + imap-status.h \ + imap-state.h \ + imap-sync.h \ + imap-sync-private.h + +pkginc_libdir=$(pkgincludedir) +pkginc_lib_HEADERS = $(headers) + +test_programs = \ + test-imap-client-hibernate +noinst_PROGRAMS = $(test_programs) + +test_imap_client_hibernate_SOURCES = \ + test-imap-client-hibernate.c $(common_sources) +test_imap_client_hibernate_LDADD = $(imap_LDADD) +test_imap_client_hibernate_DEPENDENCIES = $(imap_DEPENDENCIES) + +check-local: + for bin in $(test_programs); do \ + if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \ + done diff --git a/src/imap/Makefile.in b/src/imap/Makefile.in new file mode 100644 index 0000000..df12ec1 --- /dev/null +++ b/src/imap/Makefile.in @@ -0,0 +1,1213 @@ +# Makefile.in generated by automake 1.16.1 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994-2018 Free Software Foundation, Inc. + +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + + +VPATH = @srcdir@ +am__is_gnu_make = { \ + if test -z '$(MAKELEVEL)'; then \ + false; \ + elif test -n '$(MAKE_HOST)'; then \ + true; \ + elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ + true; \ + else \ + false; \ + fi; \ +} +am__make_running_with_option = \ + case $${target_option-} in \ + ?) ;; \ + *) echo "am__make_running_with_option: internal error: invalid" \ + "target option '$${target_option-}' specified" >&2; \ + exit 1;; \ + esac; \ + has_opt=no; \ + sane_makeflags=$$MAKEFLAGS; \ + if $(am__is_gnu_make); then \ + sane_makeflags=$$MFLAGS; \ + else \ + case $$MAKEFLAGS in \ + *\\[\ \ ]*) \ + bs=\\; \ + sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ + | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ + esac; \ + fi; \ + skip_next=no; \ + strip_trailopt () \ + { \ + flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ + }; \ + for flg in $$sane_makeflags; do \ + test $$skip_next = yes && { skip_next=no; continue; }; \ + case $$flg in \ + *=*|--*) continue;; \ + -*I) strip_trailopt 'I'; skip_next=yes;; \ + -*I?*) strip_trailopt 'I';; \ + -*O) strip_trailopt 'O'; skip_next=yes;; \ + -*O?*) strip_trailopt 'O';; \ + -*l) strip_trailopt 'l'; skip_next=yes;; \ + -*l?*) strip_trailopt 'l';; \ + -[dEDm]) skip_next=yes;; \ + -[JT]) skip_next=yes;; \ + esac; \ + case $$flg in \ + *$$target_option*) has_opt=yes; break;; \ + esac; \ + done; \ + test $$has_opt = yes +am__make_dryrun = (target_option=n; $(am__make_running_with_option)) +am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +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@ +pkglibexec_PROGRAMS = imap$(EXEEXT) +noinst_PROGRAMS = $(am__EXEEXT_1) +subdir = src/imap +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 = +am__EXEEXT_1 = test-imap-client-hibernate$(EXEEXT) +am__installdirs = "$(DESTDIR)$(pkglibexecdir)" \ + "$(DESTDIR)$(pkginc_libdir)" +PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS) +am__objects_1 = cmd-append.$(OBJEXT) cmd-capability.$(OBJEXT) \ + cmd-cancelupdate.$(OBJEXT) cmd-check.$(OBJEXT) \ + cmd-close.$(OBJEXT) cmd-copy.$(OBJEXT) cmd-create.$(OBJEXT) \ + cmd-delete.$(OBJEXT) cmd-enable.$(OBJEXT) \ + cmd-examine.$(OBJEXT) cmd-expunge.$(OBJEXT) \ + cmd-fetch.$(OBJEXT) cmd-genurlauth.$(OBJEXT) \ + cmd-getmetadata.$(OBJEXT) cmd-id.$(OBJEXT) cmd-idle.$(OBJEXT) \ + cmd-list.$(OBJEXT) cmd-logout.$(OBJEXT) cmd-lsub.$(OBJEXT) \ + cmd-namespace.$(OBJEXT) cmd-noop.$(OBJEXT) \ + cmd-notify.$(OBJEXT) cmd-rename.$(OBJEXT) \ + cmd-resetkey.$(OBJEXT) cmd-search.$(OBJEXT) \ + cmd-select.$(OBJEXT) cmd-setmetadata.$(OBJEXT) \ + cmd-sort.$(OBJEXT) cmd-status.$(OBJEXT) cmd-store.$(OBJEXT) \ + cmd-subscribe.$(OBJEXT) cmd-thread.$(OBJEXT) \ + cmd-unselect.$(OBJEXT) cmd-unsubscribe.$(OBJEXT) \ + cmd-urlfetch.$(OBJEXT) cmd-x-cancel.$(OBJEXT) \ + cmd-x-state.$(OBJEXT) +am__objects_2 = $(am__objects_1) imap-client.$(OBJEXT) \ + imap-client-hibernate.$(OBJEXT) imap-commands.$(OBJEXT) \ + imap-commands-util.$(OBJEXT) imap-expunge.$(OBJEXT) \ + imap-feature.$(OBJEXT) imap-fetch.$(OBJEXT) \ + imap-fetch-body.$(OBJEXT) imap-list.$(OBJEXT) \ + imap-master-client.$(OBJEXT) imap-notify.$(OBJEXT) \ + imap-search.$(OBJEXT) imap-search-args.$(OBJEXT) \ + imap-settings.$(OBJEXT) imap-status.$(OBJEXT) \ + imap-state.$(OBJEXT) imap-sync.$(OBJEXT) \ + mail-storage-callbacks.$(OBJEXT) +am_imap_OBJECTS = $(am__objects_2) main.$(OBJEXT) +imap_OBJECTS = $(am_imap_OBJECTS) +am__DEPENDENCIES_1 = +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 = +imap_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \ + $(imap_LDFLAGS) $(LDFLAGS) -o $@ +am_test_imap_client_hibernate_OBJECTS = \ + test-imap-client-hibernate.$(OBJEXT) $(am__objects_2) +test_imap_client_hibernate_OBJECTS = \ + $(am_test_imap_client_hibernate_OBJECTS) +am__DEPENDENCIES_2 = ../lib-imap-urlauth/libimap-urlauth.la \ + $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_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)/cmd-append.Po \ + ./$(DEPDIR)/cmd-cancelupdate.Po ./$(DEPDIR)/cmd-capability.Po \ + ./$(DEPDIR)/cmd-check.Po ./$(DEPDIR)/cmd-close.Po \ + ./$(DEPDIR)/cmd-copy.Po ./$(DEPDIR)/cmd-create.Po \ + ./$(DEPDIR)/cmd-delete.Po ./$(DEPDIR)/cmd-enable.Po \ + ./$(DEPDIR)/cmd-examine.Po ./$(DEPDIR)/cmd-expunge.Po \ + ./$(DEPDIR)/cmd-fetch.Po ./$(DEPDIR)/cmd-genurlauth.Po \ + ./$(DEPDIR)/cmd-getmetadata.Po ./$(DEPDIR)/cmd-id.Po \ + ./$(DEPDIR)/cmd-idle.Po ./$(DEPDIR)/cmd-list.Po \ + ./$(DEPDIR)/cmd-logout.Po ./$(DEPDIR)/cmd-lsub.Po \ + ./$(DEPDIR)/cmd-namespace.Po ./$(DEPDIR)/cmd-noop.Po \ + ./$(DEPDIR)/cmd-notify.Po ./$(DEPDIR)/cmd-rename.Po \ + ./$(DEPDIR)/cmd-resetkey.Po ./$(DEPDIR)/cmd-search.Po \ + ./$(DEPDIR)/cmd-select.Po ./$(DEPDIR)/cmd-setmetadata.Po \ + ./$(DEPDIR)/cmd-sort.Po ./$(DEPDIR)/cmd-status.Po \ + ./$(DEPDIR)/cmd-store.Po ./$(DEPDIR)/cmd-subscribe.Po \ + ./$(DEPDIR)/cmd-thread.Po ./$(DEPDIR)/cmd-unselect.Po \ + ./$(DEPDIR)/cmd-unsubscribe.Po ./$(DEPDIR)/cmd-urlfetch.Po \ + ./$(DEPDIR)/cmd-x-cancel.Po ./$(DEPDIR)/cmd-x-state.Po \ + ./$(DEPDIR)/imap-client-hibernate.Po \ + ./$(DEPDIR)/imap-client.Po ./$(DEPDIR)/imap-commands-util.Po \ + ./$(DEPDIR)/imap-commands.Po ./$(DEPDIR)/imap-expunge.Po \ + ./$(DEPDIR)/imap-feature.Po ./$(DEPDIR)/imap-fetch-body.Po \ + ./$(DEPDIR)/imap-fetch.Po ./$(DEPDIR)/imap-list.Po \ + ./$(DEPDIR)/imap-master-client.Po ./$(DEPDIR)/imap-notify.Po \ + ./$(DEPDIR)/imap-search-args.Po ./$(DEPDIR)/imap-search.Po \ + ./$(DEPDIR)/imap-settings.Po ./$(DEPDIR)/imap-state.Po \ + ./$(DEPDIR)/imap-status.Po ./$(DEPDIR)/imap-sync.Po \ + ./$(DEPDIR)/mail-storage-callbacks.Po ./$(DEPDIR)/main.Po \ + ./$(DEPDIR)/test-imap-client-hibernate.Po +am__mv = mv -f +COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \ + $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) +LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \ + $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \ + $(AM_CFLAGS) $(CFLAGS) +AM_V_CC = $(am__v_CC_@AM_V@) +am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@) +am__v_CC_0 = @echo " CC " $@; +am__v_CC_1 = +CCLD = $(CC) +LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \ + $(AM_LDFLAGS) $(LDFLAGS) -o $@ +AM_V_CCLD = $(am__v_CCLD_@AM_V@) +am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@) +am__v_CCLD_0 = @echo " CCLD " $@; +am__v_CCLD_1 = +SOURCES = $(imap_SOURCES) $(test_imap_client_hibernate_SOURCES) +DIST_SOURCES = $(imap_SOURCES) $(test_imap_client_hibernate_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; }; \ + } +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) +pkglibexecdir = $(libexecdir)/dovecot +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@ +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-test \ + -I$(top_srcdir)/src/lib-settings \ + -I$(top_srcdir)/src/lib-dict \ + -I$(top_srcdir)/src/lib-master \ + -I$(top_srcdir)/src/lib-mail \ + -I$(top_srcdir)/src/lib-smtp \ + -I$(top_srcdir)/src/lib-imap \ + -I$(top_srcdir)/src/lib-imap-urlauth \ + -I$(top_srcdir)/src/lib-imap-storage \ + -I$(top_srcdir)/src/lib-index \ + -I$(top_srcdir)/src/lib-storage \ + $(BINARY_CFLAGS) + +imap_LDFLAGS = -export-dynamic \ + $(BINARY_LDFLAGS) + +imap_LDADD = \ + ../lib-imap-urlauth/libimap-urlauth.la \ + $(LIBDOVECOT_STORAGE) \ + $(LIBDOVECOT) + +imap_DEPENDENCIES = \ + ../lib-imap-urlauth/libimap-urlauth.la \ + $(LIBDOVECOT_STORAGE_DEPS) \ + $(LIBDOVECOT_DEPS) + +cmds = \ + cmd-append.c \ + cmd-capability.c \ + cmd-cancelupdate.c \ + cmd-check.c \ + cmd-close.c \ + cmd-copy.c \ + cmd-create.c \ + cmd-delete.c \ + cmd-enable.c \ + cmd-examine.c \ + cmd-expunge.c \ + cmd-fetch.c \ + cmd-genurlauth.c \ + cmd-getmetadata.c \ + cmd-id.c \ + cmd-idle.c \ + cmd-list.c \ + cmd-logout.c \ + cmd-lsub.c \ + cmd-namespace.c \ + cmd-noop.c \ + cmd-notify.c \ + cmd-rename.c \ + cmd-resetkey.c \ + cmd-search.c \ + cmd-select.c \ + cmd-setmetadata.c \ + cmd-sort.c \ + cmd-status.c \ + cmd-store.c \ + cmd-subscribe.c \ + cmd-thread.c \ + cmd-unselect.c \ + cmd-unsubscribe.c \ + cmd-urlfetch.c \ + cmd-x-cancel.c \ + cmd-x-state.c + +common_sources = \ + $(cmds) \ + imap-client.c \ + imap-client-hibernate.c \ + imap-commands.c \ + imap-commands-util.c \ + imap-expunge.c \ + imap-feature.c \ + imap-fetch.c \ + imap-fetch-body.c \ + imap-list.c \ + imap-master-client.c \ + imap-notify.c \ + imap-search.c \ + imap-search-args.c \ + imap-settings.c \ + imap-status.c \ + imap-state.c \ + imap-sync.c \ + mail-storage-callbacks.c + +imap_SOURCES = \ + $(common_sources) \ + main.c + +headers = \ + imap-client.h \ + imap-commands.h \ + imap-commands-util.h \ + imap-common.h \ + imap-expunge.h \ + imap-feature.h \ + imap-fetch.h \ + imap-list.h \ + imap-master-client.h \ + imap-notify.h \ + imap-search.h \ + imap-search-args.h \ + imap-settings.h \ + imap-status.h \ + imap-state.h \ + imap-sync.h \ + imap-sync-private.h + +pkginc_libdir = $(pkgincludedir) +pkginc_lib_HEADERS = $(headers) +test_programs = \ + test-imap-client-hibernate + +test_imap_client_hibernate_SOURCES = \ + test-imap-client-hibernate.c $(common_sources) + +test_imap_client_hibernate_LDADD = $(imap_LDADD) +test_imap_client_hibernate_DEPENDENCIES = $(imap_DEPENDENCIES) +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/imap/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/imap/Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: @MAINTAINER_MODE_TRUE@ $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): @MAINTAINER_MODE_TRUE@ $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): + +clean-noinstPROGRAMS: + @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \ + echo " rm -f" $$list; \ + rm -f $$list || exit $$?; \ + test -n "$(EXEEXT)" || exit 0; \ + list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \ + echo " rm -f" $$list; \ + rm -f $$list +install-pkglibexecPROGRAMS: $(pkglibexec_PROGRAMS) + @$(NORMAL_INSTALL) + @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \ + if test -n "$$list"; then \ + echo " $(MKDIR_P) '$(DESTDIR)$(pkglibexecdir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(pkglibexecdir)" || exit 1; \ + fi; \ + for p in $$list; do echo "$$p $$p"; done | \ + sed 's/$(EXEEXT)$$//' | \ + while read p p1; do if test -f $$p \ + || test -f $$p1 \ + ; then echo "$$p"; echo "$$p"; else :; fi; \ + done | \ + sed -e 'p;s,.*/,,;n;h' \ + -e 's|.*|.|' \ + -e 'p;x;s,.*/,,;s/$(EXEEXT)$$//;$(transform);s/$$/$(EXEEXT)/' | \ + sed 'N;N;N;s,\n, ,g' | \ + $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1 } \ + { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \ + if ($$2 == $$4) files[d] = files[d] " " $$1; \ + else { print "f", $$3 "/" $$4, $$1; } } \ + END { for (d in files) print "f", d, files[d] }' | \ + while read type dir files; do \ + if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \ + test -z "$$files" || { \ + echo " $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files '$(DESTDIR)$(pkglibexecdir)$$dir'"; \ + $(INSTALL_PROGRAM_ENV) $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL_PROGRAM) $$files "$(DESTDIR)$(pkglibexecdir)$$dir" || exit $$?; \ + } \ + ; done + +uninstall-pkglibexecPROGRAMS: + @$(NORMAL_UNINSTALL) + @list='$(pkglibexec_PROGRAMS)'; test -n "$(pkglibexecdir)" || list=; \ + files=`for p in $$list; do echo "$$p"; done | \ + sed -e 'h;s,^.*/,,;s/$(EXEEXT)$$//;$(transform)' \ + -e 's/$$/$(EXEEXT)/' \ + `; \ + test -n "$$list" || exit 0; \ + echo " ( cd '$(DESTDIR)$(pkglibexecdir)' && rm -f" $$files ")"; \ + cd "$(DESTDIR)$(pkglibexecdir)" && rm -f $$files + +clean-pkglibexecPROGRAMS: + @list='$(pkglibexec_PROGRAMS)'; test -n "$$list" || exit 0; \ + echo " rm -f" $$list; \ + rm -f $$list || exit $$?; \ + test -n "$(EXEEXT)" || exit 0; \ + list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \ + echo " rm -f" $$list; \ + rm -f $$list + +imap$(EXEEXT): $(imap_OBJECTS) $(imap_DEPENDENCIES) $(EXTRA_imap_DEPENDENCIES) + @rm -f imap$(EXEEXT) + $(AM_V_CCLD)$(imap_LINK) $(imap_OBJECTS) $(imap_LDADD) $(LIBS) + +test-imap-client-hibernate$(EXEEXT): $(test_imap_client_hibernate_OBJECTS) $(test_imap_client_hibernate_DEPENDENCIES) $(EXTRA_test_imap_client_hibernate_DEPENDENCIES) + @rm -f test-imap-client-hibernate$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(test_imap_client_hibernate_OBJECTS) $(test_imap_client_hibernate_LDADD) $(LIBS) + +mostlyclean-compile: + -rm -f *.$(OBJEXT) + +distclean-compile: + -rm -f *.tab.c + +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-append.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-cancelupdate.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-capability.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-check.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-close.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-copy.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-create.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-delete.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-enable.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-examine.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-expunge.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-fetch.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-genurlauth.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-getmetadata.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-id.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-idle.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-list.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-logout.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-lsub.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-namespace.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-noop.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-notify.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-rename.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-resetkey.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-search.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-select.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-setmetadata.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-sort.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-status.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-store.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-subscribe.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-thread.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-unselect.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-unsubscribe.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-urlfetch.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-x-cancel.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/cmd-x-state.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-client-hibernate.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-client.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-commands-util.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-commands.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-expunge.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-feature.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-fetch-body.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-fetch.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-list.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-master-client.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-notify.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-search-args.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-search.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-settings.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-state.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-status.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/imap-sync.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/mail-storage-callbacks.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/main.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-imap-client-hibernate.Po@am__quote@ # am--include-marker + +$(am__depfiles_remade): + @$(MKDIR_P) $(@D) + @echo '# dummy' >$@-t && $(am__mv) $@-t $@ + +am--depfiles: $(am__depfiles_remade) + +.c.o: +@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $< +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ $< + +.c.obj: +@am__fastdepCC_TRUE@ $(AM_V_CC)$(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'` +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(COMPILE) -c -o $@ `$(CYGPATH_W) '$<'` + +.c.lo: +@am__fastdepCC_TRUE@ $(AM_V_CC)$(LTCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $< +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LTCOMPILE) -c -o $@ $< + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +install-pkginc_libHEADERS: $(pkginc_lib_HEADERS) + @$(NORMAL_INSTALL) + @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \ + if test -n "$$list"; then \ + echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \ + fi; \ + for p in $$list; do \ + if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ + echo "$$d$$p"; \ + done | $(am__base_list) | \ + while read files; do \ + echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \ + $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \ + done + +uninstall-pkginc_libHEADERS: + @$(NORMAL_UNINSTALL) + @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \ + files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \ + dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir) + +ID: $(am__tagged_files) + $(am__define_uniq_tagged_files); mkid -fID $$unique +tags: tags-am +TAGS: tags + +tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + set x; \ + here=`pwd`; \ + $(am__define_uniq_tagged_files); \ + shift; \ + if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ + test -n "$$unique" || unique=$$empty_fix; \ + if test $$# -gt 0; then \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + "$$@" $$unique; \ + else \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + $$unique; \ + fi; \ + fi +ctags: ctags-am + +CTAGS: ctags +ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + $(am__define_uniq_tagged_files); \ + test -z "$(CTAGS_ARGS)$$unique" \ + || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ + $$unique + +GTAGS: + here=`$(am__cd) $(top_builddir) && pwd` \ + && $(am__cd) $(top_srcdir) \ + && gtags -i $(GTAGS_ARGS) "$$here" +cscopelist: cscopelist-am + +cscopelist-am: $(am__tagged_files) + list='$(am__tagged_files)'; \ + case "$(srcdir)" in \ + [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \ + *) sdir=$(subdir)/$(srcdir) ;; \ + esac; \ + for i in $$list; do \ + if test -f "$$i"; then \ + echo "$(subdir)/$$i"; \ + else \ + echo "$$sdir/$$i"; \ + fi; \ + done >> $(top_builddir)/cscope.files + +distclean-tags: + -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags + +distdir: $(BUILT_SOURCES) + $(MAKE) $(AM_MAKEFLAGS) distdir-am + +distdir-am: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am + $(MAKE) $(AM_MAKEFLAGS) check-local +check: check-am +all-am: Makefile $(PROGRAMS) $(HEADERS) +installdirs: + for dir in "$(DESTDIR)$(pkglibexecdir)" "$(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-noinstPROGRAMS \ + clean-pkglibexecPROGRAMS mostlyclean-am + +distclean: distclean-am + -rm -f ./$(DEPDIR)/cmd-append.Po + -rm -f ./$(DEPDIR)/cmd-cancelupdate.Po + -rm -f ./$(DEPDIR)/cmd-capability.Po + -rm -f ./$(DEPDIR)/cmd-check.Po + -rm -f ./$(DEPDIR)/cmd-close.Po + -rm -f ./$(DEPDIR)/cmd-copy.Po + -rm -f ./$(DEPDIR)/cmd-create.Po + -rm -f ./$(DEPDIR)/cmd-delete.Po + -rm -f ./$(DEPDIR)/cmd-enable.Po + -rm -f ./$(DEPDIR)/cmd-examine.Po + -rm -f ./$(DEPDIR)/cmd-expunge.Po + -rm -f ./$(DEPDIR)/cmd-fetch.Po + -rm -f ./$(DEPDIR)/cmd-genurlauth.Po + -rm -f ./$(DEPDIR)/cmd-getmetadata.Po + -rm -f ./$(DEPDIR)/cmd-id.Po + -rm -f ./$(DEPDIR)/cmd-idle.Po + -rm -f ./$(DEPDIR)/cmd-list.Po + -rm -f ./$(DEPDIR)/cmd-logout.Po + -rm -f ./$(DEPDIR)/cmd-lsub.Po + -rm -f ./$(DEPDIR)/cmd-namespace.Po + -rm -f ./$(DEPDIR)/cmd-noop.Po + -rm -f ./$(DEPDIR)/cmd-notify.Po + -rm -f ./$(DEPDIR)/cmd-rename.Po + -rm -f ./$(DEPDIR)/cmd-resetkey.Po + -rm -f ./$(DEPDIR)/cmd-search.Po + -rm -f ./$(DEPDIR)/cmd-select.Po + -rm -f ./$(DEPDIR)/cmd-setmetadata.Po + -rm -f ./$(DEPDIR)/cmd-sort.Po + -rm -f ./$(DEPDIR)/cmd-status.Po + -rm -f ./$(DEPDIR)/cmd-store.Po + -rm -f ./$(DEPDIR)/cmd-subscribe.Po + -rm -f ./$(DEPDIR)/cmd-thread.Po + -rm -f ./$(DEPDIR)/cmd-unselect.Po + -rm -f ./$(DEPDIR)/cmd-unsubscribe.Po + -rm -f ./$(DEPDIR)/cmd-urlfetch.Po + -rm -f ./$(DEPDIR)/cmd-x-cancel.Po + -rm -f ./$(DEPDIR)/cmd-x-state.Po + -rm -f ./$(DEPDIR)/imap-client-hibernate.Po + -rm -f ./$(DEPDIR)/imap-client.Po + -rm -f ./$(DEPDIR)/imap-commands-util.Po + -rm -f ./$(DEPDIR)/imap-commands.Po + -rm -f ./$(DEPDIR)/imap-expunge.Po + -rm -f ./$(DEPDIR)/imap-feature.Po + -rm -f ./$(DEPDIR)/imap-fetch-body.Po + -rm -f ./$(DEPDIR)/imap-fetch.Po + -rm -f ./$(DEPDIR)/imap-list.Po + -rm -f ./$(DEPDIR)/imap-master-client.Po + -rm -f ./$(DEPDIR)/imap-notify.Po + -rm -f ./$(DEPDIR)/imap-search-args.Po + -rm -f ./$(DEPDIR)/imap-search.Po + -rm -f ./$(DEPDIR)/imap-settings.Po + -rm -f ./$(DEPDIR)/imap-state.Po + -rm -f ./$(DEPDIR)/imap-status.Po + -rm -f ./$(DEPDIR)/imap-sync.Po + -rm -f ./$(DEPDIR)/mail-storage-callbacks.Po + -rm -f ./$(DEPDIR)/main.Po + -rm -f ./$(DEPDIR)/test-imap-client-hibernate.Po + -rm -f Makefile +distclean-am: clean-am distclean-compile distclean-generic \ + distclean-tags + +dvi: dvi-am + +dvi-am: + +html: html-am + +html-am: + +info: info-am + +info-am: + +install-data-am: install-pkginc_libHEADERS + +install-dvi: install-dvi-am + +install-dvi-am: + +install-exec-am: install-pkglibexecPROGRAMS + +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)/cmd-append.Po + -rm -f ./$(DEPDIR)/cmd-cancelupdate.Po + -rm -f ./$(DEPDIR)/cmd-capability.Po + -rm -f ./$(DEPDIR)/cmd-check.Po + -rm -f ./$(DEPDIR)/cmd-close.Po + -rm -f ./$(DEPDIR)/cmd-copy.Po + -rm -f ./$(DEPDIR)/cmd-create.Po + -rm -f ./$(DEPDIR)/cmd-delete.Po + -rm -f ./$(DEPDIR)/cmd-enable.Po + -rm -f ./$(DEPDIR)/cmd-examine.Po + -rm -f ./$(DEPDIR)/cmd-expunge.Po + -rm -f ./$(DEPDIR)/cmd-fetch.Po + -rm -f ./$(DEPDIR)/cmd-genurlauth.Po + -rm -f ./$(DEPDIR)/cmd-getmetadata.Po + -rm -f ./$(DEPDIR)/cmd-id.Po + -rm -f ./$(DEPDIR)/cmd-idle.Po + -rm -f ./$(DEPDIR)/cmd-list.Po + -rm -f ./$(DEPDIR)/cmd-logout.Po + -rm -f ./$(DEPDIR)/cmd-lsub.Po + -rm -f ./$(DEPDIR)/cmd-namespace.Po + -rm -f ./$(DEPDIR)/cmd-noop.Po + -rm -f ./$(DEPDIR)/cmd-notify.Po + -rm -f ./$(DEPDIR)/cmd-rename.Po + -rm -f ./$(DEPDIR)/cmd-resetkey.Po + -rm -f ./$(DEPDIR)/cmd-search.Po + -rm -f ./$(DEPDIR)/cmd-select.Po + -rm -f ./$(DEPDIR)/cmd-setmetadata.Po + -rm -f ./$(DEPDIR)/cmd-sort.Po + -rm -f ./$(DEPDIR)/cmd-status.Po + -rm -f ./$(DEPDIR)/cmd-store.Po + -rm -f ./$(DEPDIR)/cmd-subscribe.Po + -rm -f ./$(DEPDIR)/cmd-thread.Po + -rm -f ./$(DEPDIR)/cmd-unselect.Po + -rm -f ./$(DEPDIR)/cmd-unsubscribe.Po + -rm -f ./$(DEPDIR)/cmd-urlfetch.Po + -rm -f ./$(DEPDIR)/cmd-x-cancel.Po + -rm -f ./$(DEPDIR)/cmd-x-state.Po + -rm -f ./$(DEPDIR)/imap-client-hibernate.Po + -rm -f ./$(DEPDIR)/imap-client.Po + -rm -f ./$(DEPDIR)/imap-commands-util.Po + -rm -f ./$(DEPDIR)/imap-commands.Po + -rm -f ./$(DEPDIR)/imap-expunge.Po + -rm -f ./$(DEPDIR)/imap-feature.Po + -rm -f ./$(DEPDIR)/imap-fetch-body.Po + -rm -f ./$(DEPDIR)/imap-fetch.Po + -rm -f ./$(DEPDIR)/imap-list.Po + -rm -f ./$(DEPDIR)/imap-master-client.Po + -rm -f ./$(DEPDIR)/imap-notify.Po + -rm -f ./$(DEPDIR)/imap-search-args.Po + -rm -f ./$(DEPDIR)/imap-search.Po + -rm -f ./$(DEPDIR)/imap-settings.Po + -rm -f ./$(DEPDIR)/imap-state.Po + -rm -f ./$(DEPDIR)/imap-status.Po + -rm -f ./$(DEPDIR)/imap-sync.Po + -rm -f ./$(DEPDIR)/mail-storage-callbacks.Po + -rm -f ./$(DEPDIR)/main.Po + -rm -f ./$(DEPDIR)/test-imap-client-hibernate.Po + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-am + +mostlyclean-am: mostlyclean-compile mostlyclean-generic \ + mostlyclean-libtool + +pdf: pdf-am + +pdf-am: + +ps: ps-am + +ps-am: + +uninstall-am: uninstall-pkginc_libHEADERS uninstall-pkglibexecPROGRAMS + +.MAKE: check-am install-am install-strip + +.PHONY: CTAGS GTAGS TAGS all all-am am--depfiles check check-am \ + check-local clean clean-generic clean-libtool \ + clean-noinstPROGRAMS clean-pkglibexecPROGRAMS 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-pkglibexecPROGRAMS \ + install-ps install-ps-am install-strip installcheck \ + installcheck-am installdirs maintainer-clean \ + maintainer-clean-generic mostlyclean mostlyclean-compile \ + mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \ + tags tags-am uninstall uninstall-am \ + uninstall-pkginc_libHEADERS uninstall-pkglibexecPROGRAMS + +.PRECIOUS: Makefile + + +check-local: + for bin in $(test_programs); do \ + if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \ + done + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/src/imap/cmd-append.c b/src/imap/cmd-append.c new file mode 100644 index 0000000..0d0c1c3 --- /dev/null +++ b/src/imap/cmd-append.c @@ -0,0 +1,957 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "ioloop.h" +#include "istream.h" +#include "istream-chain.h" +#include "ostream.h" +#include "str.h" +#include "imap-resp-code.h" +#include "istream-binary-converter.h" +#include "mail-storage-private.h" +#include "imap-parser.h" +#include "imap-date.h" +#include "imap-util.h" +#include "imap-commands.h" +#include "imap-msgpart-url.h" + +#include <sys/time.h> + +/* Don't allow internaldates to be too far in the future. At least with Maildir + they can cause problems with incremental backups since internaldate is + stored in file's mtime. But perhaps there are also some other reasons why + it might not be wanted. */ +#define INTERNALDATE_MAX_FUTURE_SECS (2*3600) + +struct cmd_append_context { + struct client *client; + struct client_command_context *cmd; + struct mailbox *box; + struct mailbox_transaction_context *t; + time_t started; + + struct istream_chain *catchain; + uoff_t cat_msg_size; + + struct istream *input; + struct istream *litinput; + uoff_t literal_size; + + struct imap_parser *save_parser; + struct mail_save_context *save_ctx; + unsigned int count; + + bool message_input:1; + bool binary_input:1; + bool catenate:1; + bool cmd_args_set:1; + bool failed:1; +}; + +static void cmd_append_finish(struct cmd_append_context *ctx); +static bool cmd_append_continue_message(struct client_command_context *cmd); +static bool cmd_append_parse_new_msg(struct client_command_context *cmd); + +static const char * +get_disconnect_reason(struct cmd_append_context *ctx, uoff_t lit_offset) +{ + string_t *str = t_str_new(128); + unsigned int secs = ioloop_time - ctx->started; + + str_printfa(str, "%s (While APPENDing: %u msgs, %u secs", + i_stream_get_disconnect_reason(ctx->input), + ctx->count, secs); + if (ctx->literal_size > 0) { + str_printfa(str, ", %"PRIuUOFF_T"/%"PRIuUOFF_T" bytes", + lit_offset, ctx->literal_size); + } + str_append_c(str, ')'); + return str_c(str); +} + +static void client_input_append(struct client_command_context *cmd) +{ + struct cmd_append_context *ctx = cmd->context; + struct client *client = cmd->client; + const char *reason; + bool finished; + uoff_t lit_offset; + + i_assert(!client->destroyed); + + client->last_input = ioloop_time; + timeout_reset(client->to_idle); + + switch (i_stream_read(client->input)) { + case -1: + /* disconnected */ + lit_offset = ctx->litinput == NULL ? 0 : + ctx->litinput->v_offset; + reason = get_disconnect_reason(ctx, lit_offset); + cmd_append_finish(cmd->context); + /* Reset command so that client_destroy() doesn't try to call + cmd_append_continue_message() anymore. */ + client_command_free(&cmd); + client_destroy(client, reason); + return; + case -2: + if (ctx->message_input) { + /* message data, this is handled internally by + mailbox_save_continue() */ + break; + } + cmd_append_finish(cmd->context); + + /* parameter word is longer than max. input buffer size. + this is most likely an error, so skip the new data + until newline is found. */ + client->input_skip_line = TRUE; + + if (!ctx->failed) + client_send_command_error(cmd, "Too long argument."); + cmd->param_error = TRUE; + client_command_free(&cmd); + return; + } + + o_stream_cork(client->output); + finished = command_exec(cmd); + if (!finished) + (void)client_handle_unfinished_cmd(cmd); + else + client_command_free(&cmd); + cmd_sync_delayed(client); + o_stream_uncork(client->output); + + client_continue_pending_input(client); +} + +static void cmd_append_finish(struct cmd_append_context *ctx) +{ + if (ctx->save_parser != NULL) + imap_parser_unref(&ctx->save_parser); + + i_assert(ctx->client->input_lock == ctx->cmd); + + io_remove(&ctx->client->io); + /* we must put back the original flush callback before beginning to + sync (the command is still unfinished at that point) */ + o_stream_set_flush_callback(ctx->client->output, + client_output, ctx->client); + + i_stream_unref(&ctx->litinput); + i_stream_unref(&ctx->input); + if (ctx->save_ctx != NULL) + mailbox_save_cancel(&ctx->save_ctx); + if (ctx->t != NULL) + mailbox_transaction_rollback(&ctx->t); + if (ctx->box != ctx->cmd->client->mailbox && ctx->box != NULL) + mailbox_free(&ctx->box); +} + +static bool cmd_append_send_literal_continue(struct cmd_append_context *ctx) +{ + if (ctx->failed) { + /* tagline was already sent, we can abort here */ + return FALSE; + } + + o_stream_nsend(ctx->client->output, "+ OK\r\n", 6); + o_stream_uncork(ctx->client->output); + o_stream_cork(ctx->client->output); + return TRUE; +} + +static int +cmd_append_catenate_mpurl(struct client_command_context *cmd, + const char *caturl, struct imap_msgpart_url *mpurl) +{ + struct cmd_append_context *ctx = cmd->context; + struct imap_msgpart_open_result mpresult; + uoff_t newsize; + const char *client_error; + int ret; + + /* catenate URL */ + ret = imap_msgpart_url_read_part(mpurl, &mpresult, &client_error); + if (ret < 0) { + client_send_box_error(cmd, ctx->box); + return -1; + } + if (ret == 0) { + /* invalid url, abort */ + client_send_tagline(cmd, + t_strdup_printf("NO [BADURL %s] %s.", + caturl, client_error)); + return -1; + } + if (mpresult.size == 0) { + /* empty input */ + return 0; + } + + newsize = ctx->cat_msg_size + mpresult.size; + if (newsize < ctx->cat_msg_size) { + client_send_tagline(cmd, + "NO [TOOBIG] Composed message grows too big."); + return -1; + } + + ctx->cat_msg_size = newsize; + /* add this input stream to chain */ + i_stream_chain_append(ctx->catchain, mpresult.input); + /* save by reading the chain stream */ + do { + ret = i_stream_read(mpresult.input); + i_assert(ret != 0); /* we can handle only blocking input here */ + } while (mailbox_save_continue(ctx->save_ctx) == 0 && ret != -1); + + if (mpresult.input->stream_errno != 0) { + mailbox_set_critical(ctx->box, + "read(%s) failed: %s (for CATENATE URL %s)", + i_stream_get_name(mpresult.input), + i_stream_get_error(mpresult.input), caturl); + client_send_box_error(cmd, ctx->box); + ret = -1; + } else if (!mpresult.input->eof) { + /* save failed */ + client_send_box_error(cmd, ctx->box); + ret = -1; + } else { + /* all the input must be consumed, so istream-chain's read() + unreferences the stream and we can free its parent mail */ + i_assert(!i_stream_have_bytes_left(mpresult.input)); + ret = 0; + } + return ret; +} + +static int +cmd_append_catenate_url(struct client_command_context *cmd, const char *caturl) +{ + struct cmd_append_context *ctx = cmd->context; + struct imap_msgpart_url *mpurl; + const char *client_error; + int ret; + + if (ctx->failed) + return -1; + + ret = imap_msgpart_url_parse(cmd->client->user, cmd->client->mailbox, + caturl, &mpurl, &client_error); + if (ret < 0) { + client_send_box_error(cmd, ctx->box); + return -1; + } + if (ret == 0) { + /* invalid url, abort */ + client_send_tagline(cmd, + t_strdup_printf("NO [BADURL %s] %s.", + caturl, client_error)); + return -1; + } + ret = cmd_append_catenate_mpurl(cmd, caturl, mpurl); + imap_msgpart_url_free(&mpurl); + return ret; +} + +static void cmd_append_catenate_text(struct client_command_context *cmd) +{ + struct cmd_append_context *ctx = cmd->context; + + if (ctx->literal_size > UOFF_T_MAX - ctx->cat_msg_size && + !ctx->failed) { + client_send_tagline(cmd, + "NO [TOOBIG] Composed message grows too big."); + ctx->failed = TRUE; + } + + /* save the mail */ + ctx->cat_msg_size += ctx->literal_size; + if (ctx->literal_size == 0) { + /* zero length literal. RFC doesn't explicitly specify + what should be done with this, so we'll simply + handle it by skipping the empty text part. */ + ctx->litinput = i_stream_create_from_data("", 0); + ctx->litinput->eof = TRUE; + } else { + ctx->litinput = i_stream_create_limit(cmd->client->input, + ctx->literal_size); + i_stream_chain_append(ctx->catchain, ctx->litinput); + } +} + +static int +cmd_append_catenate(struct client_command_context *cmd, + const struct imap_arg *args, bool *nonsync_r) +{ + struct cmd_append_context *ctx = cmd->context; + const char *catpart; + + *nonsync_r = FALSE; + + /* Handle URLs until a TEXT literal is encountered */ + while (imap_arg_get_atom(args, &catpart)) { + const char *caturl; + + if (strcasecmp(catpart, "URL") == 0 ) { + /* URL <url> */ + args++; + if (!imap_arg_get_astring(args, &caturl)) + break; + if (cmd_append_catenate_url(cmd, caturl) < 0) { + /* delay failure until we can stop + parsing input */ + ctx->failed = TRUE; + } + } else if (strcasecmp(catpart, "TEXT") == 0) { + /* TEXT <literal> */ + args++; + if (!imap_arg_get_literal_size(args, &ctx->literal_size)) + break; + if (args->literal8 && !ctx->binary_input && + !ctx->failed) { + client_send_tagline(cmd, + "NO ["IMAP_RESP_CODE_UNKNOWN_CTE"] " + "Binary input allowed only when the first part is binary."); + ctx->failed = TRUE; + } + *nonsync_r = args->type == IMAP_ARG_LITERAL_SIZE_NONSYNC; + cmd_append_catenate_text(cmd); + return 1; + } else { + break; + } + args++; + } + + if (IMAP_ARG_IS_EOL(args)) { + /* ")" */ + return 0; + } + if (!ctx->failed) + client_send_command_error(cmd, "Invalid arguments."); + return -1; +} + +static void cmd_append_finish_catenate(struct client_command_context *cmd) +{ + struct cmd_append_context *ctx = cmd->context; + + i_stream_chain_append_eof(ctx->catchain); + i_stream_unref(&ctx->input); + ctx->catenate = FALSE; + ctx->catchain = NULL; + + if (ctx->failed) { + /* APPEND has already failed */ + if (ctx->save_ctx != NULL) + mailbox_save_cancel(&ctx->save_ctx); + } else { + if (mailbox_save_finish(&ctx->save_ctx) < 0) { + client_send_box_error(cmd, ctx->box); + ctx->failed = TRUE; + } + } +} + +static bool catenate_args_can_stop(struct cmd_append_context *ctx, + const struct imap_arg *args) +{ + /* eat away literal_sizes from URLs */ + while (args->type != IMAP_ARG_EOL) { + if (imap_arg_atom_equals(args, "TEXT")) + return TRUE; + if (!imap_arg_atom_equals(args, "URL")) { + /* error - handle it later */ + return TRUE; + } + args++; + if (args->type == IMAP_ARG_LITERAL_SIZE || + args->type == IMAP_ARG_LITERAL_SIZE_NONSYNC) { + if (args->type == IMAP_ARG_LITERAL_SIZE) { + if (!cmd_append_send_literal_continue(ctx)) + return TRUE; + } + imap_parser_read_last_literal(ctx->save_parser); + return FALSE; + } + args++; + } + return TRUE; +} + +static bool cmd_append_continue_catenate(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct cmd_append_context *ctx = cmd->context; + const struct imap_arg *args; + const char *client_error; + enum imap_parser_error parse_error; + bool nonsync = FALSE; + int ret; + + if (cmd->cancel) { + /* cancel the command immediately (disconnection) */ + cmd_append_finish(ctx); + return TRUE; + } + + /* we're parsing inside CATENATE (..) list after handling a TEXT part. + it's fine that this would need to fully fit into input buffer + (although clients attempting to DoS could simply insert an extra + {1+} between the URLs) */ + do { + ret = imap_parser_read_args(ctx->save_parser, 0, + IMAP_PARSE_FLAG_LITERAL_SIZE | + IMAP_PARSE_FLAG_LITERAL8 | + IMAP_PARSE_FLAG_INSIDE_LIST, &args); + } while (ret > 0 && !catenate_args_can_stop(ctx, args)); + if (ret == -1) { + client_error = imap_parser_get_error(ctx->save_parser, + &parse_error); + switch (parse_error) { + case IMAP_PARSE_ERROR_NONE: + i_unreached(); + case IMAP_PARSE_ERROR_LITERAL_TOO_BIG: + client_send_line(client, t_strconcat("* BYE ", + (client->set->imap_literal_minus ? "[TOOBIG] " : ""), + client_error, NULL)); + client_disconnect(client, client_error); + break; + default: + if (!ctx->failed) + client_send_command_error(cmd, client_error); + } + client->input_skip_line = TRUE; + cmd_append_finish(ctx); + return TRUE; + } + if (ret < 0) { + /* need more data */ + return FALSE; + } + + if ((ret = cmd_append_catenate(cmd, args, &nonsync)) < 0) { + /* invalid parameters, abort immediately */ + cmd_append_finish(ctx); + return TRUE; + } + + if (ret == 0) { + /* ")" */ + cmd_append_finish_catenate(cmd); + + /* last catenate part */ + imap_parser_reset(ctx->save_parser); + cmd->func = cmd_append_parse_new_msg; + return cmd_append_parse_new_msg(cmd); + } + + /* TEXT <literal> */ + + if (!nonsync) { + if (!cmd_append_send_literal_continue(ctx)) { + cmd_append_finish(ctx); + return TRUE; + } + } + + i_assert(ctx->litinput != NULL); + ctx->message_input = TRUE; + cmd->func = cmd_append_continue_message; + return cmd_append_continue_message(cmd); +} + +static int +cmd_append_handle_args(struct client_command_context *cmd, + const struct imap_arg *args, bool *nonsync_r) +{ + struct client *client = cmd->client; + struct cmd_append_context *ctx = cmd->context; + const struct imap_arg *flags_list; + const struct imap_arg *cat_list = NULL; + enum mail_flags flags; + const char *const *keywords_list; + struct mail_keywords *keywords; + struct istream *input; + const char *internal_date_str; + time_t internal_date; + int ret, timezone_offset; + bool valid; + + if (!ctx->cmd_args_set) { + ctx->cmd_args_set = TRUE; + client_args_finished(cmd, args); + } + + /* [<flags>] */ + if (!imap_arg_get_list(args, &flags_list)) + flags_list = NULL; + else + args++; + + /* [<internal date>] */ + if (args->type != IMAP_ARG_STRING) + internal_date_str = NULL; + else { + internal_date_str = imap_arg_as_astring(args); + args++; + } + + /* <message literal> | CATENATE (..) */ + valid = FALSE; + *nonsync_r = FALSE; + ctx->catenate = FALSE; + if (imap_arg_get_literal_size(args, &ctx->literal_size)) { + *nonsync_r = args->type == IMAP_ARG_LITERAL_SIZE_NONSYNC; + ctx->binary_input = args->literal8; + valid = TRUE; + } else if (!imap_arg_atom_equals(args, "CATENATE")) { + /* invalid */ + } else if (!imap_arg_get_list(++args, &cat_list)) { + /* invalid */ + } else { + valid = TRUE; + ctx->catenate = TRUE; + /* We'll do BINARY conversion only if the CATENATE's first + part is a literal8. If it doesn't and a literal8 is seen + later we'll abort the append with UNKNOWN-CTE. */ + ctx->binary_input = imap_arg_atom_equals(&cat_list[0], "TEXT") && + cat_list[1].literal8; + + } + if (!IMAP_ARG_IS_EOL(&args[1])) + valid = FALSE; + if (!valid) { + client->input_skip_line = TRUE; + if (!ctx->failed) + client_send_command_error(cmd, "Invalid arguments."); + return -1; + } + + if (flags_list == NULL || ctx->failed) { + flags = 0; + keywords = NULL; + } else { + if (!client_parse_mail_flags(cmd, flags_list, + &flags, &keywords_list)) + return -1; + if (keywords_list == NULL) + keywords = NULL; + else if (mailbox_keywords_create(ctx->box, keywords_list, + &keywords) < 0) { + /* invalid keywords - delay failure */ + client_send_box_error(cmd, ctx->box); + ctx->failed = TRUE; + keywords = NULL; + } + } + + if (internal_date_str == NULL || ctx->failed) { + /* no time given, default to now. */ + internal_date = (time_t)-1; + timezone_offset = 0; + } else if (!imap_parse_datetime(internal_date_str, + &internal_date, &timezone_offset)) { + client_send_command_error(cmd, "Invalid internal date."); + if (keywords != NULL) + mailbox_keywords_unref(&keywords); + return -1; + } + + if (internal_date != (time_t)-1 && + internal_date > ioloop_time + INTERNALDATE_MAX_FUTURE_SECS) { + /* the client specified a time in the future, set it to now. */ + internal_date = (time_t)-1; + timezone_offset = 0; + } + + if (cat_list != NULL) { + ctx->cat_msg_size = 0; + ctx->input = i_stream_create_chain(&ctx->catchain, + IO_BLOCK_SIZE); + } else { + if (ctx->literal_size == 0) { + /* no message data, abort */ + if (!ctx->failed) { + client_send_tagline(cmd, + "NO Can't save a zero byte message."); + ctx->failed = TRUE; + } + if (!*nonsync_r) { + if (keywords != NULL) + mailbox_keywords_unref(&keywords); + return -1; + } + /* {0+} used. although there isn't any point in using + MULTIAPPEND here and adding more messages, it is + technically valid so we'll continue parsing.. */ + } + ctx->litinput = i_stream_create_limit(client->input, ctx->literal_size); + ctx->input = ctx->litinput; + i_stream_ref(ctx->input); + } + if (ctx->binary_input) { + input = i_stream_create_binary_converter(ctx->input); + i_stream_unref(&ctx->input); + ctx->input = input; + } + + if (!ctx->failed) { + /* save the mail */ + ctx->save_ctx = mailbox_save_alloc(ctx->t); + mailbox_save_set_flags(ctx->save_ctx, flags, keywords); + mailbox_save_set_received_date(ctx->save_ctx, + internal_date, timezone_offset); + if (mailbox_save_begin(&ctx->save_ctx, ctx->input) < 0) { + /* save initialization failed */ + client_send_box_error(cmd, ctx->box); + ctx->failed = TRUE; + } + } + if (keywords != NULL) + mailbox_keywords_unref(&keywords); + ctx->count++; + + if (cat_list == NULL) { + /* normal APPEND */ + return 1; + } else if (cat_list->type == IMAP_ARG_EOL) { + /* zero parts */ + if (!ctx->failed) + client_send_command_error(cmd, "Empty CATENATE list."); + client->input_skip_line = TRUE; + return -1; + } else if ((ret = cmd_append_catenate(cmd, cat_list, nonsync_r)) < 0) { + /* invalid parameters, abort immediately */ + return -1; + } else if (ret == 0) { + /* CATENATE consisted only of URLs */ + return 0; + } else { + /* TEXT part found from CATENATE */ + return 1; + } +} + +static bool cmd_append_finish_parsing(struct client_command_context *cmd) +{ + struct cmd_append_context *ctx = cmd->context; + enum mailbox_sync_flags sync_flags; + enum imap_sync_flags imap_flags; + struct mail_transaction_commit_changes changes; + unsigned int save_count; + string_t *msg; + int ret; + + /* eat away the trailing CRLF */ + cmd->client->input_skip_line = TRUE; + + if (ctx->failed) { + /* we failed earlier, error message is sent */ + cmd_append_finish(ctx); + return TRUE; + } + if (ctx->count == 0) { + client_send_command_error(cmd, "Missing message size."); + cmd_append_finish(ctx); + return TRUE; + } + + ret = mailbox_transaction_commit_get_changes(&ctx->t, &changes); + if (ret < 0) { + client_send_box_error(cmd, ctx->box); + cmd_append_finish(ctx); + return TRUE; + } + + msg = t_str_new(256); + save_count = seq_range_count(&changes.saved_uids); + if (save_count == 0 || changes.no_read_perm) { + /* not supported by backend (virtual) */ + str_append(msg, "OK Append completed."); + } else { + i_assert(ctx->count == save_count); + str_printfa(msg, "OK [APPENDUID %u ", + changes.uid_validity); + imap_write_seq_range(msg, &changes.saved_uids); + str_append(msg, "] Append completed."); + } + ctx->client->append_count += save_count; + pool_unref(&changes.pool); + + if (ctx->box == cmd->client->mailbox) { + sync_flags = 0; + imap_flags = IMAP_SYNC_FLAG_SAFE; + } else { + sync_flags = MAILBOX_SYNC_FLAG_FAST; + imap_flags = 0; + } + + cmd_append_finish(ctx); + return cmd_sync(cmd, sync_flags, imap_flags, str_c(msg)); +} + +static bool cmd_append_args_can_stop(struct cmd_append_context *ctx, + const struct imap_arg *args, + bool *last_literal_r) +{ + const struct imap_arg *cat_list; + + *last_literal_r = FALSE; + if (args->type == IMAP_ARG_EOL) + return TRUE; + + /* [(flags)] ["internal date"] <message literal> | CATENATE (..) */ + if (args->type == IMAP_ARG_LIST) + args++; + if (args->type == IMAP_ARG_STRING) + args++; + + if (args->type == IMAP_ARG_LITERAL_SIZE || + args->type == IMAP_ARG_LITERAL_SIZE_NONSYNC) + return TRUE; + if (imap_arg_atom_equals(args, "CATENATE") && + imap_arg_get_list(&args[1], &cat_list)) { + if (catenate_args_can_stop(ctx, cat_list)) + return TRUE; + *last_literal_r = TRUE; + } + return FALSE; +} + +static bool cmd_append_parse_new_msg(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct cmd_append_context *ctx = cmd->context; + const struct imap_arg *args; + const char *client_error; + enum imap_parser_error parse_error; + unsigned int arg_min_count; + bool nonsync, last_literal; + int ret; + + /* this function gets called 1) after parsing APPEND <mailbox> and + 2) with MULTIAPPEND extension after already saving one or more + mails. */ + if (cmd->cancel) { + /* cancel the command immediately (disconnection) */ + cmd_append_finish(ctx); + return TRUE; + } + + /* if error occurs, the CRLF is already read. */ + client->input_skip_line = FALSE; + + /* parse the entire line up to the first message literal, or in case + the input buffer is full of MULTIAPPEND CATENATE URLs, parse at + least until the beginning of the next message */ + arg_min_count = 0; last_literal = FALSE; + do { + if (!last_literal) + arg_min_count++; + else { + /* we only read the literal size. now we read the + literal itself. */ + } + ret = imap_parser_read_args(ctx->save_parser, arg_min_count, + IMAP_PARSE_FLAG_LITERAL_SIZE | + IMAP_PARSE_FLAG_LITERAL8, &args); + } while (ret >= (int)arg_min_count && + !cmd_append_args_can_stop(ctx, args, &last_literal)); + if (ret == -1) { + if (!ctx->failed) { + client_error = imap_parser_get_error(ctx->save_parser, &parse_error); + switch (parse_error) { + case IMAP_PARSE_ERROR_NONE: + i_unreached(); + case IMAP_PARSE_ERROR_LITERAL_TOO_BIG: + client_send_line(client, t_strconcat("* BYE ", + (client->set->imap_literal_minus ? "[TOOBIG] " : ""), + client_error, NULL)); + client_disconnect(client, client_error); + break; + default: + client_send_command_error(cmd, client_error); + } + } + cmd_append_finish(ctx); + return TRUE; + } + if (ret < 0) { + /* need more data */ + return FALSE; + } + + if (IMAP_ARG_IS_EOL(args)) { + /* last message */ + return cmd_append_finish_parsing(cmd); + } + + ret = cmd_append_handle_args(cmd, args, &nonsync); + if (ret < 0) { + /* invalid parameters, abort immediately */ + cmd_append_finish(ctx); + return TRUE; + } + if (ret == 0) { + /* CATENATE contained only URLs. Finish it and see if there + are more messages. */ + cmd_append_finish_catenate(cmd); + imap_parser_reset(ctx->save_parser); + return cmd_append_parse_new_msg(cmd); + } + + if (!nonsync) { + if (!cmd_append_send_literal_continue(ctx)) { + cmd_append_finish(ctx); + return TRUE; + } + } + + i_assert(ctx->litinput != NULL); + ctx->message_input = TRUE; + cmd->func = cmd_append_continue_message; + return cmd_append_continue_message(cmd); +} + +static bool cmd_append_continue_message(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct cmd_append_context *ctx = cmd->context; + int ret = 0; + + if (cmd->cancel) { + /* cancel the command immediately (disconnection) */ + cmd_append_finish(ctx); + return TRUE; + } + + if (ctx->save_ctx != NULL) { + while (ctx->litinput->v_offset != ctx->literal_size) { + ret = i_stream_read(ctx->litinput); + if (mailbox_save_continue(ctx->save_ctx) < 0) { + /* we still have to finish reading the message + from client */ + mailbox_save_cancel(&ctx->save_ctx); + break; + } + if (ret == -1 || ret == 0) + break; + } + } + + if (ctx->save_ctx == NULL) { + /* saving has already failed, we're just eating away the + literal */ + (void)i_stream_read(ctx->litinput); + i_stream_skip(ctx->litinput, + i_stream_get_data_size(ctx->litinput)); + } + + if (ctx->litinput->eof || client->input->closed) { + uoff_t lit_offset = ctx->litinput->v_offset; + + /* finished - do one more read, to make sure istream-chain + unreferences its stream, which is needed for litinput's + unreferencing to seek the client->input to correct + position. the seek is needed to avoid trying to seek + backwards in the ctx->input's parent stream. */ + i_stream_seek(ctx->input, ctx->input->v_offset); + (void)i_stream_read(ctx->input); + i_stream_unref(&ctx->litinput); + + if (ctx->failed) { + if (ctx->save_ctx != NULL) + mailbox_save_cancel(&ctx->save_ctx); + } else if (ctx->save_ctx == NULL) { + /* failed above */ + client_send_box_error(cmd, ctx->box); + ctx->failed = TRUE; + } else if (lit_offset != ctx->literal_size) { + /* client disconnected before it finished sending the + whole message. */ + ctx->failed = TRUE; + mailbox_save_cancel(&ctx->save_ctx); + client_disconnect(client, + get_disconnect_reason(ctx, lit_offset)); + } else if (ctx->catenate) { + /* CATENATE isn't finished yet */ + } else if (mailbox_save_finish(&ctx->save_ctx) < 0) { + client_send_box_error(cmd, ctx->box); + ctx->failed = TRUE; + } + + if (client->input->closed) { + cmd_append_finish(ctx); + return TRUE; + } + + /* prepare for the next message (or its part with catenate) */ + ctx->message_input = FALSE; + imap_parser_reset(ctx->save_parser); + + if (ctx->catenate) { + cmd->func = cmd_append_continue_catenate; + return cmd_append_continue_catenate(cmd); + } + + i_stream_unref(&ctx->input); + cmd->func = cmd_append_parse_new_msg; + return cmd_append_parse_new_msg(cmd); + } + return FALSE; +} + +bool cmd_append(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct cmd_append_context *ctx; + const char *mailbox; + + if (client->syncing) { + /* if transaction is created while its view is synced, + appends aren't allowed for it. */ + cmd->state = CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY; + return FALSE; + } + + /* <mailbox> */ + if (!client_read_string_args(cmd, 1, &mailbox)) + return FALSE; + + /* we keep the input locked all the time */ + client->input_lock = cmd; + + ctx = p_new(cmd->pool, struct cmd_append_context, 1); + ctx->cmd = cmd; + ctx->client = client; + ctx->started = ioloop_time; + if (client_open_save_dest_box(cmd, mailbox, &ctx->box) < 0) + ctx->failed = TRUE; + else { + event_add_str(cmd->global_event, "mailbox", + mailbox_get_vname(ctx->box)); + ctx->t = mailbox_transaction_begin(ctx->box, + MAILBOX_TRANSACTION_FLAG_EXTERNAL | + MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS, + imap_client_command_get_reason(cmd)); + } + + io_remove(&client->io); + client->io = io_add_istream(client->input, client_input_append, cmd); + /* append is special because we're only waiting on client input, not + client output, so disable the standard output handler until we're + finished */ + o_stream_unset_flush_callback(client->output); + + ctx->save_parser = imap_parser_create(client->input, client->output, + client->set->imap_max_line_length); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(ctx->save_parser); + + cmd->func = cmd_append_parse_new_msg; + cmd->context = ctx; + return cmd_append_parse_new_msg(cmd); +} diff --git a/src/imap/cmd-cancelupdate.c b/src/imap/cmd-cancelupdate.c new file mode 100644 index 0000000..8676567 --- /dev/null +++ b/src/imap/cmd-cancelupdate.c @@ -0,0 +1,45 @@ +/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-search.h" +#include "imap-commands.h" + +static bool client_search_update_cancel(struct client *client, const char *tag) +{ + struct imap_search_update *update; + unsigned int idx; + + update = client_search_update_lookup(client, tag, &idx); + if (update == NULL) + return FALSE; + + imap_search_update_free(update); + array_delete(&client->search_updates, idx, 1); + return TRUE; +} + +bool cmd_cancelupdate(struct client_command_context *cmd) +{ + const struct imap_arg *args; + const char *tag; + unsigned int i; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + for (i = 0; args[i].type == IMAP_ARG_STRING; i++) ; + if (!IMAP_ARG_IS_EOL(&args[i]) || i == 0) { + client_send_command_error(cmd, "Invalid parameters."); + return TRUE; + } + + while (imap_arg_get_quoted(args, &tag)) { + if (!client_search_update_cancel(cmd->client, tag)) { + client_send_tagline(cmd, "NO Unknown tag."); + return TRUE; + } + args++; + } + client_send_tagline(cmd, "OK Updates cancelled."); + return TRUE; +} diff --git a/src/imap/cmd-capability.c b/src/imap/cmd-capability.c new file mode 100644 index 0000000..3433c89 --- /dev/null +++ b/src/imap/cmd-capability.c @@ -0,0 +1,14 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-commands.h" +#include "str.h" + +bool cmd_capability(struct client_command_context *cmd) +{ + client_send_line(cmd->client, t_strconcat( + "* CAPABILITY ", str_c(cmd->client->capability_string), NULL)); + + client_send_tagline(cmd, "OK Capability completed."); + return TRUE; +} diff --git a/src/imap/cmd-check.c b/src/imap/cmd-check.c new file mode 100644 index 0000000..a456f7c --- /dev/null +++ b/src/imap/cmd-check.c @@ -0,0 +1,14 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-commands.h" + +bool cmd_check(struct client_command_context *cmd) +{ + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + return cmd_sync(cmd, MAILBOX_SYNC_FLAG_FULL_READ | + MAILBOX_SYNC_FLAG_FULL_WRITE, IMAP_SYNC_FLAG_SAFE, + "OK Check completed."); +} diff --git a/src/imap/cmd-close.c b/src/imap/cmd-close.c new file mode 100644 index 0000000..1f4dfeb --- /dev/null +++ b/src/imap/cmd-close.c @@ -0,0 +1,36 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-commands.h" +#include "imap-expunge.h" + +bool cmd_close(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct mailbox *mailbox = client->mailbox; + struct mail_storage *storage; + const char *errstr, *tagged_reply = "OK Close completed."; + enum mail_error error = MAIL_ERROR_NONE; + + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + i_assert(client->mailbox_change_lock == NULL); + + storage = mailbox_get_storage(mailbox); + if (imap_expunge(mailbox, NULL, &client->expunged_count) < 0) { + errstr = mailbox_get_last_error(mailbox, &error); + if (error != MAIL_ERROR_PERM) + client_send_untagged_storage_error(client, storage); + else { + tagged_reply = t_strdup_printf( + "OK Closed without expunging: %s", errstr); + } + } + if (mailbox_sync(mailbox, 0) < 0) + client_send_untagged_storage_error(client, storage); + + imap_client_close_mailbox(client); + client_send_tagline(cmd, tagged_reply); + return TRUE; +} diff --git a/src/imap/cmd-copy.c b/src/imap/cmd-copy.c new file mode 100644 index 0000000..1ce018b --- /dev/null +++ b/src/imap/cmd-copy.c @@ -0,0 +1,407 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "str.h" +#include "istream.h" +#include "ostream.h" +#include "imap-resp-code.h" +#include "imap-util.h" +#include "imap-commands.h" +#include "imap-search-args.h" + +#include <time.h> + +#define COPY_CHECK_INTERVAL 100 +#define MOVE_COMMIT_INTERVAL 1000 + +struct cmd_copy_context { + struct client_command_context *cmd; + struct mailbox *srcbox; + struct mailbox *destbox; + bool move; + + unsigned int copy_count; + + uint32_t uid_validity; + ARRAY_TYPE(seq_range) src_uids; + ARRAY_TYPE(seq_range) saved_uids; + bool hide_saved_uids; + + const char *error_string; + enum mail_error mail_error; +}; + +static int client_send_sendalive_if_needed(struct client *client) +{ + time_t now, last_io; + int ret = 0; + + if (o_stream_get_buffer_used_size(client->output) != 0) + return 0; + + now = time(NULL); + last_io = I_MAX(client->last_input, client->last_output); + if (now - last_io > MAIL_STORAGE_STAYALIVE_SECS) { + o_stream_nsend_str(client->output, "* OK Hang in there..\r\n"); + /* make sure it doesn't get stuck on the corked stream */ + if (o_stream_uncork_flush(client->output) < 0) + ret = -1; + o_stream_cork(client->output); + client->last_output = now; + } + return ret; +} + +static void copy_update_trashed(struct client *client, struct mailbox *box, + unsigned int count) +{ + const struct mailbox_settings *set; + + set = mailbox_settings_find(mailbox_get_namespace(box), + mailbox_get_vname(box)); + if (set != NULL && set->special_use[0] != '\0' && + str_array_icase_find(t_strsplit_spaces(set->special_use, " "), + "\\Trash")) + client->trashed_count += count; +} + +static bool client_is_disconnected(struct client *client) +{ + if (client->fd_in == STDIN_FILENO) { + /* Skip this check for stdio clients. It's often used in + testing where the test expects that all commands will be + run even though stdin already has reached EOF. */ + return FALSE; + } + ssize_t bytes = i_stream_read(client->input); + if (bytes == -1) + return TRUE; + if (bytes != 0) + i_stream_set_input_pending(client->input, TRUE); + return FALSE; +} + +static int fetch_and_copy(struct cmd_copy_context *copy_ctx, + const struct mail_search_args *uid_search_args) +{ + struct client *client = copy_ctx->cmd->client; + struct mailbox_transaction_context *t, *src_trans; + struct mail_search_args *search_args; + struct mail_search_context *search_ctx; + struct mail_save_context *save_ctx; + struct mail *mail; + const char *cmd_reason; + struct mail_transaction_commit_changes changes; + ARRAY_TYPE(seq_range) src_uids; + int ret; + + /* convert uidset to seqset */ + search_args = mail_search_args_dup(uid_search_args); + mail_search_args_init(search_args, copy_ctx->srcbox, TRUE, NULL); + /* make sure the number of messages didn't already change */ + i_assert(uid_search_args->args->type == SEARCH_UIDSET); + i_assert(search_args->args->type == SEARCH_SEQSET || + (search_args->args->type == SEARCH_ALL && + search_args->args->match_not)); + if (search_args->args->type != SEARCH_SEQSET || + seq_range_count(&search_args->args->value.seqset) != + seq_range_count(&uid_search_args->args->value.seqset)) { + mail_search_args_unref(&search_args); + return 0; + } + + i_assert(o_stream_is_corked(client->output) || + client->output->stream_errno != 0); + + cmd_reason = imap_client_command_get_reason(copy_ctx->cmd); + t = mailbox_transaction_begin(copy_ctx->destbox, + MAILBOX_TRANSACTION_FLAG_EXTERNAL | + MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS, + cmd_reason); + /* Refresh source index so expunged mails will be noticed */ + src_trans = mailbox_transaction_begin(copy_ctx->srcbox, + MAILBOX_TRANSACTION_FLAG_REFRESH, + cmd_reason); + search_ctx = mailbox_search_init(src_trans, search_args, + NULL, 0, NULL); + mail_search_args_unref(&search_args); + + t_array_init(&src_uids, 64); + ret = 1; + while (mailbox_search_next(search_ctx, &mail) && ret > 0) { + if (mail->expunged) { + ret = 0; + break; + } + + if ((++copy_ctx->copy_count % COPY_CHECK_INTERVAL) == 0) { + /* If we're COPYing (not MOVEing), check if client has + already disconnected. If yes, abort the COPY to + avoid client duplicating the COPY again later. + We can detect this as long as the client doesn't + fill the input buffer full. */ + if (client_send_sendalive_if_needed(client) < 0 || + (!copy_ctx->move && + client_is_disconnected(client))) { + /* Client disconnected. Use the same failure + code path as if some messages were + expunged. */ + ret = 0; + break; + } + } + + save_ctx = mailbox_save_alloc(t); + mailbox_save_copy_flags(save_ctx, mail); + + if (copy_ctx->move) { + if (mailbox_move(&save_ctx, mail) < 0) + ret = -1; + } else { + if (mailbox_copy(&save_ctx, mail) < 0) + ret = -1; + } + if (ret < 0 && mail->expunged) + ret = 0; + + if (ret > 0) + seq_range_array_add(&src_uids, mail->uid); + } + + if (ret < 0) { + copy_ctx->error_string = + mailbox_get_last_error(copy_ctx->destbox, ©_ctx->mail_error); + } + if (mailbox_search_deinit(&search_ctx) < 0 && ret >= 0) { + copy_ctx->error_string = + mailbox_get_last_error(copy_ctx->srcbox, ©_ctx->mail_error); + ret = -1; + } + + /* Do a final check before committing COPY to see if the client has + already disconnected. */ + if (!copy_ctx->move && client_is_disconnected(client)) + ret = 0; + + if (ret <= 0) + mailbox_transaction_rollback(&t); + else if (mailbox_transaction_commit_get_changes(&t, &changes) < 0) { + if (mailbox_get_last_mail_error(copy_ctx->destbox) == MAIL_ERROR_EXPUNGED) { + /* storage backend didn't notice the expunge until + at commit time. */ + ret = 0; + } else { + ret = -1; + copy_ctx->error_string = + mailbox_get_last_error(copy_ctx->destbox, ©_ctx->mail_error); + } + } else { + if (changes.no_read_perm) + copy_ctx->hide_saved_uids = TRUE; + + if (seq_range_count(&changes.saved_uids) == 0) { + /* storage doesn't support returning UIDs */ + copy_ctx->hide_saved_uids = TRUE; + } + + if (copy_ctx->uid_validity == 0) + copy_ctx->uid_validity = changes.uid_validity; + else if (copy_ctx->uid_validity != changes.uid_validity) { + /* UIDVALIDITY unexpectedly changed */ + copy_ctx->hide_saved_uids = TRUE; + } + seq_range_array_merge(©_ctx->src_uids, &src_uids); + seq_range_array_merge(©_ctx->saved_uids, &changes.saved_uids); + + i_assert(copy_ctx->copy_count == seq_range_count(©_ctx->saved_uids) || + copy_ctx->hide_saved_uids); + copy_update_trashed(client, copy_ctx->destbox, copy_ctx->copy_count); + pool_unref(&changes.pool); + } + + if (!copy_ctx->move || + copy_ctx->srcbox == copy_ctx->destbox) { + /* copying or moving within the same mailbox + succeeded or failed */ + if (mailbox_transaction_commit(&src_trans) < 0 && ret >= 0) { + copy_ctx->error_string = + mailbox_get_last_error(copy_ctx->srcbox, ©_ctx->mail_error); + ret = -1; + } + } else if (ret <= 0) { + /* move failed, don't expunge anything */ + mailbox_transaction_rollback(&src_trans); + } else { + /* move succeeded */ + if (mailbox_transaction_commit(&src_trans) < 0 || + mailbox_sync(copy_ctx->srcbox, + MAILBOX_SYNC_FLAG_EXPUNGE) < 0) { + copy_ctx->error_string = + mailbox_get_last_error(copy_ctx->srcbox, ©_ctx->mail_error); + ret = -1; + } + } + return ret; +} + +static void cmd_move_send_untagged(struct cmd_copy_context *copy_ctx, + string_t *msg, string_t *src_uidset) +{ + if (array_count(©_ctx->saved_uids) == 0) + return; + str_printfa(msg, "* OK [COPYUID %u %s ", + copy_ctx->uid_validity, str_c(src_uidset)); + imap_write_seq_range(msg, ©_ctx->saved_uids); + str_append(msg, "] Moved UIDs."); + client_send_line(copy_ctx->cmd->client, str_c(msg)); +} + +static bool cmd_copy_full(struct client_command_context *cmd, bool move) +{ + struct client *client = cmd->client; + struct mailbox *destbox; + struct mail_search_args *search_args; + struct imap_search_seqset_iter *seqset_iter = NULL; + const char *messageset, *mailbox; + enum mailbox_sync_flags sync_flags = 0; + enum imap_sync_flags imap_flags = 0; + struct cmd_copy_context copy_ctx; + string_t *msg, *src_uidset; + int ret; + + /* <message set> <mailbox> */ + if (!client_read_string_args(cmd, 2, &messageset, &mailbox)) + return FALSE; + + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + /* First convert the message set to sequences. This way nonexistent + UIDs are dropped. */ + ret = imap_search_get_seqset(cmd, messageset, cmd->uid, &search_args); + if (ret <= 0) + return ret < 0; + if (search_args->args->type == SEARCH_ALL) { + i_assert(search_args->args->match_not); + mail_search_args_unref(&search_args); + return cmd_sync(cmd, sync_flags, imap_flags, + "OK No messages found."); + } + /* Convert seqset to uidset. This is required for MOVE to work + correctly, since it opens another view for the source mailbox + that can have different sequences. */ + imap_search_anyset_to_uidset(cmd, search_args); + + if (client_open_save_dest_box(cmd, mailbox, &destbox) < 0) { + mail_search_args_unref(&search_args); + return TRUE; + } + + i_zero(©_ctx); + copy_ctx.cmd = cmd; + copy_ctx.destbox = destbox; + if (destbox == client->mailbox || !move) + copy_ctx.srcbox = client->mailbox; + else { + copy_ctx.srcbox = mailbox_alloc(mailbox_get_namespace(client->mailbox)->list, + mailbox_get_vname(client->mailbox), 0); + if (mailbox_sync(copy_ctx.srcbox, 0) < 0) { + mail_search_args_unref(&search_args); + client_send_box_error(cmd, copy_ctx.srcbox); + mailbox_free(©_ctx.srcbox); + mailbox_free(&destbox); + return TRUE; + } + } + copy_ctx.move = move; + i_array_init(©_ctx.src_uids, 8); + i_array_init(©_ctx.saved_uids, 8); + + if (move) { + /* When moving mails, perform the work in batches of + MOVE_COMMIT_INTERVAL. Each such batch has its own + transaction and search query. */ + seqset_iter = imap_search_seqset_iter_init(search_args, + client->messages_count, MOVE_COMMIT_INTERVAL); + } + do { + T_BEGIN { + ret = fetch_and_copy(©_ctx, search_args); + } T_END; + if (ret <= 0) { + /* failed */ + break; + } + } while (seqset_iter != NULL && + imap_search_seqset_iter_next(seqset_iter)); + imap_search_seqset_iter_deinit(&seqset_iter); + mail_search_args_unref(&search_args); + + src_uidset = t_str_new(256); + imap_write_seq_range(src_uidset, ©_ctx.src_uids); + + msg = t_str_new(256); + if (ret <= 0) { + if (move && array_count(©_ctx.src_uids) > 0) { + /* some of the messages were successfully moved */ + cmd_move_send_untagged(©_ctx, msg, src_uidset); + } + } else if (copy_ctx.copy_count == 0) { + str_append(msg, "OK No messages found."); + } else if (seq_range_count(©_ctx.saved_uids) == 0 || + copy_ctx.hide_saved_uids) { + /* not supported by backend (virtual) or no read permissions + for mailbox */ + str_append(msg, move ? "OK Move completed." : + "OK Copy completed."); + } else if (move) { + cmd_move_send_untagged(©_ctx, msg, src_uidset); + str_truncate(msg, 0); + str_append(msg, "OK Move completed."); + } else { + str_printfa(msg, "OK [COPYUID %u %s ", copy_ctx.uid_validity, + str_c(src_uidset)); + imap_write_seq_range(msg, ©_ctx.saved_uids); + str_append(msg, "] Copy completed."); + } + + array_free(©_ctx.src_uids); + array_free(©_ctx.saved_uids); + + if (destbox != client->mailbox) { + if (move) + sync_flags |= MAILBOX_SYNC_FLAG_EXPUNGE; + else + sync_flags |= MAILBOX_SYNC_FLAG_FAST; + imap_flags |= IMAP_SYNC_FLAG_SAFE; + mailbox_free(&destbox); + } else if (move) { + sync_flags |= MAILBOX_SYNC_FLAG_EXPUNGE; + imap_flags |= IMAP_SYNC_FLAG_SAFE; + } + if (copy_ctx.srcbox != client->mailbox) + mailbox_free(©_ctx.srcbox); + + if (ret > 0) + return cmd_sync(cmd, sync_flags, imap_flags, str_c(msg)); + else if (ret == 0) { + /* some messages were expunged, sync them */ + return cmd_sync(cmd, 0, 0, + "NO ["IMAP_RESP_CODE_EXPUNGEISSUED"] " + "Some of the requested messages no longer exist."); + } else { + client_send_error(cmd, copy_ctx.error_string, + copy_ctx.mail_error); + return TRUE; + } +} + +bool cmd_copy(struct client_command_context *cmd) +{ + return cmd_copy_full(cmd, FALSE); +} + +bool cmd_move(struct client_command_context *cmd) +{ + return cmd_copy_full(cmd, TRUE); +} diff --git a/src/imap/cmd-create.c b/src/imap/cmd-create.c new file mode 100644 index 0000000..2459173 --- /dev/null +++ b/src/imap/cmd-create.c @@ -0,0 +1,48 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-resp-code.h" +#include "mail-namespace.h" +#include "imap-commands.h" + +bool cmd_create(struct client_command_context *cmd) +{ + struct mail_namespace *ns; + const char *mailbox, *orig_mailbox; + struct mailbox *box; + bool directory; + size_t len; + + /* <mailbox> */ + if (!client_read_string_args(cmd, 1, &mailbox)) + return FALSE; + + orig_mailbox = mailbox; + ns = client_find_namespace(cmd, &mailbox); + if (ns == NULL) + return TRUE; + + len = strlen(orig_mailbox); + if (len == 0 || orig_mailbox[len-1] != mail_namespace_get_sep(ns)) + directory = FALSE; + else { + /* name ends with hierarchy separator - client is just + informing us that it wants to create children under this + mailbox. */ + directory = TRUE; + + /* drop separator from mailbox. it's already dropped when + WORKAROUND_TB_EXTRA_MAILBOX_SEP is enabled */ + if (len == strlen(mailbox)) + mailbox = t_strndup(mailbox, len-1); + } + + box = mailbox_alloc(ns->list, mailbox, 0); + event_add_str(cmd->global_event, "mailbox", mailbox_get_vname(box)); + if (mailbox_create(box, NULL, directory) < 0) + client_send_box_error(cmd, box); + else + client_send_tagline(cmd, "OK Create completed."); + mailbox_free(&box); + return TRUE; +} diff --git a/src/imap/cmd-delete.c b/src/imap/cmd-delete.c new file mode 100644 index 0000000..e095321 --- /dev/null +++ b/src/imap/cmd-delete.c @@ -0,0 +1,59 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-commands.h" + +bool cmd_delete(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct mail_namespace *ns; + struct mailbox *box; + const char *name, *client_error; + enum mail_error error; + bool disconnect = FALSE; + + /* <mailbox> */ + if (!client_read_string_args(cmd, 1, &name)) + return FALSE; + + ns = client_find_namespace(cmd, &name); + if (ns == NULL) + return TRUE; + + box = mailbox_alloc(ns->list, name, 0); + event_add_str(cmd->global_event, "mailbox", mailbox_get_vname(box)); + if (mailbox_is_any_inbox(box)) { + /* IMAP protocol allows this, but I think it's safer to + not allow it. */ + mailbox_free(&box); + client_send_tagline(cmd, "NO INBOX can't be deleted."); + return TRUE; + } + if (client->mailbox != NULL && + mailbox_backends_equal(box, client->mailbox)) { + /* deleting selected mailbox. close it first */ + client_search_updates_free(client); + mailbox_free(&client->mailbox); + disconnect = TRUE; + } + + if (mailbox_delete(box) == 0) + client_send_tagline(cmd, "OK Delete completed."); + else { + client_error = mailbox_get_last_error(box, &error); + if (error != MAIL_ERROR_EXISTS) + client_send_box_error(cmd, box); + else { + /* mailbox has children */ + client_send_tagline(cmd, t_strdup_printf("NO %s", + client_error)); + } + } + mailbox_free(&box); + + if (disconnect) { + client_disconnect_with_error(cmd->client, + "Selected mailbox was deleted, have to disconnect."); + } + return TRUE; +} diff --git a/src/imap/cmd-enable.c b/src/imap/cmd-enable.c new file mode 100644 index 0000000..904d6db --- /dev/null +++ b/src/imap/cmd-enable.c @@ -0,0 +1,35 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "str.h" +#include "imap-feature.h" + +bool cmd_enable(struct client_command_context *cmd) +{ + const struct imap_arg *args; + const char *str; + string_t *reply; + unsigned int feature_idx; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + reply = t_str_new(64); + str_append(reply, "* ENABLED"); + for (; !IMAP_ARG_IS_EOL(args); args++) { + if (!imap_arg_get_atom(args, &str)) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + if (imap_feature_lookup(str, &feature_idx)) { + client_enable(cmd->client, feature_idx); + str_append_c(reply, ' '); + str_append(reply, t_str_ucase(str)); + } + } + if (str_len(reply) > 9) + client_send_line(cmd->client, str_c(reply)); + client_send_tagline(cmd, "OK Enabled."); + return TRUE; +} + diff --git a/src/imap/cmd-examine.c b/src/imap/cmd-examine.c new file mode 100644 index 0000000..4000fa0 --- /dev/null +++ b/src/imap/cmd-examine.c @@ -0,0 +1,9 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-commands.h" + +bool cmd_examine(struct client_command_context *cmd) +{ + return cmd_select_full(cmd, TRUE); +} diff --git a/src/imap/cmd-expunge.c b/src/imap/cmd-expunge.c new file mode 100644 index 0000000..7388937 --- /dev/null +++ b/src/imap/cmd-expunge.c @@ -0,0 +1,68 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-commands.h" +#include "imap-search-args.h" +#include "imap-expunge.h" + +static bool ATTR_NULL(2) +cmd_expunge_finish(struct client_command_context *cmd, + struct mail_search_args *search_args) +{ + struct client *client = cmd->client; + const char *errstr; + enum mail_error error = MAIL_ERROR_NONE; + int ret; + + ret = imap_expunge(client->mailbox, search_args == NULL ? NULL : + search_args->args, &client->expunged_count); + if (search_args != NULL) + mail_search_args_unref(&search_args); + if (ret < 0) { + errstr = mailbox_get_last_error(client->mailbox, &error); + if (error != MAIL_ERROR_PERM) { + client_send_box_error(cmd, client->mailbox); + return TRUE; + } else { + return cmd_sync(cmd, 0, IMAP_SYNC_FLAG_SAFE, + t_strdup_printf("OK Expunge ignored: %s.", + errstr)); + } + } + + client->sync_seen_deletes = FALSE; + return cmd_sync(cmd, MAILBOX_SYNC_FLAG_EXPUNGE, + IMAP_SYNC_FLAG_SAFE, "OK Expunge completed."); +} + +bool cmd_uid_expunge(struct client_command_context *cmd) +{ + const struct imap_arg *args; + struct mail_search_args *search_args; + const char *uidset; + int ret; + + if (!client_read_args(cmd, 1, 0, &args)) + return FALSE; + + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + if (!imap_arg_get_astring(&args[0], &uidset)) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + + ret = imap_search_get_seqset(cmd, uidset, TRUE, &search_args); + if (ret <= 0) + return ret < 0; + return cmd_expunge_finish(cmd, search_args); +} + +bool cmd_expunge(struct client_command_context *cmd) +{ + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + return cmd_expunge_finish(cmd, NULL); +} diff --git a/src/imap/cmd-fetch.c b/src/imap/cmd-fetch.c new file mode 100644 index 0000000..35e85d1 --- /dev/null +++ b/src/imap/cmd-fetch.c @@ -0,0 +1,391 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "ostream.h" +#include "imap-resp-code.h" +#include "imap-commands.h" +#include "imap-fetch.h" +#include "imap-search-args.h" +#include "mail-search.h" + + +static const char *all_macro[] = { + "FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE", NULL +}; +static const char *fast_macro[] = { + "FLAGS", "INTERNALDATE", "RFC822.SIZE", NULL +}; +static const char *full_macro[] = { + "FLAGS", "INTERNALDATE", "RFC822.SIZE", "ENVELOPE", "BODY", NULL +}; + +static bool +imap_fetch_cmd_init_handler(struct imap_fetch_context *ctx, + struct client_command_context *cmd, + const char *name, const struct imap_arg **args) +{ + struct imap_fetch_init_context init_ctx; + + i_zero(&init_ctx); + init_ctx.fetch_ctx = ctx; + init_ctx.pool = ctx->ctx_pool; + init_ctx.name = name; + init_ctx.args = *args; + + if (!imap_fetch_init_handler(&init_ctx)) { + i_assert(init_ctx.error != NULL); + client_send_command_error(cmd, init_ctx.error); + return FALSE; + } + *args = init_ctx.args; + return TRUE; +} + +static bool +fetch_parse_args(struct imap_fetch_context *ctx, + struct client_command_context *cmd, + const struct imap_arg *arg, const struct imap_arg **next_arg_r) +{ + const char *str, *const *macro; + + if (cmd->uid) { + if (!imap_fetch_cmd_init_handler(ctx, cmd, "UID", &arg)) + return FALSE; + } + if (imap_arg_get_atom(arg, &str)) { + str = t_str_ucase(str); + arg++; + + /* handle macros first */ + if (strcmp(str, "ALL") == 0) + macro = all_macro; + else if (strcmp(str, "FAST") == 0) + macro = fast_macro; + else if (strcmp(str, "FULL") == 0) + macro = full_macro; + else { + macro = NULL; + if (!imap_fetch_cmd_init_handler(ctx, cmd, str, &arg)) + return FALSE; + } + if (macro != NULL) { + while (*macro != NULL) { + if (!imap_fetch_cmd_init_handler(ctx, cmd, *macro, &arg)) + return FALSE; + macro++; + } + } + *next_arg_r = arg; + } else { + *next_arg_r = arg + 1; + arg = imap_arg_as_list(arg); + if (IMAP_ARG_IS_EOL(arg)) { + client_send_command_error(cmd, + "FETCH list is empty."); + return FALSE; + } + while (imap_arg_get_atom(arg, &str)) { + str = t_str_ucase(str); + arg++; + if (!imap_fetch_cmd_init_handler(ctx, cmd, str, &arg)) + return FALSE; + } + if (!IMAP_ARG_IS_EOL(arg)) { + client_send_command_error(cmd, + "FETCH list contains non-atoms."); + return FALSE; + } + } + return TRUE; +} + +static bool +fetch_parse_modifier(struct imap_fetch_context *ctx, + struct client_command_context *cmd, + struct mail_search_args *search_args, + const char *name, const struct imap_arg **args, + bool *send_vanished) +{ + const char *str; + uint64_t modseq; + + if (strcmp(name, "CHANGEDSINCE") == 0) { + if (cmd->client->nonpermanent_modseqs) { + client_send_command_error(cmd, + "FETCH CHANGEDSINCE can't be used with non-permanent modseqs"); + return FALSE; + } + if (!imap_arg_get_atom(*args, &str) || + str_to_uint64(str, &modseq) < 0) { + client_send_command_error(cmd, + "Invalid CHANGEDSINCE modseq."); + return FALSE; + } + *args += 1; + imap_search_add_changed_since(search_args, modseq); + imap_fetch_init_nofail_handler(ctx, imap_fetch_modseq_init); + return TRUE; + } + if (strcmp(name, "VANISHED") == 0 && cmd->uid) { + if (!client_has_enabled(ctx->client, imap_feature_qresync)) { + client_send_command_error(cmd, "QRESYNC not enabled"); + return FALSE; + } + *send_vanished = TRUE; + return TRUE; + } + + client_send_command_error(cmd, "Unknown FETCH modifier"); + return FALSE; +} + +static bool +fetch_parse_modifiers(struct imap_fetch_context *ctx, + struct client_command_context *cmd, + struct mail_search_args *search_args, + const struct imap_arg *args, bool *send_vanished_r) +{ + const char *name; + + *send_vanished_r = FALSE; + + while (!IMAP_ARG_IS_EOL(args)) { + if (!imap_arg_get_atom(args, &name)) { + client_send_command_error(cmd, + "FETCH modifiers contain non-atoms."); + return FALSE; + } + args++; + if (!fetch_parse_modifier(ctx, cmd, search_args, + t_str_ucase(name), + &args, send_vanished_r)) + return FALSE; + } + if (*send_vanished_r && + (search_args->args->next == NULL || + search_args->args->next->type != SEARCH_MODSEQ)) { + client_send_command_error(cmd, + "VANISHED used without CHANGEDSINCE"); + return FALSE; + } + return TRUE; +} + +static bool cmd_fetch_finished(struct client_command_context *cmd ATTR_UNUSED) +{ + return TRUE; +} + +static bool imap_fetch_is_failed_retry(struct imap_fetch_context *ctx) +{ + if (!array_is_created(&ctx->client->fetch_failed_uids) || + !array_is_created(&ctx->fetch_failed_uids)) + return FALSE; + return seq_range_array_have_common(&ctx->client->fetch_failed_uids, + &ctx->fetch_failed_uids); + +} + +static void imap_fetch_add_failed_uids(struct imap_fetch_context *ctx) +{ + if (!array_is_created(&ctx->fetch_failed_uids)) + return; + if (!array_is_created(&ctx->client->fetch_failed_uids)) { + p_array_init(&ctx->client->fetch_failed_uids, ctx->client->pool, + array_count(&ctx->fetch_failed_uids)); + } + seq_range_array_merge(&ctx->client->fetch_failed_uids, + &ctx->fetch_failed_uids); +} + +static bool cmd_fetch_finish(struct imap_fetch_context *ctx, + struct client_command_context *cmd) +{ + static const char *ok_message = "OK Fetch completed."; + const char *tagged_reply = ok_message; + enum mail_error error; + bool seen_flags_changed = ctx->state.seen_flags_changed; + + if (ctx->state.skipped_expunged_msgs) { + tagged_reply = "OK ["IMAP_RESP_CODE_EXPUNGEISSUED"] " + "Some messages were already expunged."; + } + + if (imap_fetch_end(ctx) < 0) { + const char *client_error; + + if (cmd->client->output->closed) { + /* If we're canceling we need to finish this command + or we'll assert crash. But normally we want to + return FALSE so that the disconnect message logs + about this fetch command and that these latest + output bytes are included in it (which wouldn't + happen if we called client_disconnect() here + directly). */ + cmd->func = cmd_fetch_finished; + imap_fetch_free(&ctx); + return cmd->cancel; + } + + if (ctx->error == MAIL_ERROR_NONE) + client_error = mailbox_get_last_error(cmd->client->mailbox, &error); + else { + client_error = ctx->errstr; + error = ctx->error; + } + if (error == MAIL_ERROR_CONVERSION) { + /* BINARY found unsupported Content-Transfer-Encoding */ + tagged_reply = t_strdup_printf( + "NO ["IMAP_RESP_CODE_UNKNOWN_CTE"] %s", client_error); + } else if (error == MAIL_ERROR_INVALIDDATA) { + /* Content was invalid */ + tagged_reply = t_strdup_printf( + "NO ["IMAP_RESP_CODE_PARSE"] %s", client_error); + } else if (cmd->client->set->parsed_fetch_failure != IMAP_CLIENT_FETCH_FAILURE_NO_AFTER || + imap_fetch_is_failed_retry(ctx)) { + /* By default we never want to reply NO to FETCH + requests, because many IMAP clients become confused + about what they should on NO. A disconnection causes + less confusion. */ + const char *internal_error = + mailbox_get_last_internal_error(cmd->client->mailbox, NULL); + client_send_line(cmd->client, t_strconcat( + "* BYE FETCH failed: ", client_error, NULL)); + client_disconnect(cmd->client, t_strconcat( + "FETCH failed: ", internal_error, NULL)); + imap_fetch_free(&ctx); + return TRUE; + } else { + /* Use a tagged NO to FETCH failure, but only if client + hasn't repeated the FETCH to the same email (so that + we avoid infinitely retries from client.) */ + imap_fetch_add_failed_uids(ctx); + tagged_reply = t_strdup_printf( + "NO ["IMAP_RESP_CODE_SERVERBUG"] %s", client_error); + } + } + imap_fetch_free(&ctx); + + return cmd_sync(cmd, + (seen_flags_changed ? 0 : MAILBOX_SYNC_FLAG_FAST) | + (cmd->uid ? 0 : MAILBOX_SYNC_FLAG_NO_EXPUNGES), 0, + tagged_reply); +} + +static bool cmd_fetch_continue(struct client_command_context *cmd) +{ + struct imap_fetch_context *ctx = cmd->context; + + if (imap_fetch_more(ctx, cmd) == 0) { + /* unfinished */ + return FALSE; + } + return cmd_fetch_finish(ctx, cmd); +} + +static void cmd_fetch_set_reason_codes(struct client_command_context *cmd, + struct imap_fetch_context *ctx) +{ + /* Fetching body or header always causes the message to be opened. + Use them as the primary reason. */ + if ((ctx->fetch_data & MAIL_FETCH_STREAM_BODY) != 0) { + event_strlist_append(cmd->global_event, EVENT_REASON_CODE, + "imap:fetch_body"); + return; + } + if ((ctx->fetch_data & MAIL_FETCH_STREAM_HEADER) != 0) { + event_strlist_append(cmd->global_event, EVENT_REASON_CODE, + "imap:fetch_header"); + return; + } + + /* The rest of these can come from cache. Since especially with + prefetching we can't really know which one of them specifically + triggered opening the mail, just use all of them as the reasons. */ + if (ctx->fetch_header_fields || + HAS_ANY_BITS(ctx->fetch_data, + MAIL_FETCH_IMAP_ENVELOPE | + MAIL_FETCH_DATE)) { + event_strlist_append(cmd->global_event, EVENT_REASON_CODE, + "imap:fetch_header_fields"); + } + if (HAS_ANY_BITS(ctx->fetch_data, + MAIL_FETCH_IMAP_BODY | + MAIL_FETCH_IMAP_BODYSTRUCTURE)) { + event_strlist_append(cmd->global_event, EVENT_REASON_CODE, + "imap:fetch_bodystructure"); + } + if (HAS_ANY_BITS(ctx->fetch_data, + MAIL_FETCH_PHYSICAL_SIZE | + MAIL_FETCH_VIRTUAL_SIZE)) { + event_strlist_append(cmd->global_event, EVENT_REASON_CODE, + "imap:fetch_size"); + } +} + +bool cmd_fetch(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct imap_fetch_context *ctx; + const struct imap_arg *args, *next_arg, *list_arg; + struct mail_search_args *search_args; + struct imap_fetch_qresync_args qresync_args; + const char *messageset; + bool send_vanished = FALSE; + int ret; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + /* <messageset> <field(s)> [(modifiers)] */ + if (!imap_arg_get_atom(&args[0], &messageset) || + (args[1].type != IMAP_ARG_LIST && args[1].type != IMAP_ARG_ATOM) || + (!IMAP_ARG_IS_EOL(&args[2]) && args[2].type != IMAP_ARG_LIST)) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + + /* UID FETCH VANISHED needs the uidset, so convert it to + sequence set later */ + ret = imap_search_get_anyset(cmd, messageset, cmd->uid, &search_args); + if (ret <= 0) + return ret < 0; + + ctx = imap_fetch_alloc(client, cmd->pool, + imap_client_command_get_reason(cmd)); + + if (!fetch_parse_args(ctx, cmd, &args[1], &next_arg) || + (imap_arg_get_list(next_arg, &list_arg) && + !fetch_parse_modifiers(ctx, cmd, search_args, list_arg, + &send_vanished))) { + imap_fetch_free(&ctx); + mail_search_args_unref(&search_args); + return TRUE; + } + + if (send_vanished) { + i_zero(&qresync_args); + if (imap_fetch_send_vanished(client, client->mailbox, + search_args, &qresync_args) < 0) { + mail_search_args_unref(&search_args); + return cmd_fetch_finish(ctx, cmd); + } + } + + cmd_fetch_set_reason_codes(cmd, ctx); + imap_fetch_begin(ctx, client->mailbox, search_args); + mail_search_args_unref(&search_args); + + if (imap_fetch_more(ctx, cmd) == 0) { + /* unfinished */ + cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT; + + cmd->func = cmd_fetch_continue; + cmd->context = ctx; + return FALSE; + } + return cmd_fetch_finish(ctx, cmd); +} diff --git a/src/imap/cmd-genurlauth.c b/src/imap/cmd-genurlauth.c new file mode 100644 index 0000000..2cdbdd1 --- /dev/null +++ b/src/imap/cmd-genurlauth.c @@ -0,0 +1,54 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "str.h" +#include "imap-commands.h" +#include "imap-quote.h" +#include "imap-urlauth.h" + +bool cmd_genurlauth(struct client_command_context *cmd) +{ + const struct imap_arg *args; + string_t *response; + int ret; + + if (cmd->client->urlauth_ctx == NULL) { + client_send_command_error(cmd, "URLAUTH disabled."); + return TRUE; + } + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + response = t_str_new(1024); + str_append(response, "* GENURLAUTH"); + for (;;) { + const char *url_rump, *mechanism, *url, *client_error; + + if (IMAP_ARG_IS_EOL(args)) + break; + if (!imap_arg_get_astring(args++, &url_rump) || + !imap_arg_get_atom(args++, &mechanism)) { + client_send_command_error(cmd, "Invalid arguments."); + return FALSE; + } + + ret = imap_urlauth_generate(cmd->client->urlauth_ctx, + mechanism, url_rump, &url, + &client_error); + if (ret <= 0) { + if (ret < 0) + client_send_internal_error(cmd); + else + client_send_command_error(cmd, client_error); + return TRUE; + } + + str_append_c(response, ' '); + imap_append_astring(response, url); + } + + client_send_line(cmd->client, str_c(response)); + client_send_tagline(cmd, "OK GENURLAUTH completed."); + return TRUE; +} diff --git a/src/imap/cmd-getmetadata.c b/src/imap/cmd-getmetadata.c new file mode 100644 index 0000000..ec23135 --- /dev/null +++ b/src/imap/cmd-getmetadata.c @@ -0,0 +1,559 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "mail-storage-private.h" +#include "str.h" +#include "istream.h" +#include "istream-sized.h" +#include "ostream.h" +#include "mailbox-list-iter.h" +#include "imap-utf7.h" +#include "imap-quote.h" +#include "imap-metadata.h" + +struct imap_getmetadata_context { + struct client_command_context *cmd; + + struct mailbox *box; + struct imap_metadata_transaction *trans; + struct mailbox_list_iterate_context *list_iter; + + ARRAY_TYPE(const_string) entries; + uint32_t maxsize; + uoff_t largest_seen_size; + unsigned int depth; + + struct istream *cur_stream; + + struct imap_metadata_iter *iter; + string_t *iter_entry_prefix; + + string_t *delayed_errors; + string_t *last_error_str; + enum mail_error last_error; + + unsigned int entry_idx; + bool first_entry_sent; +}; + +static bool +cmd_getmetadata_mailbox_iter_next(struct imap_getmetadata_context *ctx); + +static bool +cmd_getmetadata_parse_options(struct imap_getmetadata_context *ctx, + const struct imap_arg *options) +{ + const char *value; + + while (!IMAP_ARG_IS_EOL(options)) { + if (imap_arg_atom_equals(options, "MAXSIZE")) { + options++; + if (!imap_arg_get_atom(options, &value) || + str_to_uint32(value, &ctx->maxsize) < 0) { + client_send_command_error(ctx->cmd, + "Invalid value for MAXSIZE option"); + return FALSE; + } + } else if (imap_arg_atom_equals(options, "DEPTH")) { + options++; + if (!imap_arg_get_atom(options, &value)) { + client_send_command_error(ctx->cmd, + "Invalid value for DEPTH option"); + return FALSE; + } + if (strcmp(value, "0") == 0) + ctx->depth = 0; + else if (strcmp(value, "1") == 0) + ctx->depth = 1; + else if (strcmp(value, "infinity") == 0) + ctx->depth = UINT_MAX; + else { + client_send_command_error(ctx->cmd, + "Invalid value for DEPTH option"); + return FALSE; + } + } else { + client_send_command_error(ctx->cmd, "Unknown option"); + return FALSE; + } + options++; + } + return TRUE; +} + +static bool +imap_metadata_parse_entry_names(struct imap_getmetadata_context *ctx, + const struct imap_arg *entries) +{ + const char *value, *client_error; + + p_array_init(&ctx->entries, ctx->cmd->pool, 4); + for (; !IMAP_ARG_IS_EOL(entries); entries++) { + if (!imap_arg_get_astring(entries, &value)) { + client_send_command_error(ctx->cmd, "Entry isn't astring"); + return FALSE; + } + if (!imap_metadata_verify_entry_name(value, &client_error)) { + client_send_command_error(ctx->cmd, client_error); + return FALSE; + } + + /* names are case-insensitive so we'll always lowercase them */ + value = p_strdup(ctx->cmd->pool, t_str_lcase(value)); + array_push_back(&ctx->entries, &value); + } + return TRUE; +} + +static string_t * +metadata_add_entry(struct imap_getmetadata_context *ctx, const char *entry) +{ + string_t *str; + + str = t_str_new(64); + if (!ctx->first_entry_sent) { + string_t *mailbox_mutf7 = t_str_new(64); + + ctx->first_entry_sent = TRUE; + str_append(str, "* METADATA "); + if (ctx->box == NULL) { + /* server metadata reply */ + str_append(str, "\"\""); + } else { + if (imap_utf8_to_utf7(mailbox_get_vname(ctx->box), mailbox_mutf7) < 0) + i_unreached(); + imap_append_astring(str, str_c(mailbox_mutf7)); + } + str_append(str, " ("); + + /* nothing can be sent until untagged METADATA is finished */ + ctx->cmd->client->output_cmd_lock = ctx->cmd; + } else { + str_append_c(str, ' '); + } + imap_append_astring(str, entry); + return str; +} + +static void +cmd_getmetadata_send_nil_reply(struct imap_getmetadata_context *ctx, + const char *entry) +{ + string_t *str; + + /* client requested a specific entry that didn't exist. + we must return it as NIL. */ + str = metadata_add_entry(ctx, entry); + str_append(str, " NIL"); + o_stream_nsend(ctx->cmd->client->output, str_data(str), str_len(str)); +} + +static void +cmd_getmetadata_handle_error_str(struct imap_getmetadata_context *ctx, + const char *error_string, + enum mail_error error) +{ + if (str_len(ctx->last_error_str) > 0) { + str_append(ctx->delayed_errors, "* NO "); + str_append_str(ctx->delayed_errors, ctx->last_error_str); + str_append(ctx->delayed_errors, "\r\n"); + str_truncate(ctx->last_error_str, 0); + } + str_append(ctx->last_error_str, error_string); + ctx->last_error = error; +} + +static bool +cmd_getmetadata_handle_error(struct imap_getmetadata_context *ctx) +{ + const char *error_string; + enum mail_error error; + + error_string = imap_metadata_transaction_get_last_error(ctx->trans, &error); + if (error == MAIL_ERROR_NOTPOSSIBLE && ctx->depth > 0) { + /* Using DEPTH to iterate children with imap_metadata=no. + Don't return an error, since some of the entries could be + returned successfully. */ + return FALSE; + } + + cmd_getmetadata_handle_error_str(ctx, error_string, error); + return TRUE; +} + +static void cmd_getmetadata_send_entry(struct imap_getmetadata_context *ctx, + const char *entry, bool require_reply) +{ + struct client *client = ctx->cmd->client; + struct mail_attribute_value value; + uoff_t value_len; + string_t *str; + + if (imap_metadata_get_stream(ctx->trans, entry, &value) < 0) { + if (cmd_getmetadata_handle_error(ctx)) + return; + } + + if (value.value != NULL) + value_len = strlen(value.value); + else if (value.value_stream != NULL) { + if (i_stream_get_size(value.value_stream, TRUE, &value_len) < 0) { + e_error(client->event, + "GETMETADATA %s: i_stream_get_size(%s) failed: %s", entry, + i_stream_get_name(value.value_stream), + i_stream_get_error(value.value_stream)); + i_stream_unref(&value.value_stream); + cmd_getmetadata_handle_error_str(ctx, + MAIL_ERRSTR_CRITICAL_MSG, MAIL_ERROR_TEMP); + return; + } + } else { + /* skip nonexistent entries */ + if (require_reply) + cmd_getmetadata_send_nil_reply(ctx, entry); + return; + } + + if (value_len > ctx->maxsize) { + /* value length is larger than specified MAXSIZE, + skip this entry */ + if (ctx->largest_seen_size < value_len) + ctx->largest_seen_size = value_len; + i_stream_unref(&value.value_stream); + return; + } + + str = metadata_add_entry(ctx, entry); + if (value.value != NULL) { + str_printfa(str, " {%"PRIuUOFF_T"}\r\n%s", value_len, value.value); + o_stream_nsend(client->output, str_data(str), str_len(str)); + } else { + str_printfa(str, " ~{%"PRIuUOFF_T"}\r\n", value_len); + o_stream_nsend(client->output, str_data(str), str_len(str)); + + ctx->cur_stream = i_stream_create_sized(value.value_stream, value_len); + i_stream_unref(&value.value_stream); + } +} + +static bool +cmd_getmetadata_stream_continue(struct imap_getmetadata_context *ctx) +{ + enum ostream_send_istream_result res; + + o_stream_set_max_buffer_size(ctx->cmd->client->output, 0); + res = o_stream_send_istream(ctx->cmd->client->output, ctx->cur_stream); + o_stream_set_max_buffer_size(ctx->cmd->client->output, SIZE_MAX); + + switch (res) { + case OSTREAM_SEND_ISTREAM_RESULT_FINISHED: + return TRUE; + case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT: + i_unreached(); + case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT: + return FALSE; + case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT: + e_error(ctx->cmd->client->event, "read(%s) failed: %s", + i_stream_get_name(ctx->cur_stream), + i_stream_get_error(ctx->cur_stream)); + client_disconnect(ctx->cmd->client, + "Internal GETMETADATA failure"); + return TRUE; + case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT: + /* client disconnected */ + return TRUE; + } + i_unreached(); +} + +static int +cmd_getmetadata_send_entry_tree(struct imap_getmetadata_context *ctx, + const char *entry) +{ + struct client *client = ctx->cmd->client; + + if (o_stream_get_buffer_used_size(client->output) >= + CLIENT_OUTPUT_OPTIMAL_SIZE) { + if (o_stream_flush(client->output) <= 0) { + o_stream_set_flush_pending(client->output, TRUE); + return 0; + } + } + + if (ctx->iter != NULL) { + const char *subentry; + + /* DEPTH iteration */ + do { + subentry = imap_metadata_iter_next(ctx->iter); + if (subentry == NULL) { + /* iteration finished, get to the next entry */ + if (imap_metadata_iter_deinit(&ctx->iter) < 0) { + if (!cmd_getmetadata_handle_error(ctx)) + i_unreached(); + } + return -1; + } + } while (ctx->depth == 1 && strchr(subentry, '/') != NULL); + entry = t_strconcat(str_c(ctx->iter_entry_prefix), subentry, NULL); + } + /* send NIL only on depth 0 query */ + cmd_getmetadata_send_entry(ctx, entry, ctx->depth == 0); + + if (ctx->cur_stream != NULL) { + if (!cmd_getmetadata_stream_continue(ctx)) + return 0; + i_stream_unref(&ctx->cur_stream); + } + + if (ctx->iter != NULL) { + /* already iterating the entry */ + return 1; + } else if (ctx->depth == 0) { + /* no iteration for the entry */ + return -1; + } else { + /* we just sent the entry root. iterate its children. */ + str_truncate(ctx->iter_entry_prefix, 0); + str_append(ctx->iter_entry_prefix, entry); + str_append_c(ctx->iter_entry_prefix, '/'); + + ctx->iter = imap_metadata_iter_init(ctx->trans, entry); + return 1; + } +} + +static void cmd_getmetadata_iter_deinit(struct imap_getmetadata_context *ctx) +{ + if (ctx->iter != NULL) + (void)imap_metadata_iter_deinit(&ctx->iter); + if (ctx->trans != NULL) + (void)imap_metadata_transaction_commit(&ctx->trans, NULL, NULL); + if (ctx->box != NULL) + mailbox_free(&ctx->box); + ctx->first_entry_sent = FALSE; + ctx->entry_idx = 0; +} + +static void cmd_getmetadata_deinit(struct imap_getmetadata_context *ctx) +{ + struct client_command_context *cmd = ctx->cmd; + + cmd_getmetadata_iter_deinit(ctx); + cmd->client->output_cmd_lock = NULL; + + if (ctx->list_iter != NULL && + mailbox_list_iter_deinit(&ctx->list_iter) < 0) + client_send_list_error(cmd, cmd->client->user->namespaces->list); + else if (ctx->last_error != 0) { + i_assert(str_len(ctx->last_error_str) > 0); + const char *tagline = + imap_get_error_string(cmd, str_c(ctx->last_error_str), + ctx->last_error); + client_send_tagline(cmd, tagline); + } else if (ctx->largest_seen_size != 0) { + client_send_tagline(cmd, t_strdup_printf( + "OK [METADATA LONGENTRIES %"PRIuUOFF_T"] " + "Getmetadata completed.", ctx->largest_seen_size)); + } else { + client_send_tagline(cmd, "OK Getmetadata completed."); + } +} + +static bool cmd_getmetadata_continue(struct client_command_context *cmd) +{ + struct imap_getmetadata_context *ctx = cmd->context; + const char *const *entries; + unsigned int count; + int ret; + + if (cmd->cancel) { + cmd_getmetadata_deinit(ctx); + return TRUE; + } + + if (ctx->cur_stream != NULL) { + if (!cmd_getmetadata_stream_continue(ctx)) + return FALSE; + i_stream_unref(&ctx->cur_stream); + } + + entries = array_get(&ctx->entries, &count); + for (; ctx->entry_idx < count; ctx->entry_idx++) { + do { + T_BEGIN { + ret = cmd_getmetadata_send_entry_tree(ctx, entries[ctx->entry_idx]); + } T_END; + if (ret == 0) + return FALSE; + } while (ret > 0); + } + if (ctx->first_entry_sent) + o_stream_nsend_str(cmd->client->output, ")\r\n"); + + if (str_len(ctx->delayed_errors) > 0) { + o_stream_nsend(cmd->client->output, + str_data(ctx->delayed_errors), + str_len(ctx->delayed_errors)); + str_truncate(ctx->delayed_errors, 0); + } + + cmd_getmetadata_iter_deinit(ctx); + if (ctx->list_iter != NULL) + return cmd_getmetadata_mailbox_iter_next(ctx); + cmd_getmetadata_deinit(ctx); + return TRUE; +} + +static bool +cmd_getmetadata_start(struct imap_getmetadata_context *ctx) +{ + struct client_command_context *cmd = ctx->cmd; + + if (ctx->depth > 0) + ctx->iter_entry_prefix = str_new(cmd->pool, 128); + imap_metadata_transaction_validated_only(ctx->trans, + !cmd->client->set->imap_metadata); + + if (!cmd_getmetadata_continue(cmd)) { + cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT; + cmd->func = cmd_getmetadata_continue; + return FALSE; + } + return TRUE; +} + +static bool +cmd_getmetadata_server(struct imap_getmetadata_context *ctx) +{ + ctx->trans = imap_metadata_transaction_begin_server(ctx->cmd->client->user); + return cmd_getmetadata_start(ctx); +} + +static int +cmd_getmetadata_try_mailbox(struct imap_getmetadata_context *ctx, + struct mail_namespace *ns, const char *mailbox) +{ + ctx->box = mailbox_alloc(ns->list, mailbox, MAILBOX_FLAG_READONLY); + event_add_str(ctx->cmd->global_event, "mailbox", + mailbox_get_vname(ctx->box)); + + enum mailbox_existence existence; + if (mailbox_exists(ctx->box, TRUE, &existence) < 0) { + return -1; + } else if (existence == MAILBOX_EXISTENCE_NONE) { + const char *err = t_strdup_printf(MAIL_ERRSTR_MAILBOX_NOT_FOUND, + mailbox_get_vname(ctx->box)); + mail_storage_set_error(ctx->box->storage, MAIL_ERROR_NOTFOUND, err); + return -1; + } + + ctx->trans = imap_metadata_transaction_begin(ctx->box); + return cmd_getmetadata_start(ctx) ? 1 : 0; +} + +static bool +cmd_getmetadata_mailbox(struct imap_getmetadata_context *ctx, + struct mail_namespace *ns, const char *mailbox) +{ + int ret; + + ret = cmd_getmetadata_try_mailbox(ctx, ns, mailbox); + if (ret < 0) { + client_send_box_error(ctx->cmd, ctx->box); + mailbox_free(&ctx->box); + } + return ret != 0; +} + +static bool +cmd_getmetadata_mailbox_iter_next(struct imap_getmetadata_context *ctx) +{ + const struct mailbox_info *info; + int ret; + + while ((info = mailbox_list_iter_next(ctx->list_iter)) != NULL) { + if ((info->flags & (MAILBOX_NOSELECT | MAILBOX_NONEXISTENT)) != 0) + continue; + ret = cmd_getmetadata_try_mailbox(ctx, info->ns, info->vname); + if (ret > 0) { + /* we'll already recursively went through + all the mailboxes (FIXME: ugly and potentially + stack consuming) */ + return TRUE; + } else if (ret == 0) { + /* need to send more data later */ + return FALSE; + } + T_BEGIN { + client_send_line(ctx->cmd->client, t_strdup_printf( + "* NO Failed to open mailbox %s: %s", + info->vname, mailbox_get_last_error(ctx->box, NULL))); + } T_END; + mailbox_free(&ctx->box); + } + cmd_getmetadata_deinit(ctx); + return TRUE; +} + +bool cmd_getmetadata(struct client_command_context *cmd) +{ + struct imap_getmetadata_context *ctx; + struct mail_namespace *ns; + const struct imap_arg *args, *options, *entries; + const char *mailbox, *entry_name; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + ctx = p_new(cmd->pool, struct imap_getmetadata_context, 1); + ctx->cmd = cmd; + ctx->maxsize = (uint32_t)-1; + ctx->cmd->context = ctx; + ctx->delayed_errors = str_new(cmd->pool, 128); + ctx->last_error_str = str_new(cmd->pool, 128); + + if (imap_arg_get_list(&args[0], &options)) { + if (!cmd_getmetadata_parse_options(ctx, options)) + return TRUE; + args++; + } + if (!imap_arg_get_astring(&args[0], &mailbox)) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + if (!imap_arg_get_list(&args[1], &entries)) { + if (!imap_arg_get_astring(&args[1], &entry_name) || + !IMAP_ARG_IS_EOL(&args[2])) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + entries = args+1; + } + if (!imap_metadata_parse_entry_names(ctx, entries)) + return TRUE; + + if (mailbox[0] == '\0') { + /* server attribute */ + return cmd_getmetadata_server(ctx); + } else if (strchr(mailbox, '*') == NULL && + strchr(mailbox, '%') == NULL) { + /* mailbox attribute */ + ns = client_find_namespace(cmd, &mailbox); + if (ns == NULL) + return TRUE; + return cmd_getmetadata_mailbox(ctx, ns, mailbox); + } else { + /* wildcards in mailbox name. this isn't supported by RFC 5464, + but it was in the earlier drafts and is already used by + some software (Horde). */ + const char *patterns[2]; + patterns[0] = mailbox; patterns[1] = NULL; + + ctx->list_iter = + mailbox_list_iter_init_namespaces( + cmd->client->user->namespaces, + patterns, MAIL_NAMESPACE_TYPE_MASK_ALL, 0); + return cmd_getmetadata_mailbox_iter_next(ctx); + } +} diff --git a/src/imap/cmd-id.c b/src/imap/cmd-id.c new file mode 100644 index 0000000..98355d4 --- /dev/null +++ b/src/imap/cmd-id.c @@ -0,0 +1,27 @@ +/* Copyright (c) 2008-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-id.h" + +bool cmd_id(struct client_command_context *cmd) +{ + const struct imap_settings *set = cmd->client->set; + const struct imap_arg *args; + const char *value; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + if (!cmd->client->id_logged) { + cmd->client->id_logged = TRUE; + value = imap_id_args_get_log_reply(args, set->imap_id_log); + if (value != NULL) + e_info(cmd->client->event, "ID sent: %s", value); + } + + client_send_line(cmd->client, t_strdup_printf( + "* ID %s", imap_id_reply_generate(set->imap_id_send))); + client_send_tagline(cmd, "OK ID completed."); + return TRUE; +} + diff --git a/src/imap/cmd-idle.c b/src/imap/cmd-idle.c new file mode 100644 index 0000000..2b31dc7 --- /dev/null +++ b/src/imap/cmd-idle.c @@ -0,0 +1,308 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "istream.h" +#include "ostream.h" +#include "crc32.h" +#include "mail-storage-settings.h" +#include "imap-commands.h" +#include "imap-keepalive.h" +#include "imap-sync.h" + +struct cmd_idle_context { + struct client *client; + struct client_command_context *cmd; + + struct imap_sync_context *sync_ctx; + struct timeout *keepalive_to, *to_hibernate; + + bool manual_cork:1; + bool sync_pending:1; +}; + +static void idle_add_keepalive_timeout(struct cmd_idle_context *ctx); +static bool cmd_idle_continue(struct client_command_context *cmd); + +static void +idle_finish(struct cmd_idle_context *ctx, bool done_ok, bool free_cmd) +{ + struct client *client = ctx->client; + + timeout_remove(&ctx->keepalive_to); + timeout_remove(&ctx->to_hibernate); + + if (ctx->sync_ctx != NULL) { + /* we're here only in connection failure cases */ + (void)imap_sync_deinit(ctx->sync_ctx, ctx->cmd); + } + + o_stream_cork(client->output); + io_remove(&client->io); + + if (client->mailbox != NULL) + mailbox_notify_changes_stop(client->mailbox); + + if (done_ok) + client_send_tagline(ctx->cmd, "OK Idle completed."); + else + client_send_tagline(ctx->cmd, "BAD Expected DONE."); + + o_stream_uncork(client->output); + if (free_cmd) + client_command_free(&ctx->cmd); +} + +static bool +idle_client_handle_input(struct cmd_idle_context *ctx, bool free_cmd) +{ + const char *line; + + while ((line = i_stream_next_line(ctx->client->input)) != NULL) { + if (ctx->client->input_skip_line) + ctx->client->input_skip_line = FALSE; + else { + idle_finish(ctx, strcasecmp(line, "DONE") == 0, + free_cmd); + return TRUE; + } + } + return FALSE; +} + +static bool idle_client_input_more(struct cmd_idle_context *ctx) +{ + struct client *client = ctx->client; + + client->last_input = ioloop_time; + timeout_reset(client->to_idle); + + switch (i_stream_read(client->input)) { + case -1: + /* disconnected */ + client_disconnect(client, NULL); + return TRUE; + case -2: + client->input_skip_line = TRUE; + idle_finish(ctx, FALSE, TRUE); + return TRUE; + } + + if (ctx->sync_ctx != NULL) { + /* we're still sending output to client. wait until it's all + sent so we don't lose any changes. */ + io_remove(&client->io); + return FALSE; + } + + return idle_client_handle_input(ctx, TRUE); +} + +static void idle_client_input(struct cmd_idle_context *ctx) +{ + struct client *client = ctx->client; + + if (idle_client_input_more(ctx)) + client_continue_pending_input(client); +} + +static void keepalive_timeout(struct cmd_idle_context *ctx) +{ + if (ctx->client->output_cmd_lock != NULL) { + /* it's busy sending output */ + return; + } + + if (o_stream_get_buffer_used_size(ctx->client->output) == 0) { + /* Sending this keeps NATs/stateful firewalls alive. + Sending this also catches dead connections. Don't send + anything if there is already data waiting in output + buffer. */ + o_stream_cork(ctx->client->output); + client_send_line(ctx->client, "* OK Still here"); + o_stream_uncork(ctx->client->output); + } + /* Make sure idling connections don't get disconnected. There are + several clients that really want to IDLE forever and there's not + much harm in letting them do so. */ + timeout_reset(ctx->client->to_idle); + /* recalculate time for the next keepalive timeout */ + idle_add_keepalive_timeout(ctx); +} + +static bool idle_sync_now(struct mailbox *box, struct cmd_idle_context *ctx) +{ + i_assert(ctx->sync_ctx == NULL); + + /* hibernation can't happen while sync is running. + the timeout is added back afterwards. */ + timeout_remove(&ctx->to_hibernate); + + ctx->sync_pending = FALSE; + ctx->sync_ctx = imap_sync_init(ctx->client, box, 0, 0); + return cmd_idle_continue(ctx->cmd); +} + +static void idle_callback(struct mailbox *box, struct cmd_idle_context *ctx) +{ + struct client *client = ctx->client; + + if (ctx->sync_ctx != NULL) + ctx->sync_pending = TRUE; + else { + ctx->manual_cork = TRUE; + (void)idle_sync_now(box, ctx); + if (client->disconnected) + client_destroy(client, NULL); + } +} + +static void idle_add_keepalive_timeout(struct cmd_idle_context *ctx) +{ + struct client *client = ctx->client; + unsigned int interval = client->set->imap_idle_notify_interval; + + if (interval == 0) + return; + + interval = imap_keepalive_interval_msecs(client->user->username, + client->user->conn.remote_ip, + interval); + + timeout_remove(&ctx->keepalive_to); + ctx->keepalive_to = timeout_add(interval, keepalive_timeout, ctx); +} + +static void idle_hibernate_timeout(struct cmd_idle_context *ctx) +{ + struct client *client = ctx->client; + const char *reason; + + i_assert(ctx->sync_ctx == NULL); + i_assert(!ctx->sync_pending); + + if (imap_client_hibernate(&client, &reason)) { + /* client may be destroyed now */ + } else { + /* failed - don't bother retrying */ + timeout_remove(&ctx->to_hibernate); + } +} + +static void idle_add_hibernate_timeout(struct cmd_idle_context *ctx) +{ + unsigned int secs = ctx->client->set->imap_hibernate_timeout; + + i_assert(ctx->to_hibernate == NULL); + + if (secs == 0) + return; + + ctx->to_hibernate = + timeout_add(secs * 1000, idle_hibernate_timeout, ctx); +} + +static bool cmd_idle_continue(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct cmd_idle_context *ctx = cmd->context; + uoff_t orig_offset = client->output->offset; + + if (cmd->cancel) { + idle_finish(ctx, FALSE, FALSE); + return TRUE; + } + + if (ctx->to_hibernate != NULL) + timeout_reset(ctx->to_hibernate); + + if (ctx->manual_cork) { + /* we're coming from idle_callback instead of a normal + I/O handler, so we'll have to do corking manually */ + o_stream_cork(client->output); + } + + if (ctx->sync_ctx != NULL) { + if (imap_sync_more(ctx->sync_ctx) == 0) { + /* unfinished */ + if (ctx->manual_cork) { + ctx->manual_cork = FALSE; + o_stream_uncork(client->output); + } + cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT; + return FALSE; + } + + if (imap_sync_deinit(ctx->sync_ctx, ctx->cmd) < 0) { + client_send_untagged_storage_error(client, + mailbox_get_storage(client->mailbox)); + mailbox_notify_changes_stop(client->mailbox); + } + ctx->sync_ctx = NULL; + } + if (client->output->offset != orig_offset && + ctx->keepalive_to != NULL) + idle_add_keepalive_timeout(ctx); + + if (ctx->sync_pending) { + /* more changes occurred while we were sending changes to + client. + + NOTE: this recurses back to this function, + so we return here instead of doing everything twice. */ + return idle_sync_now(client->mailbox, ctx); + } + if (ctx->to_hibernate == NULL) + idle_add_hibernate_timeout(ctx); + cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT; + + if (ctx->manual_cork) { + ctx->manual_cork = FALSE; + o_stream_uncork(client->output); + } + + if (client->output->closed) { + idle_finish(ctx, FALSE, FALSE); + return TRUE; + } + if (client->io == NULL) { + /* input is pending. add the io back and mark the input as + pending. we can't safely read more input immediately here. */ + client->io = io_add_istream(client->input, + idle_client_input, ctx); + i_stream_set_input_pending(client->input, TRUE); + } + return FALSE; +} + +bool cmd_idle(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct cmd_idle_context *ctx; + + ctx = p_new(cmd->pool, struct cmd_idle_context, 1); + ctx->cmd = cmd; + ctx->client = client; + idle_add_keepalive_timeout(ctx); + idle_add_hibernate_timeout(ctx); + + if (client->mailbox != NULL) + mailbox_notify_changes(client->mailbox, idle_callback, ctx); + if (!client->state_import_idle_continue) + client_send_line(client, "+ idling"); + else { + /* continuing an IDLE after hibernation */ + client->state_import_idle_continue = FALSE; + } + + io_remove(&client->io); + client->io = io_add_istream(client->input, idle_client_input, ctx); + + cmd->func = cmd_idle_continue; + cmd->context = ctx; + + /* check immediately if there are changes. if they came before we + added mailbox-notifier, we wouldn't see them otherwise. */ + if (client->mailbox != NULL) + idle_sync_now(client->mailbox, ctx); + return idle_client_handle_input(ctx, FALSE); +} diff --git a/src/imap/cmd-list.c b/src/imap/cmd-list.c new file mode 100644 index 0000000..dd6c00e --- /dev/null +++ b/src/imap/cmd-list.c @@ -0,0 +1,484 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "array.h" +#include "str.h" +#include "strescape.h" +#include "mailbox-list-iter.h" +#include "imap-utf7.h" +#include "imap-quote.h" +#include "imap-match.h" +#include "imap-status.h" +#include "imap-commands.h" +#include "imap-list.h" + +struct cmd_list_context { + struct client_command_context *cmd; + struct mail_user *user; + + enum mailbox_list_iter_flags list_flags; + struct imap_status_items status_items; + + struct mailbox_list_iterate_context *list_iter; + + bool lsub:1; + bool lsub_no_unsubscribed:1; + bool used_listext:1; + bool used_status:1; +}; + +static void +mailbox_flags2str(struct cmd_list_context *ctx, string_t *str, + const char *special_use, enum mailbox_info_flags flags) +{ + size_t orig_len = str_len(str); + + if ((flags & MAILBOX_NONEXISTENT) != 0 && !ctx->used_listext) { + flags |= MAILBOX_NOSELECT; + flags &= ENUM_NEGATE(MAILBOX_NONEXISTENT); + } + + if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_CHILDREN) == 0) + flags &= ENUM_NEGATE(MAILBOX_CHILDREN | MAILBOX_NOCHILDREN); + + if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 && + (flags & MAILBOX_SUBSCRIBED) == 0 && !ctx->used_listext) { + /* LSUB uses \Noselect for this */ + flags |= MAILBOX_NOSELECT; + } else if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_SUBSCRIBED) == 0) + flags &= ENUM_NEGATE(MAILBOX_SUBSCRIBED); + imap_mailbox_flags2str(str, flags); + + if ((ctx->list_flags & MAILBOX_LIST_ITER_RETURN_SPECIALUSE) != 0 && + special_use != NULL) { + if (str_len(str) != orig_len) + str_append_c(str, ' '); + str_append(str, special_use); + } +} + +static void +mailbox_childinfo2str(struct cmd_list_context *ctx, string_t *str, + enum mailbox_info_flags flags) +{ + if (!ctx->used_listext) + return; + + if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 && + (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0) + str_append(str, " (CHILDINFO (\"SUBSCRIBED\"))"); + if ((flags & MAILBOX_CHILD_SPECIALUSE) != 0 && + (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0) + str_append(str, " (CHILDINFO (\"SPECIAL-USE\"))"); +} + +static bool +parse_select_flags(struct cmd_list_context *ctx, const struct imap_arg *args) +{ + enum mailbox_list_iter_flags list_flags = 0; + const char *str; + + while (!IMAP_ARG_IS_EOL(args)) { + if (!imap_arg_get_atom(args, &str)) { + client_send_command_error(ctx->cmd, + "List options contains non-atoms."); + return FALSE; + } + + if (strcasecmp(str, "SUBSCRIBED") == 0) { + list_flags |= MAILBOX_LIST_ITER_SELECT_SUBSCRIBED | + MAILBOX_LIST_ITER_RETURN_SUBSCRIBED; + } else if (strcasecmp(str, "RECURSIVEMATCH") == 0) + list_flags |= MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH; + else if (strcasecmp(str, "SPECIAL-USE") == 0) { + list_flags |= MAILBOX_LIST_ITER_SELECT_SPECIALUSE | + MAILBOX_LIST_ITER_RETURN_SPECIALUSE; + } else if (strcasecmp(str, "REMOTE") == 0) { + /* not supported, ignore */ + } else { + /* skip also optional list value */ + client_send_command_error(ctx->cmd, + "Unknown select options"); + return FALSE; + } + args++; + } + + if ((list_flags & MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH) != 0 && + (list_flags & (MAILBOX_LIST_ITER_SELECT_SUBSCRIBED | + MAILBOX_LIST_ITER_SELECT_SPECIALUSE)) == 0) { + client_send_command_error(ctx->cmd, + "RECURSIVEMATCH must not be the only selection."); + return FALSE; + } + + ctx->list_flags = list_flags; + return TRUE; +} + +static bool +parse_return_flags(struct cmd_list_context *ctx, const struct imap_arg *args) +{ + enum mailbox_list_iter_flags list_flags = 0; + const struct imap_arg *list_args; + const char *str; + + while (!IMAP_ARG_IS_EOL(args)) { + if (!imap_arg_get_atom(args, &str)) { + client_send_command_error(ctx->cmd, + "List options contains non-atoms."); + return FALSE; + } + + if (strcasecmp(str, "SUBSCRIBED") == 0) + list_flags |= MAILBOX_LIST_ITER_RETURN_SUBSCRIBED; + else if (strcasecmp(str, "CHILDREN") == 0) + list_flags |= MAILBOX_LIST_ITER_RETURN_CHILDREN; + else if (strcasecmp(str, "SPECIAL-USE") == 0) + list_flags |= MAILBOX_LIST_ITER_RETURN_SPECIALUSE; + else if (strcasecmp(str, "STATUS") == 0 && + imap_arg_get_list(&args[1], &list_args)) { + if (imap_status_parse_items(ctx->cmd, list_args, + &ctx->status_items) < 0) + return FALSE; + ctx->used_status = TRUE; + args++; + } else { + /* skip also optional list value */ + client_send_command_error(ctx->cmd, + "Unknown return options"); + return FALSE; + } + args++; + } + + ctx->list_flags |= list_flags; + return TRUE; +} + +static const char *ns_prefix_mutf7(struct mail_namespace *ns) +{ + string_t *str; + + if (*ns->prefix == '\0') + return ""; + + str = t_str_new(64); + if (imap_utf8_to_utf7(ns->prefix, str) < 0) + i_panic("Namespace prefix not UTF-8: %s", ns->prefix); + return str_c(str); +} + +static void list_reply_append_ns_sep_param(string_t *str, char sep) +{ + str_append_c(str, '"'); + if (sep == '\\') + str_append(str, "\\\\"); + else + str_append_c(str, sep); + str_append_c(str, '"'); +} + +static void +list_send_status(struct cmd_list_context *ctx, const char *name, + const char *mutf7_name, enum mailbox_info_flags flags) +{ + struct imap_status_result result; + struct mail_namespace *ns; + + if ((flags & (MAILBOX_NONEXISTENT | MAILBOX_NOSELECT)) != 0) { + /* doesn't exist, don't even try to get STATUS */ + return; + } + if ((flags & MAILBOX_SUBSCRIBED) == 0 && + (ctx->list_flags & MAILBOX_LIST_ITER_SELECT_SUBSCRIBED) != 0) { + /* listing subscriptions, but only child is subscribed */ + return; + } + + /* if we're listing subscriptions and there are subscriptions=no + namespaces, ctx->ns may not point to correct one */ + ns = mail_namespace_find(ctx->user->namespaces, name); + if (imap_status_get(ctx->cmd, ns, name, + &ctx->status_items, &result) < 0) { + client_send_line(ctx->cmd->client, + t_strconcat("* ", result.errstr, NULL)); + return; + } + + imap_status_send(ctx->cmd->client, mutf7_name, + &ctx->status_items, &result); +} + +static bool cmd_list_continue(struct client_command_context *cmd) +{ + struct cmd_list_context *ctx = cmd->context; + const struct mailbox_info *info; + enum mailbox_info_flags flags; + string_t *str, *mutf7_name; + const char *name; + int ret = 0; + + if (cmd->cancel) { + if (ctx->list_iter != NULL) + (void)mailbox_list_iter_deinit(&ctx->list_iter); + return TRUE; + } + str = t_str_new(256); + mutf7_name = t_str_new(128); + while ((info = mailbox_list_iter_next(ctx->list_iter)) != NULL) { + name = info->vname; + flags = info->flags; + + if ((flags & MAILBOX_CHILD_SUBSCRIBED) != 0 && + (flags & MAILBOX_SUBSCRIBED) == 0 && + ctx->lsub_no_unsubscribed) { + /* mask doesn't end with %. we don't want to show + any extra mailboxes. */ + continue; + } + + str_truncate(mutf7_name, 0); + if (imap_utf8_to_utf7(name, mutf7_name) < 0) + i_panic("LIST: Mailbox name not UTF-8: %s", name); + + str_truncate(str, 0); + str_printfa(str, "* %s (", ctx->lsub ? "LSUB" : "LIST"); + mailbox_flags2str(ctx, str, info->special_use, flags); + str_append(str, ") "); + list_reply_append_ns_sep_param(str, + mail_namespace_get_sep(info->ns)); + str_append_c(str, ' '); + imap_append_astring(str, str_c(mutf7_name)); + mailbox_childinfo2str(ctx, str, flags); + + ret = client_send_line_next(ctx->cmd->client, str_c(str)); + if (ctx->used_status) T_BEGIN { + list_send_status(ctx, name, str_c(mutf7_name), flags); + } T_END; + if (ret == 0) { + /* buffer is full, continue later */ + return FALSE; + } + } + + if (mailbox_list_iter_deinit(&ctx->list_iter) < 0) { + client_send_list_error(cmd, ctx->user->namespaces->list); + return TRUE; + } + client_send_tagline(cmd, !ctx->lsub ? + "OK List completed." : + "OK Lsub completed."); + return TRUE; +} + +static const char *const * +list_get_ref_patterns(struct cmd_list_context *ctx, const char *ref, + const char *const *patterns) +{ + struct mail_namespace *ns; + const char *const *pat, *pattern; + ARRAY(const char *) full_patterns; + + if (*ref == '\0') + return patterns; + + ns = mail_namespace_find(ctx->user->namespaces, ref); + + t_array_init(&full_patterns, 16); + for (pat = patterns; *pat != NULL; pat++) { + pattern = mailbox_list_join_refpattern(ns->list, ref, *pat); + array_push_back(&full_patterns, &pattern); + } + array_append_zero(&full_patterns); /* NULL-terminate */ + return array_front(&full_patterns); +} + +static void cmd_list_init(struct cmd_list_context *ctx, + const char *const *patterns) +{ + enum mail_namespace_type type_mask = MAIL_NAMESPACE_TYPE_MASK_ALL; + + ctx->list_iter = + mailbox_list_iter_init_namespaces(ctx->user->namespaces, + patterns, type_mask, + ctx->list_flags); +} + +static void cmd_list_ref_root(struct client *client, const char *ref) +{ + struct mail_namespace *ns; + const char *ns_prefix; + char ns_sep; + string_t *str; + + /* Special request to return the hierarchy delimiter and mailbox root + name. If namespace has a prefix, it's returned as the mailbox root. + Otherwise we'll emulate UW-IMAP behavior. */ + ns = mail_namespace_find_visible(client->user->namespaces, ref); + if (ns != NULL) { + ns_prefix = ns_prefix_mutf7(ns); + ns_sep = mail_namespace_get_sep(ns); + } else { + ns_prefix = ""; + ns_sep = mail_namespaces_get_root_sep(client->user->namespaces); + } + + str = t_str_new(64); + str_append(str, "* LIST (\\Noselect) \""); + if (ns_sep == '\\' || ns_sep == '"') + str_append_c(str, '\\'); + str_printfa(str, "%c\" ", ns_sep); + if (*ns_prefix != '\0') { + /* non-hidden namespace, use it as the root name */ + imap_append_astring(str, ns_prefix); + } else { + /* Hidden namespace or empty namespace prefix. We could just + return an empty root name, but it's safer to emulate what + UW-IMAP does. With full filesystem access this might even + matter (root of "~user/mail/" is "~user/", not "") */ + const char *p = strchr(ref, ns_sep); + + if (p == NULL) + str_append(str, "\"\""); + else + imap_append_astring(str, t_strdup_until(ref, p + 1)); + } + client_send_line(client, str_c(str)); +} + +bool cmd_list_full(struct client_command_context *cmd, bool lsub) +{ + struct client *client = cmd->client; + const struct imap_arg *args, *list_args; + unsigned int arg_count; + struct cmd_list_context *ctx; + ARRAY(const char *) patterns = ARRAY_INIT; + const char *ref, *pattern, *const *patterns_strarr; + string_t *str; + + /* [(<selection options>)] <reference> <pattern>|(<pattern list>) + [RETURN (<return options>)] */ + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + ctx = p_new(cmd->pool, struct cmd_list_context, 1); + ctx->cmd = cmd; + ctx->lsub = lsub; + ctx->user = client->user; + + cmd->context = ctx; + + if (!lsub && imap_arg_get_list(&args[0], &list_args)) { + /* LIST-EXTENDED selection options */ + ctx->used_listext = TRUE; + if (!parse_select_flags(ctx, list_args)) + return TRUE; + args++; + } + + if (!imap_arg_get_astring(&args[0], &ref)) { + client_send_command_error(cmd, "Invalid reference."); + return TRUE; + } + str = t_str_new(64); + if (imap_utf7_to_utf8(ref, str) == 0) + ref = p_strdup(cmd->pool, str_c(str)); + str_truncate(str, 0); + + if (imap_arg_get_list_full(&args[1], &list_args, &arg_count)) { + ctx->used_listext = TRUE; + /* convert pattern list to string array */ + p_array_init(&patterns, cmd->pool, arg_count); + for (; !IMAP_ARG_IS_EOL(list_args); list_args++) { + if (!imap_arg_get_astring(list_args, &pattern)) { + client_send_command_error(cmd, + "Invalid pattern list."); + return TRUE; + } + if (imap_utf7_to_utf8(pattern, str) == 0) + pattern = p_strdup(cmd->pool, str_c(str)); + array_push_back(&patterns, &pattern); + str_truncate(str, 0); + } + args += 2; + } else { + if (!imap_arg_get_astring(&args[1], &pattern)) { + client_send_command_error(cmd, "Invalid pattern."); + return TRUE; + } + if (imap_utf7_to_utf8(pattern, str) == 0) + pattern = p_strdup(cmd->pool, str_c(str)); + + p_array_init(&patterns, cmd->pool, 1); + array_push_back(&patterns, &pattern); + args += 2; + + if (lsub) { + size_t len = strlen(pattern); + ctx->lsub_no_unsubscribed = len == 0 || + pattern[len-1] != '%'; + } + } + + if (imap_arg_atom_equals(&args[0], "RETURN") && + imap_arg_get_list(&args[1], &list_args)) { + /* LIST-EXTENDED return options */ + ctx->used_listext = TRUE; + if (!parse_return_flags(ctx, list_args)) + return TRUE; + args += 2; + } + + if (lsub) { + /* LSUB - we don't care about flags except if + tb-lsub-flags workaround is explicitly set */ + ctx->list_flags |= MAILBOX_LIST_ITER_SELECT_SUBSCRIBED | + MAILBOX_LIST_ITER_SELECT_RECURSIVEMATCH; + /* Return SPECIAL-USE flags for LSUB anyway. Outlook 2013 + does this and since it's not expensive for us to return + them, it's not worth the trouble of adding an explicit + workaround setting. */ + ctx->list_flags |= MAILBOX_LIST_ITER_RETURN_SPECIALUSE; + if ((cmd->client->set->parsed_workarounds & + WORKAROUND_TB_LSUB_FLAGS) == 0) + ctx->list_flags |= MAILBOX_LIST_ITER_RETURN_NO_FLAGS; + } else if (!ctx->used_listext) { + /* non-extended LIST: use default flags */ + ctx->list_flags |= MAILBOX_LIST_ITER_RETURN_CHILDREN | + MAILBOX_LIST_ITER_RETURN_SPECIALUSE; + } + + if (!IMAP_ARG_IS_EOL(args)) { + client_send_command_error(cmd, "Extra arguments."); + return TRUE; + } + + array_append_zero(&patterns); /* NULL-terminate */ + patterns_strarr = array_front(&patterns); + if (!ctx->used_listext && !lsub && *patterns_strarr[0] == '\0') { + /* Only LIST ref "" gets us here */ + cmd_list_ref_root(client, ref); + client_send_tagline(cmd, "OK List completed."); + } else { + patterns_strarr = + list_get_ref_patterns(ctx, ref, patterns_strarr); + cmd_list_init(ctx, patterns_strarr); + + if (!cmd_list_continue(cmd)) { + /* unfinished */ + cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT; + cmd->func = cmd_list_continue; + return FALSE; + } + + cmd->context = NULL; + return TRUE; + } + return TRUE; +} + +bool cmd_list(struct client_command_context *cmd) +{ + return cmd_list_full(cmd, FALSE); +} diff --git a/src/imap/cmd-logout.c b/src/imap/cmd-logout.c new file mode 100644 index 0000000..fbdd416 --- /dev/null +++ b/src/imap/cmd-logout.c @@ -0,0 +1,24 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "ostream.h" +#include "imap-commands.h" + +bool cmd_logout(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + + client->logged_out = TRUE; + client_send_line(client, "* BYE Logging out"); + + if (client->mailbox != NULL) { + /* this could be done at client_disconnect() as well, + but eg. mbox rewrite takes a while so the waiting is + better to happen before "OK" message. */ + imap_client_close_mailbox(client); + } + + client_send_tagline(cmd, "OK Logout completed."); + client_disconnect(client, "Logged out"); + return TRUE; +} diff --git a/src/imap/cmd-lsub.c b/src/imap/cmd-lsub.c new file mode 100644 index 0000000..55d9b68 --- /dev/null +++ b/src/imap/cmd-lsub.c @@ -0,0 +1,9 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-commands.h" + +bool cmd_lsub(struct client_command_context *cmd) +{ + return cmd_list_full(cmd, TRUE); +} diff --git a/src/imap/cmd-namespace.c b/src/imap/cmd-namespace.c new file mode 100644 index 0000000..23d2286 --- /dev/null +++ b/src/imap/cmd-namespace.c @@ -0,0 +1,99 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "str.h" +#include "imap-utf7.h" +#include "imap-quote.h" +#include "imap-commands.h" +#include "mail-namespace.h" + +struct namespace_order { + int secondary_order; + struct mail_namespace *ns; +}; + +static int namespace_order_cmp(const struct namespace_order *no1, + const struct namespace_order *no2) +{ + if (no1->ns->set->order < no2->ns->set->order) + return -1; + if (no1->ns->set->order > no2->ns->set->order) + return 1; + + if (no1->secondary_order < no2->secondary_order) + return -1; + if (no1->secondary_order > no2->secondary_order) + return 1; + return 0; +} + +static void list_namespaces(struct mail_namespace *ns, + enum mail_namespace_type type, string_t *str) +{ + ARRAY(struct namespace_order) ns_order; + struct namespace_order *no; + unsigned int count = 0; + string_t *mutf7_prefix; + char ns_sep; + + t_array_init(&ns_order, 4); + + while (ns != NULL) { + if (ns->type == type && + (ns->flags & NAMESPACE_FLAG_HIDDEN) == 0) { + no = array_append_space(&ns_order); + no->ns = ns; + no->secondary_order = ++count; + } + ns = ns->next; + } + + if (array_count(&ns_order) == 0) { + str_append(str, "NIL"); + return; + } + array_sort(&ns_order, namespace_order_cmp); + + mutf7_prefix = t_str_new(64); + str_append_c(str, '('); + array_foreach_modifiable(&ns_order, no) { + ns_sep = mail_namespace_get_sep(no->ns); + str_append_c(str, '('); + + str_truncate(mutf7_prefix, 0); + if (imap_utf8_to_utf7(no->ns->prefix, mutf7_prefix) < 0) { + i_panic("LIST: Namespace prefix not UTF-8: %s", + no->ns->prefix); + } + + imap_append_string(str, str_c(mutf7_prefix)); + str_append(str, " \""); + if (ns_sep == '\\') + str_append_c(str, '\\'); + str_append_c(str, ns_sep); + str_append(str, "\")"); + } + str_append_c(str, ')'); +} + +bool cmd_namespace(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + string_t *str; + + str = t_str_new(256); + str_append(str, "* NAMESPACE "); + + list_namespaces(client->user->namespaces, + MAIL_NAMESPACE_TYPE_PRIVATE, str); + str_append_c(str, ' '); + list_namespaces(client->user->namespaces, + MAIL_NAMESPACE_TYPE_SHARED, str); + str_append_c(str, ' '); + list_namespaces(client->user->namespaces, + MAIL_NAMESPACE_TYPE_PUBLIC, str); + + client_send_line(client, str_c(str)); + client_send_tagline(cmd, "OK Namespace completed."); + return TRUE; +} diff --git a/src/imap/cmd-noop.c b/src/imap/cmd-noop.c new file mode 100644 index 0000000..3fd2444 --- /dev/null +++ b/src/imap/cmd-noop.c @@ -0,0 +1,15 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-notify.h" +#include "imap-commands.h" + +bool cmd_noop(struct client_command_context *cmd) +{ + if (cmd->client->notify_ctx != NULL) { + /* flush any delayed notifications now. this is mainly useful + for testing. */ + imap_notify_flush(cmd->client->notify_ctx); + } + return cmd_sync(cmd, 0, IMAP_SYNC_FLAG_SAFE, "OK NOOP completed."); +} diff --git a/src/imap/cmd-notify.c b/src/imap/cmd-notify.c new file mode 100644 index 0000000..e043e97 --- /dev/null +++ b/src/imap/cmd-notify.c @@ -0,0 +1,597 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "str.h" +#include "mailbox-list-iter.h" +#include "imap-quote.h" +#include "imap-commands.h" +#include "imap-fetch.h" +#include "imap-list.h" +#include "imap-status.h" +#include "imap-notify.h" + +#define IMAP_NOTIFY_MAX_NAMES_PER_NS 100 + +static const char *imap_notify_event_names[] = { + "MessageNew", "MessageExpunge", "FlagChange", "AnnotationChange", + "MailboxName", "SubscriptionChange", "MailboxMetadataChange", + "ServerMetadataChange" +}; + +static int +cmd_notify_parse_event(const struct imap_arg *arg, + enum imap_notify_event *event_r) +{ + const char *str; + unsigned int i; + + if (!imap_arg_get_atom(arg, &str)) + return -1; + + for (i = 0; i < N_ELEMENTS(imap_notify_event_names); i++) { + if (strcasecmp(str, imap_notify_event_names[i]) == 0) { + *event_r = (enum imap_notify_event)(1 << i); + return 0; + } + } + return -1; +} + +static int +cmd_notify_parse_fetch(struct imap_notify_context *ctx, + const struct imap_arg *list) +{ + if (list->type == IMAP_ARG_EOL) + return -1; /* at least one attribute must be set */ + return imap_fetch_att_list_parse(ctx->client, ctx->pool, list, + &ctx->fetch_ctx, &ctx->error); +} + +static int +cmd_notify_set_selected(struct imap_notify_context *ctx, + const struct imap_arg *events) +{ +#define EV_NEW_OR_EXPUNGE \ + (IMAP_NOTIFY_EVENT_MESSAGE_NEW | IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE) + const struct imap_arg *list, *fetch_att_list; + const char *str; + enum imap_notify_event event; + + if (imap_arg_get_atom(events, &str) && + strcasecmp(str, "NONE") == 0) { + /* no events for selected mailbox. this is also the default + when NOTIFY command doesn't specify it explicitly */ + if (events[1].type != IMAP_ARG_EOL) + return -1; /* no extra parameters */ + return 0; + } + + if (!imap_arg_get_list(events, &list)) + return -1; + if (events[1].type != IMAP_ARG_EOL) + return -1; /* no extra parameters */ + if (list->type == IMAP_ARG_EOL) + return -1; /* at least one event */ + + for (; list->type != IMAP_ARG_EOL; list++) { + if (cmd_notify_parse_event(list, &event) < 0) + return -1; + ctx->selected_events |= event; + ctx->global_used_events |= event; + + if (event == IMAP_NOTIFY_EVENT_MESSAGE_NEW && + imap_arg_get_list(&list[1], &fetch_att_list)) { + /* MessageNew: list of fetch-att */ + if (cmd_notify_parse_fetch(ctx, fetch_att_list) < 0) + return -1; + list++; + } + } + + /* if MessageNew or MessageExpunge is specified, both of them must */ + if ((ctx->selected_events & EV_NEW_OR_EXPUNGE) != 0 && + (ctx->selected_events & EV_NEW_OR_EXPUNGE) != EV_NEW_OR_EXPUNGE) { + ctx->error = "MessageNew and MessageExpunge must be together"; + return -1; + } + + /* if FlagChange or AnnotationChange is specified, + MessageNew and MessageExpunge must also be specified */ + if ((ctx->selected_events & + (IMAP_NOTIFY_EVENT_FLAG_CHANGE | + IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE)) != 0 && + (ctx->selected_events & IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE) == 0) { + ctx->error = "FlagChange requires MessageNew and MessageExpunge"; + return -1; + } + return 0; +} + +static struct imap_notify_namespace * +imap_notify_namespace_get(struct imap_notify_context *ctx, + struct mail_namespace *ns) +{ + struct imap_notify_namespace *notify_ns; + + array_foreach_modifiable(&ctx->namespaces, notify_ns) { + if (notify_ns->ns == ns) + return notify_ns; + } + notify_ns = array_append_space(&ctx->namespaces); + notify_ns->ctx = ctx; + notify_ns->ns = ns; + p_array_init(¬ify_ns->mailboxes, ctx->pool, 4); + return notify_ns; +} + +static struct imap_notify_mailboxes * +imap_notify_mailboxes_get(struct imap_notify_namespace *notify_ns, + enum imap_notify_type type, + enum imap_notify_event events) +{ + struct imap_notify_mailboxes *notify_boxes; + + array_foreach_modifiable(¬ify_ns->mailboxes, notify_boxes) { + if (notify_boxes->type == type && + notify_boxes->events == events) + return notify_boxes; + } + notify_boxes = array_append_space(¬ify_ns->mailboxes); + notify_boxes->type = type; + notify_boxes->events = events; + p_array_init(¬ify_boxes->names, notify_ns->ctx->pool, 4); + return notify_boxes; +} + +static void +cmd_notify_add_mailbox(struct imap_notify_context *ctx, + struct mail_namespace *ns, const char *name, + enum imap_notify_type type, + enum imap_notify_event events) +{ + struct imap_notify_namespace *notify_ns; + struct imap_notify_mailboxes *notify_boxes; + const char *const *names; + unsigned int i, count; + size_t cur_len, name_len = strlen(name); + char ns_sep = mail_namespace_get_sep(ns); + + if (mail_namespace_is_removable(ns)) { + /* exclude removable namespaces */ + return; + } + + if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 && + !str_begins(name, "INBOX") && + strncasecmp(name, "INBOX", 5) == 0 && + (name[5] == '\0' || name[5] == ns_sep)) { + /* we'll do only case-sensitive comparisons later, + so sanitize INBOX to be uppercase */ + name = t_strconcat("INBOX", name + 5, NULL); + } + + notify_ns = imap_notify_namespace_get(ctx, ns); + notify_boxes = imap_notify_mailboxes_get(notify_ns, type, events); + + names = array_get(¬ify_boxes->names, &count); + for (i = 0; i < count; ) { + if (strcmp(names[i], name) == 0) { + /* exact duplicate, already added */ + return; + } + if (type != IMAP_NOTIFY_TYPE_SUBTREE) + i++; + else { + /* see if one is a subtree of the other */ + cur_len = strlen(names[i]); + if (str_begins(name, names[i]) && + names[i][cur_len] == ns_sep) { + /* already matched in this subtree */ + return; + } + if (strncmp(names[i], name, name_len) == 0 && + names[i][name_len] == ns_sep) { + /* we're adding a parent, remove the child */ + array_delete(¬ify_boxes->names, i, 1); + names = array_get(¬ify_boxes->names, &count); + } else { + i++; + } + } + } + name = p_strdup(ctx->pool, name); + array_push_back(¬ify_boxes->names, &name); + + ctx->global_max_mailbox_names = + I_MAX(ctx->global_max_mailbox_names, + array_count(¬ify_boxes->names)); +} + +static void cmd_notify_add_personal(struct imap_notify_context *ctx, + enum imap_notify_event events) +{ + struct mail_namespace *ns; + + for (ns = ctx->client->user->namespaces; ns != NULL; ns = ns->next) { + if (ns->type == MAIL_NAMESPACE_TYPE_PRIVATE) { + cmd_notify_add_mailbox(ctx, ns, "", + IMAP_NOTIFY_TYPE_SUBTREE, events); + } + } +} + +static int +imap_notify_refresh_subscriptions(struct client_command_context *cmd, + struct imap_notify_context *ctx) +{ + struct mailbox_list_iterate_context *iter; + struct mail_namespace *ns; + + if (!ctx->have_subscriptions) + return 0; + + /* make sure subscriptions are refreshed at least once */ + for (ns = ctx->client->user->namespaces; ns != NULL; ns = ns->next) { + iter = mailbox_list_iter_init(ns->list, "*", MAILBOX_LIST_ITER_SELECT_SUBSCRIBED); + (void)mailbox_list_iter_next(iter); + if (mailbox_list_iter_deinit(&iter) < 0) { + client_send_list_error(cmd, ns->list); + return -1; + } + } + return 0; +} + +static void cmd_notify_add_subscribed(struct imap_notify_context *ctx, + enum imap_notify_event events) +{ + struct mail_namespace *ns; + + ctx->have_subscriptions = TRUE; + for (ns = ctx->client->user->namespaces; ns != NULL; ns = ns->next) { + cmd_notify_add_mailbox(ctx, ns, "", + IMAP_NOTIFY_TYPE_SUBSCRIBED, events); + } +} + +static void +cmd_notify_add_mailbox_namespaces(struct imap_notify_context *ctx, + const char *name, + enum imap_notify_type type, + enum imap_notify_event events) +{ + struct mail_namespace *ns; + + ns = mail_namespace_find(ctx->client->user->namespaces, name); + cmd_notify_add_mailbox(ctx, ns, name, type, events); +} + +static int +cmd_notify_add_mailboxes(struct imap_notify_context *ctx, + const struct imap_arg *arg, + enum imap_notify_type type, + enum imap_notify_event events) +{ + const struct imap_arg *list; + const char *name; + + if (imap_arg_get_astring(arg, &name)) { + cmd_notify_add_mailbox_namespaces(ctx, name, type, events); + return 0; + } + if (!imap_arg_get_list(arg, &list)) + return -1; + + for (; list->type != IMAP_ARG_EOL; list++) { + if (!imap_arg_get_astring(list, &name)) + return -1; + + cmd_notify_add_mailbox_namespaces(ctx, name, type, events); + } + return 0; +} + +static int +cmd_notify_set(struct imap_notify_context *ctx, const struct imap_arg *args) +{ + const struct imap_arg *event_group, *mailboxes, *list; + const char *str, *filter_mailboxes; + enum imap_notify_event event, event_mask; + + if (imap_arg_get_atom(args, &str) && + strcasecmp(str, "STATUS") == 0) { + /* send STATUS replies for all matched mailboxes before + NOTIFY's OK reply */ + ctx->send_immediate_status = TRUE; + args++; + } + for (; args->type != IMAP_ARG_EOL; args++) { + if (!imap_arg_get_list(args, &event_group)) + return -1; + + /* filter-mailboxes */ + if (!imap_arg_get_atom(event_group, &filter_mailboxes)) + return -1; + event_group++; + + if (strcasecmp(filter_mailboxes, "selected") == 0 || + strcasecmp(filter_mailboxes, "selected-delayed") == 0) { + /* setting events for selected mailbox. + handle specially. */ + if (ctx->selected_set) { + ctx->error = "Duplicate selected filter"; + return -1; + } + ctx->selected_set = TRUE; + if (strcasecmp(filter_mailboxes, "selected") == 0) + ctx->selected_immediate_expunges = TRUE; + if (cmd_notify_set_selected(ctx, event_group) < 0) + return -1; + continue; + } + + if (strcasecmp(filter_mailboxes, "subtree") == 0 || + strcasecmp(filter_mailboxes, "mailboxes") == 0) { + if (event_group->type == IMAP_ARG_EOL) + return -1; + mailboxes = event_group++; + /* check that the mailboxes parameter is valid */ + if (IMAP_ARG_IS_ASTRING(mailboxes)) + ; + else if (!imap_arg_get_list(mailboxes, &list)) + return -1; + else if (list->type == IMAP_ARG_EOL) { + /* should have at least one mailbox */ + return -1; + } + } else { + mailboxes = NULL; + } + + /* parse events */ + if (imap_arg_get_atom(event_group, &str) && + strcasecmp(str, "NONE") == 0) { + /* NONE is the default, ignore this */ + continue; + } + if (!imap_arg_get_list(event_group, &list) || + list[0].type == IMAP_ARG_EOL) + return -1; + + event_mask = 0; + for (; list->type != IMAP_ARG_EOL; list++) { + if (cmd_notify_parse_event(list, &event) < 0) + return -1; + event_mask |= event; + ctx->global_used_events |= event; + } + + /* we can't currently know inboxes, so treat it the + same as personal */ + if (strcasecmp(filter_mailboxes, "inboxes") == 0 || + strcasecmp(filter_mailboxes, "personal") == 0) + cmd_notify_add_personal(ctx, event_mask); + else if (strcasecmp(filter_mailboxes, "subscribed") == 0) + cmd_notify_add_subscribed(ctx, event_mask); + else if (strcasecmp(filter_mailboxes, "subtree") == 0) { + if (cmd_notify_add_mailboxes(ctx, mailboxes, + IMAP_NOTIFY_TYPE_SUBTREE, + event_mask) < 0) + return -1; + } else if (strcasecmp(filter_mailboxes, "mailboxes") == 0) { + if (cmd_notify_add_mailboxes(ctx, mailboxes, + IMAP_NOTIFY_TYPE_MAILBOX, + event_mask) < 0) + return -1; + } else { + return -1; + } + } + return 0; +} + +static void +imap_notify_box_list_noperm(struct client *client, struct mailbox *box) +{ + string_t *str = t_str_new(128); + char ns_sep = mail_namespace_get_sep(mailbox_get_namespace(box)); + enum mailbox_info_flags mailbox_flags; + + if (mailbox_list_mailbox(mailbox_get_namespace(box)->list, + mailbox_get_name(box), &mailbox_flags) < 0) + mailbox_flags = 0; + + str_append(str, "* LIST ("); + if (imap_mailbox_flags2str(str, mailbox_flags)) + str_append_c(str, ' '); + str_append(str, "\\NoAccess) \""); + if (ns_sep == '\\') + str_append_c(str, '\\'); + str_append_c(str, ns_sep); + str_append(str, "\" "); + + imap_append_astring(str, mailbox_get_vname(box)); + client_send_line(client, str_c(str)); +} + +static void +imap_notify_box_send_status(struct client_command_context *cmd, + struct imap_notify_context *ctx, + const struct mailbox_info *info) +{ + struct mailbox *box; + struct imap_status_items items; + struct imap_status_result result; + + if ((info->flags & (MAILBOX_NONEXISTENT | MAILBOX_NOSELECT)) != 0) + return; + + /* don't send STATUS to selected mailbox */ + if (cmd->client->mailbox != NULL && + mailbox_equals(cmd->client->mailbox, info->ns, info->vname)) + return; + + i_zero(&items); + i_zero(&result); + + items.flags = IMAP_STATUS_ITEM_UIDVALIDITY | IMAP_STATUS_ITEM_UIDNEXT | + IMAP_STATUS_ITEM_MESSAGES | IMAP_STATUS_ITEM_UNSEEN; + if ((ctx->global_used_events & (IMAP_NOTIFY_EVENT_FLAG_CHANGE | + IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE)) != 0) + items.flags |= IMAP_STATUS_ITEM_HIGHESTMODSEQ; + + box = mailbox_alloc(info->ns->list, info->vname, MAILBOX_FLAG_READONLY); + (void)mailbox_enable(box, client_enabled_mailbox_features(ctx->client)); + + if (imap_status_get(cmd, info->ns, info->vname, &items, &result) < 0) { + if (result.error == MAIL_ERROR_PERM) + imap_notify_box_list_noperm(ctx->client, box); + else if (result.error != MAIL_ERROR_NOTFOUND) { + client_send_line(ctx->client, + t_strconcat("* ", result.errstr, NULL)); + } + } else { + imap_status_send(ctx->client, info->vname, &items, &result); + } + mailbox_free(&box); +} + +static bool imap_notify_ns_want_status(struct imap_notify_namespace *notify_ns) +{ + const struct imap_notify_mailboxes *notify_boxes; + + array_foreach(¬ify_ns->mailboxes, notify_boxes) { + if ((notify_boxes->events & + (IMAP_NOTIFY_EVENT_MESSAGE_NEW | + IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE | + IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE | + IMAP_NOTIFY_EVENT_FLAG_CHANGE)) != 0) + return TRUE; + } + return FALSE; +} + +static void +imap_notify_ns_send_status(struct client_command_context *cmd, + struct imap_notify_context *ctx, + struct imap_notify_namespace *notify_ns) +{ + struct mailbox_list_iterate_context *iter; + const struct imap_notify_mailboxes *notify_boxes; + const struct mailbox_info *info; + + if (!imap_notify_ns_want_status(notify_ns)) + return; + + /* set _RETURN_SUBSCRIBED flag just in case IMAP_NOTIFY_TYPE_SUBSCRIBED + is used, which requires refreshing subscriptions */ + iter = mailbox_list_iter_init(notify_ns->ns->list, "*", + MAILBOX_LIST_ITER_RETURN_SUBSCRIBED | + MAILBOX_LIST_ITER_RETURN_NO_FLAGS); + while ((info = mailbox_list_iter_next(iter)) != NULL) { + array_foreach(¬ify_ns->mailboxes, notify_boxes) { + if (imap_notify_match_mailbox(notify_ns, notify_boxes, + info->vname)) { + imap_notify_box_send_status(cmd, ctx, info); + break; + } + } + } + if (mailbox_list_iter_deinit(&iter) < 0) { + client_send_line(notify_ns->ctx->client, + "* NO Mailbox listing failed"); + } +} + +static void cmd_notify_send_status(struct client_command_context *cmd, + struct imap_notify_context *ctx) +{ + struct imap_notify_namespace *notify_ns; + + array_foreach_modifiable(&ctx->namespaces, notify_ns) + imap_notify_ns_send_status(cmd, ctx, notify_ns); +} + +bool cmd_notify(struct client_command_context *cmd) +{ + struct imap_notify_context *ctx; + const struct imap_arg *args; + const char *str; + int ret = 0; + pool_t pool; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + pool = pool_alloconly_create("imap notify context", 1024); + ctx = p_new(pool, struct imap_notify_context, 1); + ctx->pool = pool; + ctx->client = cmd->client; + p_array_init(&ctx->namespaces, pool, 4); + + if (!imap_arg_get_atom(&args[0], &str)) + ret = -1; + else if (strcasecmp(str, "NONE") == 0) + ; + else if (strcasecmp(str, "SET") == 0) + ret = cmd_notify_set(ctx, args+1); + else + ret = -1; + + if (ret < 0) { + client_send_command_error(cmd, ctx->error != NULL ? ctx->error : + "Invalid arguments."); + pool_unref(&pool); + return TRUE; + } + + if ((ctx->global_used_events & UNSUPPORTED_EVENTS) != 0) { + string_t *client_error = t_str_new(128); + unsigned int i; + + str_append(client_error, "NO [BADEVENT"); + for (i = 0; i < N_ELEMENTS(imap_notify_event_names); i++) { + if ((ctx->global_used_events & (1 << i)) != 0 && + ((1 << i) & UNSUPPORTED_EVENTS) != 0) { + str_append_c(client_error, ' '); + str_append(client_error, imap_notify_event_names[i]); + } + } + str_append(client_error, "] Unsupported NOTIFY events."); + client_send_tagline(cmd, str_c(client_error)); + pool_unref(&pool); + return TRUE; + } + + if (array_count(&ctx->namespaces) == 0) { + /* selected mailbox only */ + } else if (ctx->global_max_mailbox_names > IMAP_NOTIFY_MAX_NAMES_PER_NS) { + client_send_tagline(cmd, + "NO [NOTIFICATIONOVERFLOW] Too many mailbox names"); + pool_unref(&pool); + return TRUE; + } else if (imap_notify_refresh_subscriptions(cmd, ctx) < 0) { + /* tagline already sent */ + pool_unref(&pool); + return TRUE; + } else if (imap_notify_begin(ctx) < 0) { + client_send_tagline(cmd, + "NO [NOTIFICATIONOVERFLOW] NOTIFY not supported for these mailboxes."); + pool_unref(&pool); + return TRUE; + } + if (cmd->client->notify_ctx != NULL) + imap_notify_deinit(&cmd->client->notify_ctx); + + if (ctx->send_immediate_status) + cmd_notify_send_status(cmd, ctx); + cmd->client->notify_immediate_expunges = + ctx->selected_immediate_expunges; + cmd->client->notify_count_changes = + (ctx->selected_events & IMAP_NOTIFY_EVENT_MESSAGE_NEW) != 0; + cmd->client->notify_flag_changes = + (ctx->selected_events & IMAP_NOTIFY_EVENT_FLAG_CHANGE) != 0; + + cmd->client->notify_ctx = ctx; + return cmd_sync(cmd, 0, IMAP_SYNC_FLAG_SAFE, "OK NOTIFY completed."); +} diff --git a/src/imap/cmd-rename.c b/src/imap/cmd-rename.c new file mode 100644 index 0000000..45b44fd --- /dev/null +++ b/src/imap/cmd-rename.c @@ -0,0 +1,51 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "mail-namespace.h" +#include "imap-commands.h" + +bool cmd_rename(struct client_command_context *cmd) +{ + struct mail_namespace *old_ns, *new_ns; + struct mailbox *old_box, *new_box; + const char *oldname, *newname; + size_t oldlen; + + /* <old name> <new name> */ + if (!client_read_string_args(cmd, 2, &oldname, &newname)) + return FALSE; + + old_ns = client_find_namespace(cmd, &oldname); + if (old_ns == NULL) + return TRUE; + new_ns = client_find_namespace(cmd, &newname); + if (new_ns == NULL) + return TRUE; + + if (old_ns == new_ns) { + /* disallow box -> box/child, because it may break clients and + there's really no point in doing it anyway. */ + old_ns = mailbox_list_get_namespace(old_ns->list); + oldlen = strlen(oldname); + if (str_begins(newname, oldname) && + newname[oldlen] == mail_namespace_get_sep(old_ns)) { + client_send_tagline(cmd, + "NO Can't rename mailbox under its own child."); + return TRUE; + } + } + + old_box = mailbox_alloc(old_ns->list, oldname, 0); + new_box = mailbox_alloc(new_ns->list, newname, 0); + event_add_str(cmd->global_event, "old_mailbox", + mailbox_get_vname(old_box)); + event_add_str(cmd->global_event, "new_mailbox", + mailbox_get_vname(new_box)); + if (mailbox_rename(old_box, new_box) < 0) + client_send_box_error(cmd, old_box); + else + client_send_tagline(cmd, "OK Rename completed."); + mailbox_free(&old_box); + mailbox_free(&new_box); + return TRUE; +} diff --git a/src/imap/cmd-resetkey.c b/src/imap/cmd-resetkey.c new file mode 100644 index 0000000..3475994 --- /dev/null +++ b/src/imap/cmd-resetkey.c @@ -0,0 +1,97 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-resp-code.h" +#include "imap-commands.h" +#include "imap-urlauth.h" + +static bool cmd_resetkey_all(struct client_command_context *cmd) +{ + if (imap_urlauth_reset_all_keys(cmd->client->urlauth_ctx) < 0) { + client_send_internal_error(cmd); + return TRUE; + } + + client_send_tagline(cmd, "OK All keys removed."); + return TRUE; +} + +static bool +cmd_resetkey_mailbox(struct client_command_context *cmd, + const char *mailbox, const struct imap_arg *mech_args) +{ + struct mail_namespace *ns; + enum mailbox_flags flags = MAILBOX_FLAG_READONLY; + struct mailbox *box; + + /* check mechanism arguments (we support only INTERNAL mechanism) */ + while (!IMAP_ARG_IS_EOL(mech_args)) { + const char *mechanism; + + if (imap_arg_get_astring(mech_args, &mechanism)) { + if (strcasecmp(mechanism, "INTERNAL") != 0) { + client_send_tagline(cmd, + "NO Unsupported URLAUTH mechanism."); + return TRUE; + } + } else { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + + mech_args++; + } + + /* find mailbox namespace */ + ns = client_find_namespace(cmd, &mailbox); + if (ns == NULL) + return TRUE; + + /* open mailbox */ + box = mailbox_alloc(ns->list, mailbox, flags); + event_add_str(cmd->global_event, "mailbox", mailbox_get_vname(box)); + if (mailbox_open(box) < 0) { + client_send_box_error(cmd, box); + mailbox_free(&box); + return TRUE; + } + + /* check urlauth environment and reset requested key */ + if (imap_urlauth_reset_mailbox_key(cmd->client->urlauth_ctx, box) < 0) { + client_send_internal_error(cmd); + mailbox_free(&box); + return TRUE; + } + + /* confirm success */ + /* FIXME: RFC Says: `Any current IMAP session logged in as the user + that has the mailbox selected will receive an untagged OK response + with the URLMECH status response code'. We currently don't do that + at all. We could probably do it by communicating via mailbox list + index. */ + client_send_tagline(cmd, "OK [URLMECH INTERNAL] Key removed."); + mailbox_free(&box); + return TRUE; +} + +bool cmd_resetkey(struct client_command_context *cmd) +{ + const struct imap_arg *args; + const char *mailbox; + + if (cmd->client->urlauth_ctx == NULL) { + client_send_command_error(cmd, "URLAUTH disabled."); + return TRUE; + } + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + if (IMAP_ARG_IS_EOL(&args[0])) + return cmd_resetkey_all(cmd); + else if (imap_arg_get_astring(&args[0], &mailbox)) + return cmd_resetkey_mailbox(cmd, mailbox, &args[1]); + + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; +} diff --git a/src/imap/cmd-search.c b/src/imap/cmd-search.c new file mode 100644 index 0000000..52be6f6 --- /dev/null +++ b/src/imap/cmd-search.c @@ -0,0 +1,49 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-search-args.h" +#include "imap-search.h" + +bool cmd_search(struct client_command_context *cmd) +{ + struct imap_search_context *ctx; + struct mail_search_args *sargs; + const struct imap_arg *args; + const char *charset; + int ret; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + ctx = p_new(cmd->pool, struct imap_search_context, 1); + ctx->cmd = cmd; + + if ((ret = cmd_search_parse_return_if_found(ctx, &args)) <= 0) { + /* error / waiting for unambiguity */ + return ret < 0; + } + + if (imap_arg_atom_equals(args, "CHARSET")) { + /* CHARSET specified */ + if (!imap_arg_get_astring(&args[1], &charset)) { + client_send_command_error(cmd, + "Invalid charset argument."); + imap_search_context_free(ctx); + return TRUE; + } + args += 2; + } else { + charset = "UTF-8"; + } + + ret = imap_search_args_build(cmd, args, charset, &sargs); + if (ret <= 0) { + imap_search_context_free(ctx); + return ret < 0; + } + + return imap_search_start(ctx, sargs, NULL); +} diff --git a/src/imap/cmd-select.c b/src/imap/cmd-select.c new file mode 100644 index 0000000..c9af870 --- /dev/null +++ b/src/imap/cmd-select.c @@ -0,0 +1,426 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "seq-range-array.h" +#include "time-util.h" +#include "imap-commands.h" +#include "mail-search-build.h" +#include "imap-search-args.h" +#include "imap-seqset.h" +#include "imap-fetch.h" +#include "imap-sync.h" + + +struct imap_select_context { + struct client_command_context *cmd; + struct mail_namespace *ns; + struct mailbox *box; + + struct imap_fetch_context *fetch_ctx; + + uint32_t qresync_uid_validity; + uint64_t qresync_modseq; + ARRAY_TYPE(seq_range) qresync_known_uids; + ARRAY_TYPE(uint32_t) qresync_sample_seqset; + ARRAY_TYPE(uint32_t) qresync_sample_uidset; + + bool condstore:1; +}; + +static int select_qresync_get_uids(struct imap_select_context *ctx, + const ARRAY_TYPE(seq_range) *seqset, + const ARRAY_TYPE(seq_range) *uidset) +{ + const struct seq_range *uid_range; + struct seq_range_iter seq_iter; + unsigned int i, uid_count, diff, n = 0; + uint32_t seq; + + /* change all n:m ranges to n,m and store the results */ + uid_range = array_get(uidset, &uid_count); + + seq_range_array_iter_init(&seq_iter, seqset); + i_array_init(&ctx->qresync_sample_uidset, uid_count); + i_array_init(&ctx->qresync_sample_seqset, uid_count); + for (i = 0; i < uid_count; i++) { + if (!seq_range_array_iter_nth(&seq_iter, n++, &seq)) + return -1; + array_push_back(&ctx->qresync_sample_uidset, + &uid_range[i].seq1); + array_push_back(&ctx->qresync_sample_seqset, &seq); + + diff = uid_range[i].seq2 - uid_range[i].seq1; + if (diff > 0) { + n += diff - 1; + if (!seq_range_array_iter_nth(&seq_iter, n++, &seq)) + return -1; + + array_push_back(&ctx->qresync_sample_uidset, + &uid_range[i].seq2); + array_push_back(&ctx->qresync_sample_seqset, &seq); + } + } + if (seq_range_array_iter_nth(&seq_iter, n, &seq)) + return -1; + return 0; +} + +static bool +select_parse_qresync_known_set(struct imap_select_context *ctx, + const struct imap_arg *args, + const char **error_r) +{ + ARRAY_TYPE(seq_range) seqset, uidset; + const char *str; + + t_array_init(&seqset, 32); + if (!imap_arg_get_atom(args, &str) || + imap_seq_set_nostar_parse(str, &seqset) < 0) { + *error_r = "Invalid QRESYNC known-sequence-set"; + return FALSE; + } + args++; + + t_array_init(&uidset, 32); + if (!imap_arg_get_atom(args, &str) || + imap_seq_set_nostar_parse(str, &uidset) < 0) { + *error_r = "Invalid QRESYNC known-uid-set"; + return FALSE; + } + args++; + + if (select_qresync_get_uids(ctx, &seqset, &uidset) < 0) { + *error_r = "Invalid QRESYNC sets"; + return FALSE; + } + if (!IMAP_ARG_IS_EOL(args)) { + *error_r = "Too many parameters to QRESYNC known set"; + return FALSE; + } + return TRUE; +} + +static bool +select_parse_qresync(struct imap_select_context *ctx, + const struct imap_arg *args, const char **error_r) +{ + const struct imap_arg *list_args; + const char *str; + unsigned int count; + + if (!client_has_enabled(ctx->cmd->client, imap_feature_qresync)) { + *error_r = "QRESYNC not enabled"; + return FALSE; + } + if (!imap_arg_get_list_full(args, &args, &count)) { + *error_r = "QRESYNC parameters missing"; + return FALSE; + } + + if (!imap_arg_get_atom(&args[0], &str) || + str_to_uint32(str, &ctx->qresync_uid_validity) < 0 || + !imap_arg_get_atom(&args[1], &str) || + str_to_uint64(str, &ctx->qresync_modseq) < 0) { + *error_r = "Invalid QRESYNC parameters"; + return FALSE; + } + args += 2; + + i_array_init(&ctx->qresync_known_uids, 64); + if (imap_arg_get_atom(args, &str)) { + if (imap_seq_set_nostar_parse(str, &ctx->qresync_known_uids) < 0) { + *error_r = "Invalid QRESYNC known-uids"; + return FALSE; + } + args++; + } else { + seq_range_array_add_range(&ctx->qresync_known_uids, + 1, (uint32_t)-1); + } + if (imap_arg_get_list(args, &list_args)) { + if (!select_parse_qresync_known_set(ctx, list_args, error_r)) + return FALSE; + args++; + } + if (!IMAP_ARG_IS_EOL(args)) { + *error_r = "Invalid QRESYNC parameters"; + return FALSE; + } + return TRUE; +} + +static bool +select_parse_options(struct imap_select_context *ctx, + const struct imap_arg *args, const char **error_r) +{ + const char *name; + + while (!IMAP_ARG_IS_EOL(args)) { + if (!imap_arg_get_atom(args, &name)) { + *error_r = "SELECT options contain non-atoms."; + return FALSE; + } + name = t_str_ucase(name); + args++; + + if (strcmp(name, "CONDSTORE") == 0) + ctx->condstore = TRUE; + else if (strcmp(name, "QRESYNC") == 0) { + if (!select_parse_qresync(ctx, args, error_r)) + return FALSE; + args++; + } else { + *error_r = "Unknown FETCH modifier"; + return FALSE; + } + } + return TRUE; +} + +static void select_context_free(struct imap_select_context *ctx) +{ + if (array_is_created(&ctx->qresync_known_uids)) + array_free(&ctx->qresync_known_uids); + if (array_is_created(&ctx->qresync_sample_seqset)) + array_free(&ctx->qresync_sample_seqset); + if (array_is_created(&ctx->qresync_sample_uidset)) + array_free(&ctx->qresync_sample_uidset); +} + +static void cmd_select_finish(struct imap_select_context *ctx, int ret) +{ + const char *resp_code; + + if (ret < 0) { + if (ctx->box != NULL) + mailbox_free(&ctx->box); + ctx->cmd->client->mailbox = NULL; + } else { + resp_code = mailbox_is_readonly(ctx->box) ? + "READ-ONLY" : "READ-WRITE"; + client_send_tagline(ctx->cmd, t_strdup_printf( + "OK [%s] %s completed", resp_code, + ctx->cmd->client->mailbox_examined ? "Examine" : "Select")); + } + select_context_free(ctx); +} + +static bool cmd_select_continue(struct client_command_context *cmd) +{ + struct imap_select_context *ctx = cmd->context; + int ret; + + if (imap_fetch_more(ctx->fetch_ctx, cmd) == 0) { + /* unfinished */ + return FALSE; + } + + ret = imap_fetch_end(ctx->fetch_ctx); + if (ret < 0) + client_send_box_error(ctx->cmd, ctx->box); + imap_fetch_free(&ctx->fetch_ctx); + cmd_select_finish(ctx, ret); + return TRUE; +} + +static int select_qresync(struct imap_select_context *ctx) +{ + struct imap_fetch_context *fetch_ctx; + struct mail_search_args *search_args; + struct imap_fetch_qresync_args qresync_args; + int ret; + + search_args = mail_search_build_init(); + search_args->args = p_new(search_args->pool, struct mail_search_arg, 1); + search_args->args->type = SEARCH_UIDSET; + search_args->args->value.seqset = ctx->qresync_known_uids; + imap_search_add_changed_since(search_args, ctx->qresync_modseq); + + i_zero(&qresync_args); + qresync_args.qresync_sample_seqset = &ctx->qresync_sample_seqset; + qresync_args.qresync_sample_uidset = &ctx->qresync_sample_uidset; + + if (imap_fetch_send_vanished(ctx->cmd->client, ctx->box, + search_args, &qresync_args) < 0) { + mail_search_args_unref(&search_args); + return -1; + } + + fetch_ctx = imap_fetch_alloc(ctx->cmd->client, ctx->cmd->pool, + t_strdup_printf("%s %s", ctx->cmd->name, ctx->cmd->args)); + + imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_uid_init); + imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_flags_init); + imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_modseq_init); + + imap_fetch_begin(fetch_ctx, ctx->box, search_args); + mail_search_args_unref(&search_args); + + if (imap_fetch_more(fetch_ctx, ctx->cmd) == 0) { + /* unfinished */ + ctx->fetch_ctx = fetch_ctx; + ctx->cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT; + + ctx->cmd->func = cmd_select_continue; + ctx->cmd->context = ctx; + return 0; + } + ret = imap_fetch_end(fetch_ctx); + imap_fetch_free(&fetch_ctx); + return ret < 0 ? -1 : 1; +} + +static int +select_open(struct imap_select_context *ctx, const char *mailbox, bool readonly) +{ + struct client *client = ctx->cmd->client; + struct mailbox_status status; + enum mailbox_flags flags = 0; + int ret = 0; + + if (readonly) + flags |= MAILBOX_FLAG_READONLY; + else + flags |= MAILBOX_FLAG_DROP_RECENT; + ctx->box = mailbox_alloc(ctx->ns->list, mailbox, flags); + event_add_str(ctx->cmd->global_event, "mailbox", + mailbox_get_vname(ctx->box)); + if (mailbox_open(ctx->box) < 0) { + client_send_box_error(ctx->cmd, ctx->box); + mailbox_free(&ctx->box); + return -1; + } + + ret = mailbox_enable(ctx->box, client_enabled_mailbox_features(client)); + if (ret < 0 || + mailbox_sync(ctx->box, MAILBOX_SYNC_FLAG_FULL_READ) < 0) { + client_send_box_error(ctx->cmd, ctx->box); + return -1; + } + mailbox_get_open_status(ctx->box, STATUS_MESSAGES | STATUS_RECENT | + STATUS_FIRST_UNSEEN_SEQ | STATUS_UIDVALIDITY | + STATUS_UIDNEXT | STATUS_KEYWORDS | + STATUS_HIGHESTMODSEQ, &status); + + client->mailbox = ctx->box; + client->mailbox_examined = readonly; + client->messages_count = status.messages; + client->recent_count = status.recent; + client->uidvalidity = status.uidvalidity; + client->notify_uidnext = status.uidnext; + + client_update_mailbox_flags(client, status.keywords); + client_send_mailbox_flags(client, TRUE); + + client_send_line(client, + t_strdup_printf("* %u EXISTS", status.messages)); + client_send_line(client, + t_strdup_printf("* %u RECENT", status.recent)); + + if (status.first_unseen_seq != 0) { + client_send_line(client, + t_strdup_printf("* OK [UNSEEN %u] First unseen.", + status.first_unseen_seq)); + } + + client_send_line(client, + t_strdup_printf("* OK [UIDVALIDITY %u] UIDs valid", + status.uidvalidity)); + + client_send_line(client, + t_strdup_printf("* OK [UIDNEXT %u] Predicted next UID", + status.uidnext)); + + client->nonpermanent_modseqs = status.nonpermanent_modseqs; + if (status.nonpermanent_modseqs) { + client_send_line(client, + "* OK [NOMODSEQ] No permanent modsequences"); + } else if (!status.no_modseq_tracking) { + client_send_line(client, + t_strdup_printf("* OK [HIGHESTMODSEQ %"PRIu64"] Highest", + status.highest_modseq)); + client->sync_last_full_modseq = status.highest_modseq; + } + + if (ctx->qresync_uid_validity == status.uidvalidity && + status.uidvalidity != 0 && !client->nonpermanent_modseqs) { + if ((ret = select_qresync(ctx)) < 0) { + client_send_box_error(ctx->cmd, ctx->box); + return -1; + } + } else { + ret = 1; + } + return ret; +} + +static void close_selected_mailbox(struct client *client) +{ + if (client->mailbox == NULL) + return; + + imap_client_close_mailbox(client); + /* CLOSED response is required by QRESYNC */ + client_send_line(client, "* OK [CLOSED] Previous mailbox closed."); +} + +bool cmd_select_full(struct client_command_context *cmd, bool readonly) +{ + struct client *client = cmd->client; + struct imap_select_context *ctx; + const struct imap_arg *args, *list_args; + const char *mailbox, *client_error; + int ret; + + /* <mailbox> [(optional parameters)] */ + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + if (!imap_arg_get_astring(args, &mailbox)) { + close_selected_mailbox(client); + client_send_command_error(cmd, "Invalid arguments."); + return FALSE; + } + + ctx = p_new(cmd->pool, struct imap_select_context, 1); + ctx->cmd = cmd; + ctx->ns = client_find_namespace_full(cmd->client, &mailbox, &client_error); + if (ctx->ns == NULL) { + /* send * OK [CLOSED] before the tagged reply */ + close_selected_mailbox(client); + client_send_tagline(cmd, client_error); + return TRUE; + } + + if (imap_arg_get_list(&args[1], &list_args)) { + if (!select_parse_options(ctx, list_args, &client_error)) { + select_context_free(ctx); + /* send * OK [CLOSED] before the tagged reply */ + close_selected_mailbox(client); + client_send_command_error(ctx->cmd, client_error); + return TRUE; + } + } + + i_assert(client->mailbox_change_lock == NULL); + client->mailbox_change_lock = cmd; + + close_selected_mailbox(client); + + if (ctx->condstore) { + /* Enable while no mailbox is opened to avoid sending + HIGHESTMODSEQ for previously opened mailbox */ + client_enable(client, imap_feature_condstore); + } + + ret = select_open(ctx, mailbox, readonly); + if (ret == 0) + return FALSE; + cmd_select_finish(ctx, ret); + return TRUE; +} + +bool cmd_select(struct client_command_context *cmd) +{ + return cmd_select_full(cmd, FALSE); +} diff --git a/src/imap/cmd-setmetadata.c b/src/imap/cmd-setmetadata.c new file mode 100644 index 0000000..247afce --- /dev/null +++ b/src/imap/cmd-setmetadata.c @@ -0,0 +1,378 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "ioloop.h" +#include "istream.h" +#include "istream-seekable.h" +#include "ostream.h" +#include "str.h" +#include "imap-metadata.h" +#include "mail-storage-private.h" + +#define METADATA_MAX_INMEM_SIZE (1024*128) + +struct imap_setmetadata_context { + struct client_command_context *cmd; + struct imap_parser *parser; + + struct mailbox *box; + struct imap_metadata_transaction *trans; + + char *entry_name; + uoff_t entry_value_len; + struct istream *input; + bool failed; + bool cmd_error_sent; + bool storage_failure; +}; + +static void cmd_setmetadata_deinit(struct imap_setmetadata_context *ctx) +{ + o_stream_set_flush_callback(ctx->cmd->client->output, + client_output, ctx->cmd->client); + + ctx->cmd->client->input_lock = NULL; + imap_parser_unref(&ctx->parser); + if (ctx->trans != NULL) + imap_metadata_transaction_rollback(&ctx->trans); + if (ctx->box != NULL && ctx->box != ctx->cmd->client->mailbox) + mailbox_free(&ctx->box); + i_free(ctx->entry_name); +} + +static int +cmd_setmetadata_parse_entryvalue(struct imap_setmetadata_context *ctx, + const char **entry_r, + const struct imap_arg **value_r) +{ + const struct imap_arg *args; + const char *name, *client_error; + enum imap_parser_error parse_error; + int ret; + + /* parse the entry name */ + ret = imap_parser_read_args(ctx->parser, 1, + IMAP_PARSE_FLAG_INSIDE_LIST, &args); + if (ret >= 0) { + if (ret == 0) { + /* ')' found */ + *entry_r = NULL; + return 1; + } + if (!imap_arg_get_astring(args, &name)) { + client_send_command_error(ctx->cmd, + "Entry name isn't astring"); + return -1; + } + + ret = imap_parser_read_args(ctx->parser, 2, + IMAP_PARSE_FLAG_INSIDE_LIST | + IMAP_PARSE_FLAG_LITERAL_SIZE | + IMAP_PARSE_FLAG_LITERAL8, &args); + } + if (ret < 0) { + if (ret == -2) + return 0; + client_error = imap_parser_get_error(ctx->parser, &parse_error); + switch (parse_error) { + case IMAP_PARSE_ERROR_NONE: + i_unreached(); + case IMAP_PARSE_ERROR_LITERAL_TOO_BIG: + client_disconnect_with_error(ctx->cmd->client, + client_error); + break; + default: + client_send_command_error(ctx->cmd, client_error); + break; + } + return -1; + } + if (args[1].type == IMAP_ARG_EOL) { + client_send_command_error(ctx->cmd, "Entry value missing"); + return -1; + } + if (args[1].type == IMAP_ARG_LIST) { + client_send_command_error(ctx->cmd, "Entry value can't be a list"); + return -1; + } + if (!ctx->cmd_error_sent && + !imap_metadata_verify_entry_name(name, &client_error)) { + client_send_command_error(ctx->cmd, client_error); + ctx->cmd_error_sent = TRUE; + } + if (ctx->cmd_error_sent) { + ctx->cmd->param_error = FALSE; + ctx->cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT; + + ctx->failed = TRUE; + if (args[1].type == IMAP_ARG_LITERAL_SIZE) { + /* client won't see "+ OK", so we can abort + immediately */ + ctx->cmd->client->input_skip_line = FALSE; + return -1; + } + } + + /* entry names are case-insensitive. handle this by using only + lowercase names. */ + *entry_r = t_str_lcase(name); + *value_r = &args[1]; + return 1; +} + +static int +cmd_setmetadata_entry_read_stream(struct imap_setmetadata_context *ctx) +{ + const unsigned char *data; + size_t size; + struct mail_attribute_value value; + int ret; + + while ((ret = i_stream_read_more(ctx->input, &data, &size)) > 0) + i_stream_skip(ctx->input, size); + if (ret == 0) + return 0; + + if (ctx->input->v_offset != ctx->entry_value_len) { + /* client disconnected */ + i_assert(ctx->input->eof); + return -1; + } + + /* finished reading the value */ + i_stream_seek(ctx->input, 0); + + if (ctx->failed) { + i_stream_unref(&ctx->input); + return 1; + } + + i_zero(&value); + value.value_stream = ctx->input; + if (imap_metadata_set(ctx->trans, ctx->entry_name, &value) < 0) { + /* delay reporting the failure so we'll finish + reading the command input */ + ctx->storage_failure = TRUE; + ctx->failed = TRUE; + } + i_stream_unref(&ctx->input); + return 1; +} + +static int +cmd_setmetadata_entry(struct imap_setmetadata_context *ctx, + const char *entry_name, + const struct imap_arg *entry_value) +{ + struct istream *inputs[2]; + struct mail_attribute_value value; + string_t *path; + int ret; + + switch (entry_value->type) { + case IMAP_ARG_NIL: + case IMAP_ARG_ATOM: + case IMAP_ARG_STRING: + /* we have the value already */ + if (ctx->failed) + return 1; + i_zero(&value); + /* NOTE: The RFC doesn't allow atoms as value, but since + Dovecot has traditionally supported it this is kept for + backwards compatibility just in case some client is + using it. */ + if (entry_value->type == IMAP_ARG_NIL) + ; + else if (!imap_arg_get_atom(entry_value, &value.value)) + value.value = imap_arg_as_nstring(entry_value); + ret = imap_metadata_set(ctx->trans, entry_name, &value); + if (ret < 0) { + /* delay reporting the failure so we'll finish + reading the command input */ + ctx->storage_failure = TRUE; + ctx->failed = TRUE; + } + return 1; + case IMAP_ARG_LITERAL_SIZE: + o_stream_nsend(ctx->cmd->client->output, "+ OK\r\n", 6); + o_stream_uncork(ctx->cmd->client->output); + o_stream_cork(ctx->cmd->client->output); + /* fall through */ + case IMAP_ARG_LITERAL_SIZE_NONSYNC: + i_free(ctx->entry_name); + ctx->entry_name = i_strdup(entry_name); + ctx->entry_value_len = imap_arg_as_literal_size(entry_value); + + inputs[0] = i_stream_create_limit(ctx->cmd->client->input, + ctx->entry_value_len); + inputs[1] = NULL; + + path = t_str_new(128); + mail_user_set_get_temp_prefix(path, ctx->cmd->client->user->set); + ctx->input = i_stream_create_seekable_path(inputs, + METADATA_MAX_INMEM_SIZE, str_c(path)); + i_stream_set_name(ctx->input, i_stream_get_name(inputs[0])); + i_stream_unref(&inputs[0]); + return cmd_setmetadata_entry_read_stream(ctx); + case IMAP_ARG_LITERAL: + case IMAP_ARG_LIST: + case IMAP_ARG_EOL: + break; + } + i_unreached(); +} + +static bool cmd_setmetadata_continue(struct client_command_context *cmd) +{ + struct imap_setmetadata_context *ctx = cmd->context; + const char *entry, *client_error; + enum mail_error error; + const struct imap_arg *value; + int ret; + + if (cmd->cancel) { + cmd_setmetadata_deinit(ctx); + return TRUE; + } + + if (ctx->input != NULL) { + if ((ret = cmd_setmetadata_entry_read_stream(ctx)) == 0) + return FALSE; + if (ret < 0) { + cmd_setmetadata_deinit(ctx); + return TRUE; + } + } + + while ((ret = cmd_setmetadata_parse_entryvalue(ctx, &entry, &value)) > 0 && + entry != NULL) { + ret = ctx->failed ? 1 : + cmd_setmetadata_entry(ctx, entry, value); + imap_parser_reset(ctx->parser); + if (ret <= 0) + break; + } + if (ret == 0) + return 0; + + if (ret < 0 || ctx->cmd_error_sent) { + /* already sent the error to client */ ; + } else if (ctx->storage_failure) { + if (ctx->box == NULL) + client_disconnect_if_inconsistent(cmd->client); + client_error = imap_metadata_transaction_get_last_error + (ctx->trans, &error); + client_send_tagline(cmd, + imap_get_error_string(cmd, client_error, error)); + } else if (imap_metadata_transaction_commit(&ctx->trans, + &error, &client_error) < 0) { + if (ctx->box == NULL) + client_disconnect_if_inconsistent(cmd->client); + client_send_tagline(cmd, + imap_get_error_string(cmd, client_error, error)); + } else { + client_send_tagline(cmd, "OK Setmetadata completed."); + } + cmd_setmetadata_deinit(ctx); + return TRUE; +} + +static bool +cmd_setmetadata_start(struct imap_setmetadata_context *ctx) +{ + struct client_command_context *cmd = ctx->cmd; + struct client *client = cmd->client; + + imap_metadata_transaction_validated_only(ctx->trans, + !cmd->client->set->imap_metadata); + /* we support large literals, so read the values from client + asynchronously the same way as APPEND does. */ + client->input_lock = cmd; + ctx->parser = imap_parser_create(client->input, client->output, + client->set->imap_max_line_length); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(ctx->parser); + o_stream_unset_flush_callback(client->output); + + cmd->func = cmd_setmetadata_continue; + cmd->context = ctx; + return cmd_setmetadata_continue(cmd); +} + +static bool +cmd_setmetadata_server(struct imap_setmetadata_context *ctx) +{ + ctx->trans = imap_metadata_transaction_begin_server(ctx->cmd->client->user); + return cmd_setmetadata_start(ctx); +} + +static bool +cmd_setmetadata_mailbox(struct imap_setmetadata_context *ctx, + const char *mailbox) +{ + struct client_command_context *cmd = ctx->cmd; + struct client *client = cmd->client; + struct mail_namespace *ns; + + ns = client_find_namespace(cmd, &mailbox); + if (ns == NULL) + return TRUE; + + if (client->mailbox != NULL && !client->mailbox_examined && + mailbox_equals(client->mailbox, ns, mailbox)) + ctx->box = client->mailbox; + else { + ctx->box = mailbox_alloc(ns->list, mailbox, + MAILBOX_FLAG_ATTRIBUTE_SESSION); + enum mailbox_existence existence; + if (mailbox_exists(ctx->box, TRUE, &existence) < 0) { + client_send_box_error(cmd, ctx->box); + mailbox_free(&ctx->box); + return TRUE; + } else if (existence == MAILBOX_EXISTENCE_NONE) { + const char *err = t_strdup_printf(MAIL_ERRSTR_MAILBOX_NOT_FOUND, + mailbox_get_vname(ctx->box)); + mail_storage_set_error(ctx->box->storage, MAIL_ERROR_NOTFOUND, err); + client_send_box_error(cmd, ctx->box); + mailbox_free(&ctx->box); + return TRUE; + } + } + event_add_str(ctx->cmd->global_event, "mailbox", + mailbox_get_vname(ctx->box)); + ctx->trans = imap_metadata_transaction_begin(ctx->box); + return cmd_setmetadata_start(ctx); +} + +bool cmd_setmetadata(struct client_command_context *cmd) +{ + struct imap_setmetadata_context *ctx; + const struct imap_arg *args; + const char *mailbox; + int ret; + + ret = imap_parser_read_args(cmd->parser, 2, + IMAP_PARSE_FLAG_STOP_AT_LIST, &args); + if (ret == -1) { + client_send_command_error(cmd, NULL); + return TRUE; + } + if (ret == -2) + return FALSE; + if (!imap_arg_get_astring(&args[0], &mailbox) || + args[1].type != IMAP_ARG_LIST) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + + ctx = p_new(cmd->pool, struct imap_setmetadata_context, 1); + ctx->cmd = cmd; + ctx->cmd->context = ctx; + + if (mailbox[0] == '\0') { + /* server attribute */ + return cmd_setmetadata_server(ctx); + } + + return cmd_setmetadata_mailbox(ctx, mailbox); +} diff --git a/src/imap/cmd-sort.c b/src/imap/cmd-sort.c new file mode 100644 index 0000000..6515a67 --- /dev/null +++ b/src/imap/cmd-sort.c @@ -0,0 +1,142 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "buffer.h" +#include "imap-commands.h" +#include "imap-search-args.h" +#include "imap-search.h" + +struct sort_name { + enum mail_sort_type type; + const char *name; +}; + +static struct sort_name sort_names[] = { + { MAIL_SORT_ARRIVAL, "arrival" }, + { MAIL_SORT_CC, "cc" }, + { MAIL_SORT_DATE, "date" }, + { MAIL_SORT_FROM, "from" }, + { MAIL_SORT_SIZE, "size" }, + { MAIL_SORT_SUBJECT, "subject" }, + { MAIL_SORT_TO, "to" }, + { MAIL_SORT_RELEVANCY, "x-score" }, /* FIXME: obsolete */ + { MAIL_SORT_RELEVANCY, "relevancy" }, + { MAIL_SORT_DISPLAYFROM, "displayfrom" }, + { MAIL_SORT_DISPLAYTO, "displayto" }, + + { MAIL_SORT_END, NULL } +}; + +static int +get_sort_program(struct client_command_context *cmd, + const struct imap_arg *args, + enum mail_sort_type program[MAX_SORT_PROGRAM_SIZE]) +{ + enum mail_sort_type mask = 0; + const char *arg; + unsigned int i, pos; + bool reverse, last_reverse; + + if (IMAP_ARG_IS_EOL(args)) { + /* empty list */ + client_send_command_error(cmd, "Empty sort program."); + return -1; + } + + pos = 0; reverse = last_reverse = FALSE; + for (; imap_arg_get_astring(args, &arg); args++) { + last_reverse = strcasecmp(arg, "reverse") == 0; + if (last_reverse) { + reverse = !reverse; + continue; + } + + for (i = 0; sort_names[i].type != MAIL_SORT_END; i++) { + if (strcasecmp(arg, sort_names[i].name) == 0) + break; + } + + if (sort_names[i].type == MAIL_SORT_END) { + client_send_command_error(cmd, t_strconcat( + "Unknown sort argument: ", arg, NULL)); + return -1; + } + + if ((mask & sort_names[i].type) != 0) + continue; + mask |= sort_names[i].type; + + /* @UNSAFE: mask check should prevent us from ever + overflowing */ + i_assert(pos < MAX_SORT_PROGRAM_SIZE-1); + program[pos++] = sort_names[i].type | + (reverse ? MAIL_SORT_FLAG_REVERSE : 0); + reverse = FALSE; + } + if (last_reverse) { + client_send_command_error(cmd, "Sort list ends with REVERSE."); + return -1; + } + program[pos] = MAIL_SORT_END; + + if (!IMAP_ARG_IS_EOL(args)) { + client_send_command_error(cmd, + "Invalid sort list argument."); + return -1; + } + + return 0; +} + +bool cmd_sort(struct client_command_context *cmd) +{ + struct imap_search_context *ctx; + struct mail_search_args *sargs; + enum mail_sort_type sort_program[MAX_SORT_PROGRAM_SIZE]; + const struct imap_arg *args, *list_args; + const char *charset; + int ret; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + ctx = p_new(cmd->pool, struct imap_search_context, 1); + ctx->cmd = cmd; + + if ((ret = cmd_search_parse_return_if_found(ctx, &args)) <= 0) { + /* error / waiting for unambiguity */ + return ret < 0; + } + + /* sort program */ + if (!imap_arg_get_list(args, &list_args)) { + client_send_command_error(cmd, "Invalid sort argument."); + imap_search_context_free(ctx); + return TRUE; + } + + if (get_sort_program(cmd, list_args, sort_program) < 0) { + imap_search_context_free(ctx); + return TRUE; + } + args++; + + /* charset */ + if (!imap_arg_get_astring(args, &charset)) { + client_send_command_error(cmd, "Invalid charset argument."); + imap_search_context_free(ctx); + return TRUE; + } + args++; + + ret = imap_search_args_build(cmd, args, charset, &sargs); + if (ret <= 0) { + imap_search_context_free(ctx); + return ret < 0; + } + + return imap_search_start(ctx, sargs, sort_program); +} diff --git a/src/imap/cmd-status.c b/src/imap/cmd-status.c new file mode 100644 index 0000000..69c0a51 --- /dev/null +++ b/src/imap/cmd-status.c @@ -0,0 +1,54 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-resp-code.h" +#include "imap-commands.h" +#include "imap-sync.h" +#include "imap-status.h" + +bool cmd_status(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + const struct imap_arg *args, *list_args; + struct imap_status_items items; + struct imap_status_result result; + struct mail_namespace *ns; + const char *mailbox, *orig_mailbox; + bool selected_mailbox; + + /* <mailbox> <status items> */ + if (!client_read_args(cmd, 2, 0, &args)) + return FALSE; + + if (!imap_arg_get_astring(&args[0], &mailbox) || + !imap_arg_get_list(&args[1], &list_args)) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + + /* get the items client wants */ + if (imap_status_parse_items(cmd, list_args, &items) < 0) + return TRUE; + + orig_mailbox = mailbox; + ns = client_find_namespace(cmd, &mailbox); + if (ns == NULL) + return TRUE; + + event_add_str(cmd->global_event, "mailbox", mailbox); + selected_mailbox = client->mailbox != NULL && + mailbox_equals(client->mailbox, ns, mailbox); + if (imap_status_get(cmd, ns, mailbox, &items, &result) < 0) { + client_send_tagline(cmd, result.errstr); + return TRUE; + } + + imap_status_send(client, orig_mailbox, &items, &result); + if (!selected_mailbox) + client_send_tagline(cmd, "OK Status completed."); + else { + client_send_tagline(cmd, "OK ["IMAP_RESP_CODE_CLIENTBUG"] " + "Status on selected mailbox completed."); + } + return TRUE; +} diff --git a/src/imap/cmd-store.c b/src/imap/cmd-store.c new file mode 100644 index 0000000..126c042 --- /dev/null +++ b/src/imap/cmd-store.c @@ -0,0 +1,261 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "seq-range-array.h" +#include "str.h" +#include "imap-commands.h" +#include "imap-search-args.h" +#include "imap-util.h" + + +struct imap_store_context { + struct client_command_context *cmd; + uint64_t max_modseq; + + enum mail_flags flags; + struct mail_keywords *keywords; + + enum modify_type modify_type; + bool silent; +}; + +static bool +get_modify_type(struct imap_store_context *ctx, const char *type) +{ + if (*type == '+') { + ctx->modify_type = MODIFY_ADD; + type++; + } else if (*type == '-') { + ctx->modify_type = MODIFY_REMOVE; + type++; + } else { + ctx->modify_type = MODIFY_REPLACE; + } + + if (strncasecmp(type, "FLAGS", 5) != 0) + return FALSE; + + ctx->silent = strcasecmp(type+5, ".SILENT") == 0; + if (!ctx->silent && type[5] != '\0') + return FALSE; + return TRUE; +} + +static bool +store_parse_modifiers(struct imap_store_context *ctx, + const struct imap_arg *args) +{ + const char *name, *value; + + for (; !IMAP_ARG_IS_EOL(args); args += 2) { + if (!imap_arg_get_atom(&args[0], &name) || + !imap_arg_get_atom(&args[1], &value)) { + client_send_command_error(ctx->cmd, + "Invalid STORE modifiers."); + return FALSE; + } + + if (strcasecmp(name, "UNCHANGEDSINCE") == 0) { + if (ctx->cmd->client->nonpermanent_modseqs) { + client_send_command_error(ctx->cmd, + "STORE UNCHANGEDSINCE can't be used with non-permanent modseqs"); + return FALSE; + } + if (str_to_uint64(value, &ctx->max_modseq) < 0) { + client_send_command_error(ctx->cmd, + "Invalid modseq"); + return FALSE; + } + client_enable(ctx->cmd->client, imap_feature_condstore); + } else { + client_send_command_error(ctx->cmd, + "Unknown STORE modifier"); + return FALSE; + } + } + return TRUE; +} + +static bool +store_parse_args(struct imap_store_context *ctx, const struct imap_arg *args) +{ + struct client_command_context *cmd = ctx->cmd; + const struct imap_arg *list_args; + const char *type; + const char *const *keywords_list = NULL; + + ctx->max_modseq = (uint64_t)-1; + if (imap_arg_get_list(args, &list_args)) { + if (!store_parse_modifiers(ctx, list_args)) + return FALSE; + args++; + } + + if (!imap_arg_get_astring(args, &type) || + !get_modify_type(ctx, type)) { + client_send_command_error(cmd, "Invalid arguments."); + return FALSE; + } + args++; + + if (imap_arg_get_list(args, &list_args)) { + if (!client_parse_mail_flags(cmd, list_args, + &ctx->flags, &keywords_list)) + return FALSE; + } else { + if (!client_parse_mail_flags(cmd, args, + &ctx->flags, &keywords_list)) + return FALSE; + } + + if (keywords_list != NULL || ctx->modify_type == MODIFY_REPLACE) { + if (mailbox_keywords_create(cmd->client->mailbox, keywords_list, + &ctx->keywords) < 0) { + /* invalid keywords */ + client_send_box_error(cmd, cmd->client->mailbox); + return FALSE; + } + } + return TRUE; +} + +bool cmd_store(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + const struct imap_arg *args; + struct mail_search_args *search_args; + struct mail_search_context *search_ctx; + struct mailbox_transaction_context *t; + struct mail *mail; + struct imap_store_context ctx; + ARRAY_TYPE(seq_range) modified_set, uids; + enum mailbox_transaction_flags flags = 0; + enum imap_sync_flags imap_sync_flags = 0; + const char *set, *reply, *tagged_reply; + string_t *str; + int ret; + bool update_deletes; + unsigned int deleted_count; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + if (!imap_arg_get_atom(args, &set)) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + ret = imap_search_get_seqset(cmd, set, cmd->uid, &search_args); + if (ret <= 0) + return ret < 0; + + i_zero(&ctx); + ctx.cmd = cmd; + if (!store_parse_args(&ctx, ++args)) { + mail_search_args_unref(&search_args); + return TRUE; + } + + if (client->mailbox_examined) { + mail_search_args_unref(&search_args); + if (ctx.max_modseq < (uint64_t)-1) + reply = "NO CONDSTORE failed: Mailbox is read-only."; + else + reply = "OK Store ignored with read-only mailbox."; + return cmd_sync(cmd, MAILBOX_SYNC_FLAG_FAST | + (cmd->uid ? 0 : MAILBOX_SYNC_FLAG_NO_EXPUNGES), + 0, reply); + } + + if (ctx.silent) + flags |= MAILBOX_TRANSACTION_FLAG_HIDE; + if (ctx.max_modseq < (uint64_t)-1) { + /* update modseqs so we can check them early */ + flags |= MAILBOX_TRANSACTION_FLAG_REFRESH; + } + + t = mailbox_transaction_begin(client->mailbox, flags, + imap_client_command_get_reason(cmd)); + + search_ctx = mailbox_search_init(t, search_args, NULL, + MAIL_FETCH_FLAGS, NULL); + mail_search_args_unref(&search_args); + + i_array_init(&modified_set, 64); + if (ctx.max_modseq < (uint64_t)-1) { + /* STORE UNCHANGEDSINCE is being used */ + mailbox_transaction_set_max_modseq(t, ctx.max_modseq, + &modified_set); + } + + update_deletes = (ctx.flags & MAIL_DELETED) != 0 && + ctx.modify_type != MODIFY_REMOVE; + deleted_count = 0; + while (mailbox_search_next(search_ctx, &mail)) { + if (ctx.max_modseq < (uint64_t)-1) { + /* check early so there's less work for transaction + commit if something has to be cancelled */ + if (mail_get_modseq(mail) > ctx.max_modseq) { + seq_range_array_add(&modified_set, mail->seq); + continue; + } + } + if (update_deletes) { + if ((mail_get_flags(mail) & MAIL_DELETED) == 0) + deleted_count++; + } + if (ctx.modify_type == MODIFY_REPLACE || ctx.flags != 0) + mail_update_flags(mail, ctx.modify_type, ctx.flags); + if (ctx.modify_type == MODIFY_REPLACE || ctx.keywords != NULL) { + mail_update_keywords(mail, ctx.modify_type, + ctx.keywords); + } + } + + if (ctx.keywords != NULL) + mailbox_keywords_unref(&ctx.keywords); + + ret = mailbox_search_deinit(&search_ctx); + if (ret < 0) + mailbox_transaction_rollback(&t); + else + ret = mailbox_transaction_commit(&t); + if (ret < 0) { + array_free(&modified_set); + client_send_box_error(cmd, client->mailbox); + return TRUE; + } + client->deleted_count += deleted_count; + + if (array_count(&modified_set) == 0) + tagged_reply = "OK Store completed."; + else { + if (cmd->uid) { + i_array_init(&uids, array_count(&modified_set)*2); + mailbox_get_uid_range(client->mailbox, &modified_set, + &uids); + array_free(&modified_set); + modified_set = uids; + } + str = str_new(cmd->pool, 256); + str_append(str, "OK [MODIFIED "); + imap_write_seq_range(str, &modified_set); + str_append(str, "] Conditional store failed."); + tagged_reply = str_c(str); + } + array_free(&modified_set); + + /* With UID STORE we have to return UID for the flags as well. + Unfortunately we don't have the ability to separate those + flag changes that were caused by UID STORE and those that + came externally, so we'll just send the UID for all flag + changes that we see. */ + if (cmd->uid && (!ctx.silent || + client_has_enabled(client, imap_feature_condstore))) + imap_sync_flags |= IMAP_SYNC_FLAG_SEND_UID; + + return cmd_sync(cmd, (cmd->uid ? 0 : MAILBOX_SYNC_FLAG_NO_EXPUNGES), + imap_sync_flags, tagged_reply); +} diff --git a/src/imap/cmd-subscribe.c b/src/imap/cmd-subscribe.c new file mode 100644 index 0000000..03c2684 --- /dev/null +++ b/src/imap/cmd-subscribe.c @@ -0,0 +1,87 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-utf7.h" +#include "imap-commands.h" +#include "mail-namespace.h" + +static bool +subscribe_is_valid_name(struct client_command_context *cmd, struct mailbox *box) +{ + enum mailbox_existence existence; + + if (mailbox_exists(box, TRUE, &existence) < 0) { + client_send_box_error(cmd, box); + return FALSE; + } + if (existence == MAILBOX_EXISTENCE_NONE) { + client_send_tagline(cmd, t_strdup_printf( + "NO "MAIL_ERRSTR_MAILBOX_NOT_FOUND, + mailbox_get_vname(box))); + return FALSE; + } + return TRUE; +} + +static bool str_ends_with_char(const char *str, char c) +{ + size_t len = strlen(str); + + return len > 0 && str[len-1] == c; +} + +bool cmd_subscribe_full(struct client_command_context *cmd, bool subscribe) +{ + struct mail_namespace *ns; + struct mailbox *box, *box2; + const char *mailbox, *orig_mailbox; + bool unsubscribed_mailbox2; + char sep; + + /* <mailbox> */ + if (!client_read_string_args(cmd, 1, &mailbox)) + return FALSE; + orig_mailbox = mailbox; + + ns = client_find_namespace(cmd, &mailbox); + if (ns == NULL) + return TRUE; + + box = mailbox_alloc(ns->list, mailbox, 0); + event_add_str(cmd->global_event, "mailbox", mailbox_get_vname(box)); + if (subscribe) { + if (!subscribe_is_valid_name(cmd, box)) { + mailbox_free(&box); + return TRUE; + } + } + + sep = mail_namespace_get_sep(ns); + unsubscribed_mailbox2 = FALSE; + if (!subscribe && + str_ends_with_char(orig_mailbox, sep) && + !str_ends_with_char(mailbox, sep)) { + /* try to unsubscribe both "box" and "box/" */ + const char *name2 = t_strdup_printf("%s%c", mailbox, sep); + box2 = mailbox_alloc(ns->list, name2, 0); + if (mailbox_set_subscribed(box2, FALSE) == 0) + unsubscribed_mailbox2 = TRUE; + mailbox_free(&box2); + } + + if (mailbox_set_subscribed(box, subscribe) < 0 && + !unsubscribed_mailbox2) { + client_send_box_error(cmd, box); + } else { + client_send_tagline(cmd, subscribe ? + "OK Subscribe completed." : + "OK Unsubscribe completed."); + } + mailbox_free(&box); + return TRUE; +} + +bool cmd_subscribe(struct client_command_context *cmd) +{ + return cmd_subscribe_full(cmd, TRUE); +} diff --git a/src/imap/cmd-thread.c b/src/imap/cmd-thread.c new file mode 100644 index 0000000..64388d5 --- /dev/null +++ b/src/imap/cmd-thread.c @@ -0,0 +1,294 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "str.h" +#include "ostream.h" +#include "imap-base-subject.h" +#include "imap-commands.h" +#include "imap-search-args.h" +#include "mail-thread.h" + +static int imap_thread_write(struct mail_thread_iterate_context *iter, + string_t *str, bool root) +{ + const struct mail_thread_child_node *node; + struct mail_thread_iterate_context *child_iter; + unsigned int count; + int ret = 0; + + count = mail_thread_iterate_count(iter); + if (count == 0) + return 0; + + if (count == 1 && !root) { + /* only one child - special case to avoid extra parenthesis */ + node = mail_thread_iterate_next(iter, &child_iter); + str_printfa(str, "%u", node->uid); + if (child_iter != NULL) { + str_append_c(str, ' '); + T_BEGIN { + ret = imap_thread_write(child_iter, str, FALSE); + } T_END; + if (mail_thread_iterate_deinit(&child_iter) < 0) + return -1; + } + return ret; + } + + while ((node = mail_thread_iterate_next(iter, &child_iter)) != NULL) { + if (child_iter == NULL) { + /* no children */ + str_printfa(str, "(%u)", node->uid); + } else { + /* node with children */ + str_append_c(str, '('); + if (node->uid != 0) + str_printfa(str, "%u ", node->uid); + T_BEGIN { + ret = imap_thread_write(child_iter, str, FALSE); + } T_END; + if (mail_thread_iterate_deinit(&child_iter) < 0 || + ret < 0) + return -1; + str_append_c(str, ')'); + } + } + return 0; +} + +static int +imap_thread_write_reply(struct mail_thread_context *ctx, string_t *str, + enum mail_thread_type thread_type, bool write_seqs) +{ + struct mail_thread_iterate_context *iter; + int ret; + + iter = mail_thread_iterate_init(ctx, thread_type, write_seqs); + str_append(str, "* THREAD "); + T_BEGIN { + ret = imap_thread_write(iter, str, TRUE); + } T_END; + if (mail_thread_iterate_deinit(&iter) < 0) + ret = -1; + + str_append(str, "\r\n"); + return ret; +} + +static int imap_thread(struct client_command_context *cmd, + struct mail_search_args *search_args, + enum mail_thread_type thread_type) +{ + struct mail_thread_context *ctx; + string_t *str; + int ret; + + i_assert(thread_type == MAIL_THREAD_REFERENCES || + thread_type == MAIL_THREAD_REFS); + + str = str_new(default_pool, 1024); + ret = mail_thread_init(cmd->client->mailbox, + search_args, &ctx); + if (ret == 0) { + ret = imap_thread_write_reply(ctx, str, thread_type, + !cmd->uid); + mail_thread_deinit(&ctx); + } + + if (ret == 0) + o_stream_nsend(cmd->client->output, str_data(str), str_len(str)); + str_free(&str); + return ret; +} + +struct orderedsubject_thread { + time_t timestamp; + ARRAY_TYPE(uint32_t) msgs; +}; + +static int orderedsubject_thread_cmp(const struct orderedsubject_thread *t1, + const struct orderedsubject_thread *t2) +{ + const uint32_t *m1, *m2; + + if (t1->timestamp < t2->timestamp) + return -1; + if (t1->timestamp > t2->timestamp) + return 1; + + m1 = array_front(&t1->msgs); + m2 = array_front(&t2->msgs); + if (*m1 < *m2) + return -1; + if (*m1 > *m2) + return 1; + i_unreached(); +} + +static void +imap_orderedsubject_thread_write(struct ostream *output, string_t *reply, + const struct orderedsubject_thread *thread) +{ + const uint32_t *msgs; + unsigned int i, count; + + if (str_len(reply) > 128-10) { + o_stream_nsend(output, str_data(reply), str_len(reply)); + str_truncate(reply, 0); + } + + msgs = array_get(&thread->msgs, &count); + switch (count) { + case 1: + str_printfa(reply, "(%u)", msgs[0]); + break; + case 2: + str_printfa(reply, "(%u %u)", msgs[0], msgs[1]); + break; + default: + /* (1 (2)(3)) */ + str_printfa(reply, "(%u ", msgs[0]); + for (i = 1; i < count; i++) { + if (str_len(reply) > 128-10) { + o_stream_nsend(output, str_data(reply), + str_len(reply)); + str_truncate(reply, 0); + } + str_printfa(reply, "(%u)", msgs[i]); + } + str_append_c(reply, ')'); + } +} + +static int imap_thread_orderedsubject(struct client_command_context *cmd, + struct mail_search_args *search_args) +{ + static const enum mail_sort_type sort_program[] = { + MAIL_SORT_SUBJECT, + MAIL_SORT_DATE, + 0 + }; + struct mailbox_transaction_context *trans; + struct mail_search_context *search_ctx; + struct mail *mail; + string_t *prev_subject, *reply; + const char *subject, *base_subject; + pool_t pool; + ARRAY(struct orderedsubject_thread) threads; + const struct orderedsubject_thread *thread; + struct orderedsubject_thread *cur_thread = NULL; + uint32_t num; + bool reply_or_fw; + int ret, tz; + + prev_subject = str_new(default_pool, 128); + + /* first read all of the threads into memory */ + pool = pool_alloconly_create("orderedsubject thread", 1024); + i_array_init(&threads, 128); + trans = mailbox_transaction_begin(cmd->client->mailbox, 0, + imap_client_command_get_reason(cmd)); + search_ctx = mailbox_search_init(trans, search_args, sort_program, + 0, NULL); + while (mailbox_search_next(search_ctx, &mail)) { + if (mail_get_first_header(mail, "Subject", &subject) <= 0) + subject = ""; + T_BEGIN { + base_subject = imap_get_base_subject_cased( + pool_datastack_create(), subject, + &reply_or_fw); + if (strcmp(str_c(prev_subject), base_subject) != 0) { + /* thread changed */ + cur_thread = NULL; + } + str_truncate(prev_subject, 0); + str_append(prev_subject, base_subject); + } T_END; + + if (cur_thread == NULL) { + /* starting a new thread. get the first message's + date */ + cur_thread = array_append_space(&threads); + if (mail_get_date(mail, &cur_thread->timestamp, + &tz) == 0 && + cur_thread->timestamp == 0) { + (void)mail_get_received_date(mail, + &cur_thread->timestamp); + } + p_array_init(&cur_thread->msgs, pool, 4); + } + num = cmd->uid ? mail->uid : mail->seq; + array_push_back(&cur_thread->msgs, &num); + } + str_free(&prev_subject); + ret = mailbox_search_deinit(&search_ctx); + (void)mailbox_transaction_commit(&trans); + if (ret < 0) { + array_free(&threads); + pool_unref(&pool); + return -1; + } + + /* sort the threads by their first message's timestamp */ + array_sort(&threads, orderedsubject_thread_cmp); + + /* write the threads to client */ + reply = t_str_new(128); + str_append(reply, "* THREAD "); + array_foreach(&threads, thread) { + imap_orderedsubject_thread_write(cmd->client->output, + reply, thread); + } + str_append(reply, "\r\n"); + o_stream_nsend(cmd->client->output, str_data(reply), str_len(reply)); + + array_free(&threads); + pool_unref(&pool); + return 0; +} + +bool cmd_thread(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + enum mail_thread_type thread_type; + struct mail_search_args *sargs; + const struct imap_arg *args; + const char *charset, *str; + int ret; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + if (!imap_arg_get_astring(&args[0], &str) || + !imap_arg_get_astring(&args[1], &charset)) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + args += 2; + + if (!mail_thread_type_parse(str, &thread_type)) { + client_send_command_error(cmd, "Unknown thread algorithm."); + return TRUE; + } + + ret = imap_search_args_build(cmd, args, charset, &sargs); + if (ret <= 0) + return ret < 0; + + if (thread_type != MAIL_THREAD_ORDEREDSUBJECT) + ret = imap_thread(cmd, sargs, thread_type); + else + ret = imap_thread_orderedsubject(cmd, sargs); + mail_search_args_unref(&sargs); + if (ret < 0) { + client_send_box_error(cmd, client->mailbox); + return TRUE; + } + + return cmd_sync(cmd, MAILBOX_SYNC_FLAG_FAST | + (cmd->uid ? 0 : MAILBOX_SYNC_FLAG_NO_EXPUNGES), + 0, "OK Thread completed."); +} diff --git a/src/imap/cmd-unselect.c b/src/imap/cmd-unselect.c new file mode 100644 index 0000000..7f9f547 --- /dev/null +++ b/src/imap/cmd-unselect.c @@ -0,0 +1,18 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-commands.h" + +bool cmd_unselect(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + + if (!client_verify_open_mailbox(cmd)) + return TRUE; + + i_assert(client->mailbox_change_lock == NULL); + + imap_client_close_mailbox(client); + client_send_tagline(cmd, "OK Unselect completed."); + return TRUE; +} diff --git a/src/imap/cmd-unsubscribe.c b/src/imap/cmd-unsubscribe.c new file mode 100644 index 0000000..1d23624 --- /dev/null +++ b/src/imap/cmd-unsubscribe.c @@ -0,0 +1,9 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-commands.h" + +bool cmd_unsubscribe(struct client_command_context *cmd) +{ + return cmd_subscribe_full(cmd, FALSE); +} diff --git a/src/imap/cmd-urlfetch.c b/src/imap/cmd-urlfetch.c new file mode 100644 index 0000000..6ea5ad5 --- /dev/null +++ b/src/imap/cmd-urlfetch.c @@ -0,0 +1,410 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "strfuncs.h" +#include "str.h" +#include "array.h" +#include "net.h" +#include "istream.h" +#include "istream-sized.h" +#include "ostream.h" +#include "imap-url.h" +#include "imap-quote.h" +#include "imap-common.h" +#include "imap-commands.h" +#include "imap-urlauth.h" +#include "imap-urlauth-fetch.h" + +struct cmd_urlfetch_context { + struct imap_urlauth_fetch *ufetch; + struct istream *input; + + bool failed:1; + bool finished:1; + bool extended:1; + bool in_io_handler:1; +}; + +struct cmd_urlfetch_url { + const char *url; + + enum imap_urlauth_fetch_flags flags; +}; + +static void cmd_urlfetch_finish(struct client_command_context *cmd) +{ + struct cmd_urlfetch_context *ctx = + (struct cmd_urlfetch_context *)cmd->context; + + if (ctx->finished) + return; + ctx->finished = TRUE; + + i_stream_unref(&ctx->input); + if (ctx->ufetch != NULL) + imap_urlauth_fetch_deinit(&ctx->ufetch); + + if (ctx->failed) { + if (cmd->client->output_cmd_lock == cmd) { + /* failed in the middle of a literal. + we need to disconnect. */ + cmd->client->output_cmd_lock = NULL; + client_disconnect(cmd->client, "URLFETCH failed"); + } else { + client_send_internal_error(cmd); + } + return; + } + + client_send_tagline(cmd, "OK URLFETCH completed."); +} + +static bool cmd_urlfetch_cancel(struct client_command_context *cmd) +{ + struct cmd_urlfetch_context *ctx = + (struct cmd_urlfetch_context *)cmd->context; + + if (!cmd->cancel) + return FALSE; + + if (ctx->ufetch != NULL) { + e_debug(cmd->client->event, + "URLFETCH: Canceling command; " + "aborting URLAUTH fetch requests prematurely"); + imap_urlauth_fetch_deinit(&ctx->ufetch); + } + return TRUE; +} + +static int cmd_urlfetch_transfer_literal(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct cmd_urlfetch_context *ctx = + (struct cmd_urlfetch_context *)cmd->context; + enum ostream_send_istream_result res; + int ret; + + /* are we in the middle of an urlfetch literal? */ + if (ctx->input == NULL) + return 1; + + /* flush output to client if buffer is filled above optimum */ + if (o_stream_get_buffer_used_size(client->output) >= + CLIENT_OUTPUT_OPTIMAL_SIZE) { + if ((ret = o_stream_flush(client->output)) <= 0) + return ret; + } + + /* transfer literal to client */ + o_stream_set_max_buffer_size(client->output, 0); + res = o_stream_send_istream(client->output, ctx->input); + o_stream_set_max_buffer_size(client->output, SIZE_MAX); + + switch (res) { + case OSTREAM_SEND_ISTREAM_RESULT_FINISHED: + i_stream_unref(&ctx->input); + return 1; + case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT: + i_unreached(); + case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT: + return 0; + case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT: + e_error(client->event, "read(%s) failed: %s (URLFETCH)", + i_stream_get_name(ctx->input), + i_stream_get_error(ctx->input)); + client_disconnect(client, "URLFETCH failed"); + return -1; + case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT: + /* client disconnected */ + return -1; + } + i_unreached(); +} + +static bool cmd_urlfetch_continue(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct cmd_urlfetch_context *ctx = + (struct cmd_urlfetch_context *)cmd->context; + bool urls_pending; + int ret = 1; + + if (cmd->cancel) + return cmd_urlfetch_cancel(cmd); + + i_assert(client->output_cmd_lock == NULL || + client->output_cmd_lock == cmd); + + /* finish a pending literal transfer */ + ret = cmd_urlfetch_transfer_literal(cmd); + if (ret < 0) { + ctx->failed = TRUE; + cmd_urlfetch_finish(cmd); + return TRUE; + } + if (ret == 0) { + /* not finished; apparently output blocked again */ + return FALSE; + } + + if (ctx->extended) + client_send_line(client, ")"); + else + client_send_line(client, ""); + client->output_cmd_lock = NULL; + + ctx->in_io_handler = TRUE; + urls_pending = imap_urlauth_fetch_continue(ctx->ufetch); + ctx->in_io_handler = FALSE; + + if (urls_pending) { + /* waiting for imap urlauth service */ + cmd->state = CLIENT_COMMAND_STATE_WAIT_EXTERNAL; + cmd->func = cmd_urlfetch_cancel; + + /* retrieve next url */ + return FALSE; + } + + /* finished */ + cmd_urlfetch_finish(cmd); + return TRUE; +} + +static int cmd_urlfetch_url_success(struct client_command_context *cmd, + struct imap_urlauth_fetch_reply *reply) +{ + struct cmd_urlfetch_context *ctx = cmd->context; + string_t *response = t_str_new(256); + int ret; + + str_append(response, "* URLFETCH "); + imap_append_astring(response, reply->url); + + if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_EXTENDED) == 0) { + /* simple */ + ctx->extended = FALSE; + + str_printfa(response, " {%"PRIuUOFF_T"}", reply->size); + client_send_line(cmd->client, str_c(response)); + i_assert(reply->size == 0 || reply->input != NULL); + } else { + bool metadata = FALSE; + + /* extended */ + ctx->extended = TRUE; + + str_append(response, " ("); + if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE) != 0 && + reply->bodypartstruct != NULL) { + str_append(response, "BODYPARTSTRUCTURE ("); + str_append(response, reply->bodypartstruct); + str_append_c(response, ')'); + metadata = TRUE; + } + if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0 || + (reply->flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0) { + if (metadata) + str_append_c(response, ' '); + if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0) { + str_append(response, "BODY "); + } else { + str_append(response, "BINARY "); + if (reply->binary_has_nuls) + str_append_c(response, '~'); + } + str_printfa(response, "{%"PRIuUOFF_T"}", reply->size); + i_assert(reply->size == 0 || reply->input != NULL); + } else { + str_append_c(response, ')'); + ctx->extended = FALSE; + } + + client_send_line(cmd->client, str_c(response)); + } + + if (reply->input != NULL) { + ctx->input = i_stream_create_sized(reply->input, reply->size); + + ret = cmd_urlfetch_transfer_literal(cmd); + if (ret < 0) { + ctx->failed = TRUE; + return -1; + } + if (ret == 0) { + /* not finished; apparently output blocked */ + cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT; + cmd->func = cmd_urlfetch_continue; + cmd->client->output_cmd_lock = cmd; + return 0; + } + + if (ctx->extended) + client_send_line(cmd->client, ")"); + else + client_send_line(cmd->client, ""); + } + return 1; +} + +static int +cmd_urlfetch_url_callback(struct imap_urlauth_fetch_reply *reply, + bool last, void *context) +{ + struct client_command_context *cmd = context; + struct client *client = cmd->client; + struct cmd_urlfetch_context *ctx = cmd->context; + bool in_io_handler = ctx->in_io_handler; + int ret; + + if (!in_io_handler) + o_stream_cork(client->output); + if (reply == NULL) { + /* fatal failure */ + ctx->failed = TRUE; + ret = -1; + } else if (reply->succeeded) { + /* URL fetch succeeded */ + ret = cmd_urlfetch_url_success(cmd, reply); + } else { + /* URL fetch failed */ + string_t *response = t_str_new(128); + + str_append(response, "* URLFETCH "); + imap_append_astring(response, reply->url); + str_append(response, " NIL"); + client_send_line(client, str_c(response)); + if (reply->error != NULL) { + client_send_line(client, t_strdup_printf( + "* NO %s.", reply->error)); + } + ret = 1; + } + + if ((last && cmd->state == CLIENT_COMMAND_STATE_WAIT_EXTERNAL) || + ret < 0) { + cmd_urlfetch_finish(cmd); + client_command_free(&cmd); + } + if (!in_io_handler) + o_stream_uncork(client->output); + return ret; +} + +static int +cmd_urlfetch_parse_arg(struct client_command_context *cmd, + const struct imap_arg *arg, + struct cmd_urlfetch_url *ufurl_r) +{ + enum imap_urlauth_fetch_flags url_flags = 0; + const struct imap_arg *params; + const char *url_text; + + if (imap_arg_get_list(arg, ¶ms)) + url_flags |= IMAP_URLAUTH_FETCH_FLAG_EXTENDED; + else + params = arg; + + /* read url */ + if (!imap_arg_get_astring(params++, &url_text)) { + client_send_command_error(cmd, "Invalid arguments."); + return -1; + } + ufurl_r->url = t_strdup(url_text); + if (url_flags == 0) + return 0; + + while (!IMAP_ARG_IS_EOL(params)) { + const char *fetch_param; + + if (!imap_arg_get_atom(params++, &fetch_param)) { + client_send_command_error(cmd, + "Invalid URL fetch parameter."); + return -1; + } + + if (strcasecmp(fetch_param, "BODY") == 0) + url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODY; + else if (strcasecmp(fetch_param, "BINARY") == 0) + url_flags |= IMAP_URLAUTH_FETCH_FLAG_BINARY; + else if (strcasecmp(fetch_param, "BODYPARTSTRUCTURE") == 0) + url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE; + else { + client_send_command_error(cmd, + t_strdup_printf("Unknown URL fetch parameter: %s", + fetch_param)); + return -1; + } + } + + if ((url_flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0 && + (url_flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0) { + client_send_command_error(cmd, + "URL cannot have both BODY and BINARY fetch parameters."); + return -1; + } + + if (url_flags == IMAP_URLAUTH_FETCH_FLAG_EXTENDED) + url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODY; + ufurl_r->flags = url_flags; + return 0; +} + +bool cmd_urlfetch(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct cmd_urlfetch_context *ctx; + ARRAY(struct cmd_urlfetch_url) urls; + const struct cmd_urlfetch_url *url; + const struct imap_arg *args; + struct cmd_urlfetch_url *ufurl; + + if (client->urlauth_ctx == NULL) { + client_send_command_error(cmd, "URLAUTH disabled."); + return TRUE; + } + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + if (IMAP_ARG_IS_EOL(args)) { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } + + t_array_init(&urls, 32); + + /* parse url arguments and group them per userid */ + for (; !IMAP_ARG_IS_EOL(args); args++) { + ufurl = array_append_space(&urls); + if (cmd_urlfetch_parse_arg(cmd, args, ufurl) < 0) + return TRUE; + } + cmd->context = ctx = p_new(cmd->pool, struct cmd_urlfetch_context, 1); + cmd->func = cmd_urlfetch_cancel; + cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT; + + ctx->ufetch = imap_urlauth_fetch_init(client->urlauth_ctx, + cmd_urlfetch_url_callback, cmd); + + ctx->in_io_handler = TRUE; + array_foreach(&urls, url) { + if (imap_urlauth_fetch_url(ctx->ufetch, url->url, url->flags) < 0) { + /* fatal error */ + ctx->failed = TRUE; + break; + } + } + ctx->in_io_handler = FALSE; + + if ((ctx->failed || !imap_urlauth_fetch_is_pending(ctx->ufetch)) + && cmd->client->output_cmd_lock != cmd) { + /* finished */ + cmd_urlfetch_finish(cmd); + return TRUE; + } + + if (cmd->client->output_cmd_lock != cmd) + cmd->state = CLIENT_COMMAND_STATE_WAIT_EXTERNAL; + return FALSE; +} diff --git a/src/imap/cmd-x-cancel.c b/src/imap/cmd-x-cancel.c new file mode 100644 index 0000000..3f49451 --- /dev/null +++ b/src/imap/cmd-x-cancel.c @@ -0,0 +1,28 @@ +/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-commands.h" + +bool cmd_x_cancel(struct client_command_context *cmd) +{ + struct client_command_context *cancel_cmd; + const char *tag; + + /* <tag> */ + if (!client_read_string_args(cmd, 1, &tag)) + return FALSE; + + cancel_cmd = cmd->client->command_queue; + for (; cancel_cmd != NULL; cancel_cmd = cancel_cmd->next) { + if (cancel_cmd->tag != NULL && cancel_cmd != cmd && + strcmp(cancel_cmd->tag, tag) == 0) { + client_command_cancel(&cancel_cmd); + client_send_tagline(cmd, "OK Command cancelled."); + return TRUE; + } + } + + client_send_tagline(cmd, "NO Command tag not found."); + return TRUE; +} + diff --git a/src/imap/cmd-x-state.c b/src/imap/cmd-x-state.c new file mode 100644 index 0000000..8809998 --- /dev/null +++ b/src/imap/cmd-x-state.c @@ -0,0 +1,68 @@ +/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "base64.h" +#include "str.h" +#include "imap-commands.h" +#include "imap-state.h" + +bool cmd_x_state(struct client_command_context *cmd) +{ + /* FIXME: state importing can cause unnecessarily large memory usage + by specifying an old modseq, because the EXPUNGE/FETCH replies + aren't currently sent asynchronously. so this command is disabled + for now. */ +#if 0 + const struct imap_arg *args; + const char *str, *error; + buffer_t *state, *state_encoded; + int ret; + + if (!client_read_args(cmd, 0, 0, &args)) + return FALSE; + + state = buffer_create_dynamic(cmd->pool, 256); + if (imap_arg_get_astring(&args[0], &str)) { + if (cmd->client->mailbox != NULL) { + client_send_tagline(cmd, + "BAD Can't be used in SELECTED state"); + return TRUE; + } + if (base64_decode(str, strlen(str), NULL, state) < 0) + ret = 0; + else { + ret = imap_state_import_external(cmd->client, + state->data, state->used, &error); + } + if (ret < 0) { + client_send_tagline(cmd, t_strdup_printf( + "NO Failed to restore state: %s", error)); + } else if (ret == 0) { + client_send_tagline(cmd, t_strdup_printf( + "BAD Broken state: %s", error)); + } else { + client_send_tagline(cmd, "OK State imported."); + } + return TRUE; + } else if (args[0].type == IMAP_ARG_EOL) { + if (!imap_state_export_external(cmd->client, state, &error)) { + client_send_tagline(cmd, t_strdup_printf( + "NO Can't save state: %s", error)); + return TRUE; + } + state_encoded = buffer_create_dynamic(cmd->pool, + MAX_BASE64_ENCODED_SIZE(state->used)+10); + str_append(state_encoded, "* STATE "); + base64_encode(state->data, state->used, state_encoded); + client_send_line(cmd->client, str_c(state_encoded)); + client_send_tagline(cmd, "OK State exported."); + return TRUE; + } else { + client_send_command_error(cmd, "Invalid arguments."); + return TRUE; + } +#else + client_send_command_error(cmd, "Command is disabled for now."); + return TRUE; +#endif +} diff --git a/src/imap/imap-client-hibernate.c b/src/imap/imap-client-hibernate.c new file mode 100644 index 0000000..b29f500 --- /dev/null +++ b/src/imap/imap-client-hibernate.c @@ -0,0 +1,294 @@ +/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "fdpass.h" +#include "net.h" +#include "ostream.h" +#include "write-full.h" +#include "base64.h" +#include "str.h" +#include "strescape.h" +#include "master-service.h" +#include "mailbox-watch.h" +#include "imap-state.h" +#include "imap-client.h" + +#include <sys/stat.h> + +#define IMAP_HIBERNATE_SOCKET_NAME "imap-hibernate" +#define IMAP_HIBERNATE_SEND_TIMEOUT_SECS 10 +#define IMAP_HIBERNATE_HANDSHAKE "VERSION\timap-hibernate\t1\t0\n" + +static int +imap_hibernate_handshake(int fd, const char *path, const char **error_r) +{ + char buf[1024]; + ssize_t ret; + + if (write_full(fd, IMAP_HIBERNATE_HANDSHAKE, + strlen(IMAP_HIBERNATE_HANDSHAKE)) < 0) { + *error_r = t_strdup_printf("write(%s) failed: %m", path); + return -1; + } else if ((ret = read(fd, buf, sizeof(buf)-1)) < 0) { + *error_r = t_strdup_printf("read(%s) failed: %m", path); + return -1; + } else if (ret > 0 && buf[ret-1] == '\n') { + buf[ret-1] = '\0'; + if (version_string_verify(buf, "imap-hibernate", 1)) + return 0; + } else { + buf[ret] = '\0'; + } + *error_r = t_strdup_printf("%s sent invalid VERSION handshake: %s", + path, buf); + return -1; +} + +static void imap_hibernate_write_cmd(struct client *client, string_t *cmd, + const buffer_t *state, int fd_notify) +{ + struct mail_user *user = client->user; + struct stat peer_st; + const char *tag; + + tag = client->command_queue == NULL ? NULL : client->command_queue->tag; + + str_append_tabescaped(cmd, user->username); + str_append_c(cmd, '\t'); + str_append_tabescaped(cmd, user->set->mail_log_prefix); + str_printfa(cmd, "\tidle_notify_interval=%u", + client->set->imap_idle_notify_interval); + if (fstat(client->fd_in, &peer_st) == 0) { + str_printfa(cmd, "\tpeer_dev_major=%lu\tpeer_dev_minor=%lu\tpeer_ino=%llu", + (unsigned long)major(peer_st.st_dev), + (unsigned long)minor(peer_st.st_dev), + (unsigned long long)peer_st.st_ino); + } + + str_append(cmd, "\tsession="); + str_append_tabescaped(cmd, user->session_id); + if (user->session_create_time != 0) { + str_printfa(cmd, "\tsession_created=%s", + dec2str(user->session_create_time)); + } + if (user->conn.local_ip != NULL) + str_printfa(cmd, "\tlip=%s", net_ip2addr(user->conn.local_ip)); + if (user->conn.local_port != 0) + str_printfa(cmd, "\tlport=%u", user->conn.local_port); + if (user->conn.remote_ip != NULL) + str_printfa(cmd, "\trip=%s", net_ip2addr(user->conn.remote_ip)); + if (user->conn.remote_port != 0) + str_printfa(cmd, "\trport=%u", user->conn.remote_port); + if (client->userdb_fields != NULL) { + string_t *userdb_fields = t_str_new(256); + unsigned int i; + + for (i = 0; client->userdb_fields[i] != NULL; i++) { + if (i > 0) + str_append_c(userdb_fields, '\t'); + str_append_tabescaped(userdb_fields, client->userdb_fields[i]); + } + str_append(cmd, "\tuserdb_fields="); + str_append_tabescaped(cmd, str_c(userdb_fields)); + } + if (user->uid != (uid_t)-1) + str_printfa(cmd, "\tuid=%s", dec2str(user->uid)); + if (user->gid != (gid_t)-1) + str_printfa(cmd, "\tgid=%s", dec2str(user->gid)); + if (client->mailbox != NULL) { + str_append(cmd, "\tmailbox="); + str_append_tabescaped(cmd, mailbox_get_vname(client->mailbox)); + } + if (tag != NULL) { + str_append(cmd, "\ttag="); + str_append_tabescaped(cmd, tag); + } + str_append(cmd, "\tstats="); + str_append_tabescaped(cmd, client_stats(client)); + if (client->command_queue != NULL && + strcasecmp(client->command_queue->name, "IDLE") == 0) + str_append(cmd, "\tidle-cmd"); + if (fd_notify != -1) + str_append(cmd, "\tnotify_fd"); + str_append(cmd, "\tstate="); + base64_encode(state->data, state->used, cmd); + str_append_c(cmd, '\n'); +} + +static int +imap_hibernate_process_send_cmd(int fd_socket, const char *path, + const string_t *cmd, int fd_client, + const char **error_r) +{ + ssize_t ret; + + i_assert(fd_socket != -1); + i_assert(str_len(cmd) > 1); + + if (imap_hibernate_handshake(fd_socket, path, error_r) < 0) + return -1; + if ((ret = fd_send(fd_socket, fd_client, str_data(cmd), 1)) < 0) { + *error_r = t_strdup_printf("fd_send(%s) failed: %m", path); + return -1; + } + i_assert(ret == 1); + if (write_full(fd_socket, str_data(cmd)+1, str_len(cmd)-1) < 0) { + *error_r = t_strdup_printf("write(%s) failed: %m", path); + return -1; + } + return 0; +} + +static int +imap_hibernate_process_read(int fd, const char *path, const char **error_r) +{ + char buf[1024]; + ssize_t ret; + + if ((ret = read(fd, buf, sizeof(buf)-1)) < 0) { + *error_r = t_strdup_printf("read(%s) failed: %m", path); + return -1; + } else if (ret == 0) { + *error_r = t_strdup_printf("%s disconnected", path); + return -1; + } else if (buf[0] != '+') { + buf[ret] = '\0'; + *error_r = t_strdup_printf("%s returned failure: %s", path, + ret > 0 && buf[0] == '-' ? buf+1 : buf); + return -1; + } else { + return 0; + } +} + +static int +imap_hibernate_process_send(struct client *client, const buffer_t *state, + int fd_notify, int *fd_r, const char **error_r) +{ + string_t *cmd = t_str_new(512); + const char *path; + ssize_t ret = 0; + int fd; + + i_assert(state->used > 0); + + *fd_r = -1; + + path = t_strconcat(client->user->set->base_dir, + "/"IMAP_HIBERNATE_SOCKET_NAME, NULL); + fd = net_connect_unix_with_retries(path, 1000); + if (fd == -1) { + *error_r = t_strdup_printf( + "net_connect_unix(%s) failed: %m", path); + return -1; + } + net_set_nonblock(fd, FALSE); + + imap_hibernate_write_cmd(client, cmd, state, fd_notify); + + alarm(IMAP_HIBERNATE_SEND_TIMEOUT_SECS); + if (imap_hibernate_process_send_cmd(fd, path, cmd, client->fd_in, error_r) < 0 || + imap_hibernate_process_read(fd, path, error_r) < 0) + ret = -1; + else if (fd_notify != -1) { + if ((ret = fd_send(fd, fd_notify, "\n", 1)) < 0) + *error_r = t_strdup_printf("fd_send(%s) failed: %m", path); + else + ret = imap_hibernate_process_read(fd, path, error_r); + } + alarm(0); + if (ret < 0) { + net_disconnect(fd); + return -1; + } + *fd_r = fd; + return 0; +} + +bool imap_client_hibernate(struct client **_client, const char **reason_r) +{ + struct client *client = *_client; + buffer_t *state; + const char *error; + int ret, fd_notify = -1, fd_hibernate = -1; + + *reason_r = NULL; + + if (client->fd_in != client->fd_out) { + /* we won't try to hibernate stdio clients */ + *reason_r = "stdio clients can't be hibernated"; + return FALSE; + } + if (o_stream_get_buffer_used_size(client->output) > 0) { + /* wait until we've sent the pending output to client */ + *reason_r = "output pending to client"; + return FALSE; + } + + struct event_passthrough *e = + event_create_passthrough(client->event)-> + set_name("imap_client_hibernated"); + if (client->mailbox != NULL) + e->add_str("mailbox", mailbox_get_vname(client->mailbox)); + + state = buffer_create_dynamic(default_pool, 1024); + ret = imap_state_export_internal(client, state, &error); + if (ret < 0) { + e->add_str("error", error); + e_error(e->event(), "Couldn't hibernate imap client: " + "Couldn't export state: %s (mailbox=%s)", error, + client->mailbox == NULL ? "" : + mailbox_get_vname(client->mailbox)); + *reason_r = error; + } else if (ret == 0) { + e->add_str("error", error); + e_debug(e->event(), "Couldn't hibernate imap client: " + "Couldn't export state: %s (mailbox=%s)", error, + client->mailbox == NULL ? "" : + mailbox_get_vname(client->mailbox)); + *reason_r = error; + } + if (ret > 0 && client->mailbox != NULL) { + fd_notify = mailbox_watch_extract_notify_fd(client->mailbox, + &error); + if (fd_notify == -1) { + e->add_str("error", error); + e_debug(e->event(), "Couldn't hibernate imap client: " + "Couldn't extract notifications fd: %s", + error); + *reason_r = error; + ret = -1; + } + } + if (ret > 0) { + if (imap_hibernate_process_send(client, state, fd_notify, + &fd_hibernate, &error) < 0) { + e->add_str("error", error); + e_error(e->event(), + "Couldn't hibernate imap client: %s", error); + *reason_r = error; + ret = -1; + } + } + i_close_fd(&fd_notify); + if (ret > 0) { + /* hide the disconnect log message, because the client didn't + actually log out */ + e_debug(e->event(), + "Successfully hibernated imap client in mailbox %s", + client->mailbox == NULL ? "<none>" : + mailbox_get_vname(client->mailbox)); + client->disconnected = TRUE; + client->hibernated = TRUE; + client_destroy(client, NULL); + *_client = NULL; + } + /* notify imap-hibernate that we're done by closing the connection. + do this only after client is destroyed. this way imap-hibernate + won't try to launch another imap process too early and cause + problems (like sending duplicate session ID to stats process) */ + if (fd_hibernate != -1) + net_disconnect(fd_hibernate); + buffer_free(&state); + return ret > 0; +} diff --git a/src/imap/imap-client.c b/src/imap/imap-client.c new file mode 100644 index 0000000..b5418f6 --- /dev/null +++ b/src/imap/imap-client.c @@ -0,0 +1,1676 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "ioloop.h" +#include "llist.h" +#include "str.h" +#include "hostpid.h" +#include "net.h" +#include "iostream.h" +#include "iostream-rawlog.h" +#include "istream.h" +#include "istream-concat.h" +#include "ostream.h" +#include "time-util.h" +#include "var-expand.h" +#include "master-service.h" +#include "imap-resp-code.h" +#include "imap-util.h" +#include "imap-urlauth.h" +#include "mail-error.h" +#include "mail-namespace.h" +#include "mail-storage-service.h" +#include "mail-autoexpunge.h" +#include "imap-state.h" +#include "imap-search.h" +#include "imap-notify.h" +#include "imap-commands.h" +#include "imap-feature.h" + +#include <unistd.h> + +/* If the last command took longer than this to run, log statistics on + where the time was spent. */ +#define IMAP_CLIENT_DISCONNECT_LOG_STATS_CMD_MIN_RUNNING_MSECS 1000 + +extern struct mail_storage_callbacks mail_storage_callbacks; +extern struct imap_client_vfuncs imap_client_vfuncs; + +struct imap_module_register imap_module_register = { 0 }; + +struct client *imap_clients = NULL; +unsigned int imap_client_count = 0; + +unsigned int imap_feature_condstore = UINT_MAX; +unsigned int imap_feature_qresync = UINT_MAX; + +static const char *client_command_state_names[CLIENT_COMMAND_STATE_DONE+1] = { + "wait-input", + "wait-output", + "wait-external", + "wait-unambiguity", + "wait-sync", + "done" +}; + +static void client_idle_timeout(struct client *client) +{ + if (client->output_cmd_lock == NULL) + client_send_line(client, "* BYE Disconnected for inactivity."); + client_destroy(client, t_strdup_printf( + "Inactivity - no input for %"PRIdTIME_T" secs", + ioloop_time - client->last_input)); +} + +static void client_init_urlauth(struct client *client) +{ + struct imap_urlauth_config config; + + i_zero(&config); + config.url_host = client->set->imap_urlauth_host; + config.url_port = client->set->imap_urlauth_port; + config.socket_path = t_strconcat(client->user->set->base_dir, + "/"IMAP_URLAUTH_SOCKET_NAME, NULL); + config.session_id = client->user->session_id; + config.access_user = client->user->username; + config.access_service = "imap"; + config.access_anonymous = client->user->anonymous; + + client->urlauth_ctx = imap_urlauth_init(client->user, &config); +} + +static bool user_has_special_use_mailboxes(struct mail_user *user) +{ + struct mail_namespace_settings *ns_set; + + /* + * We have to iterate over namespace and mailbox *settings* since + * the namespaces haven't been set up yet. The namespaces haven't + * been set up so that we don't hold up the OK response to LOGIN + * when using slow lib-storage backends. + */ + + /* no namespaces => no special use flags */ + if (!array_is_created(&user->set->namespaces)) + return FALSE; + + array_foreach_elem(&user->set->namespaces, ns_set) { + struct mailbox_settings *box_set; + + /* no mailboxes => no special use flags */ + if (!array_is_created(&ns_set->mailboxes)) + continue; + + array_foreach_elem(&ns_set->mailboxes, box_set) { + if (box_set->special_use != NULL) + return TRUE; + } + } + + return FALSE; +} + +struct client *client_create(int fd_in, int fd_out, + struct event *event, struct mail_user *user, + struct mail_storage_service_user *service_user, + const struct imap_settings *set, + const struct smtp_submit_settings *smtp_set) +{ + const struct mail_storage_settings *mail_set; + struct client *client; + const char *ident; + pool_t pool; + + /* always use nonblocking I/O */ + net_set_nonblock(fd_in, TRUE); + net_set_nonblock(fd_out, TRUE); + + pool = pool_alloconly_create("imap client", 2048); + client = p_new(pool, struct client, 1); + client->pool = pool; + client->v = imap_client_vfuncs; + client->event = event; + event_ref(client->event); + client->set = set; + client->smtp_set = smtp_set; + client->service_user = service_user; + client->fd_in = fd_in; + client->fd_out = fd_out; + client->input = i_stream_create_fd(fd_in, + set->imap_max_line_length); + client->output = o_stream_create_fd(fd_out, SIZE_MAX); + o_stream_set_no_error_handling(client->output, TRUE); + i_stream_set_name(client->input, "<imap client>"); + o_stream_set_name(client->output, "<imap client>"); + + o_stream_set_flush_callback(client->output, client_output, client); + + p_array_init(&client->module_contexts, client->pool, 5); + client->last_input = ioloop_time; + client->to_idle = timeout_add(CLIENT_IDLE_TIMEOUT_MSECS, + client_idle_timeout, client); + + client->command_pool = + pool_alloconly_create(MEMPOOL_GROWING"client command", 1024*2); + client->user = user; + client->notify_count_changes = TRUE; + client->notify_flag_changes = TRUE; + p_array_init(&client->enabled_features, client->pool, 8); + + client->capability_string = + str_new(client->pool, sizeof(CAPABILITY_STRING)+64); + + if (*set->imap_capability == '\0') + str_append(client->capability_string, CAPABILITY_STRING); + else if (*set->imap_capability != '+') { + str_append(client->capability_string, set->imap_capability); + } else { + str_append(client->capability_string, CAPABILITY_STRING); + str_append_c(client->capability_string, ' '); + str_append(client->capability_string, set->imap_capability + 1); + } + if (client->set->imap_literal_minus) + client_add_capability(client, "LITERAL-"); + else + client_add_capability(client, "LITERAL+"); + if (user->fuzzy_search) { + /* Enable FUZZY capability only when it actually has + a chance of working */ + client_add_capability(client, "SEARCH=FUZZY"); + } + + mail_set = mail_user_set_get_storage_set(user); + if (mail_set->mailbox_list_index) { + /* NOTIFY is enabled only when mailbox list indexes are + enabled, although even that doesn't necessarily guarantee + it always */ + client_add_capability(client, "NOTIFY"); + } + + if (*set->imap_urlauth_host != '\0' && + *mail_set->mail_attribute_dict != '\0') { + /* Enable URLAUTH capability only when dict is + configured correctly */ + client_init_urlauth(client); + client_add_capability(client, "URLAUTH"); + client_add_capability(client, "URLAUTH=BINARY"); + } + if (set->imap_metadata && *mail_set->mail_attribute_dict != '\0') + client_add_capability(client, "METADATA"); + if (user_has_special_use_mailboxes(user)) { + /* Advertise SPECIAL-USE only if there are actually some + SPECIAL-USE flags in mailbox configuration. */ + client_add_capability(client, "SPECIAL-USE"); + } + + ident = mail_user_get_anvil_userip_ident(client->user); + if (ident != NULL) { + master_service_anvil_send(master_service, t_strconcat( + "CONNECT\t", my_pid, "\timap/", ident, "\n", NULL)); + client->anvil_sent = TRUE; + } + + imap_client_count++; + DLLIST_PREPEND(&imap_clients, client); + if (hook_client_created != NULL) + hook_client_created(&client); + + imap_refresh_proctitle(); + return client; +} + +void client_create_finish_io(struct client *client) +{ + if (client->set->rawlog_dir[0] != '\0') { + (void)iostream_rawlog_create(client->set->rawlog_dir, + &client->input, &client->output); + } + client->io = io_add_istream(client->input, client_input, client); +} + +int client_create_finish(struct client *client, const char **error_r) +{ + if (mail_namespaces_init(client->user, error_r) < 0) + return -1; + mail_namespaces_set_storage_callbacks(client->user->namespaces, + &mail_storage_callbacks, client); + client->v.init(client); + return 0; +} + +void client_add_istream_prefix(struct client *client, + const unsigned char *data, size_t size) +{ + i_assert(client->io == NULL); + + struct istream *inputs[] = { + i_stream_create_copy_from_data(data, size), + client->input, + NULL + }; + client->input = i_stream_create_concat(inputs); + i_stream_copy_fd(client->input, inputs[1]); + i_stream_unref(&inputs[0]); + i_stream_unref(&inputs[1]); + + i_stream_set_input_pending(client->input, TRUE); +} + +static void client_default_init(struct client *client ATTR_UNUSED) +{ + /* nothing */ +} + +void client_command_cancel(struct client_command_context **_cmd) +{ + struct client_command_context *cmd = *_cmd; + bool cmd_ret; + + switch (cmd->state) { + case CLIENT_COMMAND_STATE_WAIT_INPUT: + /* a bit kludgy check: cancel command only if it has context + set. currently only append command matches this check. all + other commands haven't even started the processing yet. */ + if (cmd->context == NULL) + break; + /* fall through */ + case CLIENT_COMMAND_STATE_WAIT_EXTERNAL: + case CLIENT_COMMAND_STATE_WAIT_OUTPUT: + cmd->cancel = TRUE; + break; + case CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY: + case CLIENT_COMMAND_STATE_WAIT_SYNC: + /* commands haven't started yet */ + break; + case CLIENT_COMMAND_STATE_DONE: + i_unreached(); + } + + cmd_ret = !cmd->cancel || cmd->func == NULL ? TRUE : + command_exec(cmd); + if (!cmd_ret) { + if (cmd->client->output->closed) + i_panic("command didn't cancel itself: %s", cmd->name); + } else { + client_command_free(*_cmd != NULL ? _cmd : &cmd); + } +} + +const char *client_stats(struct client *client) +{ + const struct var_expand_table logout_tab[] = { + { 'i', dec2str(i_stream_get_absolute_offset(client->input)), "input" }, + { 'o', dec2str(client->output->offset), "output" }, + { '\0', client->user->session_id, "session" }, + { '\0', dec2str(client->fetch_hdr_count), "fetch_hdr_count" }, + { '\0', dec2str(client->fetch_hdr_bytes), "fetch_hdr_bytes" }, + { '\0', dec2str(client->fetch_body_count), "fetch_body_count" }, + { '\0', dec2str(client->fetch_body_bytes), "fetch_body_bytes" }, + { '\0', dec2str(client->deleted_count), "deleted" }, + { '\0', dec2str(client->expunged_count), "expunged" }, + { '\0', dec2str(client->trashed_count), "trashed" }, + { '\0', dec2str(client->autoexpunged_count), "autoexpunged" }, + { '\0', dec2str(client->append_count), "appended" }, + { '\0', NULL, NULL } + }; + const struct var_expand_table *user_tab = + mail_user_var_expand_table(client->user); + const struct var_expand_table *tab = + t_var_expand_merge_tables(logout_tab, user_tab); + string_t *str; + const char *error; + + str = t_str_new(128); + if (var_expand_with_funcs(str, client->set->imap_logout_format, + tab, mail_user_var_expand_func_table, + client->user, &error) < 0) { + e_error(client->event, + "Failed to expand imap_logout_format=%s: %s", + client->set->imap_logout_format, error); + } + return str_c(str); +} + +void client_destroy(struct client *client, const char *reason) +{ + client->v.destroy(client, reason); +} + +static void +client_command_stats_append(string_t *str, + const struct client_command_stats *stats, + const char *wait_condition, + size_t buffered_size) +{ + uint64_t ioloop_wait_usecs; + unsigned int msecs_in_ioloop; + + ioloop_wait_usecs = io_loop_get_wait_usecs(current_ioloop); + msecs_in_ioloop = (ioloop_wait_usecs - + stats->start_ioloop_wait_usecs + 999) / 1000; + str_printfa(str, "running for %d.%03d + waiting ", + (int)((stats->running_usecs+999)/1000 / 1000), + (int)((stats->running_usecs+999)/1000 % 1000)); + if (wait_condition[0] != '\0') + str_printfa(str, "%s ", wait_condition); + str_printfa(str, "for %d.%03d secs", + msecs_in_ioloop / 1000, msecs_in_ioloop % 1000); + if (stats->lock_wait_usecs > 0) { + int lock_wait_msecs = (stats->lock_wait_usecs+999)/1000; + str_printfa(str, ", %d.%03d in locks", + lock_wait_msecs/1000, lock_wait_msecs%1000); + } + str_printfa(str, ", %"PRIu64" B in + %"PRIu64, + stats->bytes_in, stats->bytes_out); + if (buffered_size > 0) + str_printfa(str, "+%zu", buffered_size); + str_append(str, " B out"); +} + +static const char *client_get_last_command_status(struct client *client) +{ + if (client->logged_out) + return ""; + if (client->last_cmd_name == NULL) + return " (No commands sent)"; + + /* client disconnected without sending LOGOUT. if the last command + took over 1 second to run, log it. */ + const struct client_command_stats *stats = &client->last_cmd_stats; + + string_t *str = t_str_new(128); + int last_run_secs = timeval_diff_msecs(&ioloop_timeval, + &stats->last_run_timeval); + str_printfa(str, " (%s finished %d.%03d secs ago", + client->last_cmd_name, last_run_secs/1000, + last_run_secs%1000); + + if (timeval_diff_msecs(&stats->last_run_timeval, &stats->start_time) >= + IMAP_CLIENT_DISCONNECT_LOG_STATS_CMD_MIN_RUNNING_MSECS) { + str_append(str, " - "); + client_command_stats_append(str, stats, "", 0); + } + str_append_c(str, ')'); + return str_c(str); +} + +static const char *client_get_commands_status(struct client *client) +{ + struct client_command_context *cmd, *last_cmd = NULL; + struct client_command_stats all_stats; + string_t *str; + enum io_condition cond; + const char *cond_str; + + if (client->command_queue == NULL) + return client_get_last_command_status(client); + + i_zero(&all_stats); + str = t_str_new(128); + str_append(str, " ("); + for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) { + if (cmd->name == NULL) { + /* (parts of a) tag were received, but not yet + the command name */ + continue; + } + str_append(str, cmd->name); + if (cmd->next != NULL) + str_append_c(str, ','); + all_stats.running_usecs += cmd->stats.running_usecs; + all_stats.lock_wait_usecs += cmd->stats.lock_wait_usecs; + all_stats.bytes_in += cmd->stats.bytes_in; + all_stats.bytes_out += cmd->stats.bytes_out; + last_cmd = cmd; + } + if (last_cmd == NULL) + return client_get_last_command_status(client); + + cond = io_loop_find_fd_conditions(current_ioloop, client->fd_out); + if ((cond & (IO_READ | IO_WRITE)) == (IO_READ | IO_WRITE)) + cond_str = "input/output"; + else if ((cond & IO_READ) != 0) + cond_str = "input"; + else if ((cond & IO_WRITE) != 0) + cond_str = "output"; + else + cond_str = "nothing"; + + all_stats.start_ioloop_wait_usecs = + last_cmd->stats.start_ioloop_wait_usecs; + str_append_c(str, ' '); + client_command_stats_append(str, &all_stats, cond_str, + o_stream_get_buffer_used_size(client->output)); + str_printfa(str, ", state=%s)", + client_command_state_names[last_cmd->state]); + return str_c(str); +} + +static void client_log_disconnect(struct client *client, const char *reason) +{ + e_info(client->event, "Disconnected: %s %s", reason, client_stats(client)); +} + +static void client_default_destroy(struct client *client, const char *reason) +{ + struct client_command_context *cmd; + + i_assert(!client->destroyed); + client->destroyed = TRUE; + client->disconnected = TRUE; + + if (client->disconnect_reason != NULL) + reason = client->disconnect_reason; + if (reason == NULL) + reason = t_strconcat( + io_stream_get_disconnect_reason(client->input, + client->output), + client_get_commands_status(client), NULL); + + i_stream_close(client->input); + o_stream_close(client->output); + + /* finish off all the queued commands. */ + if (client->output_cmd_lock != NULL) + client_command_cancel(&client->output_cmd_lock); + while (client->command_queue != NULL) { + cmd = client->command_queue; + client_command_cancel(&cmd); + } + /* handle the input_lock command last. it might have been waiting on + other queued commands (although we probably should just drop the + command at that point since it hasn't started running. but this may + change in future). */ + if (client->input_lock != NULL) + client_command_cancel(&client->input_lock); + + if (client->notify_ctx != NULL) + imap_notify_deinit(&client->notify_ctx); + if (client->urlauth_ctx != NULL) + imap_urlauth_deinit(&client->urlauth_ctx); + /* Keep mailbox closing close to last, so anything that could + potentially have transactions open will close them first. */ + if (client->mailbox != NULL) + imap_client_close_mailbox(client); + if (client->anvil_sent) { + master_service_anvil_send(master_service, t_strconcat( + "DISCONNECT\t", my_pid, "\timap/", + mail_user_get_anvil_userip_ident(client->user), + "\n", NULL)); + } + + if (client->free_parser != NULL) + imap_parser_unref(&client->free_parser); + io_remove(&client->io); + timeout_remove(&client->to_idle_output); + timeout_remove(&client->to_idle); + + /* i/ostreams are already closed at this stage, so fd can be closed */ + fd_close_maybe_stdio(&client->fd_in, &client->fd_out); + + /* Autoexpunging might run for a long time. Disconnect the client + before it starts, and refresh proctitle so it's clear that it's + doing autoexpunging. We've also sent DISCONNECT to anvil already, + because this is background work and shouldn't really be counted + as an active IMAP session for the user. + + Don't autoexpunge if the client is hibernated - it shouldn't be any + different from the non-hibernating IDLE case. For frequent + hibernations it could also be doing unnecessarily much work. */ + imap_refresh_proctitle(); + if (!client->hibernated) { + client->autoexpunged_count = mail_user_autoexpunge(client->user); + client_log_disconnect(client, reason); + } + mail_user_deinit(&client->user); + + /* free the i/ostreams after mail_user_unref(), which could trigger + mail_storage_callbacks notifications that write to the ostream. */ + i_stream_destroy(&client->input); + o_stream_destroy(&client->output); + + if (array_is_created(&client->search_saved_uidset)) + array_free(&client->search_saved_uidset); + if (array_is_created(&client->search_updates)) + array_free(&client->search_updates); + pool_unref(&client->command_pool); + mail_storage_service_user_unref(&client->service_user); + + imap_client_count--; + DLLIST_REMOVE(&imap_clients, client); + + event_unref(&client->event); + i_free(client->last_cmd_name); + pool_unref(&client->pool); + + master_service_client_connection_destroyed(master_service); + imap_refresh_proctitle(); +} + +static void client_destroy_timeout(struct client *client) +{ + client_destroy(client, NULL); +} + +void client_disconnect(struct client *client, const char *reason) +{ + if (client->disconnected) + return; + + client->disconnected = TRUE; + client->disconnect_reason = p_strdup(client->pool, reason); + /* Finish the ostream. With IMAP COMPRESS this sends the EOF marker. */ + (void)o_stream_finish(client->output); + o_stream_uncork(client->output); + + i_stream_close(client->input); + o_stream_close(client->output); + + timeout_remove(&client->to_idle); + client->to_idle = timeout_add(0, client_destroy_timeout, client); +} + +void client_disconnect_with_error(struct client *client, + const char *client_error) +{ + client_send_line(client, t_strconcat("* BYE ", client_error, NULL)); + client_disconnect(client, client_error); +} + +void client_add_capability(struct client *client, const char *capability) +{ + /* require a single capability at a time (feels cleaner) */ + i_assert(strchr(capability, ' ') == NULL); + + if (client->set->imap_capability[0] != '\0' && + client->set->imap_capability[0] != '+') { + /* explicit capability - don't change it */ + return; + } + str_append_c(client->capability_string, ' '); + str_append(client->capability_string, capability); +} + +void client_send_line(struct client *client, const char *data) +{ + (void)client_send_line_next(client, data); +} + +int client_send_line_next(struct client *client, const char *data) +{ + struct const_iovec iov[2]; + + if (client->output->closed) + return -1; + + iov[0].iov_base = data; + iov[0].iov_len = strlen(data); + iov[1].iov_base = "\r\n"; + iov[1].iov_len = 2; + + if (o_stream_sendv(client->output, iov, 2) < 0) + return -1; + client->last_output = ioloop_time; + + if (o_stream_get_buffer_used_size(client->output) >= + CLIENT_OUTPUT_OPTIMAL_SIZE) { + /* buffer full, try flushing */ + return o_stream_flush(client->output); + } + return 1; +} + +static void +client_cmd_append_timing_stats(struct client_command_context *cmd, + string_t *str) +{ + unsigned int msecs_in_cmd, msecs_in_ioloop; + uint64_t ioloop_wait_usecs; + unsigned int msecs_since_cmd; + + if (cmd->stats.start_time.tv_sec == 0) + return; + command_stats_flush(cmd); + + ioloop_wait_usecs = io_loop_get_wait_usecs(current_ioloop); + msecs_in_cmd = (cmd->stats.running_usecs + 999) / 1000; + msecs_in_ioloop = (ioloop_wait_usecs - + cmd->stats.start_ioloop_wait_usecs + 999) / 1000; + msecs_since_cmd = timeval_diff_msecs(&ioloop_timeval, + &cmd->stats.last_run_timeval); + + if (str_data(str)[str_len(str)-1] == '.') + str_truncate(str, str_len(str)-1); + str_printfa(str, " (%d.%03d + %d.%03d ", + msecs_in_cmd / 1000, msecs_in_cmd % 1000, + msecs_in_ioloop / 1000, msecs_in_ioloop % 1000); + if (msecs_since_cmd > 0) { + str_printfa(str, "+ %d.%03d ", + msecs_since_cmd / 1000, msecs_since_cmd % 1000); + } + str_append(str, "secs)."); +} + +void client_send_tagline(struct client_command_context *cmd, const char *data) +{ + cmd->client->v.send_tagline(cmd, data); +} + +static void +client_default_send_tagline(struct client_command_context *cmd, const char *data) +{ + struct client *client = cmd->client; + const char *tag = cmd->tag; + + if (client->output->closed || cmd->cancel) + return; + + i_assert(!cmd->tagline_sent); + cmd->tagline_sent = TRUE; + cmd->tagline_reply = p_strdup(cmd->pool, data); + + if (tag == NULL || *tag == '\0') + tag = "*"; + + T_BEGIN { + string_t *str = t_str_new(256); + str_printfa(str, "%s %s", tag, data); + client_cmd_append_timing_stats(cmd, str); + str_append(str, "\r\n"); + o_stream_nsend(client->output, str_data(str), str_len(str)); + } T_END; + + client->last_output = ioloop_time; +} + +static int +client_default_sync_notify_more(struct imap_sync_context *ctx ATTR_UNUSED) +{ + return 1; +} + +void client_send_command_error(struct client_command_context *cmd, + const char *client_error) +{ + struct client *client = cmd->client; + const char *error, *cmd_name; + enum imap_parser_error parse_error; + + if (client_error == NULL) { + client_error = imap_parser_get_error(cmd->parser, &parse_error); + switch (parse_error) { + case IMAP_PARSE_ERROR_NONE: + i_unreached(); + case IMAP_PARSE_ERROR_LITERAL_TOO_BIG: + client_disconnect_with_error(client, client_error); + return; + default: + break; + } + } + + if (cmd->tag == NULL) + error = t_strconcat("BAD Error in IMAP tag: ", client_error, NULL); + else if (cmd->name == NULL) + error = t_strconcat("BAD Error in IMAP command: ", client_error, NULL); + else { + cmd_name = t_str_ucase(cmd->name); + error = t_strconcat("BAD Error in IMAP command ", + cmd_name, ": ", client_error, NULL); + } + + client_send_tagline(cmd, error); + + if (++client->bad_counter >= CLIENT_MAX_BAD_COMMANDS) { + client_disconnect_with_error(client, + "Too many invalid IMAP commands."); + } + + cmd->param_error = TRUE; + /* client_read_args() failures rely on this being set, so that the + command processing is stopped even while command function returns + FALSE. */ + cmd->state = CLIENT_COMMAND_STATE_DONE; +} + +void client_send_internal_error(struct client_command_context *cmd) +{ + client_send_tagline(cmd, + t_strflocaltime("NO "MAIL_ERRSTR_CRITICAL_MSG_STAMP, ioloop_time)); +} + +bool client_read_args(struct client_command_context *cmd, unsigned int count, + unsigned int flags, const struct imap_arg **args_r) +{ + int ret; + + i_assert(count <= INT_MAX); + + ret = imap_parser_read_args(cmd->parser, count, flags, args_r); + if (ret >= (int)count) { + /* all parameters read successfully */ + i_assert(cmd->client->input_lock == NULL || + cmd->client->input_lock == cmd); + + client_args_finished(cmd, *args_r); + cmd->client->input_lock = NULL; + return TRUE; + } else if (ret == -2) { + /* need more data */ + if (cmd->client->input->closed) { + /* disconnected */ + cmd->state = CLIENT_COMMAND_STATE_DONE; + } + return FALSE; + } else { + /* error, or missing arguments */ + client_send_command_error(cmd, ret < 0 ? NULL : + "Missing arguments"); + return FALSE; + } +} + +bool client_read_string_args(struct client_command_context *cmd, + unsigned int count, ...) +{ + const struct imap_arg *imap_args; + va_list va; + const char *str; + unsigned int i; + + if (!client_read_args(cmd, count, 0, &imap_args)) + return FALSE; + + va_start(va, count); + for (i = 0; i < count; i++) { + const char **ret = va_arg(va, const char **); + + if (IMAP_ARG_IS_EOL(&imap_args[i])) { + client_send_command_error(cmd, "Missing arguments."); + break; + } + + if (!imap_arg_get_astring(&imap_args[i], &str)) { + client_send_command_error(cmd, "Invalid arguments."); + break; + } + + if (ret != NULL) + *ret = str; + } + va_end(va); + + return i == count; +} + +void client_args_finished(struct client_command_context *cmd, + const struct imap_arg *args) +{ + string_t *str = t_str_new(256); + + if (cmd->args != NULL && cmd->args[0] != '\0') { + str_append(str, cmd->args); + str_append_c(str, ' '); + } + imap_write_args(str, args); + cmd->args = p_strdup(cmd->pool, str_c(str)); + event_add_str(cmd->event, "cmd_args", cmd->args); + + str_truncate(str, 0); + if (cmd->human_args != NULL && cmd->human_args[0] != '\0') { + str_append(str, cmd->human_args); + str_append_c(str, ' '); + } + imap_write_args_for_human(str, args); + cmd->human_args = p_strdup(cmd->pool, str_c(str)); + event_add_str(cmd->event, "cmd_human_args", cmd->human_args); +} + +static struct client_command_context * +client_command_find_with_flags(struct client_command_context *new_cmd, + enum command_flags flags, + enum client_command_state max_state) +{ + struct client_command_context *cmd; + + cmd = new_cmd->client->command_queue; + for (; cmd != NULL; cmd = cmd->next) { + /* The tagline_sent check is a bit kludgy here. Plugins may + hook into sync_notify_more() and send the tagline before + finishing the command. During this stage the state was been + dropped from _WAIT_SYNC to _WAIT_OUTPUT, so the <= max_state + check doesn't work correctly here. (Perhaps we should add + a new _WAIT_SYNC_OUTPUT?) */ + if (cmd->state <= max_state && !cmd->tagline_sent && + cmd != new_cmd && (cmd->cmd_flags & flags) != 0) + return cmd; + } + return NULL; +} + +static bool client_command_is_ambiguous(struct client_command_context *cmd) +{ + enum command_flags flags; + enum client_command_state max_state = + CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY; + bool broken_client = FALSE; + + if ((cmd->cmd_flags & COMMAND_FLAG_REQUIRES_SYNC) != 0 && + !imap_sync_is_allowed(cmd->client)) + return TRUE; + + if (cmd->search_save_result_used) { + /* if there are pending commands that update the search + save result, wait */ + struct client_command_context *old_cmd = cmd->next; + + for (; old_cmd != NULL; old_cmd = old_cmd->next) { + if (old_cmd->search_save_result) + return TRUE; + } + } + + if ((cmd->cmd_flags & COMMAND_FLAG_BREAKS_MAILBOX) == + COMMAND_FLAG_BREAKS_MAILBOX) { + /* there must be no other command running that uses the + selected mailbox */ + flags = COMMAND_FLAG_USES_MAILBOX; + max_state = CLIENT_COMMAND_STATE_DONE; + } else if ((cmd->cmd_flags & COMMAND_FLAG_USES_SEQS) != 0) { + /* no existing command must be breaking sequences */ + flags = COMMAND_FLAG_BREAKS_SEQS; + broken_client = TRUE; + } else if ((cmd->cmd_flags & COMMAND_FLAG_BREAKS_SEQS) != 0) { + /* if existing command uses sequences, we'll have to block */ + flags = COMMAND_FLAG_USES_SEQS; + } else { + return FALSE; + } + + if (client_command_find_with_flags(cmd, flags, max_state) == NULL) { + if (cmd->client->syncing) { + /* don't do anything until syncing is finished */ + return TRUE; + } + if (cmd->client->mailbox_change_lock != NULL && + cmd->client->mailbox_change_lock != cmd) { + /* don't do anything until mailbox is fully + opened/closed */ + return TRUE; + } + return FALSE; + } + + if (broken_client) { + client_send_line(cmd->client, + "* BAD ["IMAP_RESP_CODE_CLIENTBUG"] " + "Command pipelining results in ambiguity."); + } + + return TRUE; +} + +struct client_command_context *client_command_alloc(struct client *client) +{ + struct client_command_context *cmd; + + cmd = p_new(client->command_pool, struct client_command_context, 1); + cmd->client = client; + cmd->pool = client->command_pool; + cmd->global_event = event_create(client->event); + cmd->event = event_create(cmd->global_event); + cmd->stats.start_time = ioloop_timeval; + cmd->stats.last_run_timeval = ioloop_timeval; + cmd->stats.start_ioloop_wait_usecs = + io_loop_get_wait_usecs(current_ioloop); + p_array_init(&cmd->module_contexts, cmd->pool, 5); + + DLLIST_PREPEND(&client->command_queue, cmd); + client->command_queue_size++; + + imap_client_notify_command_allocated(client); + return cmd; +} + +void client_command_init_finished(struct client_command_context *cmd) +{ + event_add_str(cmd->event, "cmd_tag", cmd->tag); + /* use "unknown" until we checked that the command name is known/valid */ + event_add_str(cmd->event, "cmd_name", "unknown"); + /* the actual command name received from client - as-is */ + event_add_str(cmd->event, "cmd_input_name", cmd->name); +} + +static struct client_command_context * +client_command_new(struct client *client) +{ + struct client_command_context *cmd; + + cmd = client_command_alloc(client); + if (client->free_parser != NULL) { + cmd->parser = client->free_parser; + client->free_parser = NULL; + } else { + cmd->parser = + imap_parser_create(client->input, client->output, + client->set->imap_max_line_length); + if (client->set->imap_literal_minus) + imap_parser_enable_literal_minus(cmd->parser); + } + return cmd; +} + +void client_add_missing_io(struct client *client) +{ + if (client->io == NULL && !client->disconnected) + client->io = io_add_istream(client->input, client_input, client); +} + +void client_command_free(struct client_command_context **_cmd) +{ + struct client_command_context *cmd = *_cmd; + struct client *client = cmd->client; + enum client_command_state state = cmd->state; + + *_cmd = NULL; + + i_assert(!cmd->executing); + i_assert(client->output_cmd_lock == NULL); + + /* reset input idle time because command output might have taken a + long time and we don't want to disconnect client immediately then */ + client->last_input = ioloop_time; + timeout_reset(client->to_idle); + + if (cmd->cancel) { + cmd->cancel = FALSE; + client_send_tagline(cmd, "NO Command cancelled."); + } + + i_free(client->last_cmd_name); + client->last_cmd_name = i_strdup(cmd->name); + client->last_cmd_stats = cmd->stats; + + if (!cmd->param_error) + client->bad_counter = 0; + + if (client->input_lock == cmd) + client->input_lock = NULL; + if (client->mailbox_change_lock == cmd) + client->mailbox_change_lock = NULL; + + event_set_name(cmd->event, "imap_command_finished"); + if (cmd->tagline_reply != NULL) { + event_add_str(cmd->event, "tagged_reply_state", + t_strcut(cmd->tagline_reply, ' ')); + event_add_str(cmd->event, "tagged_reply", cmd->tagline_reply); + } + event_add_timeval(cmd->event, "last_run_time", + &cmd->stats.last_run_timeval); + event_add_int(cmd->event, "running_usecs", cmd->stats.running_usecs); + event_add_int(cmd->event, "lock_wait_usecs", cmd->stats.lock_wait_usecs); + event_add_int(cmd->event, "bytes_in", cmd->stats.bytes_in); + event_add_int(cmd->event, "bytes_out", cmd->stats.bytes_out); + + e_debug(cmd->event, "Command finished: %s %s", cmd->name, + cmd->human_args != NULL ? cmd->human_args : ""); + event_unref(&cmd->event); + event_unref(&cmd->global_event); + + if (cmd->parser != NULL) { + if (client->free_parser == NULL) { + imap_parser_reset(cmd->parser); + client->free_parser = cmd->parser; + } else { + imap_parser_unref(&cmd->parser); + } + } + + client->command_queue_size--; + DLLIST_REMOVE(&client->command_queue, cmd); + cmd = NULL; + + if (client->command_queue == NULL) { + /* no commands left in the queue, we can clear the pool */ + p_clear(client->command_pool); + timeout_remove(&client->to_idle_output); + } + imap_client_notify_command_freed(client); + imap_refresh_proctitle(); + + /* if command finished from external event, check input for more + unhandled commands since we may not be executing from client_input + or client_output. */ + if (state == CLIENT_COMMAND_STATE_WAIT_EXTERNAL && + !client->disconnected) { + client_add_missing_io(client); + io_set_pending(client->io); + } +} + +static void client_check_command_hangs(struct client *client) +{ + struct client_command_context *cmd; + unsigned int unfinished_count = 0; + bool have_wait_unfinished = FALSE; + + for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) { + switch (cmd->state) { + case CLIENT_COMMAND_STATE_WAIT_INPUT: + /* We need to be reading input for this command. + However, if there is already an output lock for + another command we'll wait for it to finish first. + This is needed because if there are any literals + we'd need to send "+ OK" responses. */ + i_assert(client->io != NULL || + (client->output_cmd_lock != NULL && + client->output_cmd_lock != client->input_lock)); + unfinished_count++; + break; + case CLIENT_COMMAND_STATE_WAIT_OUTPUT: + i_assert((io_loop_find_fd_conditions(current_ioloop, client->fd_out) & IO_WRITE) != 0); + unfinished_count++; + break; + case CLIENT_COMMAND_STATE_WAIT_EXTERNAL: + unfinished_count++; + break; + case CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY: + have_wait_unfinished = TRUE; + break; + case CLIENT_COMMAND_STATE_WAIT_SYNC: + if ((io_loop_find_fd_conditions(current_ioloop, client->fd_out) & IO_WRITE) == 0) + have_wait_unfinished = TRUE; + else { + /* we have an output callback, which will be + called soon and it'll run cmd_sync_delayed(). + FIXME: is this actually wanted? */ + } + break; + case CLIENT_COMMAND_STATE_DONE: + i_unreached(); + } + } + i_assert(!have_wait_unfinished || unfinished_count > 0); +} + +static bool client_remove_pending_unambiguity(struct client *client) +{ + if (client->input_lock != NULL) { + /* there's a command that has locked the input */ + struct client_command_context *cmd = client->input_lock; + + if (cmd->state != CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY) + return TRUE; + + /* the command is waiting for existing ambiguity causing + commands to finish. */ + if (client_command_is_ambiguous(cmd)) { + /* we could be waiting for existing sync to finish */ + if (!cmd_sync_delayed(client)) + return FALSE; + if (client_command_is_ambiguous(cmd)) + return FALSE; + } + cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT; + } + return TRUE; +} + +void client_continue_pending_input(struct client *client) +{ + i_assert(!client->handling_input); + + if (client->disconnected) { + client_destroy(client, NULL); + return; + } + + /* this function is called at the end of I/O callbacks (and only there). + fix up the command states and verify that they're correct. */ + while (client_remove_pending_unambiguity(client)) { + client_add_missing_io(client); + + /* if there's unread data in buffer, handle it. */ + if (i_stream_get_data_size(client->input) == 0 || + client->disconnected) + break; + + struct ostream *output = client->output; + o_stream_ref(output); + o_stream_cork(output); + bool ret = client_handle_input(client); + o_stream_uncork(output); + o_stream_unref(&output); + if (!ret) + break; + } + if (client->input->closed || client->output->closed) + client_destroy(client, NULL); + else + client_check_command_hangs(client); +} + +/* Skip incoming data until newline is found, + returns TRUE if newline was found. */ +static bool client_skip_line(struct client *client) +{ + const unsigned char *data; + size_t i, data_size; + + data = i_stream_get_data(client->input, &data_size); + + for (i = 0; i < data_size; i++) { + if (data[i] == '\n') { + client->input_skip_line = FALSE; + i++; + break; + } + } + + i_stream_skip(client->input, i); + return !client->input_skip_line; +} + +static void client_idle_output_timeout(struct client *client) +{ + client_destroy(client, t_strdup_printf( + "Client has not read server output for for %"PRIdTIME_T" secs", + ioloop_time - client->last_output)); +} + +bool client_handle_unfinished_cmd(struct client_command_context *cmd) +{ + if (cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT) { + /* need more input */ + return FALSE; + } + if (cmd->state != CLIENT_COMMAND_STATE_WAIT_OUTPUT) { + /* waiting for something */ + if (cmd->state == CLIENT_COMMAND_STATE_WAIT_SYNC) { + /* this is mainly for APPEND. */ + client_add_missing_io(cmd->client); + } + return TRUE; + } + + /* output is blocking, we can execute more commands */ + o_stream_set_flush_pending(cmd->client->output, TRUE); + if (cmd->client->to_idle_output == NULL) { + /* disconnect sooner if client isn't reading our output */ + cmd->client->to_idle_output = + timeout_add(CLIENT_OUTPUT_TIMEOUT_MSECS, + client_idle_output_timeout, cmd->client); + } + return TRUE; +} + +static void +client_command_failed_early(struct client_command_context **_cmd, + const char *error) +{ + struct client_command_context *cmd = *_cmd; + + /* ignore the rest of this line */ + cmd->client->input_skip_line = TRUE; + + io_loop_time_refresh(); + command_stats_start(cmd); + client_send_command_error(cmd, error); + cmd->param_error = TRUE; + client_command_free(_cmd); +} + +static bool client_command_input(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + struct command *command; + const char *tag, *name; + int ret; + + if (cmd->func != NULL) { + /* command is being executed - continue it */ + if (command_exec(cmd)) { + /* command execution was finished */ + client_command_free(&cmd); + client_add_missing_io(client); + return TRUE; + } + + return client_handle_unfinished_cmd(cmd); + } + + if (cmd->tag == NULL) { + ret = imap_parser_read_tag(cmd->parser, &tag); + if (ret == 0) + return FALSE; /* need more data */ + if (ret < 0) { + client_command_failed_early(&cmd, "Invalid tag."); + return TRUE; + } + cmd->tag = p_strdup(cmd->pool, tag); + } + + if (cmd->name == NULL) { + ret = imap_parser_read_command_name(cmd->parser, &name); + if (ret == 0) + return FALSE; /* need more data */ + if (ret < 0) { + client_command_failed_early(&cmd, "Invalid command name."); + return TRUE; + } + + /* UID commands are a special case. better to handle them + here. */ + if (!cmd->uid && strcasecmp(name, "UID") == 0) { + cmd->uid = TRUE; + return client_command_input(cmd); + } + cmd->name = !cmd->uid ? p_strdup(cmd->pool, name) : + p_strconcat(cmd->pool, "UID ", name, NULL); + client_command_init_finished(cmd); + imap_refresh_proctitle(); + } + + client->input_skip_line = TRUE; + + if (cmd->name[0] == '\0') { + /* command not given - cmd->func is already NULL. */ + } else if ((command = command_find(cmd->name)) != NULL) { + cmd->func = command->func; + cmd->cmd_flags = command->flags; + /* valid command - overwrite the "unknown" string set earlier */ + event_add_str(cmd->global_event, "cmd_name", command->name); + event_strlist_append(cmd->global_event, "reason_code", + event_reason_code_prefix("imap", "cmd_", command->name)); + event_add_str(cmd->event, "cmd_name", command->name); + if (client_command_is_ambiguous(cmd)) { + /* do nothing until existing commands are finished */ + i_assert(cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT); + cmd->state = CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY; + io_remove(&client->io); + return FALSE; + } + } + + if (cmd->func == NULL) { + /* unknown command */ + client_command_failed_early(&cmd, "Unknown command."); + return TRUE; + } else { + i_assert(!client->disconnected); + + return client_command_input(cmd); + } +} + +static bool client_handle_next_command(struct client *client, bool *remove_io_r) +{ + *remove_io_r = FALSE; + + if (client->input_lock != NULL) { + if (client->input_lock->state == + CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY || + /* we can't send literal "+ OK" replies if output is + locked by another command. */ + (client->output_cmd_lock != NULL && + client->output_cmd_lock != client->input_lock)) { + *remove_io_r = TRUE; + return FALSE; + } + return client_command_input(client->input_lock); + } + + if (client->input_skip_line) { + /* first eat the previous command line */ + if (!client_skip_line(client)) + return FALSE; + client->input_skip_line = FALSE; + } + + /* don't bother creating a new client command before there's at least + some input */ + if (i_stream_get_data_size(client->input) == 0) + return FALSE; + + /* beginning a new command */ + if (client->command_queue_size >= CLIENT_COMMAND_QUEUE_MAX_SIZE || + client->output_cmd_lock != NULL) { + /* wait for some of the commands to finish */ + *remove_io_r = TRUE; + return FALSE; + } + + client->input_lock = client_command_new(client); + return client_command_input(client->input_lock); +} + +bool client_handle_input(struct client *client) +{ + bool ret, remove_io, handled_commands = FALSE; + + i_assert(o_stream_is_corked(client->output) || + client->output->stream_errno != 0); + i_assert(!client->disconnected); + + client->handling_input = TRUE; + do { + T_BEGIN { + ret = client_handle_next_command(client, &remove_io); + } T_END; + if (ret) + handled_commands = TRUE; + } while (ret && !client->disconnected && client->io != NULL); + client->handling_input = FALSE; + + if (remove_io) + io_remove(&client->io); + else + client_add_missing_io(client); + if (!handled_commands) + return FALSE; + + if (client->input_lock == NULL) { + /* finished handling all commands. sync them all at once now. */ + cmd_sync_delayed(client); + } else if (client->input_lock->state == CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY) { + /* the command may be waiting for previous command to sync. */ + cmd_sync_delayed(client); + } + return TRUE; +} + +void client_input(struct client *client) +{ + struct client_command_context *cmd; + struct ostream *output = client->output; + ssize_t bytes; + + i_assert(client->io != NULL); + + client->last_input = ioloop_time; + timeout_reset(client->to_idle); + + bytes = i_stream_read(client->input); + if (bytes == -1) { + /* disconnected */ + client_destroy(client, NULL); + return; + } + + o_stream_ref(output); + o_stream_cork(output); + if (!client_handle_input(client) && bytes == -2) { + /* parameter word is longer than max. input buffer size. + this is most likely an error, so skip the new data + until newline is found. */ + client->input_skip_line = TRUE; + + cmd = client->input_lock != NULL ? client->input_lock : + client_command_new(client); + cmd->param_error = TRUE; + client_send_command_error(cmd, "Too long argument."); + client_command_free(&cmd); + } + o_stream_uncork(output); + o_stream_unref(&output); + imap_refresh_proctitle(); + + client_continue_pending_input(client); +} + +static void client_output_cmd(struct client_command_context *cmd) +{ + bool finished; + + /* continue processing command */ + finished = command_exec(cmd); + + if (!finished) + (void)client_handle_unfinished_cmd(cmd); + else { + /* command execution was finished */ + client_command_free(&cmd); + } +} + +static void client_output_commands(struct client *client) +{ + struct client_command_context *cmd; + + /* mark all commands non-executed */ + for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) + cmd->temp_executed = FALSE; + + if (client->output_cmd_lock != NULL) { + client->output_cmd_lock->temp_executed = TRUE; + client_output_cmd(client->output_cmd_lock); + } + while (client->output_cmd_lock == NULL) { + /* go through the entire commands list every time in case + multiple commands were freed. temp_executed keeps track of + which messages we've called so far */ + cmd = client->command_queue; + for (; cmd != NULL; cmd = cmd->next) { + if (!cmd->temp_executed && + cmd->state == CLIENT_COMMAND_STATE_WAIT_OUTPUT) { + cmd->temp_executed = TRUE; + client_output_cmd(cmd); + break; + } + } + if (cmd == NULL) { + /* all commands executed */ + break; + } + } +} + +int client_output(struct client *client) +{ + int ret; + + i_assert(!client->destroyed); + + client->last_output = ioloop_time; + timeout_reset(client->to_idle); + if (client->to_idle_output != NULL) + timeout_reset(client->to_idle_output); + + if ((ret = o_stream_flush(client->output)) < 0) { + client_destroy(client, NULL); + return 1; + } + + client_output_commands(client); + (void)cmd_sync_delayed(client); + + imap_refresh_proctitle_delayed(); + if (client->output->closed) + client_destroy(client, NULL); + else { + /* corking is added automatically by ostream-file. we need to + uncork here before client_check_command_hangs() is called, + because otherwise it can assert-crash due to ioloop not + having IO_WRITE callback set for the ostream. */ + o_stream_uncork(client->output); + client_continue_pending_input(client); + } + return ret; +} + +bool client_handle_search_save_ambiguity(struct client_command_context *cmd) +{ + struct client_command_context *old_cmd = cmd->next; + + /* search only commands that were added before this command + (commands are prepended to the queue, so they're after ourself) */ + for (; old_cmd != NULL; old_cmd = old_cmd->next) { + if (old_cmd->search_save_result) + break; + } + if (old_cmd == NULL) + return FALSE; + + /* ambiguity, wait until it's over */ + i_assert(cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT); + cmd->client->input_lock = cmd; + cmd->state = CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY; + cmd->search_save_result_used = TRUE; + io_remove(&cmd->client->io); + return TRUE; +} + +void client_enable(struct client *client, unsigned int feature_idx) +{ + if (client_has_enabled(client, feature_idx)) + return; + + const struct imap_feature *feat = imap_feature_idx(feature_idx); + feat->callback(client); + /* set after the callback, so the callback can see what features were + previously set */ + bool value = TRUE; + array_idx_set(&client->enabled_features, feature_idx, &value); +} + +bool client_has_enabled(struct client *client, unsigned int feature_idx) +{ + if (feature_idx >= array_count(&client->enabled_features)) + return FALSE; + const bool *featurep = + array_idx(&client->enabled_features, feature_idx); + return *featurep; +} + +static void imap_client_enable_condstore(struct client *client) +{ + struct mailbox_status status; + int ret; + + if (client->mailbox == NULL) + return; + + if ((client_enabled_mailbox_features(client) & MAILBOX_FEATURE_CONDSTORE) != 0) + return; + + ret = mailbox_enable(client->mailbox, MAILBOX_FEATURE_CONDSTORE); + if (ret == 0) { + /* CONDSTORE being enabled while mailbox is selected. + Notify client of the latest HIGHESTMODSEQ. */ + ret = mailbox_get_status(client->mailbox, + STATUS_HIGHESTMODSEQ, &status); + if (ret == 0) { + client_send_line(client, t_strdup_printf( + "* OK [HIGHESTMODSEQ %"PRIu64"] Highest", + status.highest_modseq)); + } + } + if (ret < 0) { + client_send_untagged_storage_error(client, + mailbox_get_storage(client->mailbox)); + } +} + +static void imap_client_enable_qresync(struct client *client) +{ + /* enable also CONDSTORE */ + client_enable(client, imap_feature_condstore); +} + +enum mailbox_feature client_enabled_mailbox_features(struct client *client) +{ + enum mailbox_feature mailbox_features = 0; + const struct imap_feature *feature; + const bool *client_enabled; + unsigned int count; + + client_enabled = array_get(&client->enabled_features, &count); + for (unsigned int idx = 0; idx < count; idx++) { + if (client_enabled[idx]) { + feature = imap_feature_idx(idx); + mailbox_features |= feature->mailbox_features; + } + } + return mailbox_features; +} + +const char *const *client_enabled_features(struct client *client) +{ + ARRAY_TYPE(const_string) feature_strings; + const struct imap_feature *feature; + const bool *client_enabled; + unsigned int count; + + t_array_init(&feature_strings, 8); + client_enabled = array_get(&client->enabled_features, &count); + for (unsigned int idx = 0; idx < count; idx++) { + if (client_enabled[idx]) { + feature = imap_feature_idx(idx); + array_push_back(&feature_strings, &feature->feature); + } + } + array_append_zero(&feature_strings); + return array_front(&feature_strings); +} + +struct imap_search_update * +client_search_update_lookup(struct client *client, const char *tag, + unsigned int *idx_r) +{ + struct imap_search_update *updates; + unsigned int i, count; + + if (!array_is_created(&client->search_updates)) + return NULL; + + updates = array_get_modifiable(&client->search_updates, &count); + for (i = 0; i < count; i++) { + if (strcmp(updates[i].tag, tag) == 0) { + *idx_r = i; + return &updates[i]; + } + } + return NULL; +} + +void client_search_updates_free(struct client *client) +{ + struct imap_search_update *update; + + if (!array_is_created(&client->search_updates)) + return; + + array_foreach_modifiable(&client->search_updates, update) + imap_search_update_free(update); + array_clear(&client->search_updates); +} + +void clients_init(void) +{ + imap_feature_condstore = + imap_feature_register("CONDSTORE", MAILBOX_FEATURE_CONDSTORE, + imap_client_enable_condstore); + imap_feature_qresync = + imap_feature_register("QRESYNC", MAILBOX_FEATURE_CONDSTORE, + imap_client_enable_qresync); +} + +void clients_destroy_all(void) +{ + while (imap_clients != NULL) { + mail_storage_service_io_activate_user(imap_clients->service_user); + client_send_line(imap_clients, "* BYE Server shutting down."); + client_destroy(imap_clients, "Server shutting down."); + } +} + +struct imap_client_vfuncs imap_client_vfuncs = { + .init = client_default_init, + .destroy = client_default_destroy, + + .send_tagline = client_default_send_tagline, + .sync_notify_more = client_default_sync_notify_more, + + .state_export = imap_state_export_base, + .state_import = imap_state_import_base, +}; diff --git a/src/imap/imap-client.h b/src/imap/imap-client.h new file mode 100644 index 0000000..a307955 --- /dev/null +++ b/src/imap/imap-client.h @@ -0,0 +1,361 @@ +#ifndef IMAP_CLIENT_H +#define IMAP_CLIENT_H + +#include "imap-commands.h" +#include "message-size.h" + +#define CLIENT_COMMAND_QUEUE_MAX_SIZE 4 +/* Maximum number of CONTEXT=SEARCH UPDATEs. Clients probably won't need more + than a few, so this is mainly to avoid more or less accidental pointless + resource usage. */ +#define CLIENT_MAX_SEARCH_UPDATES 10 + +struct client; +struct mail_storage; +struct mail_storage_service_ctx; +struct lda_settings; +struct imap_parser; +struct imap_arg; +struct imap_urlauth_context; + +struct mailbox_keywords { + /* All keyword names. The array itself exists in mail_index. + Keywords are currently only appended, they're never removed. */ + const ARRAY_TYPE(keywords) *names; + /* Number of keywords announced to client via FLAGS/PERMANENTFLAGS. + This relies on keywords not being removed while mailbox is + selected. */ + unsigned int announce_count; +}; + +struct imap_search_update { + char *tag; + struct mail_search_result *result; + bool return_uids; + + pool_t fetch_pool; + struct imap_fetch_context *fetch_ctx; +}; + +enum client_command_state { + /* Waiting for more input */ + CLIENT_COMMAND_STATE_WAIT_INPUT, + /* Waiting to be able to send more output */ + CLIENT_COMMAND_STATE_WAIT_OUTPUT, + /* Waiting for external interaction */ + CLIENT_COMMAND_STATE_WAIT_EXTERNAL, + /* Wait for other commands to finish execution */ + CLIENT_COMMAND_STATE_WAIT_UNAMBIGUITY, + /* Waiting for other commands to finish so we can sync */ + CLIENT_COMMAND_STATE_WAIT_SYNC, + /* Command is finished */ + CLIENT_COMMAND_STATE_DONE +}; + +struct client_command_stats { + /* time when command handling was started - typically this is after + reading all the parameters. */ + struct timeval start_time; + /* time when command handling was last finished. this is before + mailbox syncing is done. */ + struct timeval last_run_timeval; + /* io_loop_get_wait_usecs()'s value when the command was started */ + uint64_t start_ioloop_wait_usecs; + /* how many usecs this command itself has spent running */ + uint64_t running_usecs; + /* how many usecs this command itself has spent waiting for locks */ + uint64_t lock_wait_usecs; + /* how many bytes of client input/output command has used */ + uint64_t bytes_in, bytes_out; +}; + +struct client_command_stats_start { + struct timeval timeval; + uint64_t lock_wait_usecs; + uint64_t bytes_in, bytes_out; +}; + +struct client_command_context { + struct client_command_context *prev, *next; + struct client *client; + struct event *event; + /* global_event is pushed to the global event stack while the command + is running. It has only the minimal fields that are actually wanted + to be in all the events while it's being run. */ + struct event *global_event; + + pool_t pool; + /* IMAP command tag */ + const char *tag; + /* Name of this command */ + const char *name; + /* Parameters for this command. These are generated from parsed IMAP + arguments, so they may not be exactly the same as how client sent + them. */ + const char *args; + /* Parameters for this command generated with + imap_write_args_for_human(), so it's suitable for logging. */ + const char *human_args; + enum command_flags cmd_flags; + const char *tagline_reply; + + command_func_t *func; + void *context; + + /* Module-specific contexts. */ + ARRAY(union imap_module_context *) module_contexts; + + struct imap_parser *parser; + enum client_command_state state; + struct client_command_stats stats; + struct client_command_stats_start stats_start; + + struct imap_client_sync_context *sync; + + bool uid:1; /* used UID command */ + bool cancel:1; /* command is wanted to be cancelled */ + bool param_error:1; + bool search_save_result:1; /* search result is being updated */ + bool search_save_result_used:1; /* command uses search save */ + bool temp_executed:1; /* temporary execution state tracking */ + bool tagline_sent:1; + bool executing:1; +}; + +struct imap_client_vfuncs { + /* Perform client initialization. This is called when client creation is + finished completely. Particulary, at this point the namespaces are + fully initialized, which is not the case for the client create hook. + */ + void (*init)(struct client *client); + /* Destroy the client.*/ + void (*destroy)(struct client *client, const char *reason); + + /* Send a tagged response line. */ + void (*send_tagline)(struct client_command_context *cmd, + const char *data); + /* Run "mailbox syncing". This can send any unsolicited untagged + replies. Returns 1 = done, 0 = wait for more space in output buffer, + -1 = failed. */ + int (*sync_notify_more)(struct imap_sync_context *ctx); + + /* Export client state into buffer. Returns 1 if ok, 0 if some state + couldn't be preserved, -1 if temporary internal error occurred. */ + int (*state_export)(struct client *client, bool internal, + buffer_t *dest, const char **error_r); + /* Import a single block of client state from the given data. Returns + number of bytes successfully imported from the block, or 0 if state + is corrupted or contains unknown data (e.g. some plugin is no longer + loaded), -1 if temporary internal error occurred. */ + ssize_t (*state_import)(struct client *client, bool internal, + const unsigned char *data, size_t size, + const char **error_r); +}; + +struct client { + struct client *prev, *next; + + struct imap_client_vfuncs v; + struct event *event; + const char *const *userdb_fields; /* for internal session saving/restoring */ + + int fd_in, fd_out; + struct io *io; + struct istream *input; + struct ostream *output; + struct timeout *to_idle, *to_idle_output, *to_delayed_input; + + pool_t pool; + struct mail_storage_service_user *service_user; + const struct imap_settings *set; + const struct smtp_submit_settings *smtp_set; + string_t *capability_string; + const char *disconnect_reason; + + struct mail_user *user; + struct mailbox *mailbox; + struct mailbox_keywords keywords; + unsigned int sync_counter; + uint32_t messages_count, recent_count, uidvalidity; + ARRAY(bool) enabled_features; + + time_t last_input, last_output; + unsigned int bad_counter; + + /* one parser is kept here to be used for new commands */ + struct imap_parser *free_parser; + /* command_pool is cleared when the command queue gets empty */ + pool_t command_pool; + /* New commands are always prepended to the queue */ + struct client_command_context *command_queue; + unsigned int command_queue_size; + + char *last_cmd_name; + struct client_command_stats last_cmd_stats; + + uint64_t sync_last_full_modseq; + uint64_t highest_fetch_modseq; + ARRAY_TYPE(seq_range) fetch_failed_uids; + + /* For imap_logout_format statistics: */ + unsigned int fetch_hdr_count, fetch_body_count; + uint64_t fetch_hdr_bytes, fetch_body_bytes; + unsigned int deleted_count, expunged_count, trashed_count; + unsigned int autoexpunged_count, append_count; + + /* SEARCHRES extension: Last saved SEARCH result */ + ARRAY_TYPE(seq_range) search_saved_uidset; + /* SEARCH=CONTEXT extension: Searches that get updated */ + ARRAY(struct imap_search_update) search_updates; + /* NOTIFY extension */ + struct imap_notify_context *notify_ctx; + uint32_t notify_uidnext; + + /* client input/output is locked by this command */ + struct client_command_context *input_lock; + struct client_command_context *output_cmd_lock; + /* command changing the mailbox */ + struct client_command_context *mailbox_change_lock; + + /* IMAP URLAUTH context (RFC4467) */ + struct imap_urlauth_context *urlauth_ctx; + + /* Module-specific contexts. */ + ARRAY(union imap_module_context *) module_contexts; + + /* syncing marks this TRUE when it sees \Deleted flags. this is by + EXPUNGE for Outlook-workaround. */ + bool sync_seen_deletes:1; + bool logged_out:1; + bool disconnected:1; + bool hibernated:1; + bool destroyed:1; + bool handling_input:1; + bool syncing:1; + bool id_logged:1; + bool mailbox_examined:1; + bool anvil_sent:1; + bool tls_compression:1; + bool input_skip_line:1; /* skip all the data until we've + found a new line */ + bool modseqs_sent_since_sync:1; + bool notify_immediate_expunges:1; + bool notify_count_changes:1; + bool notify_flag_changes:1; + bool nonpermanent_modseqs:1; + bool state_import_bad_idle_done:1; + bool state_import_idle_continue:1; +}; + +struct imap_module_register { + unsigned int id; +}; + +union imap_module_context { + struct imap_client_vfuncs super; + struct imap_module_register *reg; +}; +extern struct imap_module_register imap_module_register; + +extern struct client *imap_clients; +extern unsigned int imap_client_count; + +extern unsigned int imap_feature_condstore; +extern unsigned int imap_feature_qresync; + +/* Create new client with specified input/output handles. socket specifies + if the handle is a socket. */ +struct client *client_create(int fd_in, int fd_out, + struct event *event, struct mail_user *user, + struct mail_storage_service_user *service_user, + const struct imap_settings *set, + const struct smtp_submit_settings *smtp_set); +void client_create_finish_io(struct client *client); +/* Finish creating the client. Returns 0 if ok, -1 if there's an error. */ +int client_create_finish(struct client *client, const char **error_r); +void client_add_istream_prefix(struct client *client, + const unsigned char *data, size_t size); +void client_destroy(struct client *client, const char *reason) ATTR_NULL(2); + +/* Disconnect client connection */ +void client_disconnect(struct client *client, const char *reason); +void client_disconnect_with_error(struct client *client, + const char *client_error); + +/* Add the given capability to the CAPABILITY reply. If imap_capability setting + has an explicit capability, nothing is changed. */ +void client_add_capability(struct client *client, const char *capability); + +/* Send a line of data to client. */ +void client_send_line(struct client *client, const char *data); +/* Send a line of data to client. Returns 1 if ok, 0 if buffer is getting full, + -1 if error. This should be used when you're (potentially) sending a lot of + lines to client. */ +int client_send_line_next(struct client *client, const char *data); +/* Send line of data to client, prefixed with client->tag. You need to prefix + the data with "OK ", "NO " or "BAD ". */ +void client_send_tagline(struct client_command_context *cmd, const char *data); + +/* Send a BAD command reply to client via client_send_tagline(). If there have + been too many command errors, the client is disconnected. client_error may + be NULL, in which case the error is looked up from imap_parser. */ +void client_send_command_error(struct client_command_context *cmd, + const char *client_error); + +/* Send a NO command reply with the default internal error message to client + via client_send_tagline(). */ +void client_send_internal_error(struct client_command_context *cmd); + +/* Read a number of arguments. Returns TRUE if everything was read or + FALSE if either needs more data or error occurred. */ +bool client_read_args(struct client_command_context *cmd, unsigned int count, + unsigned int flags, const struct imap_arg **args_r); +/* Reads a number of string arguments. ... is a list of pointers where to + store the arguments. */ +bool client_read_string_args(struct client_command_context *cmd, + unsigned int count, ...); +void client_args_finished(struct client_command_context *cmd, + const struct imap_arg *args); + +/* SEARCHRES extension: Call if $ is being used/updated, returns TRUE if we + have to wait for an existing SEARCH SAVE to finish. */ +bool client_handle_search_save_ambiguity(struct client_command_context *cmd); + +void client_enable(struct client *client, unsigned int feature_idx); +/* Returns TRUE if the given feature is enabled */ +bool client_has_enabled(struct client *client, unsigned int feature_idx); +/* Returns mailbox features that are currently enabled. */ +enum mailbox_feature client_enabled_mailbox_features(struct client *client); +/* Returns all enabled features as strings. */ +const char *const *client_enabled_features(struct client *client); + +/* Send client processing to imap-idle process. If successful, returns TRUE + and destroys the client. If hibernation failed, the exact reason is + returned (mainly for unit tests). */ +bool imap_client_hibernate(struct client **client, const char **reason_r); + +struct imap_search_update * +client_search_update_lookup(struct client *client, const char *tag, + unsigned int *idx_r); +void client_search_updates_free(struct client *client); + +struct client_command_context *client_command_alloc(struct client *client); +void client_command_init_finished(struct client_command_context *cmd); +void client_command_cancel(struct client_command_context **cmd); +void client_command_free(struct client_command_context **cmd); + +bool client_handle_unfinished_cmd(struct client_command_context *cmd); +/* Handle any pending command input. This must be run at the end of all + I/O callbacks after they've (potentially) finished some commands. */ +void client_continue_pending_input(struct client *client); +void client_add_missing_io(struct client *client); +const char *client_stats(struct client *client); + +void client_input(struct client *client); +bool client_handle_input(struct client *client); +int client_output(struct client *client); + +void clients_init(void); +void clients_destroy_all(void); + +#endif diff --git a/src/imap/imap-commands-util.c b/src/imap/imap-commands-util.c new file mode 100644 index 0000000..2d454a7 --- /dev/null +++ b/src/imap/imap-commands-util.c @@ -0,0 +1,402 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "array.h" +#include "buffer.h" +#include "str.h" +#include "str-sanitize.h" +#include "imap-resp-code.h" +#include "imap-parser.h" +#include "imap-sync.h" +#include "imap-utf7.h" +#include "imap-util.h" +#include "mail-storage.h" +#include "mail-namespace.h" +#include "imap-commands-util.h" + +struct mail_namespace * +client_find_namespace_full(struct client *client, + const char **mailbox, const char **client_error_r) +{ + struct mail_namespace *namespaces = client->user->namespaces; + struct mail_namespace *ns; + string_t *utf8_name; + + utf8_name = t_str_new(64); + if (imap_utf7_to_utf8(*mailbox, utf8_name) < 0) { + *client_error_r = "NO Mailbox name is not valid mUTF-7"; + return NULL; + } + + ns = mail_namespace_find(namespaces, str_c(utf8_name)); + if ((ns->flags & NAMESPACE_FLAG_AUTOCREATED) != 0 && + ns->prefix_len == 0) { + /* this matched only the autocreated prefix="" namespace. + give a nice human-readable error message */ + *client_error_r = t_strdup_printf( + "NO Client tried to access nonexistent namespace. " + "(Mailbox name should probably be prefixed with: %s)", + mail_namespace_find_inbox(namespaces)->prefix); + return NULL; + } + + if ((client->set->parsed_workarounds & + WORKAROUND_TB_EXTRA_MAILBOX_SEP) != 0 && + str_len(utf8_name) > 0 && + str_c(utf8_name)[str_len(utf8_name)-1] == mail_namespace_get_sep(ns)) { + /* drop the extra trailing hierarchy separator */ + str_truncate(utf8_name, str_len(utf8_name)-1); + } + + *mailbox = str_c(utf8_name); + return ns; +} + +struct mail_namespace * +client_find_namespace(struct client_command_context *cmd, const char **mailbox) +{ + struct mail_namespace *ns; + const char *client_error; + + ns = client_find_namespace_full(cmd->client, mailbox, &client_error); + if (ns == NULL) + client_send_tagline(cmd, client_error); + return ns; +} + +bool client_verify_open_mailbox(struct client_command_context *cmd) +{ + if (cmd->client->mailbox != NULL) { + event_add_str(cmd->global_event, "mailbox", + mailbox_get_vname(cmd->client->mailbox)); + return TRUE; + } else { + client_send_tagline(cmd, "BAD No mailbox selected."); + return FALSE; + } +} + +void imap_client_close_mailbox(struct client *client) +{ + struct mailbox *box; + + i_assert(client->mailbox != NULL); + + if (array_is_created(&client->fetch_failed_uids)) + array_clear(&client->fetch_failed_uids); + client_search_updates_free(client); + array_free(&client->search_saved_uidset); + + box = client->mailbox; + client->mailbox = NULL; + + mailbox_free(&box); + client_update_mailbox_flags(client, NULL); +} + +int client_open_save_dest_box(struct client_command_context *cmd, + const char *name, struct mailbox **destbox_r) +{ + struct mail_namespace *ns; + struct mailbox *box; + const char *error_string; + enum mail_error error; + + ns = client_find_namespace(cmd, &name); + if (ns == NULL) + return -1; + + if (cmd->client->mailbox != NULL && + mailbox_equals(cmd->client->mailbox, ns, name)) { + *destbox_r = cmd->client->mailbox; + return 0; + } + box = mailbox_alloc(ns->list, name, MAILBOX_FLAG_SAVEONLY); + if (mailbox_open(box) < 0) { + error_string = mailbox_get_last_error(box, &error); + if (error == MAIL_ERROR_NOTFOUND) { + client_send_tagline(cmd, t_strdup_printf( + "NO [TRYCREATE] %s", error_string)); + } else { + client_send_box_error(cmd, box); + } + mailbox_free(&box); + return -1; + } + if (mailbox_enable(box, client_enabled_mailbox_features(cmd->client)) < 0) { + client_send_box_error(cmd, box); + mailbox_free(&box); + return -1; + } + *destbox_r = box; + return 0; +} + +const char *imap_client_command_get_reason(struct client_command_context *cmd) +{ + return cmd->args[0] == '\0' ? cmd->name : + t_strdup_printf("%s %s", cmd->name, cmd->human_args); +} + +const char * +imap_get_error_string(struct client_command_context *cmd, + const char *error_string, enum mail_error error) +{ + const char *resp_code = NULL; + + switch (error) { + case MAIL_ERROR_NONE: + break; + case MAIL_ERROR_TEMP: + case MAIL_ERROR_LOOKUP_ABORTED: /* BUG: shouldn't be visible here */ + resp_code = IMAP_RESP_CODE_SERVERBUG; + break; + case MAIL_ERROR_UNAVAILABLE: + resp_code = IMAP_RESP_CODE_UNAVAILABLE; + break; + case MAIL_ERROR_NOTPOSSIBLE: + case MAIL_ERROR_PARAMS: + resp_code = IMAP_RESP_CODE_CANNOT; + break; + case MAIL_ERROR_PERM: + resp_code = IMAP_RESP_CODE_NOPERM; + break; + case MAIL_ERROR_NOQUOTA: + resp_code = IMAP_RESP_CODE_OVERQUOTA; + break; + case MAIL_ERROR_NOTFOUND: + if ((cmd->cmd_flags & COMMAND_FLAG_USE_NONEXISTENT) != 0) + resp_code = IMAP_RESP_CODE_NONEXISTENT; + break; + case MAIL_ERROR_EXISTS: + resp_code = IMAP_RESP_CODE_ALREADYEXISTS; + break; + case MAIL_ERROR_EXPUNGED: + resp_code = IMAP_RESP_CODE_EXPUNGEISSUED; + break; + case MAIL_ERROR_INUSE: + resp_code = IMAP_RESP_CODE_INUSE; + break; + case MAIL_ERROR_CONVERSION: + case MAIL_ERROR_INVALIDDATA: + break; + case MAIL_ERROR_LIMIT: + resp_code = IMAP_RESP_CODE_LIMIT; + break; + } + if (resp_code == NULL || *error_string == '[') + return t_strconcat("NO ", error_string, NULL); + else + return t_strdup_printf("NO [%s] %s", resp_code, error_string); +} + +void client_send_error(struct client_command_context *cmd, + const char *error_string, enum mail_error error) +{ + client_send_tagline(cmd, imap_get_error_string(cmd, error_string, + error)); + client_disconnect_if_inconsistent(cmd->client); +} + +void client_send_list_error(struct client_command_context *cmd, + struct mailbox_list *list) +{ + const char *error_string; + enum mail_error error; + + error_string = mailbox_list_get_last_error(list, &error); + client_send_tagline(cmd, imap_get_error_string(cmd, error_string, + error)); +} + +void client_disconnect_if_inconsistent(struct client *client) +{ + if (client->mailbox != NULL && + mailbox_is_inconsistent(client->mailbox)) { + /* we can't do forced CLOSE, so have to disconnect */ + client_disconnect_with_error(client, + "IMAP session state is inconsistent, please relogin."); + } +} + +void client_send_box_error(struct client_command_context *cmd, + struct mailbox *box) +{ + client_send_storage_error(cmd, mailbox_get_storage(box)); +} + +void client_send_storage_error(struct client_command_context *cmd, + struct mail_storage *storage) +{ + const char *error_string; + enum mail_error error; + + error_string = mail_storage_get_last_error(storage, &error); + client_send_error(cmd, error_string, error); +} + +void client_send_untagged_storage_error(struct client *client, + struct mail_storage *storage) +{ + const char *error_string; + enum mail_error error; + + error_string = mail_storage_get_last_error(storage, &error); + client_send_line(client, t_strconcat("* NO ", error_string, NULL)); + + client_disconnect_if_inconsistent(client); +} + +bool client_parse_mail_flags(struct client_command_context *cmd, + const struct imap_arg *args, + enum mail_flags *flags_r, + const char *const **keywords_r) +{ + const char *atom; + enum mail_flags flag; + ARRAY(const char *) keywords; + + *flags_r = 0; + *keywords_r = NULL; + p_array_init(&keywords, cmd->pool, 16); + + while (!IMAP_ARG_IS_EOL(args)) { + if (!imap_arg_get_atom(args, &atom)) { + client_send_command_error(cmd, + "Flags list contains non-atoms."); + return FALSE; + } + + if (*atom == '\\') { + /* system flag */ + atom = t_str_ucase(atom); + flag = imap_parse_system_flag(atom); + if (flag != 0 && flag != MAIL_RECENT) + *flags_r |= flag; + else { + client_send_command_error(cmd, t_strconcat( + "Invalid system flag ", atom, NULL)); + return FALSE; + } + } else { + /* keyword validity checks are done by lib-storage */ + array_push_back(&keywords, &atom); + } + + args++; + } + + if (array_count(&keywords) == 0) + *keywords_r = NULL; + else { + array_append_zero(&keywords); /* NULL-terminate */ + *keywords_r = array_front(&keywords); + } + return TRUE; +} + +void client_send_mailbox_flags(struct client *client, bool selecting) +{ + struct mailbox_status status; + unsigned int count = array_count(client->keywords.names); + const char *const *keywords; + string_t *str; + + if (!selecting && count == client->keywords.announce_count) { + /* no changes to keywords and we're not selecting a mailbox */ + return; + } + + client->keywords.announce_count = count; + mailbox_get_open_status(client->mailbox, STATUS_PERMANENT_FLAGS, + &status); + + keywords = count == 0 ? NULL : + array_front(client->keywords.names); + str = t_str_new(128); + str_append(str, "* FLAGS ("); + imap_write_flags(str, status.flags, keywords); + str_append_c(str, ')'); + client_send_line(client, str_c(str)); + + if (!status.permanent_keywords) + keywords = NULL; + + str_truncate(str, 0); + str_append(str, "* OK [PERMANENTFLAGS ("); + imap_write_flags(str, status.permanent_flags, keywords); + if (status.allow_new_keywords) { + if (status.permanent_flags != 0 || keywords != NULL) + str_append_c(str, ' '); + str_append(str, "\\*"); + } + str_append(str, ")] "); + + if (mailbox_is_readonly(client->mailbox)) + str_append(str, "Read-only mailbox."); + else + str_append(str, "Flags permitted."); + client_send_line(client, str_c(str)); +} + +void client_update_mailbox_flags(struct client *client, + const ARRAY_TYPE(keywords) *keywords) +{ + client->keywords.names = keywords; + client->keywords.announce_count = 0; +} + +const char *const * +client_get_keyword_names(struct client *client, ARRAY_TYPE(keywords) *dest, + const ARRAY_TYPE(keyword_indexes) *src) +{ + unsigned int kw_index; + const char *const *all_names; + unsigned int all_count; + + client_send_mailbox_flags(client, FALSE); + + /* convert indexes to names */ + all_names = array_get(client->keywords.names, &all_count); + array_clear(dest); + array_foreach_elem(src, kw_index) { + i_assert(kw_index < all_count); + array_push_back(dest, &all_names[kw_index]); + } + + array_append_zero(dest); + return array_front(dest); +} + +void msgset_generator_init(struct msgset_generator_context *ctx, string_t *str) +{ + i_zero(ctx); + ctx->str = str; + ctx->last_uid = (uint32_t)-1; +} + +void msgset_generator_next(struct msgset_generator_context *ctx, uint32_t uid) +{ + i_assert(uid > 0); + + if (uid-1 != ctx->last_uid) { + if (ctx->first_uid == 0) + ; + else if (ctx->first_uid == ctx->last_uid) + str_printfa(ctx->str, "%u,", ctx->first_uid); + else { + str_printfa(ctx->str, "%u:%u,", + ctx->first_uid, ctx->last_uid); + } + ctx->first_uid = uid; + } + ctx->last_uid = uid; +} + +void msgset_generator_finish(struct msgset_generator_context *ctx) +{ + if (ctx->first_uid == ctx->last_uid) + str_printfa(ctx->str, "%u", ctx->first_uid); + else + str_printfa(ctx->str, "%u:%u", ctx->first_uid, ctx->last_uid); +} diff --git a/src/imap/imap-commands-util.h b/src/imap/imap-commands-util.h new file mode 100644 index 0000000..997ed02 --- /dev/null +++ b/src/imap/imap-commands-util.h @@ -0,0 +1,81 @@ +#ifndef IMAP_COMMANDS_UTIL_H +#define IMAP_COMMANDS_UTIL_H + +struct msgset_generator_context { + string_t *str; + uint32_t first_uid, last_uid; +}; + +struct mail_full_flags; +struct mailbox_keywords; + +/* Finds namespace for given mailbox from namespaces. If namespace isn't found + or mailbox name is invalid, sends a tagged NO reply to client. */ +struct mail_namespace * +client_find_namespace(struct client_command_context *cmd, const char **mailbox); +struct mail_namespace * +client_find_namespace_full(struct client *client, + const char **mailbox, const char **client_error_r); + +/* Returns TRUE if mailbox is selected. If not, sends "No mailbox selected" + error message to client. */ +bool client_verify_open_mailbox(struct client_command_context *cmd); +/* Close the selected mailbox. */ +void imap_client_close_mailbox(struct client *client); + +/* Open APPEND/COPY destination mailbox. */ +int client_open_save_dest_box(struct client_command_context *cmd, + const char *name, struct mailbox **destbox_r); + +/* Returns string based in IMAP command name and parameters. */ +const char *imap_client_command_get_reason(struct client_command_context *cmd); +/* Set transaction's reason to the IMAP command name and parameters. */ +void imap_transaction_set_cmd_reason(struct mailbox_transaction_context *trans, + struct client_command_context *cmd); +const char * +imap_get_error_string(struct client_command_context *cmd, + const char *error_string, enum mail_error error); + +void client_disconnect_if_inconsistent(struct client *client); + +/* Send an explicit error message to client. */ +void client_send_error(struct client_command_context *cmd, + const char *error_string, enum mail_error error); +/* Send last mailbox list error message to client. */ +void client_send_list_error(struct client_command_context *cmd, + struct mailbox_list *list); +/* Send last mail storage error message to client. */ +void client_send_storage_error(struct client_command_context *cmd, + struct mail_storage *storage); +/* Send last mailbox's storage error message to client. */ +void client_send_box_error(struct client_command_context *cmd, + struct mailbox *box); + +/* Send untagged error message to client. */ +void client_send_untagged_storage_error(struct client *client, + struct mail_storage *storage); + +/* Parse flags. Returns TRUE if successful, if not sends an error message to + client. */ +bool client_parse_mail_flags(struct client_command_context *cmd, + const struct imap_arg *args, + enum mail_flags *flags_r, + const char *const **keywords_r); + +/* Send FLAGS + PERMANENTFLAGS to client if they have changed, + or if selecting=TRUE. */ +void client_send_mailbox_flags(struct client *client, bool selecting); +/* Update client->keywords array. Use keywords=NULL when unselecting. */ +void client_update_mailbox_flags(struct client *client, + const ARRAY_TYPE(keywords) *keywords) + ATTR_NULL(2); +/* Convert keyword indexes to keyword names in selected mailbox. */ +const char *const * +client_get_keyword_names(struct client *client, ARRAY_TYPE(keywords) *dest, + const ARRAY_TYPE(keyword_indexes) *src); + +void msgset_generator_init(struct msgset_generator_context *ctx, string_t *str); +void msgset_generator_next(struct msgset_generator_context *ctx, uint32_t uid); +void msgset_generator_finish(struct msgset_generator_context *ctx); + +#endif diff --git a/src/imap/imap-commands.c b/src/imap/imap-commands.c new file mode 100644 index 0000000..b78d0a1 --- /dev/null +++ b/src/imap/imap-commands.c @@ -0,0 +1,247 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "array.h" +#include "buffer.h" +#include "ioloop.h" +#include "istream.h" +#include "ostream.h" +#include "time-util.h" +#include "imap-commands.h" + + +struct command_hook { + command_hook_callback_t *pre; + command_hook_callback_t *post; +}; + +static const struct command imap4rev1_commands[] = { + { "CAPABILITY", cmd_capability, 0 }, + { "LOGOUT", cmd_logout, COMMAND_FLAG_BREAKS_MAILBOX }, + { "NOOP", cmd_noop, COMMAND_FLAG_BREAKS_SEQS }, + + { "APPEND", cmd_append, COMMAND_FLAG_BREAKS_SEQS | + /* finish syncing and sending + all tagged commands before + we wait for APPEND input */ + COMMAND_FLAG_BREAKS_MAILBOX }, + { "EXAMINE", cmd_examine, COMMAND_FLAG_BREAKS_MAILBOX }, + { "CREATE", cmd_create, 0 }, + { "DELETE", cmd_delete, COMMAND_FLAG_BREAKS_MAILBOX | + COMMAND_FLAG_USE_NONEXISTENT }, + { "RENAME", cmd_rename, COMMAND_FLAG_USE_NONEXISTENT }, + { "LIST", cmd_list, 0 }, + { "LSUB", cmd_lsub, 0 }, + { "SELECT", cmd_select, COMMAND_FLAG_BREAKS_MAILBOX }, + { "STATUS", cmd_status, 0 }, + { "SUBSCRIBE", cmd_subscribe, 0 }, + { "UNSUBSCRIBE", cmd_unsubscribe, COMMAND_FLAG_USE_NONEXISTENT }, + + { "CHECK", cmd_check, COMMAND_FLAG_BREAKS_SEQS }, + { "CLOSE", cmd_close, COMMAND_FLAG_BREAKS_MAILBOX }, + { "COPY", cmd_copy, COMMAND_FLAG_USES_SEQS | + COMMAND_FLAG_BREAKS_SEQS }, + { "EXPUNGE", cmd_expunge, COMMAND_FLAG_BREAKS_SEQS }, + { "FETCH", cmd_fetch, COMMAND_FLAG_USES_SEQS }, + { "SEARCH", cmd_search, COMMAND_FLAG_USES_SEQS }, + { "STORE", cmd_store, COMMAND_FLAG_USES_SEQS }, + { "UID COPY", cmd_copy, COMMAND_FLAG_BREAKS_SEQS }, + { "UID FETCH", cmd_fetch, COMMAND_FLAG_BREAKS_SEQS }, + { "UID SEARCH", cmd_search, COMMAND_FLAG_BREAKS_SEQS }, + { "UID STORE", cmd_store, COMMAND_FLAG_BREAKS_SEQS } +}; +#define IMAP4REV1_COMMANDS_COUNT N_ELEMENTS(imap4rev1_commands) + +static const struct command imap_ext_commands[] = { + /* IMAP extensions: */ + { "CANCELUPDATE", cmd_cancelupdate,0 }, + { "ENABLE", cmd_enable, 0 }, + { "ID", cmd_id, 0 }, + { "IDLE", cmd_idle, COMMAND_FLAG_BREAKS_SEQS | + COMMAND_FLAG_REQUIRES_SYNC | + /* finish syncing and sending + all tagged commands before + IDLE is started */ + COMMAND_FLAG_BREAKS_MAILBOX }, + { "GETMETADATA", cmd_getmetadata, 0 }, + { "SETMETADATA", cmd_setmetadata, 0 }, + { "NAMESPACE", cmd_namespace, 0 }, + { "NOTIFY", cmd_notify, COMMAND_FLAG_BREAKS_SEQS }, + { "SORT", cmd_sort, COMMAND_FLAG_USES_SEQS }, + { "THREAD", cmd_thread, COMMAND_FLAG_USES_SEQS }, + { "UID EXPUNGE", cmd_uid_expunge, COMMAND_FLAG_BREAKS_SEQS }, + { "MOVE", cmd_move, COMMAND_FLAG_USES_SEQS | + COMMAND_FLAG_BREAKS_SEQS }, + { "UID MOVE", cmd_move, COMMAND_FLAG_BREAKS_SEQS }, + { "UID SORT", cmd_sort, COMMAND_FLAG_BREAKS_SEQS }, + { "UID THREAD", cmd_thread, COMMAND_FLAG_BREAKS_SEQS }, + { "UNSELECT", cmd_unselect, COMMAND_FLAG_BREAKS_MAILBOX }, + { "X-CANCEL", cmd_x_cancel, 0 }, + { "X-STATE", cmd_x_state, COMMAND_FLAG_REQUIRES_SYNC }, + { "XLIST", cmd_list, 0 }, + /* IMAP URLAUTH (RFC4467): */ + { "GENURLAUTH", cmd_genurlauth, 0 }, + { "RESETKEY", cmd_resetkey, 0 }, + { "URLFETCH", cmd_urlfetch, 0 } +}; +#define IMAP_EXT_COMMANDS_COUNT N_ELEMENTS(imap_ext_commands) + +ARRAY_TYPE(command) imap_commands; +static bool commands_unsorted; +static ARRAY(struct command_hook) command_hooks; + +void command_register(const char *name, command_func_t *func, + enum command_flags flags) +{ + struct command cmd; + + i_zero(&cmd); + cmd.name = name; + cmd.func = func; + cmd.flags = flags; + array_push_back(&imap_commands, &cmd); + + commands_unsorted = TRUE; +} + +void command_unregister(const char *name) +{ + const struct command *cmd; + unsigned int i, count; + + cmd = array_get(&imap_commands, &count); + for (i = 0; i < count; i++) { + if (strcasecmp(cmd[i].name, name) == 0) { + array_delete(&imap_commands, i, 1); + return; + } + } + + i_error("Trying to unregister unknown command '%s'", name); +} + +void command_register_array(const struct command *cmdarr, unsigned int count) +{ + commands_unsorted = TRUE; + array_append(&imap_commands, cmdarr, count); +} + +void command_unregister_array(const struct command *cmdarr, unsigned int count) +{ + while (count > 0) { + command_unregister(cmdarr->name); + count--; cmdarr++; + } +} + +void command_hook_register(command_hook_callback_t *pre, + command_hook_callback_t *post) +{ + struct command_hook hook; + + hook.pre = pre; + hook.post = post; + array_push_back(&command_hooks, &hook); +} + +void command_hook_unregister(command_hook_callback_t *pre, + command_hook_callback_t *post) +{ + const struct command_hook *hooks; + unsigned int i, count; + + hooks = array_get(&command_hooks, &count); + for (i = 0; i < count; i++) { + if (hooks[i].pre == pre && hooks[i].post == post) { + array_delete(&command_hooks, i, 1); + return; + } + } + i_panic("command_hook_unregister(): hook not registered"); +} + +void command_stats_start(struct client_command_context *cmd) +{ + cmd->stats_start.timeval = ioloop_timeval; + cmd->stats_start.lock_wait_usecs = file_lock_wait_get_total_usecs(); + cmd->stats_start.bytes_in = i_stream_get_absolute_offset(cmd->client->input); + cmd->stats_start.bytes_out = cmd->client->output->offset; +} + +void command_stats_flush(struct client_command_context *cmd) +{ + io_loop_time_refresh(); + cmd->stats.running_usecs += + timeval_diff_usecs(&ioloop_timeval, &cmd->stats_start.timeval); + cmd->stats.lock_wait_usecs += + file_lock_wait_get_total_usecs() - + cmd->stats_start.lock_wait_usecs; + cmd->stats.bytes_in += i_stream_get_absolute_offset(cmd->client->input) - + cmd->stats_start.bytes_in; + cmd->stats.bytes_out += cmd->client->output->offset - + cmd->stats_start.bytes_out; + /* allow flushing multiple times */ + command_stats_start(cmd); +} + +bool command_exec(struct client_command_context *cmd) +{ + const struct command_hook *hook; + bool finished; + + i_assert(!cmd->executing); + + io_loop_time_refresh(); + command_stats_start(cmd); + + event_push_global(cmd->global_event); + cmd->executing = TRUE; + array_foreach(&command_hooks, hook) + hook->pre(cmd); + finished = cmd->func(cmd); + array_foreach(&command_hooks, hook) + hook->post(cmd); + cmd->executing = FALSE; + event_pop_global(cmd->global_event); + if (cmd->state == CLIENT_COMMAND_STATE_DONE) + finished = TRUE; + + command_stats_flush(cmd); + return finished; +} + +static int command_cmp(const struct command *c1, const struct command *c2) +{ + return strcasecmp(c1->name, c2->name); +} + +static int command_bsearch(const char *name, const struct command *cmd) +{ + return strcasecmp(name, cmd->name); +} + +struct command *command_find(const char *name) +{ + if (commands_unsorted) { + array_sort(&imap_commands, command_cmp); + commands_unsorted = FALSE; + } + + return array_bsearch(&imap_commands, name, command_bsearch); +} + +void commands_init(void) +{ + i_array_init(&imap_commands, 64); + i_array_init(&command_hooks, 4); + commands_unsorted = FALSE; + + command_register_array(imap4rev1_commands, IMAP4REV1_COMMANDS_COUNT); + command_register_array(imap_ext_commands, IMAP_EXT_COMMANDS_COUNT); +} + +void commands_deinit(void) +{ + array_free(&imap_commands); + array_free(&command_hooks); +} diff --git a/src/imap/imap-commands.h b/src/imap/imap-commands.h new file mode 100644 index 0000000..651a6df --- /dev/null +++ b/src/imap/imap-commands.h @@ -0,0 +1,137 @@ +#ifndef IMAP_COMMANDS_H +#define IMAP_COMMANDS_H + +struct client_command_context; + +#include "mail-storage.h" +#include "mail-namespace.h" +#include "imap-parser.h" +#include "imap-sync.h" +#include "imap-commands-util.h" + +typedef bool command_func_t(struct client_command_context *cmd); +typedef void command_hook_callback_t(struct client_command_context *ctx); + +enum command_flags { + /* Command uses sequences as its input parameters */ + COMMAND_FLAG_USES_SEQS = 0x01, + /* Command may reply with EXPUNGE, causing sequences to break */ + COMMAND_FLAG_BREAKS_SEQS = 0x02, + /* Command changes the mailbox */ + COMMAND_FLAG_BREAKS_MAILBOX = 0x04 | COMMAND_FLAG_BREAKS_SEQS, + + /* Command uses selected mailbox */ + COMMAND_FLAG_USES_MAILBOX = COMMAND_FLAG_BREAKS_MAILBOX | + COMMAND_FLAG_USES_SEQS, + + /* Command requires mailbox syncing before it can do its job. */ + COMMAND_FLAG_REQUIRES_SYNC = 0x08, + /* Command allows replying with [NONEXISTENT] imap resp code. + Dovecot internally returns it for all kinds of commands, + but unfortunately RFC 5530 specifies it only for "delete something" + operations. */ + COMMAND_FLAG_USE_NONEXISTENT = 0x10 +}; + +struct command { + const char *name; + command_func_t *func; + + enum command_flags flags; +}; +ARRAY_DEFINE_TYPE(command, struct command); + +extern ARRAY_TYPE(command) imap_commands; + +/* Register command. Given name parameter must be permanently stored until + command is unregistered. */ +void command_register(const char *name, command_func_t *func, + enum command_flags flags); +void command_unregister(const char *name); + +/* Register array of commands. */ +void command_register_array(const struct command *cmdarr, unsigned int count); +void command_unregister_array(const struct command *cmdarr, unsigned int count); + +/* Register hook callbacks that are called before and after all commands */ +void command_hook_register(command_hook_callback_t *pre, + command_hook_callback_t *post); +void command_hook_unregister(command_hook_callback_t *pre, + command_hook_callback_t *post); +/* Execute command and hooks */ +bool command_exec(struct client_command_context *cmd); +/* Starts counting command statistics. */ +void command_stats_start(struct client_command_context *cmd); +/* Finish counting command statistics. This is called automatically when + command_exec() returns, but it should be called explicitly if the stats are + needed during command_exec(). */ +void command_stats_flush(struct client_command_context *cmd); + +struct command *command_find(const char *name); + +void commands_init(void); +void commands_deinit(void); + +/* IMAP4rev1 commands: */ + +/* Non-Authenticated State */ +bool cmd_logout(struct client_command_context *cmd); + +bool cmd_capability(struct client_command_context *cmd); +bool cmd_noop(struct client_command_context *cmd); + +/* Authenticated State */ +bool cmd_select(struct client_command_context *cmd); +bool cmd_examine(struct client_command_context *cmd); + +bool cmd_create(struct client_command_context *cmd); +bool cmd_delete(struct client_command_context *cmd); +bool cmd_rename(struct client_command_context *cmd); + +bool cmd_subscribe(struct client_command_context *cmd); +bool cmd_unsubscribe(struct client_command_context *cmd); + +bool cmd_list(struct client_command_context *cmd); +bool cmd_lsub(struct client_command_context *cmd); + +bool cmd_status(struct client_command_context *cmd); +bool cmd_append(struct client_command_context *cmd); + +/* Selected state */ +bool cmd_check(struct client_command_context *cmd); +bool cmd_close(struct client_command_context *cmd); +bool cmd_expunge(struct client_command_context *cmd); +bool cmd_search(struct client_command_context *cmd); +bool cmd_fetch(struct client_command_context *cmd); +bool cmd_store(struct client_command_context *cmd); +bool cmd_copy(struct client_command_context *cmd); +bool cmd_uid(struct client_command_context *cmd); + +/* IMAP extensions: */ +bool cmd_cancelupdate(struct client_command_context *cmd); +bool cmd_enable(struct client_command_context *cmd); +bool cmd_id(struct client_command_context *cmd); +bool cmd_idle(struct client_command_context *cmd); +bool cmd_namespace(struct client_command_context *cmd); +bool cmd_getmetadata(struct client_command_context *cmd); +bool cmd_setmetadata(struct client_command_context *cmd); +bool cmd_notify(struct client_command_context *cmd); +bool cmd_sort(struct client_command_context *cmd); +bool cmd_thread(struct client_command_context *cmd); +bool cmd_uid_expunge(struct client_command_context *cmd); +bool cmd_move(struct client_command_context *cmd); +bool cmd_unselect(struct client_command_context *cmd); +bool cmd_x_cancel(struct client_command_context *cmd); +bool cmd_x_state(struct client_command_context *cmd); + +/* IMAP URLAUTH (RFC4467): */ +bool cmd_genurlauth(struct client_command_context *cmd); +bool cmd_resetkey(struct client_command_context *cmd); +bool cmd_urlfetch(struct client_command_context *cmd); + +/* private: */ +bool cmd_list_full(struct client_command_context *cmd, bool lsub); +bool cmd_select_full(struct client_command_context *cmd, bool readonly); +bool cmd_subscribe_full(struct client_command_context *cmd, bool subscribe); + +#endif diff --git a/src/imap/imap-common.h b/src/imap/imap-common.h new file mode 100644 index 0000000..1609a8b --- /dev/null +++ b/src/imap/imap-common.h @@ -0,0 +1,40 @@ +#ifndef IMAP_COMMON_H +#define IMAP_COMMON_H + +/* Disconnect client after idling this many milliseconds */ +#define CLIENT_IDLE_TIMEOUT_MSECS (60*30*1000) + +/* If we can't send anything to client for this long, disconnect the client */ +#define CLIENT_OUTPUT_TIMEOUT_MSECS (5*60*1000) + +/* Stop buffering more data into output stream after this many bytes */ +#define CLIENT_OUTPUT_OPTIMAL_SIZE 2048 + +/* Disconnect client when it sends too many bad commands in a row */ +#define CLIENT_MAX_BAD_COMMANDS 20 + +#include "lib.h" +#include "imap-client.h" +#include "imap-settings.h" + +struct mail_storage_service_input; + +typedef void imap_client_created_func_t(struct client **client); + +extern imap_client_created_func_t *hook_client_created; +extern bool imap_debug; +extern struct event_category event_category_imap; + +/* Sets the hook_client_created and returns the previous hook, + which the new_hook should call if it's non-NULL. */ +imap_client_created_func_t * ATTR_NOWARN_UNUSED_RESULT +imap_client_created_hook_set(imap_client_created_func_t *new_hook); + +void imap_refresh_proctitle(void); +void imap_refresh_proctitle_delayed(void); + +int client_create_from_input(const struct mail_storage_service_input *input, + int fd_in, int fd_out, struct client **client_r, + const char **error_r); + +#endif diff --git a/src/imap/imap-expunge.c b/src/imap/imap-expunge.c new file mode 100644 index 0000000..ff1a213 --- /dev/null +++ b/src/imap/imap-expunge.c @@ -0,0 +1,111 @@ +/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "mail-storage.h" +#include "mail-search-build.h" +#include "imap-search-args.h" +#include "imap-expunge.h" + +#define IMAP_EXPUNGE_BATCH_SIZE 1000 + +/* get a seqset of all the mails with \Deleted */ +static int imap_expunge_get_seqset(struct mailbox *box, + struct mail_search_arg *next_search_arg, + ARRAY_TYPE(seq_range) *seqset) +{ + struct mailbox_transaction_context *t; + struct mail_search_args *search_args; + struct mail_search_context *ctx; + struct mail *mail; + int ret; + + search_args = mail_search_build_init(); + search_args->args = p_new(search_args->pool, struct mail_search_arg, 1); + search_args->args->type = SEARCH_FLAGS; + search_args->args->value.flags = MAIL_DELETED; + search_args->args->next = next_search_arg; + + /* Refresh the flags so we'll expunge all messages marked as \Deleted + by any session. */ + t = mailbox_transaction_begin(box, MAILBOX_TRANSACTION_FLAG_REFRESH, + "EXPUNGE"); + ctx = mailbox_search_init(t, search_args, NULL, 0, NULL); + + /* collect the seqs into a seqset */ + while (mailbox_search_next(ctx, &mail)) + seq_range_array_add(seqset, mail->seq); + + ret = mailbox_search_deinit(&ctx); + /* commit in case a plugin made changes - failures should not abort the expunge */ + (void) mailbox_transaction_commit(&t); + mail_search_args_unref(&search_args); + + if (ret < 0) + array_free(seqset); + + return ret; +} + +int imap_expunge(struct mailbox *box, struct mail_search_arg *next_search_arg, + unsigned int *expunged_count) +{ + struct imap_search_seqset_iter *seqset_iter; + struct mail_search_args *search_args; + struct mailbox_status status; + bool expunges = FALSE; + int ret; + + if (mailbox_is_readonly(box)) { + /* silently ignore */ + return 0; + } + + mailbox_get_open_status(box, STATUS_MESSAGES, &status); + + search_args = mail_search_build_init(); + search_args->args = p_new(search_args->pool, struct mail_search_arg, 1); + search_args->args->type = SEARCH_SEQSET; + p_array_init(&search_args->args->value.seqset, search_args->pool, 16); + + if (imap_expunge_get_seqset(box, next_search_arg, + &search_args->args->value.seqset) < 0) { + mail_search_args_unref(&search_args); + return -1; + } + + seqset_iter = imap_search_seqset_iter_init(search_args, status.messages, + IMAP_EXPUNGE_BATCH_SIZE); + + do { + struct mailbox_transaction_context *t; + struct mail_search_context *ctx; + struct mail *mail; + + t = mailbox_transaction_begin(box, 0, "EXPUNGE"); + ctx = mailbox_search_init(t, search_args, NULL, 0, NULL); + + while (mailbox_search_next(ctx, &mail)) { + *expunged_count += 1; + mail_expunge(mail); + expunges = TRUE; + } + + ret = mailbox_search_deinit(&ctx); + if (ret < 0) { + mailbox_transaction_rollback(&t); + break; + } else { + ret = mailbox_transaction_commit(&t); + if (ret < 0) + break; + } + } while (imap_search_seqset_iter_next(seqset_iter)); + + imap_search_seqset_iter_deinit(&seqset_iter); + mail_search_args_unref(&search_args); + + if (ret < 0) + return ret; + + return expunges ? 1 : 0; +} diff --git a/src/imap/imap-expunge.h b/src/imap/imap-expunge.h new file mode 100644 index 0000000..e5b3825 --- /dev/null +++ b/src/imap/imap-expunge.h @@ -0,0 +1,10 @@ +#ifndef IMAP_EXPUNGE_H +#define IMAP_EXPUNGE_H + +struct mail_search_arg; + +int imap_expunge(struct mailbox *box, struct mail_search_arg *next_search_arg, + unsigned int *expunged_count) + ATTR_NULL(2); + +#endif diff --git a/src/imap/imap-feature.c b/src/imap/imap-feature.c new file mode 100644 index 0000000..d8d52b8 --- /dev/null +++ b/src/imap/imap-feature.c @@ -0,0 +1,46 @@ +/* Copyright (c) 2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "imap-feature.h" + +static ARRAY_TYPE(imap_feature) feature_register = ARRAY_INIT; + +bool imap_feature_lookup(const char *name, unsigned int *feature_idx_r) +{ + for (unsigned int idx = 0; idx < array_count(&feature_register); idx++) { + const struct imap_feature *feat = + array_idx(&feature_register, idx); + if (strcasecmp(name, feat->feature) == 0) { + *feature_idx_r = idx; + return TRUE; + } + } + return FALSE; +} + +const struct imap_feature *imap_feature_idx(unsigned int feature_idx) +{ + return array_idx(&feature_register, feature_idx); +} + +unsigned int +imap_feature_register(const char *feature, enum mailbox_feature mailbox_features, + imap_client_enable_callback_t *callback) +{ + struct imap_feature *feat = array_append_space(&feature_register); + feat->feature = feature; + feat->mailbox_features = mailbox_features; + feat->callback = callback; + return array_count(&feature_register)-1; +} + +void imap_features_init(void) +{ + i_assert(!array_is_created(&feature_register)); + i_array_init(&feature_register, 8); +} + +void imap_features_deinit(void) +{ + array_free(&feature_register); +} diff --git a/src/imap/imap-feature.h b/src/imap/imap-feature.h new file mode 100644 index 0000000..a96aa6e --- /dev/null +++ b/src/imap/imap-feature.h @@ -0,0 +1,24 @@ +#ifndef IMAP_FEATURE_H +#define IMAP_FEATURE_H + +typedef void imap_client_enable_callback_t(struct client *); + +struct imap_feature { + const char *feature; + enum mailbox_feature mailbox_features; + imap_client_enable_callback_t *callback; + bool enabled; +}; +ARRAY_DEFINE_TYPE(imap_feature, struct imap_feature); + +bool imap_feature_lookup(const char *name, unsigned int *feature_idx_r); +const struct imap_feature *imap_feature_idx(unsigned int feature_idx); + +unsigned int +imap_feature_register(const char *feature, enum mailbox_feature mailbox_features, + imap_client_enable_callback_t *callback); + +void imap_features_init(void); +void imap_features_deinit(void); + +#endif diff --git a/src/imap/imap-fetch-body.c b/src/imap/imap-fetch-body.c new file mode 100644 index 0000000..b467334 --- /dev/null +++ b/src/imap/imap-fetch-body.c @@ -0,0 +1,722 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "buffer.h" +#include "str.h" +#include "strescape.h" +#include "istream.h" +#include "ostream.h" +#include "istream-header-filter.h" +#include "message-parser.h" +#include "mail-storage-private.h" +#include "imap-quote.h" +#include "imap-parser.h" +#include "imap-msgpart.h" +#include "imap-fetch.h" + +#include <ctype.h> +#include <unistd.h> + +struct imap_fetch_body_data { + const char *section; /* NOTE: always uppercased */ + struct imap_msgpart *msgpart; + + bool partial:1; + bool binary:1; + bool binary_size:1; +}; + +struct imap_fetch_preview_data { + /* If TRUE, lazy modifier is specified. */ + bool lazy:1; + /* Uses the pre-RFC 8970 standard (requires algorithm to be returned + * as part of FETCH response). */ + bool old_standard:1; + /* If TRUE, uses "PREVIEW" command; if FALSE, uses older "SNIPPET" + * command. */ + bool preview_cmd:1; +}; + +static void fetch_read_error(struct imap_fetch_context *ctx, + const char **disconnect_reason_r) +{ + struct imap_fetch_state *state = &ctx->state; + + if (state->cur_input->stream_errno == ENOENT) { + if (state->cur_mail->expunged) { + *disconnect_reason_r = "Mail expunged while it was being FETCHed"; + return; + } + } + mail_set_critical(state->cur_mail, + "read(%s) failed: %s (FETCH %s)", + i_stream_get_name(state->cur_input), + i_stream_get_error(state->cur_input), + state->cur_human_name); + *disconnect_reason_r = "FETCH read() failed"; +} + +static const char *get_body_name(const struct imap_fetch_body_data *body) +{ + string_t *str; + + str = t_str_new(128); + if (body->binary_size) + str_append(str, "BINARY.SIZE"); + else if (body->binary) + str_append(str, "BINARY"); + else + str_append(str, "BODY"); + str_printfa(str, "[%s]", body->section); + if (body->partial) { + str_printfa(str, "<%"PRIuUOFF_T">", + imap_msgpart_get_partial_offset(body->msgpart)); + } + return str_c(str); +} + +static string_t *get_prefix(struct imap_fetch_state *state, + const struct imap_fetch_body_data *body, + uoff_t size, bool has_nuls) +{ + string_t *str; + + str = t_str_new(128); + if (state->cur_first) + state->cur_first = FALSE; + else + str_append_c(str, ' '); + + str_append(str, get_body_name(body)); + + if (size == UOFF_T_MAX) + str_append(str, " NIL"); + else if (has_nuls && body->binary) + str_printfa(str, " ~{%"PRIuUOFF_T"}\r\n", size); + else + str_printfa(str, " {%"PRIuUOFF_T"}\r\n", size); + return str; +} + +static int fetch_stream_continue(struct imap_fetch_context *ctx) +{ + struct imap_fetch_state *state = &ctx->state; + const char *disconnect_reason; + uoff_t orig_input_offset = state->cur_input->v_offset; + enum ostream_send_istream_result res; + + o_stream_set_max_buffer_size(ctx->client->output, 0); + res = o_stream_send_istream(ctx->client->output, state->cur_input); + o_stream_set_max_buffer_size(ctx->client->output, SIZE_MAX); + + if (ctx->state.cur_stats_sizep != NULL) { + *ctx->state.cur_stats_sizep += + state->cur_input->v_offset - orig_input_offset; + } + + switch (res) { + case OSTREAM_SEND_ISTREAM_RESULT_FINISHED: + if (state->cur_input->v_offset != state->cur_size) { + /* Input stream gave less data than expected */ + mail_set_cache_corrupted(state->cur_mail, + state->cur_size_field, t_strdup_printf( + "read(%s): FETCH %s got too little data: " + "%"PRIuUOFF_T" vs %"PRIuUOFF_T, + i_stream_get_name(state->cur_input), + state->cur_human_name, + state->cur_input->v_offset, state->cur_size)); + client_disconnect(ctx->client, "FETCH failed"); + return -1; + } + return 1; + case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT: + i_unreached(); + case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT: + return 0; + case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT: + fetch_read_error(ctx, &disconnect_reason); + client_disconnect(ctx->client, disconnect_reason); + return -1; + case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT: + /* client disconnected */ + return -1; + } + i_unreached(); +} + +static const char * +get_body_human_name(pool_t pool, struct imap_fetch_body_data *body) +{ + string_t *str; + uoff_t partial_offset, partial_size; + + str = t_str_new(64); + if (body->binary) + str_append(str, "BINARY["); + else + str_append(str, "BODY["); + str_append(str, body->section); + str_append_c(str, ']'); + + partial_offset = imap_msgpart_get_partial_offset(body->msgpart); + partial_size = imap_msgpart_get_partial_size(body->msgpart); + if (partial_offset != 0 || partial_size != UOFF_T_MAX) { + str_printfa(str, "<%"PRIuUOFF_T, partial_offset); + if (partial_size != UOFF_T_MAX) + str_printfa(str, ".%"PRIuUOFF_T, partial_size); + str_append_c(str, '>'); + } + return p_strdup(pool, str_c(str)); +} + +static void fetch_state_update_stats(struct imap_fetch_context *ctx, + const struct imap_msgpart *msgpart) +{ + if (!imap_msgpart_contains_body(msgpart)) { + ctx->client->fetch_hdr_count++; + ctx->state.cur_stats_sizep = &ctx->client->fetch_hdr_bytes; + } else { + ctx->client->fetch_body_count++; + ctx->state.cur_stats_sizep = &ctx->client->fetch_body_bytes; + } +} + +static int fetch_body_msgpart(struct imap_fetch_context *ctx, struct mail *mail, + struct imap_fetch_body_data *body) +{ + struct imap_msgpart_open_result result; + string_t *str; + + if (mail == NULL) { + imap_msgpart_free(&body->msgpart); + return 1; + } + + if (imap_msgpart_open(mail, body->msgpart, &result) < 0) + return -1; + i_assert(result.input->v_offset == 0); + ctx->state.cur_input = result.input; + ctx->state.cur_size = result.size; + ctx->state.cur_size_field = result.size_field; + ctx->state.cur_human_name = get_body_human_name(ctx->ctx_pool, body); + + fetch_state_update_stats(ctx, body->msgpart); + str = get_prefix(&ctx->state, body, ctx->state.cur_size, + result.binary_decoded_input_has_nuls); + o_stream_nsend(ctx->client->output, str_data(str), str_len(str)); + + ctx->state.cont_handler = fetch_stream_continue; + return ctx->state.cont_handler(ctx); +} + +static int fetch_binary_size(struct imap_fetch_context *ctx, struct mail *mail, + struct imap_fetch_body_data *body) +{ + string_t *str; + uoff_t size; + + if (mail == NULL) { + imap_msgpart_free(&body->msgpart); + return 1; + } + + if (imap_msgpart_size(mail, body->msgpart, &size) < 0) + return -1; + + str = t_str_new(128); + if (ctx->state.cur_first) + ctx->state.cur_first = FALSE; + else + str_append_c(str, ' '); + str_printfa(str, "%s %"PRIuUOFF_T, get_body_name(body), size); + + if (o_stream_send(ctx->client->output, str_data(str), str_len(str)) < 0) + return -1; + return 1; +} + +/* Parse next digits in string into integer. Returns -1 if the integer + becomes too big and wraps. */ +static int read_uoff_t(const char **p, uoff_t *value) +{ + return str_parse_uoff(*p, value, p); +} + +static int +body_header_fields_parse(struct imap_fetch_init_context *ctx, + struct imap_fetch_body_data *body, const char *prefix, + const struct imap_arg *args, unsigned int args_count) +{ + string_t *str; + const char *value; + size_t i; + + ctx->fetch_ctx->fetch_header_fields = TRUE; + + str = str_new(ctx->pool, 128); + str_append(str, prefix); + str_append(str, " ("); + + for (i = 0; i < args_count; i++) { + if (!imap_arg_get_astring(&args[i], &value)) { + ctx->error = "Invalid BODY[..] parameter: " + "Header list contains non-strings"; + return -1; + } + value = t_str_ucase(value); + + if (i != 0) + str_append_c(str, ' '); + + if (args[i].type == IMAP_ARG_ATOM) + str_append(str, value); + else + imap_append_quoted(str, value); + } + str_append_c(str, ')'); + body->section = str_c(str); + return 0; +} + +static int body_parse_partial(struct imap_fetch_body_data *body, + const char *p, const char **error_r) +{ + uoff_t offset, size = UOFF_T_MAX; + + if (*p == '\0') + return 0; + /* <start.end> */ + if (*p != '<') { + *error_r = "Unexpected data after ']'"; + return -1; + } + + p++; + body->partial = TRUE; + + if (read_uoff_t(&p, &offset) < 0 || offset > OFF_T_MAX) { + /* wrapped */ + *error_r = "Too big partial start"; + return -1; + } + + if (*p == '.') { + p++; + if (read_uoff_t(&p, &size) < 0 || size > OFF_T_MAX) { + /* wrapped */ + *error_r = "Too big partial end"; + return -1; + } + } + + if (*p != '>') { + *error_r = "Missing '>' in partial"; + return -1; + } + if (p[1] != '\0') { + *error_r = "Unexpected data after '>'"; + return -1; + } + imap_msgpart_set_partial(body->msgpart, offset, size); + return 0; +} + +bool imap_fetch_body_section_init(struct imap_fetch_init_context *ctx) +{ + struct imap_fetch_body_data *body; + const struct imap_arg *list_args; + unsigned int list_count; + const char *str, *p, *error; + + i_assert(str_begins(ctx->name, "BODY")); + p = ctx->name + 4; + + body = p_new(ctx->pool, struct imap_fetch_body_data, 1); + + if (str_begins(p, ".PEEK")) + p += 5; + else + ctx->fetch_ctx->flags_update_seen = TRUE; + if (*p != '[') { + ctx->error = "Invalid BODY[..] parameter: Missing '['"; + return FALSE; + } + + if (imap_arg_get_list_full(&ctx->args[0], &list_args, &list_count)) { + /* BODY[HEADER.FIELDS.. (headers list)] */ + if (!imap_arg_get_atom(&ctx->args[1], &str) || + str[0] != ']') { + ctx->error = "Invalid BODY[..] parameter: Missing ']'"; + return FALSE; + } + if (body_header_fields_parse(ctx, body, p+1, + list_args, list_count) < 0) + return FALSE; + p = str+1; + ctx->args += 2; + } else { + /* no headers list */ + body->section = p+1; + p = strchr(body->section, ']'); + if (p == NULL) { + ctx->error = "Invalid BODY[..] parameter: Missing ']'"; + return FALSE; + } + body->section = p_strdup_until(ctx->pool, body->section, p); + p++; + } + if (imap_msgpart_parse(body->section, &body->msgpart) < 0) { + ctx->error = "Invalid BODY[..] section"; + return FALSE; + } + ctx->fetch_ctx->fetch_data |= + imap_msgpart_get_fetch_data(body->msgpart); + imap_msgpart_get_wanted_headers(body->msgpart, &ctx->fetch_ctx->all_headers); + + if (body_parse_partial(body, p, &error) < 0) { + ctx->error = p_strdup_printf(ctx->pool, + "Invalid BODY[..] parameter: %s", error); + return FALSE; + } + + /* update the section name for the imap_fetch_add_handler() */ + ctx->name = p_strdup(ctx->pool, get_body_name(body)); + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_WANT_DEINIT, + "NIL", fetch_body_msgpart, body); + return TRUE; +} + +bool imap_fetch_binary_init(struct imap_fetch_init_context *ctx) +{ + struct imap_fetch_body_data *body; + const struct imap_arg *list_args; + unsigned int list_count; + const char *str, *p, *error; + + i_assert(str_begins(ctx->name, "BINARY")); + p = ctx->name + 6; + + body = p_new(ctx->pool, struct imap_fetch_body_data, 1); + body->binary = TRUE; + + if (str_begins(p, ".SIZE")) { + /* fetch decoded size of the section */ + p += 5; + body->binary_size = TRUE; + } else if (str_begins(p, ".PEEK")) { + p += 5; + } else { + ctx->fetch_ctx->flags_update_seen = TRUE; + } + if (*p != '[') { + ctx->error = "Invalid BINARY[..] parameter: Missing '['"; + return FALSE; + } + + if (imap_arg_get_list_full(&ctx->args[0], &list_args, &list_count)) { + /* BINARY[HEADER.FIELDS.. (headers list)] */ + if (!imap_arg_get_atom(&ctx->args[1], &str) || + str[0] != ']') { + ctx->error = "Invalid BINARY[..] parameter: Missing ']'"; + return FALSE; + } + if (body_header_fields_parse(ctx, body, p+1, + list_args, list_count) < 0) + return FALSE; + p = str+1; + ctx->args += 2; + } else { + /* no headers list */ + body->section = p+1; + p = strchr(body->section, ']'); + if (p == NULL) { + ctx->error = "Invalid BINARY[..] parameter: Missing ']'"; + return FALSE; + } + body->section = p_strdup_until(ctx->pool, body->section, p); + p++; + } + if (imap_msgpart_parse(body->section, &body->msgpart) < 0) { + ctx->error = "Invalid BINARY[..] section"; + return FALSE; + } + imap_msgpart_set_decode_to_binary(body->msgpart); + ctx->fetch_ctx->fetch_data |= + imap_msgpart_get_fetch_data(body->msgpart); + + if (!body->binary_size) { + if (body_parse_partial(body, p, &error) < 0) { + ctx->error = p_strdup_printf(ctx->pool, + "Invalid BINARY[..] parameter: %s", error); + return FALSE; + } + } + + /* update the section name for the imap_fetch_add_handler() */ + ctx->name = p_strdup(ctx->pool, get_body_name(body)); + if (body->binary_size) { + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_WANT_DEINIT, + "0", fetch_binary_size, body); + } else { + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_WANT_DEINIT, + "NIL", fetch_body_msgpart, body); + } + return TRUE; +} + +static int ATTR_NULL(3) +fetch_rfc822_size(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + uoff_t size; + + if (mail_get_virtual_size(mail, &size) < 0) + return -1; + + str_printfa(ctx->state.cur_str, "RFC822.SIZE %"PRIuUOFF_T" ", size); + return 1; +} + +static int +fetch_and_free_msgpart(struct imap_fetch_context *ctx, + struct mail *mail, struct imap_msgpart **_msgpart) +{ + struct imap_msgpart_open_result result; + int ret; + + ret = imap_msgpart_open(mail, *_msgpart, &result); + imap_msgpart_free(_msgpart); + if (ret < 0) + return -1; + i_assert(result.input->v_offset == 0); + ctx->state.cur_input = result.input; + ctx->state.cur_size = result.size; + ctx->state.cur_size_field = result.size_field; + ctx->state.cont_handler = fetch_stream_continue; + return 0; +} + +static int ATTR_NULL(3) +fetch_rfc822(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + struct imap_msgpart *msgpart; + const char *str; + + msgpart = imap_msgpart_full(); + fetch_state_update_stats(ctx, msgpart); + if (fetch_and_free_msgpart(ctx, mail, &msgpart) < 0) + return -1; + + str = t_strdup_printf(" RFC822 {%"PRIuUOFF_T"}\r\n", + ctx->state.cur_size); + if (ctx->state.cur_first) { + str++; ctx->state.cur_first = FALSE; + } + o_stream_nsend_str(ctx->client->output, str); + + ctx->state.cur_human_name = "RFC822"; + return ctx->state.cont_handler(ctx); +} + +static int ATTR_NULL(3) +fetch_rfc822_header(struct imap_fetch_context *ctx, + struct mail *mail, void *context ATTR_UNUSED) +{ + struct imap_msgpart *msgpart; + const char *str; + + msgpart = imap_msgpart_header(); + fetch_state_update_stats(ctx, msgpart); + if (fetch_and_free_msgpart(ctx, mail, &msgpart) < 0) + return -1; + + str = t_strdup_printf(" RFC822.HEADER {%"PRIuUOFF_T"}\r\n", + ctx->state.cur_size); + if (ctx->state.cur_first) { + str++; ctx->state.cur_first = FALSE; + } + o_stream_nsend_str(ctx->client->output, str); + + ctx->state.cur_human_name = "RFC822.HEADER"; + return ctx->state.cont_handler(ctx); +} + +static int ATTR_NULL(3) +fetch_rfc822_text(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + struct imap_msgpart *msgpart; + const char *str; + + msgpart = imap_msgpart_body(); + fetch_state_update_stats(ctx, msgpart); + if (fetch_and_free_msgpart(ctx, mail, &msgpart) < 0) + return -1; + + str = t_strdup_printf(" RFC822.TEXT {%"PRIuUOFF_T"}\r\n", + ctx->state.cur_size); + if (ctx->state.cur_first) { + str++; ctx->state.cur_first = FALSE; + } + o_stream_nsend_str(ctx->client->output, str); + + ctx->state.cur_human_name = "RFC822.TEXT"; + return ctx->state.cont_handler(ctx); +} + +bool imap_fetch_rfc822_init(struct imap_fetch_init_context *ctx) +{ + const char *name = ctx->name; + + if (name[6] == '\0') { + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_STREAM_HEADER | + MAIL_FETCH_STREAM_BODY; + ctx->fetch_ctx->flags_update_seen = TRUE; + imap_fetch_add_handler(ctx, 0, "NIL", + fetch_rfc822, NULL); + return TRUE; + } + + if (strcmp(name+6, ".SIZE") == 0) { + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_VIRTUAL_SIZE; + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + "0", fetch_rfc822_size, NULL); + return TRUE; + } + if (strcmp(name+6, ".HEADER") == 0) { + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_STREAM_HEADER; + imap_fetch_add_handler(ctx, 0, "NIL", + fetch_rfc822_header, NULL); + return TRUE; + } + if (strcmp(name+6, ".TEXT") == 0) { + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_STREAM_BODY; + ctx->fetch_ctx->flags_update_seen = TRUE; + imap_fetch_add_handler(ctx, 0, "NIL", + fetch_rfc822_text, NULL); + return TRUE; + } + + ctx->error = t_strconcat("Unknown parameter ", name, NULL); + return FALSE; +} + +static int ATTR_NULL(3) +fetch_snippet(struct imap_fetch_context *ctx, struct mail *mail, + struct imap_fetch_preview_data *preview) +{ + enum mail_lookup_abort temp_lookup_abort = preview->lazy ? MAIL_LOOKUP_ABORT_NOT_IN_CACHE_START_CACHING : mail->lookup_abort; + enum mail_lookup_abort orig_lookup_abort = mail->lookup_abort; + const char *resp, *snippet; + int ret; + + mail->lookup_abort = temp_lookup_abort; + ret = mail_get_special(mail, MAIL_FETCH_BODY_SNIPPET, &snippet); + mail->lookup_abort = orig_lookup_abort; + + resp = preview->preview_cmd ? "PREVIEW" : "SNIPPET"; + + if (ret == 0) { + /* got it => nothing to do */ + snippet++; /* skip over snippet version byte */ + } else if (mailbox_get_last_mail_error(mail->box) != MAIL_ERROR_LOOKUP_ABORTED) { + /* actual error => bail */ + return -1; + } else { + /* + * Two ways to get here: + * - not in cache && lazy => give up + * - not in cache && !lazy => someone higher up set + * MAIL_LOOKUP_ABORT_NOT_IN_CACHE and so even though we got + * a non-lazy request we failed the cache lookup. + * + * This is not an error, but since the scenario is + * sufficiently convoluted this else branch serves to + * document it. + * + * This path will return NIL as the preview response. + */ + } + + str_append(ctx->state.cur_str, resp); + if (preview->old_standard) + str_append(ctx->state.cur_str, " (FUZZY "); + else + str_append(ctx->state.cur_str, " "); + if (ret == 0) + imap_append_string(ctx->state.cur_str, snippet); + else + str_append(ctx->state.cur_str, "NIL"); + if (preview->old_standard) + str_append(ctx->state.cur_str, ")"); + str_append_c(ctx->state.cur_str, ' '); + + return 1; +} + +static bool fetch_preview_init(struct imap_fetch_init_context *ctx, + bool preview_cmd) +{ + const struct imap_arg *list_args; + unsigned int list_count; + struct imap_fetch_preview_data *preview; + + preview = p_new(ctx->pool, struct imap_fetch_preview_data, 1); + preview->preview_cmd = preview_cmd; + /* We can tell we are using the "Old" (pre-RFC 8970) standard + * if: + * 1) SNIPPET command is used + * 2) FUZZY algorithm is explicitly requested + * The one usage we cannot catch is if PREVIEW command is used, and + * no algorithm is specified - the pre-RFC standard requires that the + * algorithm must be output as part of the response; the RFC standard + * does not output the algorithm. There is unfortunately nothing we + * can do about this, so we need to send the standards compliant + * way. */ + preview->old_standard = !preview_cmd; + + if (imap_arg_get_list_full(&ctx->args[0], &list_args, &list_count)) { + unsigned int i; + + for (i = 0; i < list_count; i++) { + const char *str; + + if (!imap_arg_get_atom(&list_args[i], &str)) { + ctx->error = "Invalid PREVIEW modifier"; + return FALSE; + } + + if (strcasecmp(str, "LAZY") == 0) { + preview->lazy = TRUE; + } else if (strcasecmp(str, "LAZY=FUZZY") == 0) { + preview->lazy = TRUE; + preview->old_standard = TRUE; + } else if (strcasecmp(str, "FUZZY") == 0) { + preview->old_standard = TRUE; + } else { + ctx->error = t_strdup_printf("'%s' is not a " + "supported PREVIEW modifier", + str); + return FALSE; + } + } + + ctx->args += list_count; + } + + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_BODY_SNIPPET; + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + "NIL", fetch_snippet, preview); + return TRUE; +} + +bool imap_fetch_preview_init(struct imap_fetch_init_context *ctx) +{ + return fetch_preview_init(ctx, TRUE); +} + +bool imap_fetch_snippet_init(struct imap_fetch_init_context *ctx) +{ + return fetch_preview_init(ctx, FALSE); +} diff --git a/src/imap/imap-fetch.c b/src/imap/imap-fetch.c new file mode 100644 index 0000000..db811f7 --- /dev/null +++ b/src/imap/imap-fetch.c @@ -0,0 +1,1040 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "array.h" +#include "buffer.h" +#include "istream.h" +#include "ostream.h" +#include "str.h" +#include "message-size.h" +#include "imap-date.h" +#include "imap-utf7.h" +#include "mail-search-build.h" +#include "imap-commands.h" +#include "imap-quote.h" +#include "imap-fetch.h" +#include "imap-util.h" + +#include <ctype.h> + +#define BODY_NIL_REPLY \ + "\"text\" \"plain\" NIL NIL NIL \"7bit\" 0 0" +#define ENVELOPE_NIL_REPLY \ + "(NIL NIL NIL NIL NIL NIL NIL NIL NIL NIL)" + +static ARRAY(struct imap_fetch_handler) fetch_handlers; + +static int imap_fetch_handler_cmp(const struct imap_fetch_handler *h1, + const struct imap_fetch_handler *h2) +{ + return strcmp(h1->name, h2->name); +} + +void imap_fetch_handlers_register(const struct imap_fetch_handler *handlers, + size_t count) +{ + array_append(&fetch_handlers, handlers, count); + array_sort(&fetch_handlers, imap_fetch_handler_cmp); +} + +void imap_fetch_handler_unregister(const char *name) +{ + const struct imap_fetch_handler *handler, *first_handler; + + first_handler = array_front(&fetch_handlers); + handler = imap_fetch_handler_lookup(name); + i_assert(handler != NULL); + array_delete(&fetch_handlers, handler - first_handler, 1); +} + +static int +imap_fetch_handler_bsearch(const char *name, const struct imap_fetch_handler *h) +{ + return strcmp(name, h->name); +} + +const struct imap_fetch_handler *imap_fetch_handler_lookup(const char *name) +{ + return array_bsearch(&fetch_handlers, name, imap_fetch_handler_bsearch); +} + +bool imap_fetch_init_handler(struct imap_fetch_init_context *init_ctx) +{ + const struct imap_fetch_handler *handler; + const char *lookup_name, *p; + + for (p = init_ctx->name; i_isalnum(*p) || *p == '-'; p++) ; + lookup_name = t_strdup_until(init_ctx->name, p); + + handler = array_bsearch(&fetch_handlers, lookup_name, + imap_fetch_handler_bsearch); + if (handler == NULL) { + init_ctx->error = t_strdup_printf("Unknown parameter: %s", + init_ctx->name); + return FALSE; + } + if (!handler->init(init_ctx)) { + i_assert(init_ctx->error != NULL); + return FALSE; + } + return TRUE; +} + +void imap_fetch_init_nofail_handler(struct imap_fetch_context *ctx, + bool (*init)(struct imap_fetch_init_context *)) +{ + struct imap_fetch_init_context init_ctx; + + i_zero(&init_ctx); + init_ctx.fetch_ctx = ctx; + init_ctx.pool = ctx->ctx_pool; + + if (!init(&init_ctx)) + i_unreached(); +} + +int imap_fetch_att_list_parse(struct client *client, pool_t pool, + const struct imap_arg *list, + struct imap_fetch_context **fetch_ctx_r, + const char **client_error_r) +{ + struct imap_fetch_init_context init_ctx; + const char *str; + + i_zero(&init_ctx); + init_ctx.fetch_ctx = imap_fetch_alloc(client, pool, "NOTIFY"); + init_ctx.pool = pool; + init_ctx.args = list; + + while (imap_arg_get_atom(init_ctx.args, &str)) { + init_ctx.name = t_str_ucase(str); + init_ctx.args++; + if (!imap_fetch_init_handler(&init_ctx)) { + *client_error_r = t_strconcat("Invalid fetch-att list: ", + init_ctx.error, NULL); + imap_fetch_free(&init_ctx.fetch_ctx); + return -1; + } + } + if (!IMAP_ARG_IS_EOL(init_ctx.args)) { + *client_error_r = "fetch-att list contains non-atoms."; + imap_fetch_free(&init_ctx.fetch_ctx); + return -1; + } + *fetch_ctx_r = init_ctx.fetch_ctx; + return 0; +} + +struct imap_fetch_context * +imap_fetch_alloc(struct client *client, pool_t pool, const char *reason) +{ + struct imap_fetch_context *ctx; + + ctx = p_new(pool, struct imap_fetch_context, 1); + ctx->client = client; + ctx->ctx_pool = pool; + ctx->reason = p_strdup(pool, reason); + pool_ref(pool); + + p_array_init(&ctx->all_headers, pool, 64); + p_array_init(&ctx->handlers, pool, 16); + p_array_init(&ctx->tmp_keywords, pool, + client->keywords.announce_count + 8); + return ctx; +} + +#undef imap_fetch_add_handler +void imap_fetch_add_handler(struct imap_fetch_init_context *ctx, + enum imap_fetch_handler_flags flags, + const char *nil_reply, + imap_fetch_handler_t *handler, void *context) +{ + /* partially because of broken clients, but also partially because + it potentially can make client implementations faster, we have a + buffered parameter which basically means that the handler promises + to write the output in fetch_ctx->state.cur_str. The cur_str is then + sent to client before calling any non-buffered handlers. + + We try to keep the handler registration order the same as the + client requested them. This is especially useful to get UID + returned first, which some clients rely on.. + */ + const struct imap_fetch_context_handler *ctx_handler; + struct imap_fetch_context_handler h; + + if (context == NULL) { + /* don't allow duplicate handlers */ + array_foreach(&ctx->fetch_ctx->handlers, ctx_handler) { + if (ctx_handler->handler == handler && + ctx_handler->context == NULL) + return; + } + } + + i_zero(&h); + h.handler = handler; + h.context = context; + h.buffered = (flags & IMAP_FETCH_HANDLER_FLAG_BUFFERED) != 0; + h.want_deinit = (flags & IMAP_FETCH_HANDLER_FLAG_WANT_DEINIT) != 0; + h.name = p_strdup(ctx->pool, ctx->name); + h.nil_reply = p_strdup(ctx->pool, nil_reply); + + if (!h.buffered) + array_push_back(&ctx->fetch_ctx->handlers, &h); + else { + array_insert(&ctx->fetch_ctx->handlers, + ctx->fetch_ctx->buffered_handlers_count, &h, 1); + ctx->fetch_ctx->buffered_handlers_count++; + } +} + +static void +expunges_drop_known(struct mailbox *box, + const struct imap_fetch_qresync_args *qresync_args, + struct mailbox_transaction_context *trans, + ARRAY_TYPE(seq_range) *expunged_uids) +{ + struct mailbox_status status; + struct mail *mail; + const uint32_t *seqs, *uids; + unsigned int i, count; + + seqs = array_get(qresync_args->qresync_sample_seqset, &count); + uids = array_front(qresync_args->qresync_sample_uidset); + i_assert(array_count(qresync_args->qresync_sample_uidset) == count); + i_assert(count > 0); + + mailbox_get_open_status(box, STATUS_MESSAGES, &status); + mail = mail_alloc(trans, 0, NULL); + + /* FIXME: we could do removals from the middle as well */ + for (i = 0; i < count && seqs[i] <= status.messages; i++) { + mail_set_seq(mail, seqs[i]); + if (uids[i] != mail->uid) + break; + } + if (i > 0) + seq_range_array_remove_range(expunged_uids, 1, uids[i-1]); + mail_free(&mail); +} + +static int +get_expunges_fallback(struct mailbox *box, + const struct imap_fetch_qresync_args *qresync_args, + const ARRAY_TYPE(seq_range) *uid_filter_arr, + ARRAY_TYPE(seq_range) *expunged_uids) +{ + struct mailbox_transaction_context *trans; + struct mail_search_args *search_args; + struct mail_search_context *search_ctx; + struct mail *mail; + const struct seq_range *uid_filter; + struct mailbox_status status; + unsigned int i, count; + uint32_t next_uid; + int ret = 0; + + uid_filter = array_get(uid_filter_arr, &count); + i_assert(count > 0); + i = 0; + next_uid = uid_filter[0].seq1; + + /* search UIDs only in given range */ + search_args = mail_search_build_init(); + search_args->args = p_new(search_args->pool, struct mail_search_arg, 1); + search_args->args->type = SEARCH_UIDSET; + p_array_init(&search_args->args->value.seqset, search_args->pool, count); + array_append_array(&search_args->args->value.seqset, uid_filter_arr); + + trans = mailbox_transaction_begin(box, 0, "FETCH send VANISHED"); + search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL); + mail_search_args_unref(&search_args); + + while (mailbox_search_next(search_ctx, &mail)) { + if (mail->uid == next_uid) { + if (next_uid < uid_filter[i].seq2) + next_uid++; + else if (++i < count) + next_uid = uid_filter[i].seq1; + else + break; + } else { + /* next_uid .. mail->uid-1 are expunged */ + i_assert(mail->uid > next_uid); + while (mail->uid > uid_filter[i].seq2) { + seq_range_array_add_range(expunged_uids, + next_uid, + uid_filter[i].seq2); + i++; + i_assert(i < count); + next_uid = uid_filter[i].seq1; + } + if (next_uid != mail->uid) { + seq_range_array_add_range(expunged_uids, + next_uid, + mail->uid - 1); + } + if (uid_filter[i].seq2 != mail->uid) + next_uid = mail->uid + 1; + else if (++i < count) + next_uid = uid_filter[i].seq1; + else + break; + } + } + if (i < count) { + i_assert(next_uid <= uid_filter[i].seq2); + seq_range_array_add_range(expunged_uids, next_uid, + uid_filter[i].seq2); + i++; + } + for (; i < count; i++) { + seq_range_array_add_range(expunged_uids, uid_filter[i].seq1, + uid_filter[i].seq2); + } + + mailbox_get_open_status(box, STATUS_UIDNEXT, &status); + seq_range_array_remove_range(expunged_uids, status.uidnext, + (uint32_t)-1); + + if (mailbox_search_deinit(&search_ctx) < 0) + ret = -1; + + if (ret == 0 && qresync_args->qresync_sample_seqset != NULL && + array_is_created(qresync_args->qresync_sample_seqset)) + expunges_drop_known(box, qresync_args, trans, expunged_uids); + + (void)mailbox_transaction_commit(&trans); + return ret; +} + +int imap_fetch_send_vanished(struct client *client, struct mailbox *box, + const struct mail_search_args *search_args, + const struct imap_fetch_qresync_args *qresync_args) +{ + const struct mail_search_arg *uidarg = search_args->args; + const struct mail_search_arg *modseqarg = uidarg->next; + const ARRAY_TYPE(seq_range) *uid_filter; + uint64_t modseq; + ARRAY_TYPE(seq_range) expunged_uids_range; + string_t *str; + int ret = 0; + + i_assert(uidarg->type == SEARCH_UIDSET); + i_assert(modseqarg->type == SEARCH_MODSEQ); + + uid_filter = &uidarg->value.seqset; + modseq = modseqarg->value.modseq->modseq - 1; + + i_array_init(&expunged_uids_range, array_count(uid_filter)); + if (!mailbox_get_expunged_uids(box, modseq, uid_filter, &expunged_uids_range)) { + /* return all expunged UIDs */ + if (get_expunges_fallback(box, qresync_args, uid_filter, + &expunged_uids_range) < 0) { + array_clear(&expunged_uids_range); + ret = -1; + } + } + if (array_count(&expunged_uids_range) > 0) { + str = str_new(default_pool, 128); + str_append(str, "* VANISHED (EARLIER) "); + imap_write_seq_range(str, &expunged_uids_range); + str_append(str, "\r\n"); + o_stream_nsend(client->output, str_data(str), str_len(str)); + str_free(&str); + } + array_free(&expunged_uids_range); + return ret; +} + +static void imap_fetch_init(struct imap_fetch_context *ctx) +{ + if (ctx->initialized) + return; + ctx->initialized = TRUE; + + if (ctx->flags_update_seen && !ctx->flags_have_handler) { + ctx->flags_show_only_seen_changes = TRUE; + imap_fetch_init_nofail_handler(ctx, imap_fetch_flags_init); + } + if ((ctx->fetch_data & + (MAIL_FETCH_STREAM_HEADER | MAIL_FETCH_STREAM_BODY)) != 0) + ctx->fetch_data |= MAIL_FETCH_NUL_STATE; +} + +void imap_fetch_begin(struct imap_fetch_context *ctx, struct mailbox *box, + struct mail_search_args *search_args) +{ + enum mailbox_transaction_flags trans_flags = + MAILBOX_TRANSACTION_FLAG_REFRESH; + struct mailbox_header_lookup_ctx *wanted_headers = NULL; + const char *const *headers; + + i_assert(!ctx->state.fetching); + + imap_fetch_init(ctx); + i_zero(&ctx->state); + + if (array_count(&ctx->all_headers) > 0 && + ((ctx->fetch_data & (MAIL_FETCH_STREAM_HEADER | + MAIL_FETCH_STREAM_BODY)) == 0)) { + array_append_zero(&ctx->all_headers); + + headers = array_front(&ctx->all_headers); + wanted_headers = mailbox_header_lookup_init(box, headers); + array_pop_back(&ctx->all_headers); + } + + if (ctx->flags_update_seen) { + /* Hide the implicit \Seen flag addition. Otherwise a separate + untagged FETCH FLAGS (\Seen) would be sent on top of the + one FLAGS (\Seen) already added in the main FETCH reply. + + We don't set this always, because some plugins might want + to do their own flag changes which we don't want hidden. + (Of course this isn't perfect since if implicit \Seen flags + are added, other flag changes are also hidden.) */ + trans_flags |= MAILBOX_TRANSACTION_FLAG_HIDE; + } + ctx->state.trans = mailbox_transaction_begin(box, trans_flags, + ctx->reason); + + mail_search_args_init(search_args, box, TRUE, + &ctx->client->search_saved_uidset); + ctx->state.search_ctx = + mailbox_search_init(ctx->state.trans, search_args, NULL, + ctx->fetch_data, wanted_headers); + ctx->state.cur_str = str_new(default_pool, 8192); + ctx->state.fetching = TRUE; + + mailbox_header_lookup_unref(&wanted_headers); +} + +static int imap_fetch_flush_buffer(struct imap_fetch_context *ctx) +{ + const unsigned char *data; + size_t len; + + data = str_data(ctx->state.cur_str); + len = str_len(ctx->state.cur_str); + + if (len == 0) + return 0; + + /* there's an extra space at the end if we added any fetch items + to buffer */ + if (data[len-1] == ' ') { + len--; + ctx->state.cur_first = FALSE; + } + ctx->state.line_partial = TRUE; + + if (o_stream_send(ctx->client->output, data, len) < 0) + return -1; + + str_truncate(ctx->state.cur_str, 0); + return 0; +} + +static int imap_fetch_send_nil_reply(struct imap_fetch_context *ctx) +{ + const struct imap_fetch_context_handler *handler; + + if (!ctx->state.cur_first) + str_append_c(ctx->state.cur_str, ' '); + + handler = array_idx(&ctx->handlers, ctx->state.cur_handler); + str_printfa(ctx->state.cur_str, "%s %s ", + handler->name, handler->nil_reply); + + if (!handler->buffered) { + if (imap_fetch_flush_buffer(ctx) < 0) + return -1; + } + return 0; +} + +static void imap_fetch_fix_empty_reply(struct imap_fetch_context *ctx) +{ + if (ctx->state.line_partial && ctx->state.cur_first) { + /* we've flushed an empty "FETCH (" reply so + far. we can't take it back, but RFC 3501 + doesn't allow returning empty "FETCH ()" + either, so just add the current message's + UID there. */ + str_printfa(ctx->state.cur_str, "UID %u ", + ctx->state.cur_mail->uid); + } +} + +static bool imap_fetch_cur_failed(struct imap_fetch_context *ctx) +{ + ctx->failures = TRUE; + if (ctx->client->set->parsed_fetch_failure == + IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_IMMEDIATELY) + return FALSE; + + if (!array_is_created(&ctx->fetch_failed_uids)) + p_array_init(&ctx->fetch_failed_uids, ctx->ctx_pool, 8); + seq_range_array_add(&ctx->fetch_failed_uids, ctx->state.cur_mail->uid); + + if (ctx->error == MAIL_ERROR_NONE) { + /* preserve the first error, since it may change in storage. */ + ctx->errstr = p_strdup(ctx->ctx_pool, + mailbox_get_last_error(ctx->state.cur_mail->box, &ctx->error)); + } + return TRUE; +} + +static int imap_fetch_more_int(struct imap_fetch_context *ctx, bool cancel) +{ + struct imap_fetch_state *state = &ctx->state; + struct client *client = ctx->client; + const struct imap_fetch_context_handler *handlers; + unsigned int count; + int ret; + + if (state->cont_handler != NULL) { + ret = state->cont_handler(ctx); + if (ret == 0) + return 0; + + if (ret < 0) { + if (client->output->closed) + return -1; + + if (state->cur_mail->expunged) { + /* not an error, just lost it. */ + state->skipped_expunged_msgs = TRUE; + if (imap_fetch_send_nil_reply(ctx) < 0) + return -1; + } else { + if (!imap_fetch_cur_failed(ctx)) + return -1; + } + } + + state->cont_handler = NULL; + state->cur_handler++; + if (state->cur_input != NULL) + i_stream_unref(&state->cur_input); + } + + handlers = array_get(&ctx->handlers, &count); + for (;;) { + if (o_stream_get_buffer_used_size(client->output) >= + CLIENT_OUTPUT_OPTIMAL_SIZE) { + ret = o_stream_flush(client->output); + if (ret <= 0) + return ret; + } + + if (state->cur_mail == NULL) { + if (cancel) + return 1; + + if (!mailbox_search_next(state->search_ctx, + &state->cur_mail)) + break; + + str_printfa(state->cur_str, "* %u FETCH (", + state->cur_mail->seq); + ctx->fetched_mails_count++; + state->cur_first = TRUE; + state->cur_str_prefix_size = str_len(state->cur_str); + i_assert(!state->line_partial); + } + + for (; state->cur_handler < count; state->cur_handler++) { + if (str_len(state->cur_str) > 0 && + !handlers[state->cur_handler].buffered) { + /* first non-buffered handler. + flush the buffer. */ + if (imap_fetch_flush_buffer(ctx) < 0) + return -1; + } + + i_assert(state->cur_input == NULL); + T_BEGIN { + const struct imap_fetch_context_handler *h = + &handlers[state->cur_handler]; + + ret = h->handler(ctx, state->cur_mail, + h->context); + } T_END; + + if (ret == 0) + return 0; + + if (ret < 0) { + if (state->cur_mail->expunged) { + /* not an error, just lost it. */ + state->skipped_expunged_msgs = TRUE; + if (imap_fetch_send_nil_reply(ctx) < 0) + return -1; + } else { + i_assert(ret < 0 || + state->cont_handler != NULL); + if (!imap_fetch_cur_failed(ctx)) + return -1; + } + } + + state->cont_handler = NULL; + if (state->cur_input != NULL) + i_stream_unref(&state->cur_input); + } + + imap_fetch_fix_empty_reply(ctx); + if (str_len(state->cur_str) > 0 && + (state->line_partial || + str_len(state->cur_str) != state->cur_str_prefix_size)) { + /* no non-buffered handlers */ + if (imap_fetch_flush_buffer(ctx) < 0) + return -1; + } + + if (state->line_partial) + o_stream_nsend(client->output, ")\r\n", 3); + client->last_output = ioloop_time; + + state->cur_mail = NULL; + state->cur_handler = 0; + state->line_partial = FALSE; + } + + return ctx->failures ? -1 : 1; +} + +int imap_fetch_more(struct imap_fetch_context *ctx, + struct client_command_context *cmd) +{ + int ret; + + i_assert(ctx->client->output_cmd_lock == NULL || + ctx->client->output_cmd_lock == cmd); + + ret = imap_fetch_more_int(ctx, cmd->cancel); + if (ret < 0) + ctx->state.failed = TRUE; + if (ctx->state.line_partial) { + /* nothing can be sent until FETCH is finished */ + ctx->client->output_cmd_lock = cmd; + } + if (cmd->cancel && ctx->client->output_cmd_lock != NULL) { + /* canceling didn't really work. we must not output + anything anymore. */ + if (!ctx->client->destroyed) + client_disconnect(ctx->client, "Failed to cancel FETCH"); + ctx->client->output_cmd_lock = NULL; + } + return ret; +} + +int imap_fetch_more_no_lock_update(struct imap_fetch_context *ctx) +{ + int ret; + + ret = imap_fetch_more_int(ctx, FALSE); + if (ret < 0) { + ctx->state.failed = TRUE; + if (ctx->state.line_partial) { + /* we can't send any more replies to client, because + the FETCH reply wasn't fully sent. */ + client_disconnect(ctx->client, + "NOTIFY failed in the middle of FETCH reply"); + } + } + return ret; +} + +int imap_fetch_end(struct imap_fetch_context *ctx) +{ + struct imap_fetch_state *state = &ctx->state; + + if (ctx->state.fetching) { + ctx->state.fetching = FALSE; + if (state->line_partial) { + imap_fetch_fix_empty_reply(ctx); + if (imap_fetch_flush_buffer(ctx) < 0) + state->failed = TRUE; + if (o_stream_send(ctx->client->output, ")\r\n", 3) < 0) + state->failed = TRUE; + } + } + ctx->client->output_cmd_lock = NULL; + + str_free(&state->cur_str); + + i_stream_unref(&state->cur_input); + + if (state->search_ctx != NULL) { + if (mailbox_search_deinit(&state->search_ctx) < 0) + state->failed = TRUE; + } + + if (state->trans != NULL) { + /* even if something failed, we want to commit changes to + cache, as well as possible \Seen flag changes for FETCH + replies we returned so far. */ + if (mailbox_transaction_commit(&state->trans) < 0) + state->failed = TRUE; + } + return state->failed ? -1 : 0; +} + +void imap_fetch_free(struct imap_fetch_context **_ctx) +{ + struct imap_fetch_context *ctx = *_ctx; + const struct imap_fetch_context_handler *handler; + + *_ctx = NULL; + + (void)imap_fetch_end(ctx); + + array_foreach(&ctx->handlers, handler) { + if (handler->want_deinit) + handler->handler(ctx, NULL, handler->context); + } + pool_unref(&ctx->ctx_pool); +} + +static int fetch_body(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + const char *body; + + if (mail_get_special(mail, MAIL_FETCH_IMAP_BODY, &body) < 0) + return -1; + + if (ctx->state.cur_first) + ctx->state.cur_first = FALSE; + else { + if (o_stream_send(ctx->client->output, " ", 1) < 0) + return -1; + } + + if (o_stream_send(ctx->client->output, "BODY (", 6) < 0 || + o_stream_send_str(ctx->client->output, body) < 0 || + o_stream_send(ctx->client->output, ")", 1) < 0) + return -1; + return 1; +} + +static bool fetch_body_init(struct imap_fetch_init_context *ctx) +{ + if (ctx->name[4] == '\0') { + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_IMAP_BODY; + imap_fetch_add_handler(ctx, 0, "("BODY_NIL_REPLY")", + fetch_body, NULL); + return TRUE; + } + return imap_fetch_body_section_init(ctx); +} + +static int fetch_bodystructure(struct imap_fetch_context *ctx, + struct mail *mail, void *context ATTR_UNUSED) +{ + const char *bodystructure; + + if (mail_get_special(mail, MAIL_FETCH_IMAP_BODYSTRUCTURE, + &bodystructure) < 0) + return -1; + + if (ctx->state.cur_first) + ctx->state.cur_first = FALSE; + else { + if (o_stream_send(ctx->client->output, " ", 1) < 0) + return -1; + } + + if (o_stream_send(ctx->client->output, "BODYSTRUCTURE (", 15) < 0 || + o_stream_send_str(ctx->client->output, bodystructure) < 0 || + o_stream_send(ctx->client->output, ")", 1) < 0) + return -1; + + return 1; +} + +static bool fetch_bodystructure_init(struct imap_fetch_init_context *ctx) +{ + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_IMAP_BODYSTRUCTURE; + imap_fetch_add_handler(ctx, 0, "("BODY_NIL_REPLY" NIL NIL NIL NIL)", + fetch_bodystructure, NULL); + return TRUE; +} + +static int fetch_envelope(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + const char *envelope; + + if (mail_get_special(mail, MAIL_FETCH_IMAP_ENVELOPE, &envelope) < 0) + return -1; + + if (ctx->state.cur_first) + ctx->state.cur_first = FALSE; + else { + if (o_stream_send(ctx->client->output, " ", 1) < 0) + return -1; + } + + if (o_stream_send(ctx->client->output, "ENVELOPE (", 10) < 0 || + o_stream_send_str(ctx->client->output, envelope) < 0 || + o_stream_send(ctx->client->output, ")", 1) < 0) + return -1; + return 1; +} + +static bool fetch_envelope_init(struct imap_fetch_init_context *ctx) +{ + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_IMAP_ENVELOPE; + imap_fetch_add_handler(ctx, 0, ENVELOPE_NIL_REPLY, + fetch_envelope, NULL); + return TRUE; +} + +static int fetch_flags(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + enum mail_flags flags; + const char *const *keywords; + + flags = mail_get_flags(mail); + if (ctx->flags_update_seen && (flags & MAIL_SEEN) == 0 && + !mailbox_is_readonly(mail->box)) { + /* Add \Seen flag */ + ctx->state.seen_flags_changed = TRUE; + flags |= MAIL_SEEN; + mail_update_flags(mail, MODIFY_ADD, MAIL_SEEN); + } else if (ctx->flags_show_only_seen_changes) { + return 1; + } + + keywords = client_get_keyword_names(ctx->client, &ctx->tmp_keywords, + mail_get_keyword_indexes(mail)); + + str_append(ctx->state.cur_str, "FLAGS ("); + imap_write_flags(ctx->state.cur_str, flags, keywords); + str_append(ctx->state.cur_str, ") "); + return 1; +} + +bool imap_fetch_flags_init(struct imap_fetch_init_context *ctx) +{ + ctx->fetch_ctx->flags_have_handler = TRUE; + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_FLAGS; + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + "()", fetch_flags, NULL); + return TRUE; +} + +static int fetch_internaldate(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + time_t date; + + if (mail_get_received_date(mail, &date) < 0) + return -1; + + str_printfa(ctx->state.cur_str, "INTERNALDATE \"%s\" ", + imap_to_datetime(date)); + return 1; +} + +static bool fetch_internaldate_init(struct imap_fetch_init_context *ctx) +{ + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_RECEIVED_DATE; + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + "\"01-Jan-1970 00:00:00 +0000\"", + fetch_internaldate, NULL); + return TRUE; +} + +static int fetch_modseq(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + uint64_t modseq; + + modseq = mail_get_modseq(mail); + if (ctx->client->highest_fetch_modseq < modseq) + ctx->client->highest_fetch_modseq = modseq; + str_printfa(ctx->state.cur_str, "MODSEQ (%"PRIu64") ", modseq); + return 1; +} + +bool imap_fetch_modseq_init(struct imap_fetch_init_context *ctx) +{ + if (ctx->fetch_ctx->client->nonpermanent_modseqs) { + ctx->error = "FETCH MODSEQ can't be used with non-permanent modseqs"; + return FALSE; + } + client_enable(ctx->fetch_ctx->client, imap_feature_condstore); + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + NULL, fetch_modseq, NULL); + return TRUE; +} + +static int fetch_uid(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + str_printfa(ctx->state.cur_str, "UID %u ", mail->uid); + return 1; +} + +bool imap_fetch_uid_init(struct imap_fetch_init_context *ctx) +{ + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + NULL, fetch_uid, NULL); + return TRUE; +} + +static int imap_fetch_savedate(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + time_t date; + int ret; + + ret = mail_get_save_date(mail, &date); + if (ret < 0) + return -1; + + if (ret == 0) + str_append(ctx->state.cur_str, "SAVEDATE NIL "); + else { + str_printfa(ctx->state.cur_str, "SAVEDATE \"%s\" ", + imap_to_datetime(date)); + } + return 1; +} + +static bool imap_fetch_savedate_init(struct imap_fetch_init_context *ctx) +{ + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_SAVE_DATE; + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + "NIL", imap_fetch_savedate, NULL); + return TRUE; +} + +static int fetch_guid(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + const char *value; + + if (mail_get_special(mail, MAIL_FETCH_GUID, &value) < 0) + return -1; + + str_append(ctx->state.cur_str, "X-GUID "); + imap_append_astring(ctx->state.cur_str, value); + str_append_c(ctx->state.cur_str, ' '); + return 1; +} + +static bool fetch_guid_init(struct imap_fetch_init_context *ctx) +{ + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_GUID; + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + "", fetch_guid, NULL); + return TRUE; +} + +static int fetch_x_mailbox(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + const char *name; + string_t *mutf7_name; + + if (mail_get_special(mail, MAIL_FETCH_MAILBOX_NAME, &name) < 0) { + /* This can happen with virtual mailbox if the backend mail + is expunged. */ + return -1; + } + + mutf7_name = t_str_new(strlen(name)*2); + if (imap_utf8_to_utf7(name, mutf7_name) < 0) + i_panic("FETCH: Mailbox name not UTF-8: %s", name); + + str_append(ctx->state.cur_str, "X-MAILBOX "); + imap_append_astring(ctx->state.cur_str, str_c(mutf7_name)); + str_append_c(ctx->state.cur_str, ' '); + return 1; +} + +static bool fetch_x_mailbox_init(struct imap_fetch_init_context *ctx) +{ + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + NULL, fetch_x_mailbox, NULL); + return TRUE; +} + +static int fetch_x_real_uid(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + struct mail *real_mail; + + if (mail_get_backend_mail(mail, &real_mail) < 0) + return -1; + str_printfa(ctx->state.cur_str, "X-REAL-UID %u ", real_mail->uid); + return 1; +} + +static bool fetch_x_real_uid_init(struct imap_fetch_init_context *ctx) +{ + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + NULL, fetch_x_real_uid, NULL); + return TRUE; +} + +static int fetch_x_savedate(struct imap_fetch_context *ctx, struct mail *mail, + void *context ATTR_UNUSED) +{ + time_t date; + + if (mail_get_save_date(mail, &date) < 0) + return -1; + + str_printfa(ctx->state.cur_str, "X-SAVEDATE \"%s\" ", + imap_to_datetime(date)); + return 1; +} + +static bool fetch_x_savedate_init(struct imap_fetch_init_context *ctx) +{ + ctx->fetch_ctx->fetch_data |= MAIL_FETCH_SAVE_DATE; + imap_fetch_add_handler(ctx, IMAP_FETCH_HANDLER_FLAG_BUFFERED, + "\"01-Jan-1970 00:00:00 +0000\"", + fetch_x_savedate, NULL); + return TRUE; +} + +static const struct imap_fetch_handler +imap_fetch_default_handlers[] = { + { "BINARY", imap_fetch_binary_init }, + { "BODY", fetch_body_init }, + { "BODYSTRUCTURE", fetch_bodystructure_init }, + { "ENVELOPE", fetch_envelope_init }, + { "FLAGS", imap_fetch_flags_init }, + { "INTERNALDATE", fetch_internaldate_init }, + { "MODSEQ", imap_fetch_modseq_init }, + { "PREVIEW", imap_fetch_preview_init }, + { "RFC822", imap_fetch_rfc822_init }, + { "SNIPPET", imap_fetch_snippet_init }, + { "UID", imap_fetch_uid_init }, + { "SAVEDATE", imap_fetch_savedate_init }, + { "X-GUID", fetch_guid_init }, + { "X-MAILBOX", fetch_x_mailbox_init }, + { "X-REAL-UID", fetch_x_real_uid_init }, + { "X-SAVEDATE", fetch_x_savedate_init } +}; + +void imap_fetch_handlers_init(void) +{ + i_array_init(&fetch_handlers, 32); + imap_fetch_handlers_register(imap_fetch_default_handlers, + N_ELEMENTS(imap_fetch_default_handlers)); +} + +void imap_fetch_handlers_deinit(void) +{ + array_free(&fetch_handlers); +} diff --git a/src/imap/imap-fetch.h b/src/imap/imap-fetch.h new file mode 100644 index 0000000..ba8cfc8 --- /dev/null +++ b/src/imap/imap-fetch.h @@ -0,0 +1,166 @@ +#ifndef IMAP_FETCH_H +#define IMAP_FETCH_H + +struct imap_fetch_context; + +enum imap_fetch_handler_flags { + IMAP_FETCH_HANDLER_FLAG_BUFFERED = 0x01, + IMAP_FETCH_HANDLER_FLAG_WANT_DEINIT = 0x02 +}; + +/* Returns 1 = ok, 0 = client output buffer full, call again, -1 = error. + mail = NULL for deinit. */ +typedef int imap_fetch_handler_t(struct imap_fetch_context *ctx, + struct mail *mail, void *context); + +struct imap_fetch_init_context { + struct imap_fetch_context *fetch_ctx; + pool_t pool; + + const char *name; + const struct imap_arg *args; + + const char *error; +}; + +struct imap_fetch_handler { + const char *name; + + /* Returns FALSE and sets ctx->error if arg is invalid */ + bool (*init)(struct imap_fetch_init_context *ctx); +}; + +struct imap_fetch_context_handler { + imap_fetch_handler_t *handler; + void *context; + + const char *name; + const char *nil_reply; + + bool buffered:1; + bool want_deinit:1; +}; + +struct imap_fetch_qresync_args { + const ARRAY_TYPE(uint32_t) *qresync_sample_seqset; + const ARRAY_TYPE(uint32_t) *qresync_sample_uidset; +}; + +struct imap_fetch_state { + struct mailbox_transaction_context *trans; + struct mail_search_context *search_ctx; + + struct mail *cur_mail; + unsigned int cur_handler; + const char *cur_human_name; + uoff_t cur_size; + enum mail_fetch_field cur_size_field; + string_t *cur_str; + size_t cur_str_prefix_size; + struct istream *cur_input; + bool skip_cr; + int (*cont_handler)(struct imap_fetch_context *ctx); + uint64_t *cur_stats_sizep; + + bool fetching:1; + bool seen_flags_changed:1; + /* TRUE if the first FETCH parameter result hasn't yet been sent to + the IMAP client. Note that this doesn't affect buffered content in + cur_str until it gets flushed out. */ + bool cur_first:1; + /* TRUE if the cur_str prefix has been flushed. More data may still + be added to it. */ + bool line_partial:1; + bool skipped_expunged_msgs:1; + bool failed:1; +}; + +struct imap_fetch_context { + struct client *client; + pool_t ctx_pool; + const char *reason; + + enum mail_fetch_field fetch_data; + ARRAY_TYPE(const_string) all_headers; + + ARRAY(struct imap_fetch_context_handler) handlers; + unsigned int buffered_handlers_count; + + ARRAY_TYPE(keywords) tmp_keywords; + + struct imap_fetch_state state; + ARRAY_TYPE(seq_range) fetch_failed_uids; + unsigned int fetched_mails_count; + + enum mail_error error; + const char *errstr; + + bool initialized:1; + bool failures:1; + bool flags_have_handler:1; + bool flags_update_seen:1; + bool flags_show_only_seen_changes:1; + /* HEADER.FIELDS or HEADER.FIELDS.NOT is fetched */ + bool fetch_header_fields:1; +}; + +void imap_fetch_handlers_register(const struct imap_fetch_handler *handlers, + size_t count); +void imap_fetch_handler_unregister(const char *name); + +void imap_fetch_add_handler(struct imap_fetch_init_context *ctx, + enum imap_fetch_handler_flags flags, + const char *nil_reply, + imap_fetch_handler_t *handler, void *context) + ATTR_NULL(3, 5); +#define imap_fetch_add_handler(ctx, flags, nil_reply, handler, context) \ + imap_fetch_add_handler(ctx, flags, nil_reply - \ + CALLBACK_TYPECHECK(handler, int (*)( \ + struct imap_fetch_context *, struct mail *, \ + typeof(context))), \ + (imap_fetch_handler_t *)handler, context) + +int imap_fetch_att_list_parse(struct client *client, pool_t pool, + const struct imap_arg *list, + struct imap_fetch_context **fetch_ctx_r, + const char **client_error_r); + +struct imap_fetch_context * +imap_fetch_alloc(struct client *client, pool_t pool, const char *reason); +void imap_fetch_free(struct imap_fetch_context **ctx); +bool imap_fetch_init_handler(struct imap_fetch_init_context *init_ctx); +void imap_fetch_init_nofail_handler(struct imap_fetch_context *ctx, + bool (*init)(struct imap_fetch_init_context *)); +const struct imap_fetch_handler *imap_fetch_handler_lookup(const char *name); + +void imap_fetch_begin(struct imap_fetch_context *ctx, struct mailbox *box, + struct mail_search_args *search_args); +int imap_fetch_send_vanished(struct client *client, struct mailbox *box, + const struct mail_search_args *search_args, + const struct imap_fetch_qresync_args *qresync_args); +/* Returns 1 if finished, 0 if more data is needed, -1 if error. + When 0 is returned, line_partial=TRUE if literal is open and must be + finished before anything else to client. */ +int imap_fetch_more(struct imap_fetch_context *ctx, + struct client_command_context *cmd); +/* Like imap_fetch_more(), but don't check/update output_lock. + The caller must handle this itself. */ +int imap_fetch_more_no_lock_update(struct imap_fetch_context *ctx); +int imap_fetch_end(struct imap_fetch_context *ctx); +int imap_fetch_more(struct imap_fetch_context *ctx, + struct client_command_context *cmd); + +bool imap_fetch_flags_init(struct imap_fetch_init_context *ctx); +bool imap_fetch_modseq_init(struct imap_fetch_init_context *ctx); +bool imap_fetch_uid_init(struct imap_fetch_init_context *ctx); + +bool imap_fetch_body_section_init(struct imap_fetch_init_context *ctx); +bool imap_fetch_rfc822_init(struct imap_fetch_init_context *ctx); +bool imap_fetch_binary_init(struct imap_fetch_init_context *ctx); +bool imap_fetch_preview_init(struct imap_fetch_init_context *ctx); +bool imap_fetch_snippet_init(struct imap_fetch_init_context *ctx); + +void imap_fetch_handlers_init(void); +void imap_fetch_handlers_deinit(void); + +#endif diff --git a/src/imap/imap-list.c b/src/imap/imap-list.c new file mode 100644 index 0000000..38a81ba --- /dev/null +++ b/src/imap/imap-list.c @@ -0,0 +1,35 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "str.h" +#include "imap-list.h" + +bool imap_mailbox_flags2str(string_t *str, enum mailbox_info_flags flags) +{ + size_t orig_len = str_len(str); + + if ((flags & MAILBOX_SUBSCRIBED) != 0) + str_append(str, "\\Subscribed "); + + if ((flags & MAILBOX_NOSELECT) != 0) + str_append(str, "\\Noselect "); + if ((flags & MAILBOX_NONEXISTENT) != 0) + str_append(str, "\\NonExistent "); + + if ((flags & MAILBOX_CHILDREN) != 0) + str_append(str, "\\HasChildren "); + else if ((flags & MAILBOX_NOINFERIORS) != 0) + str_append(str, "\\NoInferiors "); + else if ((flags & MAILBOX_NOCHILDREN) != 0) + str_append(str, "\\HasNoChildren "); + + if ((flags & MAILBOX_MARKED) != 0) + str_append(str, "\\Marked "); + if ((flags & MAILBOX_UNMARKED) != 0) + str_append(str, "\\UnMarked "); + + if (str_len(str) == orig_len) + return FALSE; + str_truncate(str, str_len(str)-1); + return TRUE; +} diff --git a/src/imap/imap-list.h b/src/imap/imap-list.h new file mode 100644 index 0000000..6a0e076 --- /dev/null +++ b/src/imap/imap-list.h @@ -0,0 +1,7 @@ +#ifndef IMAP_LIST_H +#define IMAP_LIST_H + +/* Returns TRUE if anything was added to the string. */ +bool imap_mailbox_flags2str(string_t *str, enum mailbox_info_flags flags); + +#endif diff --git a/src/imap/imap-master-client.c b/src/imap/imap-master-client.c new file mode 100644 index 0000000..66f7f3d --- /dev/null +++ b/src/imap/imap-master-client.c @@ -0,0 +1,415 @@ +/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "connection.h" +#include "istream.h" +#include "istream-unix.h" +#include "ostream.h" +#include "base64.h" +#include "str.h" +#include "strescape.h" +#include "str-sanitize.h" +#include "time-util.h" +#include "master-service.h" +#include "mail-storage-service.h" +#include "imap-client.h" +#include "imap-state.h" +#include "imap-master-client.h" + +struct imap_master_client { + struct connection conn; + bool imap_client_created; +}; + +struct imap_master_input { + /* input we've already read from the IMAP client. */ + buffer_t *client_input; + /* output that imap-hibernate was supposed to send to IMAP client, + but couldn't send it yet. */ + buffer_t *client_output; + /* IMAP connection state */ + buffer_t *state; + /* command tag */ + const char *tag; + /* Timestamp when hibernation started */ + struct timeval hibernation_start_time; + + dev_t peer_dev; + ino_t peer_ino; + + bool state_import_bad_idle_done; + bool state_import_idle_continue; +}; + +static struct connection_list *master_clients = NULL; + +static void imap_master_client_destroy(struct connection *conn) +{ + struct imap_master_client *client = (struct imap_master_client *)conn; + + if (!client->imap_client_created) + master_service_client_connection_destroyed(master_service); + connection_deinit(conn); + i_free(conn); +} + +static int +imap_master_client_parse_input(const char *const *args, pool_t pool, + struct mail_storage_service_input *input_r, + struct imap_master_input *master_input_r, + const char **error_r) +{ + const char *key, *value; + unsigned int peer_dev_major = 0, peer_dev_minor = 0; + + i_zero(input_r); + i_zero(master_input_r); + master_input_r->client_input = buffer_create_dynamic(pool, 64); + master_input_r->client_output = buffer_create_dynamic(pool, 16); + master_input_r->state = buffer_create_dynamic(pool, 512); + + input_r->module = input_r->service = "imap"; + /* we never want to do userdb lookup again when restoring the client. + we have the userdb_fields cached already. */ + input_r->flags_override_remove = MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP; + + if (args[0] == NULL) { + *error_r = "Missing username in input"; + return -1; + } + input_r->username = args[0]; + + for (args++; *args != NULL; args++) { + value = strchr(*args, '='); + if (value != NULL) + key = t_strdup_until(*args, value++); + else { + key = *args; + value = ""; + } + if (strcmp(key, "lip") == 0) { + if (net_addr2ip(value, &input_r->local_ip) < 0) { + *error_r = t_strdup_printf( + "Invalid lip value: %s", value); + return -1; + } + } else if (strcmp(key, "lport") == 0) { + if (net_str2port(value, &input_r->local_port) < 0) { + *error_r = t_strdup_printf( + "Invalid lport value: %s", value); + return -1; + } + } else if (strcmp(key, "rip") == 0) { + if (net_addr2ip(value, &input_r->remote_ip) < 0) { + *error_r = t_strdup_printf( + "Invalid rip value: %s", value); + return -1; + } + } else if (strcmp(key, "rport") == 0) { + if (net_str2port(value, &input_r->remote_port) < 0) { + *error_r = t_strdup_printf( + "Invalid rport value: %s", value); + return -1; + } + } else if (strcmp(key, "peer_dev_major") == 0) { + if (str_to_uint(value, &peer_dev_major) < 0) { + *error_r = t_strdup_printf( + "Invalid peer_dev_major value: %s", value); + return -1; + } + } else if (strcmp(key, "peer_dev_minor") == 0) { + if (str_to_uint(value, &peer_dev_minor) < 0) { + *error_r = t_strdup_printf( + "Invalid peer_dev_minor value: %s", value); + return -1; + } + } else if (strcmp(key, "peer_ino") == 0) { + if (str_to_ino(value, &master_input_r->peer_ino) < 0) { + *error_r = t_strdup_printf( + "Invalid peer_ino value: %s", value); + return -1; + } + } else if (strcmp(key, "session") == 0) { + input_r->session_id = value; + } else if (strcmp(key, "session_created") == 0) { + if (str_to_time(value, &input_r->session_create_time) < 0) { + *error_r = t_strdup_printf( + "Invalid session_created value: %s", value); + return -1; + } + } else if (strcmp(key, "hibernation_started") == 0) { + if (str_to_timeval(value, &master_input_r->hibernation_start_time) < 0) { + *error_r = t_strdup_printf( + "Invalid hibernation_started value: %s", value); + return -1; + } + } else if (strcmp(key, "userdb_fields") == 0) { + input_r->userdb_fields = + t_strsplit_tabescaped(value); + } else if (strcmp(key, "client_input") == 0) { + if (base64_decode(value, strlen(value), NULL, + master_input_r->client_input) < 0) { + *error_r = t_strdup_printf( + "Invalid client_input base64 value: %s", value); + return -1; + } + } else if (strcmp(key, "client_output") == 0) { + if (base64_decode(value, strlen(value), NULL, + master_input_r->client_output) < 0) { + *error_r = t_strdup_printf( + "Invalid client_output base64 value: %s", value); + return -1; + } + } else if (strcmp(key, "state") == 0) { + if (base64_decode(value, strlen(value), NULL, + master_input_r->state) < 0) { + *error_r = t_strdup_printf( + "Invalid state base64 value: %s", value); + return -1; + } + } else if (strcmp(key, "tag") == 0) { + master_input_r->tag = t_strdup(value); + } else if (strcmp(key, "bad-done") == 0) { + master_input_r->state_import_bad_idle_done = TRUE; + } else if (strcmp(key, "idle-continue") == 0) { + master_input_r->state_import_idle_continue = TRUE; + } + } + if (peer_dev_major != 0 || peer_dev_minor != 0) { + master_input_r->peer_dev = + makedev(peer_dev_major, peer_dev_minor); + } + return 0; +} + +static int imap_master_client_verify(const struct imap_master_input *master_input, + int fd_client, const char **error_r) +{ + struct stat peer_st; + + if (master_input->peer_ino == 0) + return 0; + + /* make sure we have the right fd */ + if (fstat(fd_client, &peer_st) < 0) { + *error_r = t_strdup_printf("fstat(peer) failed: %m"); + return -1; + } + if (peer_st.st_ino != master_input->peer_ino || + !CMP_DEV_T(peer_st.st_dev, master_input->peer_dev)) { + *error_r = t_strdup_printf( + "BUG: Expected peer device=%lu,%lu inode=%s doesn't match " + "client fd's actual device=%lu,%lu inode=%s", + (unsigned long)major(peer_st.st_dev), + (unsigned long)minor(peer_st.st_dev), dec2str(peer_st.st_ino), + (unsigned long)major(master_input->peer_dev), + (unsigned long)minor(master_input->peer_dev), + dec2str(master_input->peer_ino)); + return -1; + } + return 0; +} + +static int +imap_master_client_input_args(struct connection *conn, const char *const *args, + int fd_client, pool_t pool) +{ + struct imap_master_client *client = (struct imap_master_client *)conn; + struct client *imap_client; + struct mail_storage_service_input input; + struct imap_master_input master_input; + const char *error = NULL, *reason; + int ret; + + if (imap_master_client_parse_input(args, pool, &input, &master_input, + &error) < 0) { + e_error(conn->event, "imap-master: Failed to parse client input: %s", error); + o_stream_nsend_str(conn->output, t_strdup_printf( + "-Failed to parse client input: %s\n", error)); + i_close_fd(&fd_client); + return -1; + } + if (imap_master_client_verify(&master_input, fd_client, &error) < 0) { + e_error(conn->event, "imap-master: Failed to verify client input: %s", error); + o_stream_nsend_str(conn->output, t_strdup_printf( + "-Failed to verify client input: %s\n", error)); + i_close_fd(&fd_client); + return -1; + } + + /* NOTE: before client_create_from_input() on failures we need to close + fd_client, but afterward it gets closed by client_destroy() */ + ret = client_create_from_input(&input, fd_client, fd_client, + &imap_client, &error); + if (ret < 0) { + e_error(conn->event, + "imap-master(%s): Failed to create client: %s", + input.username, error); + o_stream_nsend_str(conn->output, t_strdup_printf( + "-Failed to create client: %s\n", error)); + i_close_fd(&fd_client); + return -1; + } + client->imap_client_created = TRUE; + + long long hibernation_usecs = + timeval_diff_usecs(&ioloop_timeval, + &master_input.hibernation_start_time); + struct event *event = event_create(imap_client->event); + event_set_name(event, "imap_client_unhibernated"); + event_add_int(event, "hibernation_usecs", hibernation_usecs); + imap_client->state_import_bad_idle_done = + master_input.state_import_bad_idle_done; + imap_client->state_import_idle_continue = + master_input.state_import_idle_continue; + if (imap_client->state_import_bad_idle_done) { + reason = "IDLE was stopped with BAD command"; + event_add_str(event, "reason", "idle_bad_reply"); + } else if (imap_client->state_import_idle_continue) { + reason = "mailbox changes need to be sent"; + event_add_str(event, "reason", "mailbox_changes"); + } else { + reason = "IDLE was stopped with DONE"; + event_add_str(event, "reason", "idle_done"); + } + + /* Send a success notification before we start anything that lasts + potentially a long time. imap-hibernate process is waiting for us + to answer. Even if we fail later, we log the error anyway. From now + on it's our responsibility to also log the imap_client_unhibernated + event. */ + o_stream_nsend_str(conn->output, "+\n"); + (void)o_stream_flush(conn->output); + + if (master_input.client_input->used > 0) { + client_add_istream_prefix(imap_client, + master_input.client_input->data, + master_input.client_input->used); + } + + client_create_finish_io(imap_client); + if (client_create_finish(imap_client, &error) < 0) { + event_add_str(event, "error", error); + e_error(event, "imap-master: %s", error); + event_unref(&event); + client_destroy(imap_client, error); + return -1; + } + /* log prefix is set at this point, so we don't need to add the + username anymore to the log messages */ + + o_stream_nsend(imap_client->output, + master_input.client_output->data, + master_input.client_output->used); + + struct event_reason *event_reason = + event_reason_begin("imap:unhibernate"); + ret = imap_state_import_internal(imap_client, master_input.state->data, + master_input.state->used, &error); + event_reason_end(&event_reason); + + if (ret <= 0) { + error = t_strdup_printf("Failed to import client state: %s", error); + event_add_str(event, "error", error); + e_error(event, "imap-master: %s", error); + event_unref(&event); + client_destroy(imap_client, "Client state initialization failed"); + return -1; + } + if (imap_client->mailbox != NULL) { + /* Would be nice to set this earlier, but the previous errors + happen rarely enough that it shouldn't really matter. */ + event_add_str(event, "mailbox", + mailbox_get_vname(imap_client->mailbox)); + } + + if (master_input.tag != NULL) + imap_state_import_idle_cmd_tag(imap_client, master_input.tag); + + e_debug(event, "imap-master: Unhibernated because %s " + "(hibernated for %llu.%06llu secs)", reason, + hibernation_usecs/1000000, hibernation_usecs%1000000); + event_unref(&event); + + /* make sure all pending input gets handled */ + if (master_input.client_input->used > 0) { + e_debug(imap_client->event, + "imap-master: Pending client input: '%s'", + str_sanitize(str_c(master_input.client_input), 128)); + io_set_pending(imap_client->io); + } + + imap_refresh_proctitle(); + /* we'll always disconnect the client afterwards */ + return -1; +} + +static int +imap_master_client_input_line(struct connection *conn, const char *line) +{ + char *const *args; + pool_t pool; + int fd_client, ret; + + if (!conn->version_received) { + if (connection_handshake_args_default(conn, t_strsplit_tabescaped(line)) < 0) + return -1; + conn->version_received = TRUE; + return 1; + } + + fd_client = i_stream_unix_get_read_fd(conn->input); + if (fd_client == -1) { + e_error(conn->event, "imap-master: IMAP client fd not received"); + return -1; + } + + if (imap_debug) + e_debug(conn->event, "imap-master: Client input: %s", line); + + pool = pool_alloconly_create("imap master client cmd", 1024); + args = p_strsplit_tabescaped(pool, line); + ret = imap_master_client_input_args(conn, (const void *)args, + fd_client, pool); + pool_unref(&pool); + return ret; +} + +void imap_master_client_create(int fd) +{ + struct imap_master_client *client; + + client = i_new(struct imap_master_client, 1); + client->conn.unix_socket = TRUE; + connection_init_server(master_clients, &client->conn, + "imap-master", fd, fd); + + /* read the first file descriptor that we can */ + i_stream_unix_set_read_fd(client->conn.input); +} + +static struct connection_settings client_set = { + .service_name_in = "imap-master", + .service_name_out = "imap-master", + .major_version = 1, + .minor_version = 0, + + .input_max_size = SIZE_MAX, + .output_max_size = SIZE_MAX, + .client = FALSE +}; + +static const struct connection_vfuncs client_vfuncs = { + .destroy = imap_master_client_destroy, + .input_line = imap_master_client_input_line +}; + +void imap_master_clients_init(void) +{ + master_clients = connection_list_init(&client_set, &client_vfuncs); +} + +void imap_master_clients_deinit(void) +{ + connection_list_deinit(&master_clients); +} diff --git a/src/imap/imap-master-client.h b/src/imap/imap-master-client.h new file mode 100644 index 0000000..478a64a --- /dev/null +++ b/src/imap/imap-master-client.h @@ -0,0 +1,9 @@ +#ifndef IMAP_MASTER_CLIENT_H +#define IMAP_MASTER_CLIENT_H + +void imap_master_client_create(int fd); + +void imap_master_clients_init(void); +void imap_master_clients_deinit(void); + +#endif diff --git a/src/imap/imap-notify.c b/src/imap/imap-notify.c new file mode 100644 index 0000000..fa2c9ef --- /dev/null +++ b/src/imap/imap-notify.c @@ -0,0 +1,523 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "str.h" +#include "ostream.h" +#include "imap-quote.h" +#include "mailbox-list-iter.h" +#include "mailbox-list-notify.h" +#include "mail-search.h" +#include "mail-search-build.h" +#include "imap-commands.h" +#include "imap-fetch.h" +#include "imap-list.h" +#include "imap-status.h" +#include "imap-notify.h" + +#define IMAP_NOTIFY_WATCH_ADD_DELAY_MSECS 1000 + +static int imap_notify_list(struct imap_notify_namespace *notify_ns, + const struct mailbox_list_notify_rec *rec, + enum mailbox_info_flags flags) +{ + string_t *str = t_str_new(128); + char ns_sep = mail_namespace_get_sep(notify_ns->ns); + + str_append(str, "* LIST ("); + imap_mailbox_flags2str(str, flags); + str_append(str, ") \""); + if (ns_sep == '\\') + str_append_c(str, '\\'); + str_append_c(str, ns_sep); + str_append(str, "\" "); + + imap_append_astring(str, rec->vname); + if (rec->old_vname != NULL) { + str_append(str, " (\"OLDNAME\" ("); + imap_append_astring(str, rec->old_vname); + str_append(str, "))"); + } + return client_send_line_next(notify_ns->ctx->client, str_c(str)); +} + +static int imap_notify_status(struct imap_notify_namespace *notify_ns, + const struct mailbox_list_notify_rec *rec) +{ + struct client *client = notify_ns->ctx->client; + struct mailbox *box; + struct imap_status_items items; + struct imap_status_result result; + enum mail_error error; + int ret = 1; + + i_zero(&items); + if (client_has_enabled(client, imap_feature_condstore)) + items.flags |= IMAP_STATUS_ITEM_HIGHESTMODSEQ; + + box = mailbox_alloc(notify_ns->ns->list, rec->vname, 0); + if ((rec->events & MAILBOX_LIST_NOTIFY_UIDVALIDITY) != 0) { + items.flags |= IMAP_STATUS_ITEM_UIDVALIDITY | + IMAP_STATUS_ITEM_UIDNEXT | IMAP_STATUS_ITEM_MESSAGES | + IMAP_STATUS_ITEM_UNSEEN; + } + if ((rec->events & (MAILBOX_LIST_NOTIFY_APPENDS | + MAILBOX_LIST_NOTIFY_EXPUNGES)) != 0) + items.flags |= IMAP_STATUS_ITEM_UIDNEXT | + IMAP_STATUS_ITEM_MESSAGES | IMAP_STATUS_ITEM_UNSEEN; + if ((rec->events & MAILBOX_LIST_NOTIFY_SEEN_CHANGES) != 0) + items.flags |= IMAP_STATUS_ITEM_UNSEEN; + if ((rec->events & MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES) != 0) { + /* if HIGHESTMODSEQ isn't being sent, don't send anything */ + } + if (imap_status_items_is_empty(&items)) { + /* don't send anything */ + } else if (imap_status_get_result(client, box, &items, &result) < 0) { + /* hide permission errors from client. we don't want to leak + information about existence of mailboxes where user doesn't + have access to */ + (void)mailbox_get_last_error(box, &error); + if (error != MAIL_ERROR_PERM) + ret = -1; + } else { + ret = imap_status_send(client, rec->vname, &items, &result); + } + mailbox_free(&box); + return ret; +} + +static int +imap_notify_next(struct imap_notify_namespace *notify_ns, + const struct mailbox_list_notify_rec *rec) +{ + enum mailbox_info_flags mailbox_flags; + int ret; + + if ((rec->events & MAILBOX_LIST_NOTIFY_CREATE) != 0) { + if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name, + &mailbox_flags) < 0) + mailbox_flags = 0; + if ((ret = imap_notify_list(notify_ns, rec, mailbox_flags)) <= 0) + return ret; + } + if ((rec->events & MAILBOX_LIST_NOTIFY_DELETE) != 0) { + if ((ret = imap_notify_list(notify_ns, rec, MAILBOX_NONEXISTENT)) < 0) + return ret; + } + if ((rec->events & MAILBOX_LIST_NOTIFY_RENAME) != 0) { + if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name, + &mailbox_flags) < 0) + mailbox_flags = 0; + if ((ret = imap_notify_list(notify_ns, rec, mailbox_flags)) < 0) + return ret; + } + if ((rec->events & MAILBOX_LIST_NOTIFY_SUBSCRIBE) != 0) { + if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name, + &mailbox_flags) < 0) + mailbox_flags = 0; + if ((ret = imap_notify_list(notify_ns, rec, + mailbox_flags | MAILBOX_SUBSCRIBED)) < 0) + return ret; + } + if ((rec->events & MAILBOX_LIST_NOTIFY_UNSUBSCRIBE) != 0) { + if (mailbox_list_mailbox(notify_ns->ns->list, rec->storage_name, + &mailbox_flags) < 0) + mailbox_flags = 0; + if ((ret = imap_notify_list(notify_ns, rec, mailbox_flags)) < 0) + return ret; + } + if ((rec->events & (MAILBOX_LIST_NOTIFY_UIDVALIDITY | + MAILBOX_LIST_NOTIFY_APPENDS | + MAILBOX_LIST_NOTIFY_EXPUNGES | + MAILBOX_LIST_NOTIFY_SEEN_CHANGES | + MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES)) != 0) { + if ((ret = imap_notify_status(notify_ns, rec)) < 0) + return ret; + } + return 1; +} + +static bool +imap_notify_match_event(struct imap_notify_namespace *notify_ns, + const struct imap_notify_mailboxes *notify_boxes, + const struct mailbox_list_notify_rec *rec) +{ + enum imap_notify_event wanted_events = notify_boxes->events; + struct mailbox *box; + + /* check for mailbox list events first */ + if ((wanted_events & IMAP_NOTIFY_EVENT_MAILBOX_NAME) != 0) { + if ((rec->events & (MAILBOX_LIST_NOTIFY_CREATE | + MAILBOX_LIST_NOTIFY_DELETE | + MAILBOX_LIST_NOTIFY_RENAME)) != 0) + return TRUE; + } + if ((wanted_events & IMAP_NOTIFY_EVENT_SUBSCRIPTION_CHANGE) != 0) { + if ((rec->events & (MAILBOX_LIST_NOTIFY_SUBSCRIBE | + MAILBOX_LIST_NOTIFY_UNSUBSCRIBE)) != 0) + return TRUE; + } + + /* if this is an event for the selected mailbox, ignore it */ + box = notify_ns->ctx->client->mailbox; + if (box != NULL && mailbox_equals(box, notify_ns->ns, rec->vname)) + return FALSE; + + if ((wanted_events & (IMAP_NOTIFY_EVENT_MESSAGE_NEW | + IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE | + IMAP_NOTIFY_EVENT_FLAG_CHANGE)) != 0) { + if ((rec->events & MAILBOX_LIST_NOTIFY_UIDVALIDITY) != 0) + return TRUE; + } + if ((wanted_events & IMAP_NOTIFY_EVENT_MESSAGE_NEW) != 0) { + if ((rec->events & MAILBOX_LIST_NOTIFY_APPENDS) != 0) + return TRUE; + } + if ((wanted_events & IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE) != 0) { + if ((rec->events & MAILBOX_LIST_NOTIFY_EXPUNGES) != 0) + return TRUE; + } + if ((wanted_events & IMAP_NOTIFY_EVENT_FLAG_CHANGE) != 0) { + if ((rec->events & (MAILBOX_LIST_NOTIFY_SEEN_CHANGES | + MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES)) != 0) + return TRUE; + } + return FALSE; +} + +bool imap_notify_match_mailbox(struct imap_notify_namespace *notify_ns, + const struct imap_notify_mailboxes *notify_boxes, + const char *vname) +{ + struct mailbox *box; + const char *name; + size_t name_len; + char ns_sep; + bool ret; + + switch (notify_boxes->type) { + case IMAP_NOTIFY_TYPE_SUBSCRIBED: + box = mailbox_alloc(notify_ns->ns->list, vname, 0); + ret = mailbox_is_subscribed(box); + mailbox_free(&box); + return ret; + case IMAP_NOTIFY_TYPE_SUBTREE: + ns_sep = mail_namespace_get_sep(notify_ns->ns); + array_foreach_elem(¬ify_boxes->names, name) { + name_len = strlen(name); + if (name_len == 0) { + /* everything under root. NOTIFY spec itself + doesn't define this, but we use it for + implementing "personal" */ + return TRUE; + } + if (str_begins(vname, name) && + (vname[name_len] == '\0' || + vname[name_len] == ns_sep)) + return TRUE; + } + break; + case IMAP_NOTIFY_TYPE_MAILBOX: + array_foreach_elem(¬ify_boxes->names, name) { + if (strcmp(name, vname) == 0) + return TRUE; + } + break; + } + return FALSE; +} + +static bool +imap_notify_match(struct imap_notify_namespace *notify_ns, + const struct mailbox_list_notify_rec *rec) +{ + const struct imap_notify_mailboxes *notify_boxes; + + array_foreach(¬ify_ns->mailboxes, notify_boxes) { + if (imap_notify_match_event(notify_ns, notify_boxes, rec) && + imap_notify_match_mailbox(notify_ns, notify_boxes, rec->vname)) + return TRUE; + } + return FALSE; +} + +static int imap_client_notify_ns(struct imap_notify_namespace *notify_ns) +{ + const struct mailbox_list_notify_rec *rec; + int ret, ret2 = 1; + + if (notify_ns->notify == NULL) + return 0; /* notifications not supported in this namespace */ + + while ((ret = mailbox_list_notify_next(notify_ns->notify, &rec)) > 0) { + if (imap_notify_match(notify_ns, rec)) T_BEGIN { + ret2 = imap_notify_next(notify_ns, rec); + } T_END; + if (ret2 <= 0) + break; + } + if (ret < 0) { + /* failed to get some notifications */ + return -1; + } + return ret2; +} + +static int +imap_client_notify_selected(struct client *client) +{ + struct imap_fetch_context *fetch_ctx = client->notify_ctx->fetch_ctx; + int ret; + + if (!fetch_ctx->state.fetching) + return 1; + + if ((ret = imap_fetch_more_no_lock_update(fetch_ctx)) == 0) + return 0; + /* finished the FETCH */ + if (imap_fetch_end(fetch_ctx) < 0) + return -1; + return ret; +} + +static int imap_client_notify_more(struct client *client) +{ + struct imap_notify_namespace *notify_ns; + int ret = 1; + + struct event_reason *reason = event_reason_begin("imap:notify_update"); + + /* send notifications for selected mailbox first. note that it may + leave the client's output stream in the middle of a FETCH reply. */ + if (client->notify_ctx->fetch_ctx != NULL) { + if ((ret = imap_client_notify_selected(client)) < 0) { + client->notify_ctx->fetch_ctx->state.failed = FALSE; + ret = -1; + } + } + + /* send notifications for non-selected mailboxes */ + array_foreach_modifiable(&client->notify_ctx->namespaces, notify_ns) { + if (ret == 0) + break; + if (imap_client_notify_ns(notify_ns) < 0) + ret = -1; + } + + if (ret < 0) { + client_send_line(client, + "* NO NOTIFY error, some events may have got lost"); + } + event_reason_end(&reason); + return ret; +} + +int imap_client_notify_newmails(struct client *client) +{ + struct imap_fetch_context *fetch_ctx = client->notify_ctx->fetch_ctx; + struct mailbox_status status; + struct mail_search_args *search_args; + struct mail_search_arg *arg; + + i_assert(client->mailbox != NULL); + + if (fetch_ctx == NULL) { + /* FETCH notifications not enabled in this session */ + return 1; + } + if (client->notify_ctx->notifying) + return imap_client_notify_more(client); + client->notify_ctx->notifying = TRUE; + + i_assert(!fetch_ctx->state.fetching); + + mailbox_get_open_status(client->mailbox, STATUS_UIDNEXT, &status); + + search_args = mail_search_build_init(); + arg = mail_search_build_add(search_args, SEARCH_UIDSET); + p_array_init(&arg->value.seqset, search_args->pool, 1); + seq_range_array_add_range(&arg->value.seqset, + client->notify_uidnext, status.uidnext-1); + client->notify_uidnext = status.uidnext; + + imap_fetch_begin(fetch_ctx, client->mailbox, search_args); + mail_search_args_unref(&search_args); + + return imap_client_notify_more(client); +} + +void imap_client_notify_finished(struct client *client) +{ + if (client->notify_ctx != NULL) + client->notify_ctx->notifying = FALSE; +} + +static void notify_callback(struct imap_notify_namespace *notify_ns) +{ + struct event_reason *reason = event_reason_begin("imap:notify_update"); + o_stream_cork(notify_ns->ctx->client->output); + imap_client_notify_ns(notify_ns); + o_stream_uncork(notify_ns->ctx->client->output); + event_reason_end(&reason); +} + +static enum mailbox_list_notify_event +imap_events_to_notify(enum imap_notify_event events) +{ + enum mailbox_list_notify_event ret = 0; + + if ((events & IMAP_NOTIFY_EVENT_MESSAGE_NEW) != 0) { + ret |= MAILBOX_LIST_NOTIFY_APPENDS | + MAILBOX_LIST_NOTIFY_UIDVALIDITY; + } + if ((events & IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE) != 0) { + ret |= MAILBOX_LIST_NOTIFY_EXPUNGES | + MAILBOX_LIST_NOTIFY_UIDVALIDITY; + } + if ((events & IMAP_NOTIFY_EVENT_FLAG_CHANGE) != 0) { + ret |= MAILBOX_LIST_NOTIFY_SEEN_CHANGES | + MAILBOX_LIST_NOTIFY_MODSEQ_CHANGES | + MAILBOX_LIST_NOTIFY_UIDVALIDITY; + } + if ((events & IMAP_NOTIFY_EVENT_MAILBOX_NAME) != 0) { + ret |= MAILBOX_LIST_NOTIFY_CREATE | + MAILBOX_LIST_NOTIFY_DELETE | + MAILBOX_LIST_NOTIFY_RENAME; + } + if ((events & IMAP_NOTIFY_EVENT_SUBSCRIPTION_CHANGE) != 0) { + ret |= MAILBOX_LIST_NOTIFY_SUBSCRIBE | + MAILBOX_LIST_NOTIFY_UNSUBSCRIBE; + } + return ret; +} + +static void imap_notify_callback(struct mailbox *box, struct client *client) +{ + struct client_command_context *cmd; + enum mailbox_sync_flags sync_flags = 0; + + i_assert(client->command_queue_size == 0); + i_assert(box == client->mailbox); + + /* create a fake command to handle this */ + cmd = client_command_alloc(client); + cmd->tag = "*"; + cmd->name = "NOTIFY-CALLBACK"; + client_command_init_finished(cmd); + + if (!client->notify_ctx->selected_immediate_expunges) + sync_flags |= MAILBOX_SYNC_FLAG_NO_EXPUNGES; + if (cmd_sync(cmd, sync_flags, 0, NULL)) + i_unreached(); + (void)cmd_sync_delayed(client); +} + +static void imap_notify_watch_selected_mailbox(struct client *client) +{ + i_assert(client->command_queue_size == 0); + + if (client->mailbox == NULL) { + /* mailbox not selected */ + return; + } + if (client->notify_ctx == NULL || !client->notify_ctx->selected_set) { + /* client doesn't want selected mailbox notifications */ + return; + + } + mailbox_notify_changes(client->mailbox, imap_notify_callback, client); + client->notify_ctx->watching_mailbox = TRUE; +} + +static void imap_notify_watch_timeout(struct client *client) +{ + timeout_remove(&client->notify_ctx->to_watch); + imap_notify_watch_selected_mailbox(client); +} + +void imap_client_notify_command_freed(struct client *client) +{ + struct imap_notify_context *ctx = client->notify_ctx; + + if (ctx == NULL) + return; + + if (client->command_queue_size > 0) { + /* don't add it until all commands are finished */ + i_assert(ctx->to_watch == NULL); + return; + } + + /* add mailbox watch back after a small delay. if another command + is started this timeout is aborted. */ + ctx->to_watch = timeout_add(IMAP_NOTIFY_WATCH_ADD_DELAY_MSECS, + imap_notify_watch_timeout, client); +} + +void imap_client_notify_command_allocated(struct client *client) +{ + struct imap_notify_context *ctx = client->notify_ctx; + + if (ctx == NULL) + return; + + /* remove mailbox watcher before starting any commands */ + if (ctx->watching_mailbox) { + mailbox_notify_changes_stop(client->mailbox); + ctx->watching_mailbox = FALSE; + } + timeout_remove(&ctx->to_watch); +} + +int imap_notify_begin(struct imap_notify_context *ctx) +{ + struct imap_notify_namespace *notify_ns; + const struct imap_notify_mailboxes *notify_boxes; + enum mailbox_list_notify_event notify_events; + int ret = -1; + + array_foreach_modifiable(&ctx->namespaces, notify_ns) { + notify_events = 0; + array_foreach(¬ify_ns->mailboxes, notify_boxes) { + notify_events |= + imap_events_to_notify(notify_boxes->events); + } + if (mailbox_list_notify_init(notify_ns->ns->list, notify_events, + ¬ify_ns->notify) < 0) { + /* notifications not supported */ + } else { + ret = 0; + mailbox_list_notify_wait(notify_ns->notify, + notify_callback, notify_ns); + } + } + /* enable NOTIFY as long as even one namespace supports it, + ignore the rest */ + return ret; +} + +void imap_notify_deinit(struct imap_notify_context **_ctx) +{ + struct imap_notify_context *ctx = *_ctx; + struct imap_notify_namespace *notify_ns; + + *_ctx = NULL; + + array_foreach_modifiable(&ctx->namespaces, notify_ns) { + if (notify_ns->notify != NULL) + mailbox_list_notify_deinit(¬ify_ns->notify); + } + timeout_remove(&ctx->to_watch); + if (ctx->fetch_ctx != NULL) + imap_fetch_free(&ctx->fetch_ctx); + pool_unref(&ctx->pool); +} + +void imap_notify_flush(struct imap_notify_context *ctx) +{ + struct imap_notify_namespace *notify_ns; + + array_foreach_modifiable(&ctx->namespaces, notify_ns) { + if (notify_ns->notify != NULL) + mailbox_list_notify_flush(notify_ns->notify); + } +} diff --git a/src/imap/imap-notify.h b/src/imap/imap-notify.h new file mode 100644 index 0000000..936d622 --- /dev/null +++ b/src/imap/imap-notify.h @@ -0,0 +1,75 @@ +#ifndef IMAP_NOTIFY_H +#define IMAP_NOTIFY_H + +enum imap_notify_type { + IMAP_NOTIFY_TYPE_SUBSCRIBED, + IMAP_NOTIFY_TYPE_SUBTREE, + IMAP_NOTIFY_TYPE_MAILBOX +}; + +enum imap_notify_event { + IMAP_NOTIFY_EVENT_MESSAGE_NEW = 0x01, + IMAP_NOTIFY_EVENT_MESSAGE_EXPUNGE = 0x02, + IMAP_NOTIFY_EVENT_FLAG_CHANGE = 0x04, + IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE = 0x08, + IMAP_NOTIFY_EVENT_MAILBOX_NAME = 0x10, + IMAP_NOTIFY_EVENT_SUBSCRIPTION_CHANGE = 0x20, + IMAP_NOTIFY_EVENT_MAILBOX_METADATA_CHANGE = 0x40, + IMAP_NOTIFY_EVENT_SERVER_METADATA_CHANGE = 0x80 +}; +#define UNSUPPORTED_EVENTS \ + (IMAP_NOTIFY_EVENT_ANNOTATION_CHANGE | \ + IMAP_NOTIFY_EVENT_MAILBOX_METADATA_CHANGE | \ + IMAP_NOTIFY_EVENT_SERVER_METADATA_CHANGE) + +struct imap_notify_mailboxes { + enum imap_notify_event events; + enum imap_notify_type type; + ARRAY_TYPE(const_string) names; +}; + +struct imap_notify_namespace { + struct imap_notify_context *ctx; + struct mail_namespace *ns; + + struct mailbox_list_notify *notify; + ARRAY(struct imap_notify_mailboxes) mailboxes; +}; + +struct imap_notify_context { + pool_t pool; + struct client *client; + const char *error; + + ARRAY(struct imap_notify_namespace) namespaces; + enum imap_notify_event selected_events; + enum imap_notify_event global_used_events; + unsigned int global_max_mailbox_names; + + struct imap_fetch_context *fetch_ctx; + struct timeout *to_watch; + + bool have_subscriptions:1; + bool selected_set:1; + bool selected_immediate_expunges:1; + bool send_immediate_status:1; + bool watching_mailbox:1; + bool notifying:1; +}; + +bool imap_notify_match_mailbox(struct imap_notify_namespace *notify_ns, + const struct imap_notify_mailboxes *notify_boxes, + const char *vname); + +int imap_client_notify_newmails(struct client *client); +void imap_client_notify_finished(struct client *client); + +void imap_client_notify_command_allocated(struct client *client); +void imap_client_notify_command_freed(struct client *client); + +int imap_notify_begin(struct imap_notify_context *ctx); +void imap_notify_deinit(struct imap_notify_context **ctx); + +void imap_notify_flush(struct imap_notify_context *ctx); + +#endif diff --git a/src/imap/imap-search-args.c b/src/imap/imap-search-args.c new file mode 100644 index 0000000..67e8679 --- /dev/null +++ b/src/imap/imap-search-args.c @@ -0,0 +1,353 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "mail-storage.h" +#include "mail-search-parser.h" +#include "mail-search-build.h" +#include "imap-search-args.h" +#include "imap-parser.h" +#include "imap-seqset.h" + + +struct search_build_data { + pool_t pool; + struct mailbox *box; + const char *error; +}; + +static bool search_args_have_searchres(struct mail_search_arg *sargs) +{ + for (; sargs != NULL; sargs = sargs->next) { + switch (sargs->type) { + case SEARCH_UIDSET: + if (strcmp(sargs->value.str, "$") == 0) + return TRUE; + break; + case SEARCH_SUB: + case SEARCH_OR: + if (search_args_have_searchres(sargs->value.subargs)) + return TRUE; + break; + default: + break; + } + } + return FALSE; +} + +static void imap_search_saved_uidset_clear(struct client_command_context *cmd) +{ + if (array_is_created(&cmd->client->search_saved_uidset)) + array_clear(&cmd->client->search_saved_uidset); + else + i_array_init(&cmd->client->search_saved_uidset, 128); +} + +int imap_search_args_build(struct client_command_context *cmd, + const struct imap_arg *args, const char *charset, + struct mail_search_args **search_args_r) +{ + struct mail_search_parser *parser; + struct mail_search_args *sargs; + const char *client_error; + int ret; + + if (IMAP_ARG_IS_EOL(args)) { + client_send_command_error(cmd, "Missing search parameters"); + return -1; + } + + parser = mail_search_parser_init_imap(args); + ret = mail_search_build(mail_search_register_get_imap(), + parser, &charset, &sargs, &client_error); + mail_search_parser_deinit(&parser); + if (ret < 0) { + if (charset == NULL) { + if (cmd->search_save_result) + imap_search_saved_uidset_clear(cmd); + client_send_tagline(cmd, t_strconcat( + "NO [BADCHARSET] ", client_error, NULL)); + } else { + client_send_command_error(cmd, client_error); + } + return -1; + } + + if (search_args_have_searchres(sargs->args)) { + if (client_handle_search_save_ambiguity(cmd)) + return 0; + } + + mail_search_args_init(sargs, cmd->client->mailbox, TRUE, + &cmd->client->search_saved_uidset); + if (cmd->search_save_result) { + /* clear the SAVE resultset only after potentially using $ + in the search args themselves */ + imap_search_saved_uidset_clear(cmd); + } + *search_args_r = sargs; + return 1; +} + +static bool +msgset_is_valid(ARRAY_TYPE(seq_range) *seqset, uint32_t messages_count) +{ + const struct seq_range *range; + unsigned int count; + + /* when there are no messages, all messagesets are invalid. + if there's at least one message: + - * gives seq1 = seq2 = (uint32_t)-1 + - n:* should work if n <= messages_count + - n:m or m should work if m <= messages_count + */ + range = array_get(seqset, &count); + if (count == 0 || messages_count == 0) + return FALSE; + + if (range[count-1].seq2 == (uint32_t)-1) { + if (range[count-1].seq1 > messages_count && + range[count-1].seq1 != (uint32_t)-1) + return FALSE; + } else { + if (range[count-1].seq2 > messages_count) + return FALSE; + } + return TRUE; +} + +static int imap_search_get_msgset_arg(struct client_command_context *cmd, + const char *messageset, + struct mail_search_args **args_r, + const char **error_r) +{ + struct mail_search_args *args; + + args = mail_search_build_init(); + args->args = p_new(args->pool, struct mail_search_arg, 1); + args->args->type = SEARCH_SEQSET; + p_array_init(&args->args->value.seqset, args->pool, 16); + if (imap_seq_set_parse(messageset, &args->args->value.seqset) < 0 || + !msgset_is_valid(&args->args->value.seqset, + cmd->client->messages_count)) { + *error_r = "Invalid messageset"; + mail_search_args_unref(&args); + return -1; + } + *args_r = args; + return 0; +} + +static int +imap_search_get_uidset_arg(const char *uidset, struct mail_search_args **args_r, + const char **error_r) +{ + struct mail_search_args *args; + + args = mail_search_build_init(); + args->args = p_new(args->pool, struct mail_search_arg, 1); + args->args->type = SEARCH_UIDSET; + p_array_init(&args->args->value.seqset, args->pool, 16); + if (imap_seq_set_parse(uidset, &args->args->value.seqset) < 0) { + *error_r = "Invalid uidset"; + mail_search_args_unref(&args); + return -1; + } + + *args_r = args; + return 0; +} + +int imap_search_get_seqset(struct client_command_context *cmd, + const char *set, bool uid, + struct mail_search_args **search_args_r) +{ + int ret; + + ret = imap_search_get_anyset(cmd, set, uid, search_args_r); + if (ret > 0) { + mail_search_args_init(*search_args_r, + cmd->client->mailbox, TRUE, + &cmd->client->search_saved_uidset); + } + return ret; +} + +void imap_search_anyset_to_uidset(struct client_command_context *cmd, + struct mail_search_args *args) +{ + struct mail_search_arg *arg = args->args; + + i_assert(arg->next == NULL); + switch (arg->type) { + case SEARCH_ALL: + if (arg->match_not) + break; + t_array_init(&arg->value.seqset, 1); + seq_range_array_add_range(&arg->value.seqset, 1, (uint32_t)-1); + /* fall through */ + case SEARCH_SEQSET: { + ARRAY_TYPE(seq_range) seqs = arg->value.seqset; + + arg->type = SEARCH_UIDSET; + p_array_init(&arg->value.seqset, args->pool, 16); + mailbox_get_uid_range(cmd->client->mailbox, &seqs, + &arg->value.seqset); + break; + } + case SEARCH_UIDSET: + break; + default: + i_unreached(); + } +} + +static int imap_search_get_searchres(struct client_command_context *cmd, + struct mail_search_args **search_args_r) +{ + struct mail_search_args *search_args; + + if (client_handle_search_save_ambiguity(cmd)) + return 0; + + search_args = mail_search_build_init(); + search_args->args = p_new(search_args->pool, struct mail_search_arg, 1); + if (array_is_created(&cmd->client->search_saved_uidset)) { + search_args->args->type = SEARCH_UIDSET; + p_array_init(&search_args->args->value.seqset, + search_args->pool, + array_count(&cmd->client->search_saved_uidset)); + array_append_array(&search_args->args->value.seqset, + &cmd->client->search_saved_uidset); + } else { + /* $ not set yet, match nothing */ + search_args->args->type = SEARCH_ALL; + search_args->args->match_not = TRUE; + } + *search_args_r = search_args; + return 1; +} + +int imap_search_get_anyset(struct client_command_context *cmd, + const char *set, bool uid, + struct mail_search_args **search_args_r) +{ + const char *client_error = NULL; + int ret; + + if (strcmp(set, "$") == 0) { + /* SEARCHRES extension: replace $ with the last saved + search result */ + return imap_search_get_searchres(cmd, search_args_r); + } + if (!uid) { + ret = imap_search_get_msgset_arg(cmd, set, search_args_r, + &client_error); + } else { + ret = imap_search_get_uidset_arg(set, search_args_r, + &client_error); + } + if (ret < 0) { + client_send_command_error(cmd, client_error); + return -1; + } + return 1; +} + +void imap_search_add_changed_since(struct mail_search_args *search_args, + uint64_t modseq) +{ + struct mail_search_arg *search_arg; + + search_arg = p_new(search_args->pool, struct mail_search_arg, 1); + search_arg->type = SEARCH_MODSEQ; + search_arg->value.modseq = + p_new(search_args->pool, struct mail_search_modseq, 1); + search_arg->value.modseq->modseq = modseq + 1; + + search_arg->next = search_args->args->next; + search_args->args->next = search_arg; +} + +struct imap_search_seqset_iter { + struct mail_search_args *search_args; + ARRAY_TYPE(seq_range) seqset_left; + unsigned int batch_size; +}; + +static void imap_search_seqset_next_batch(struct imap_search_seqset_iter *iter) +{ + array_clear(&iter->search_args->args->value.seqset); + seq_range_array_merge_n(&iter->search_args->args->value.seqset, + &iter->seqset_left, iter->batch_size); +} + +struct imap_search_seqset_iter * +imap_search_seqset_iter_init(struct mail_search_args *search_args, + uint32_t messages_count, unsigned int batch_size) +{ + struct imap_search_seqset_iter *iter; + + i_assert(search_args->args->next == NULL); + + iter = i_new(struct imap_search_seqset_iter, 1); + iter->search_args = search_args; + iter->batch_size = batch_size; + mail_search_args_ref(iter->search_args); + + switch (search_args->args->type) { + case SEARCH_SEQSET: + case SEARCH_UIDSET: + break; + case SEARCH_ALL: + if (search_args->args->match_not) { + /* $ used before search result was saved */ + return iter; + } + /* 1:* - convert to seqset */ + search_args->args->type = SEARCH_SEQSET; + p_array_init(&search_args->args->value.seqset, + search_args->pool, 1); + seq_range_array_add_range(&search_args->args->value.seqset, + 1, messages_count); + break; + default: + i_panic("Unexpected search_args type %d", + search_args->args->type); + } + + i_assert(search_args->args->type == SEARCH_SEQSET || + search_args->args->type == SEARCH_UIDSET); + + i_array_init(&iter->seqset_left, + array_count(&search_args->args->value.seqset)); + array_append_array(&iter->seqset_left, &search_args->args->value.seqset); + imap_search_seqset_next_batch(iter); + return iter; +} + +void imap_search_seqset_iter_deinit(struct imap_search_seqset_iter **_iter) +{ + struct imap_search_seqset_iter *iter = *_iter; + + if (iter == NULL) + return; + + mail_search_args_unref(&iter->search_args); + array_free(&iter->seqset_left); + i_free(iter); +} + +bool imap_search_seqset_iter_next(struct imap_search_seqset_iter *iter) +{ + if (!array_is_created(&iter->seqset_left) || + array_count(&iter->seqset_left) == 0) + return FALSE; + + /* remove the last batch of searched mails from seqset_left */ + if (seq_range_array_remove_seq_range(&iter->seqset_left, + &iter->search_args->args->value.seqset) == 0) + i_unreached(); + imap_search_seqset_next_batch(iter); + return array_count(&iter->search_args->args->value.seqset) > 0; +} diff --git a/src/imap/imap-search-args.h b/src/imap/imap-search-args.h new file mode 100644 index 0000000..4bf6c4c --- /dev/null +++ b/src/imap/imap-search-args.h @@ -0,0 +1,47 @@ +#ifndef IMAP_SEARCH_ARGS_H +#define IMAP_SEARCH_ARGS_H + +#include "mail-search.h" + +struct imap_arg; +struct mailbox; +struct client_command_context; + +/* Builds search arguments based on IMAP arguments. Returns -1 if search + arguments are invalid, 0 if we have to wait for unambiguity, + 1 if we can continue. */ +int imap_search_args_build(struct client_command_context *cmd, + const struct imap_arg *args, const char *charset, + struct mail_search_args **search_args_r); + +/* Returns -1 if set is invalid, 0 if we have to wait for unambiguity, + 1 if we were successful. search_args_r is set to contain either a seqset + or uidset. */ +int imap_search_get_anyset(struct client_command_context *cmd, + const char *set, bool uid, + struct mail_search_args **search_args_r); +/* Like imap_search_get_anyset(), but always returns a seqset. */ +int imap_search_get_seqset(struct client_command_context *cmd, + const char *set, bool uid, + struct mail_search_args **search_args_r); +/* Convert search args returned by imap_search_get_anyset() to SEARCH_UIDSET. */ +void imap_search_anyset_to_uidset(struct client_command_context *cmd, + struct mail_search_args *args); + +void imap_search_add_changed_since(struct mail_search_args *search_args, + uint64_t modseq); + +/* Iterate search_args in batches of messages. The search_args itself is + modified each time imap_search_seqset_iter_next() is called. Note that + search_args is expected to come from imap_search_get_anyset(), so it should + have a single parameter containing SEARCH_ALL, SEARCH_SEQSET or + SEARCH_UIDSET. */ +struct imap_search_seqset_iter * +imap_search_seqset_iter_init(struct mail_search_args *search_args, + uint32_t messages_count, unsigned int batch_size); +/* Iterate the next batch. Returns TRUE if the batch was updated, FALSE if + all the batches have been iterated. */ +bool imap_search_seqset_iter_next(struct imap_search_seqset_iter *iter); +void imap_search_seqset_iter_deinit(struct imap_search_seqset_iter **iter); + +#endif diff --git a/src/imap/imap-search.c b/src/imap/imap-search.c new file mode 100644 index 0000000..8b5d660 --- /dev/null +++ b/src/imap/imap-search.c @@ -0,0 +1,612 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "ostream.h" +#include "str.h" +#include "seq-range-array.h" +#include "time-util.h" +#include "imap-resp-code.h" +#include "imap-quote.h" +#include "imap-seqset.h" +#include "imap-util.h" +#include "mail-search-build.h" +#include "imap-fetch.h" +#include "imap-commands.h" +#include "imap-search-args.h" +#include "imap-search.h" + + +static int imap_search_deinit(struct imap_search_context *ctx); + +static int +imap_partial_range_parse(struct imap_search_context *ctx, const char *str) +{ + ctx->partial1 = 0; + ctx->partial2 = 0; + for (; *str >= '0' && *str <= '9'; str++) + ctx->partial1 = ctx->partial1 * 10 + *str-'0'; + if (*str != ':' || ctx->partial1 == 0) + return -1; + for (str++; *str >= '0' && *str <= '9'; str++) + ctx->partial2 = ctx->partial2 * 10 + *str-'0'; + if (*str != '\0' || ctx->partial2 == 0) + return -1; + + if (ctx->partial1 > ctx->partial2) { + uint32_t temp = ctx->partial2; + ctx->partial2 = ctx->partial1; + ctx->partial1 = temp; + } + + return 0; +} + +static bool +search_parse_fetch_att(struct imap_search_context *ctx, + const struct imap_arg *update_args) +{ + const char *client_error; + + ctx->fetch_pool = pool_alloconly_create("search update fetch", 512); + if (imap_fetch_att_list_parse(ctx->cmd->client, ctx->fetch_pool, + update_args, &ctx->fetch_ctx, + &client_error) < 0) { + client_send_command_error(ctx->cmd, t_strconcat( + "SEARCH UPDATE fetch-att: ", client_error, NULL)); + pool_unref(&ctx->fetch_pool); + return FALSE; + } + return TRUE; +} + +static bool +search_parse_return_options(struct imap_search_context *ctx, + const struct imap_arg *args) +{ + struct client_command_context *cmd = ctx->cmd; + const struct imap_arg *update_args; + const char *name, *str; + unsigned int idx; + + while (!IMAP_ARG_IS_EOL(args)) { + if (!imap_arg_get_atom(args, &name)) { + client_send_command_error(cmd, + "SEARCH return options contain non-atoms."); + return FALSE; + } + name = t_str_ucase(name); + args++; + if (strcmp(name, "MIN") == 0) + ctx->return_options |= SEARCH_RETURN_MIN; + else if (strcmp(name, "MAX") == 0) + ctx->return_options |= SEARCH_RETURN_MAX; + else if (strcmp(name, "ALL") == 0) + ctx->return_options |= SEARCH_RETURN_ALL; + else if (strcmp(name, "COUNT") == 0) + ctx->return_options |= SEARCH_RETURN_COUNT; + else if (strcmp(name, "SAVE") == 0) + ctx->return_options |= SEARCH_RETURN_SAVE; + else if (strcmp(name, "CONTEXT") == 0) { + /* no-op */ + } else if (strcmp(name, "UPDATE") == 0) { + if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0) { + client_send_command_error(cmd, + "SEARCH return options have duplicate UPDATE."); + return FALSE; + } + ctx->return_options |= SEARCH_RETURN_UPDATE; + if (imap_arg_get_list(args, &update_args)) { + if (!search_parse_fetch_att(ctx, update_args)) + return FALSE; + args++; + } + } else if (strcmp(name, "RELEVANCY") == 0) + ctx->return_options |= SEARCH_RETURN_RELEVANCY; + else if (strcmp(name, "PARTIAL") == 0) { + if (ctx->partial1 != 0) { + client_send_command_error(cmd, + "PARTIAL can be used only once."); + return FALSE; + } + ctx->return_options |= SEARCH_RETURN_PARTIAL; + if (!imap_arg_get_atom(args, &str)) { + client_send_command_error(cmd, + "PARTIAL range missing."); + return FALSE; + } + if (imap_partial_range_parse(ctx, str) < 0) { + client_send_command_error(cmd, + "PARTIAL range broken."); + return FALSE; + } + args++; + } else { + client_send_command_error(cmd, + "Unknown SEARCH return option"); + return FALSE; + } + } + + if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0 && + client_search_update_lookup(cmd->client, cmd->tag, &idx) != NULL) { + client_send_command_error(cmd, "Duplicate search update tag"); + return FALSE; + } + if ((ctx->return_options & SEARCH_RETURN_PARTIAL) != 0 && + (ctx->return_options & SEARCH_RETURN_ALL) != 0) { + client_send_command_error(cmd, "PARTIAL conflicts with ALL"); + return FALSE; + } + + if (ctx->return_options == 0) + ctx->return_options = SEARCH_RETURN_ALL; + ctx->return_options |= SEARCH_RETURN_ESEARCH; + return TRUE; +} + +static void imap_search_args_check(struct imap_search_context *ctx, + const struct mail_search_arg *sargs) +{ + for (; sargs != NULL; sargs = sargs->next) { + switch (sargs->type) { + case SEARCH_SEQSET: + ctx->have_seqsets = TRUE; + break; + case SEARCH_MODSEQ: + ctx->have_modseqs = TRUE; + break; + case SEARCH_OR: + case SEARCH_SUB: + imap_search_args_check(ctx, sargs->value.subargs); + break; + default: + break; + } + } +} + +static void imap_search_result_save(struct imap_search_context *ctx) +{ + struct client *client = ctx->cmd->client; + struct mail_search_result *result; + struct imap_search_update *update; + + if (!array_is_created(&client->search_updates)) + i_array_init(&client->search_updates, 32); + else if (array_count(&client->search_updates) >= + CLIENT_MAX_SEARCH_UPDATES) { + /* too many updates */ + string_t *str = t_str_new(256); + str_append(str, "* NO [NOUPDATE "); + imap_append_quoted(str, ctx->cmd->tag); + str_append_c(str, ']'); + client_send_line(client, str_c(str)); + ctx->return_options &= ENUM_NEGATE(SEARCH_RETURN_UPDATE); + imap_search_context_free(ctx); + return; + } + result = mailbox_search_result_save(ctx->search_ctx, + MAILBOX_SEARCH_RESULT_FLAG_UPDATE | + MAILBOX_SEARCH_RESULT_FLAG_QUEUE_SYNC); + + update = array_append_space(&client->search_updates); + update->tag = i_strdup(ctx->cmd->tag); + update->result = result; + update->return_uids = ctx->cmd->uid; + update->fetch_pool = ctx->fetch_pool; + update->fetch_ctx = ctx->fetch_ctx; + ctx->fetch_pool = NULL; + ctx->fetch_ctx = NULL; +} + +static void imap_search_send_result_standard(struct imap_search_context *ctx) +{ + const struct seq_range *range; + string_t *str; + uint32_t seq; + + str = t_str_new(1024); + str_append(str, ctx->sorting ? "* SORT" : "* SEARCH"); + array_foreach(&ctx->result, range) { + for (seq = range->seq1; seq <= range->seq2; seq++) + str_printfa(str, " %u", seq); + if (str_len(str) >= 1024-32) { + o_stream_nsend(ctx->cmd->client->output, + str_data(str), str_len(str)); + str_truncate(str, 0); + } + } + + if (ctx->highest_seen_modseq != 0) { + str_printfa(str, " (MODSEQ %"PRIu64")", + ctx->highest_seen_modseq); + } + str_append(str, "\r\n"); + o_stream_nsend(ctx->cmd->client->output, str_data(str), str_len(str)); +} + +static void +imap_search_send_partial(struct imap_search_context *ctx, string_t *str) +{ + str_printfa(str, " PARTIAL (%u:%u ", ctx->partial1, ctx->partial2); + if (array_count(&ctx->result) == 0) { + /* no results (in range) */ + str_append(str, "NIL"); + } else { + imap_write_seq_range(str, &ctx->result); + } + str_append_c(str, ')'); +} + +static void +imap_search_send_relevancy(struct imap_search_context *ctx, string_t *dest) +{ + const float *scores; + unsigned int i, count; + float diff, imap_score; + + scores = array_get(&ctx->relevancy_scores, &count); + if (count == 0) + return; + + /* we'll need to convert float scores to numbers 1..100 + FIXME: would be a good idea to try to detect non-linear score + mappings and convert them better.. */ + diff = ctx->max_relevancy - ctx->min_relevancy; + if (diff == 0) + diff = 1.0; + for (i = 0; i < count; i++) { + if (i > 0) + str_append_c(dest, ' '); + imap_score = (scores[i] - ctx->min_relevancy) / diff * 100.0; + if (imap_score < 1) + str_append(dest, "1"); + else + str_printfa(dest, "%u", (unsigned int)imap_score); + } +} + +static void imap_search_send_result(struct imap_search_context *ctx) +{ + struct client *client = ctx->cmd->client; + string_t *str; + + if ((ctx->return_options & SEARCH_RETURN_ESEARCH) == 0) { + imap_search_send_result_standard(ctx); + return; + } + + if (ctx->return_options == + (SEARCH_RETURN_ESEARCH | SEARCH_RETURN_SAVE)) { + /* we only wanted to save the result, don't return + ESEARCH result. */ + return; + } + + str = str_new(default_pool, 1024); + str_append(str, "* ESEARCH (TAG "); + imap_append_string(str, ctx->cmd->tag); + str_append_c(str, ')'); + + if (ctx->cmd->uid) + str_append(str, " UID"); + + if ((ctx->return_options & SEARCH_RETURN_MIN) != 0 && ctx->min_id != 0) + str_printfa(str, " MIN %u", ctx->min_id); + if ((ctx->return_options & SEARCH_RETURN_MAX) != 0 && + ctx->max_seq != 0) { + uint32_t id = ctx->cmd->uid ? ctx->max_uid : ctx->max_seq; + str_printfa(str, " MAX %u", id); + } + + if ((ctx->return_options & SEARCH_RETURN_ALL) != 0 && + array_count(&ctx->result) > 0) { + str_append(str, " ALL "); + imap_write_seq_range(str, &ctx->result); + } + if ((ctx->return_options & SEARCH_RETURN_RELEVANCY) != 0) { + str_append(str, " RELEVANCY ("); + imap_search_send_relevancy(ctx, str); + str_append_c(str, ')'); + } + + if ((ctx->return_options & SEARCH_RETURN_PARTIAL) != 0) + imap_search_send_partial(ctx, str); + + if ((ctx->return_options & SEARCH_RETURN_COUNT) != 0) + str_printfa(str, " COUNT %u", ctx->result_count); + if (ctx->highest_seen_modseq != 0) { + str_printfa(str, " MODSEQ %"PRIu64, + ctx->highest_seen_modseq); + } + str_append(str, "\r\n"); + o_stream_nsend(client->output, str_data(str), str_len(str)); + str_free(&str); +} + +static void +search_update_mail(struct imap_search_context *ctx, struct mail *mail) +{ + uint64_t modseq; + + if (ctx->max_update_seq == mail->seq) { + /* MIN already handled this mail */ + return; + } + ctx->max_update_seq = mail->seq; + + if ((ctx->return_options & SEARCH_RETURN_MODSEQ) != 0) { + modseq = mail_get_modseq(mail); + if (ctx->highest_seen_modseq < modseq) + ctx->highest_seen_modseq = modseq; + } + if ((ctx->return_options & SEARCH_RETURN_SAVE) != 0) { + seq_range_array_add(&ctx->cmd->client->search_saved_uidset, + mail->uid); + } + if ((ctx->return_options & SEARCH_RETURN_RELEVANCY) != 0) { + const char *str; + float score; + + if (mail_get_special(mail, MAIL_FETCH_SEARCH_RELEVANCY, &str) < 0) + score = 0; + else + score = strtod(str, NULL); + array_push_back(&ctx->relevancy_scores, &score); + if (ctx->min_relevancy > score) + ctx->min_relevancy = score; + if (ctx->max_relevancy < score) + ctx->max_relevancy = score; + } +} + +static void search_add_result_id(struct imap_search_context *ctx, uint32_t id) +{ + struct seq_range *range; + unsigned int count; + + /* only append the data. this is especially important when we're + returning a sort result. */ + range = array_get_modifiable(&ctx->result, &count); + if (count > 0 && id == range[count-1].seq2 + 1) { + range[count-1].seq2++; + } else { + range = array_append_space(&ctx->result); + range->seq1 = range->seq2 = id; + } +} + +static bool cmd_search_more(struct client_command_context *cmd) +{ + struct imap_search_context *ctx = cmd->context; + enum search_return_options opts = ctx->return_options; + struct mail *mail; + enum mailbox_sync_flags sync_flags; + uint32_t id; + const char *ok_reply; + bool tryagain, lost_data; + + if (cmd->cancel) { + (void)imap_search_deinit(ctx); + return TRUE; + } + + while (mailbox_search_next_nonblock(ctx->search_ctx, + &mail, &tryagain)) { + id = cmd->uid ? mail->uid : mail->seq; + ctx->result_count++; + + ctx->max_seq = mail->seq; + ctx->max_uid = mail->uid; + if (HAS_ANY_BITS(opts, SEARCH_RETURN_MIN) && ctx->min_id == 0) { + /* MIN not set yet */ + ctx->min_id = id; + search_update_mail(ctx, mail); + } + if (HAS_ANY_BITS(opts, SEARCH_RETURN_ALL)) { + /* ALL and PARTIAL are mutually exclusive */ + i_assert(HAS_NO_BITS(opts, SEARCH_RETURN_PARTIAL)); + search_add_result_id(ctx, id); + } else if ((opts & SEARCH_RETURN_PARTIAL) != 0) { + /* only update if it's within range */ + i_assert(HAS_NO_BITS(opts, SEARCH_RETURN_ALL)); + if (ctx->partial1 <= ctx->result_count && + ctx->partial2 >= ctx->result_count) + search_add_result_id(ctx, id); + else if (HAS_ALL_BITS(opts, SEARCH_RETURN_COUNT | + SEARCH_RETURN_SAVE)) { + /* (SAVE COUNT PARTIAL n:m) must include all + results in SAVE, but not include mails + outside the PARTIAL range in MODSEQ or + RELEVANCY */ + seq_range_array_add(&cmd->client->search_saved_uidset, + mail->uid); + continue; + } else { + continue; + } + } else if (HAS_ANY_BITS(opts, SEARCH_RETURN_COUNT)) { + /* with COUNT don't add it to results, but handle + SAVE and MODSEQ */ + } else if (HAS_ANY_BITS(opts, SEARCH_RETURN_MIN | + SEARCH_RETURN_MAX)) { + /* MIN and/or MAX only requested, but we don't know if + this is MAX until the search is finished. */ + continue; + } else if (HAS_ANY_BITS(opts, SEARCH_RETURN_SAVE)) { + /* Only SAVE used */ + } + search_update_mail(ctx, mail); + } + if (tryagain) + return FALSE; + + if ((opts & SEARCH_RETURN_MAX) != 0 && ctx->max_seq != 0 && + ctx->max_update_seq != ctx->max_seq && + HAS_ANY_BITS(opts, SEARCH_RETURN_MODSEQ | + SEARCH_RETURN_SAVE | SEARCH_RETURN_RELEVANCY)) { + /* finish handling MAX */ + mail = mail_alloc(ctx->trans, 0, NULL); + mail_set_seq(mail, ctx->max_seq); + search_update_mail(ctx, mail); + mail_free(&mail); + } + + lost_data = mailbox_search_seen_lost_data(ctx->search_ctx); + if (imap_search_deinit(ctx) < 0) { + client_send_box_error(cmd, cmd->client->mailbox); + return TRUE; + } + + sync_flags = MAILBOX_SYNC_FLAG_FAST; + if (!cmd->uid || ctx->have_seqsets) + sync_flags |= MAILBOX_SYNC_FLAG_NO_EXPUNGES; + ok_reply = t_strdup_printf("OK %s%s completed", + lost_data ? "["IMAP_RESP_CODE_EXPUNGEISSUED"] " : "", + !ctx->sorting ? "Search" : "Sort"); + return cmd_sync(cmd, sync_flags, 0, ok_reply); +} + +static void cmd_search_more_callback(struct client_command_context *cmd) +{ + struct client *client = cmd->client; + bool finished; + + o_stream_cork(client->output); + finished = command_exec(cmd); + o_stream_uncork(client->output); + + if (!finished) + (void)client_handle_unfinished_cmd(cmd); + else + client_command_free(&cmd); + cmd_sync_delayed(client); + + client_continue_pending_input(client); +} + +int cmd_search_parse_return_if_found(struct imap_search_context *ctx, + const struct imap_arg **_args) +{ + const struct imap_arg *list_args, *args = *_args; + struct client_command_context *cmd = ctx->cmd; + + if (!imap_arg_atom_equals(&args[0], "RETURN") || + !imap_arg_get_list(&args[1], &list_args)) { + ctx->return_options = SEARCH_RETURN_ALL; + return 1; + } + + if (!search_parse_return_options(ctx, list_args)) { + imap_search_context_free(ctx); + return -1; + } + + if ((ctx->return_options & SEARCH_RETURN_SAVE) != 0) { + /* wait if there is another SEARCH SAVE command running. */ + if (client_handle_search_save_ambiguity(cmd)) { + imap_search_context_free(ctx); + return 0; + } + + cmd->search_save_result = TRUE; + } + + *_args = args + 2; + return 1; +} + +bool imap_search_start(struct imap_search_context *ctx, + struct mail_search_args *sargs, + const enum mail_sort_type *sort_program) +{ + struct client_command_context *cmd = ctx->cmd; + + imap_search_args_check(ctx, sargs->args); + + if (ctx->have_modseqs) { + ctx->return_options |= SEARCH_RETURN_MODSEQ; + client_enable(cmd->client, imap_feature_condstore); + } + + ctx->box = cmd->client->mailbox; + ctx->trans = mailbox_transaction_begin(ctx->box, 0, + imap_client_command_get_reason(cmd)); + ctx->sargs = sargs; + ctx->search_ctx = + mailbox_search_init(ctx->trans, sargs, sort_program, 0, NULL); + ctx->sorting = sort_program != NULL; + i_array_init(&ctx->result, 128); + if ((ctx->return_options & SEARCH_RETURN_UPDATE) != 0) + imap_search_result_save(ctx); + else { + i_assert(ctx->fetch_ctx == NULL); + } + if ((ctx->return_options & SEARCH_RETURN_RELEVANCY) != 0) + i_array_init(&ctx->relevancy_scores, 128); + + cmd->func = cmd_search_more; + cmd->context = ctx; + + if (cmd_search_more(cmd)) + return TRUE; + + /* we may have moved onto syncing by now */ + if (cmd->func == cmd_search_more) { + ctx->to = timeout_add(0, cmd_search_more_callback, cmd); + cmd->state = CLIENT_COMMAND_STATE_WAIT_EXTERNAL; + } + return FALSE; +} + +static int imap_search_deinit(struct imap_search_context *ctx) +{ + int ret = 0; + + if (mailbox_search_deinit(&ctx->search_ctx) < 0) + ret = -1; + + /* Send the result also after failing. It might have something useful, + even though it didn't fully succeed. The client should be able to + realize that there was some failure because NO is returned. */ + if (!ctx->cmd->cancel && + (ret == 0 || array_count(&ctx->result) > 0)) + imap_search_send_result(ctx); + + if (ret < 0 || ctx->cmd->cancel) { + /* search failed */ + if ((ctx->return_options & SEARCH_RETURN_SAVE) != 0) + array_clear(&ctx->cmd->client->search_saved_uidset); + } + + (void)mailbox_transaction_commit(&ctx->trans); + + timeout_remove(&ctx->to); + if (array_is_created(&ctx->relevancy_scores)) + array_free(&ctx->relevancy_scores); + array_free(&ctx->result); + mail_search_args_deinit(ctx->sargs); + mail_search_args_unref(&ctx->sargs); + imap_search_context_free(ctx); + + ctx->cmd->context = NULL; + return ret; +} + +void imap_search_context_free(struct imap_search_context *ctx) +{ + if (ctx->fetch_ctx != NULL) { + imap_fetch_free(&ctx->fetch_ctx); + pool_unref(&ctx->fetch_pool); + } +} + +void imap_search_update_free(struct imap_search_update *update) +{ + if (update->fetch_ctx != NULL) { + imap_fetch_free(&update->fetch_ctx); + pool_unref(&update->fetch_pool); + } + mailbox_search_result_free(&update->result); + i_free(update->tag); +} diff --git a/src/imap/imap-search.h b/src/imap/imap-search.h new file mode 100644 index 0000000..f665375 --- /dev/null +++ b/src/imap/imap-search.h @@ -0,0 +1,61 @@ +#ifndef IMAP_SEARCH_H +#define IMAP_SEARCH_H + +#include <sys/time.h> + +enum search_return_options { + SEARCH_RETURN_ESEARCH = 0x0001, + SEARCH_RETURN_MIN = 0x0002, + SEARCH_RETURN_MAX = 0x0004, + SEARCH_RETURN_ALL = 0x0008, + SEARCH_RETURN_COUNT = 0x0010, + SEARCH_RETURN_MODSEQ = 0x0020, + SEARCH_RETURN_SAVE = 0x0040, + SEARCH_RETURN_UPDATE = 0x0080, + SEARCH_RETURN_PARTIAL = 0x0100, + SEARCH_RETURN_RELEVANCY = 0x0200 +/* Options that don't return any seq/uid results, and also don't affect + SEARCHRES $ when combined with MIN/MAX. */ +#define SEARCH_RETURN_NORESULTS \ + (SEARCH_RETURN_ESEARCH | SEARCH_RETURN_MODSEQ | SEARCH_RETURN_SAVE | \ + SEARCH_RETURN_UPDATE | SEARCH_RETURN_RELEVANCY) +}; + +struct imap_search_context { + struct client_command_context *cmd; + struct mailbox *box; + struct mailbox_transaction_context *trans; + struct mail_search_context *search_ctx; + + pool_t fetch_pool; + struct imap_fetch_context *fetch_ctx; + + struct mail_search_args *sargs; + enum search_return_options return_options; + uint32_t partial1, partial2; + + struct timeout *to; + ARRAY_TYPE(seq_range) result; + unsigned int result_count; + uint32_t min_id, max_update_seq, max_seq, max_uid; + + ARRAY(float) relevancy_scores; + float min_relevancy, max_relevancy; + + uint64_t highest_seen_modseq; + + bool have_seqsets:1; + bool have_modseqs:1; + bool sorting:1; +}; + +int cmd_search_parse_return_if_found(struct imap_search_context *ctx, + const struct imap_arg **args); +void imap_search_context_free(struct imap_search_context *ctx); + +bool imap_search_start(struct imap_search_context *ctx, + struct mail_search_args *sargs, + const enum mail_sort_type *sort_program) ATTR_NULL(3); +void imap_search_update_free(struct imap_search_update *update); + +#endif diff --git a/src/imap/imap-settings.c b/src/imap/imap-settings.c new file mode 100644 index 0000000..2acf9dd --- /dev/null +++ b/src/imap/imap-settings.c @@ -0,0 +1,197 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "buffer.h" +#include "hostpid.h" +#include "settings-parser.h" +#include "service-settings.h" +#include "mail-storage-settings.h" +#include "smtp-submit-settings.h" +#include "imap-settings.h" + +#include <stddef.h> +#include <unistd.h> + +static bool imap_settings_verify(void *_set, pool_t pool, + const char **error_r); + +/* <settings checks> */ +static struct file_listener_settings imap_unix_listeners_array[] = { + { "login/imap", 0666, "", "" }, + { "imap-master", 0600, "", "" } +}; +static struct file_listener_settings *imap_unix_listeners[] = { + &imap_unix_listeners_array[0], + &imap_unix_listeners_array[1] +}; +static buffer_t imap_unix_listeners_buf = { + { { imap_unix_listeners, sizeof(imap_unix_listeners) } } +}; +/* </settings checks> */ + +struct service_settings imap_service_settings = { + .name = "imap", + .protocol = "imap", + .type = "", + .executable = "imap", + .user = "", + .group = "", + .privileged_group = "", + .extra_groups = "$default_internal_group", + .chroot = "", + + .drop_priv_before_exec = FALSE, + + .process_min_avail = 0, + .process_limit = 1024, + .client_limit = 1, + .service_count = 1, + .idle_kill = 0, + .vsz_limit = UOFF_T_MAX, + + .unix_listeners = { { &imap_unix_listeners_buf, + sizeof(imap_unix_listeners[0]) } }, + .fifo_listeners = ARRAY_INIT, + .inet_listeners = ARRAY_INIT +}; + +#undef DEF +#undef DEFLIST +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct imap_settings) +#define DEFLIST(field, name, defines) \ + { .type = SET_DEFLIST, .key = name, \ + .offset = offsetof(struct imap_settings, field), \ + .list_info = defines } + +static const struct setting_define imap_setting_defines[] = { + DEF(BOOL, verbose_proctitle), + DEF(STR_VARS, rawlog_dir), + + DEF(SIZE, imap_max_line_length), + DEF(TIME, imap_idle_notify_interval), + DEF(STR, imap_capability), + DEF(STR, imap_client_workarounds), + DEF(STR, imap_logout_format), + DEF(STR, imap_id_send), + DEF(STR, imap_id_log), + DEF(ENUM, imap_fetch_failure), + DEF(BOOL, imap_metadata), + DEF(BOOL, imap_literal_minus), + DEF(TIME, imap_hibernate_timeout), + + DEF(STR, imap_urlauth_host), + DEF(IN_PORT, imap_urlauth_port), + + SETTING_DEFINE_LIST_END +}; + +static const struct imap_settings imap_default_settings = { + .verbose_proctitle = FALSE, + .rawlog_dir = "", + + /* RFC-2683 recommends at least 8000 bytes. Some clients however don't + break large message sets to multiple commands, so we're pretty + liberal by default. */ + .imap_max_line_length = 64*1024, + .imap_idle_notify_interval = 2*60, + .imap_capability = "", + .imap_client_workarounds = "", + .imap_logout_format = "in=%i out=%o deleted=%{deleted} " + "expunged=%{expunged} trashed=%{trashed} " + "hdr_count=%{fetch_hdr_count} hdr_bytes=%{fetch_hdr_bytes} " + "body_count=%{fetch_body_count} body_bytes=%{fetch_body_bytes}", + .imap_id_send = "name *", + .imap_id_log = "", + .imap_fetch_failure = "disconnect-immediately:disconnect-after:no-after", + .imap_metadata = FALSE, + .imap_literal_minus = FALSE, + .imap_hibernate_timeout = 0, + + .imap_urlauth_host = "", + .imap_urlauth_port = 143 +}; + +static const struct setting_parser_info *imap_setting_dependencies[] = { + &mail_user_setting_parser_info, + &smtp_submit_setting_parser_info, + NULL +}; + +const struct setting_parser_info imap_setting_parser_info = { + .module_name = "imap", + .defines = imap_setting_defines, + .defaults = &imap_default_settings, + + .type_offset = SIZE_MAX, + .struct_size = sizeof(struct imap_settings), + + .parent_offset = SIZE_MAX, + + .check_func = imap_settings_verify, + .dependencies = imap_setting_dependencies +}; + +/* <settings checks> */ +struct imap_client_workaround_list { + const char *name; + enum imap_client_workarounds num; +}; + +static const struct imap_client_workaround_list imap_client_workaround_list[] = { + { "delay-newmail", WORKAROUND_DELAY_NEWMAIL }, + { "tb-extra-mailbox-sep", WORKAROUND_TB_EXTRA_MAILBOX_SEP }, + { "tb-lsub-flags", WORKAROUND_TB_LSUB_FLAGS }, + { NULL, 0 } +}; + +static int +imap_settings_parse_workarounds(struct imap_settings *set, + const char **error_r) +{ + enum imap_client_workarounds client_workarounds = 0; + const struct imap_client_workaround_list *list; + const char *const *str; + + str = t_strsplit_spaces(set->imap_client_workarounds, " ,"); + for (; *str != NULL; str++) { + list = imap_client_workaround_list; + for (; list->name != NULL; list++) { + if (strcasecmp(*str, list->name) == 0) { + client_workarounds |= list->num; + break; + } + } + if (list->name == NULL) { + *error_r = t_strdup_printf("imap_client_workarounds: " + "Unknown workaround: %s", *str); + return -1; + } + } + set->parsed_workarounds = client_workarounds; + return 0; +} + + +static bool +imap_settings_verify(void *_set, pool_t pool ATTR_UNUSED, const char **error_r) +{ + struct imap_settings *set = _set; + + if (imap_settings_parse_workarounds(set, error_r) < 0) + return FALSE; + + if (strcmp(set->imap_fetch_failure, "disconnect-immediately") == 0) + set->parsed_fetch_failure = IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_IMMEDIATELY; + else if (strcmp(set->imap_fetch_failure, "disconnect-after") == 0) + set->parsed_fetch_failure = IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_AFTER; + else if (strcmp(set->imap_fetch_failure, "no-after") == 0) + set->parsed_fetch_failure = IMAP_CLIENT_FETCH_FAILURE_NO_AFTER; + else { + *error_r = t_strdup_printf("Unknown imap_fetch_failure: %s", + set->imap_fetch_failure); + return FALSE; + } + return TRUE; +} +/* </settings checks> */ diff --git a/src/imap/imap-settings.h b/src/imap/imap-settings.h new file mode 100644 index 0000000..f4ff7bc --- /dev/null +++ b/src/imap/imap-settings.h @@ -0,0 +1,49 @@ +#ifndef IMAP_SETTINGS_H +#define IMAP_SETTINGS_H + +#include "net.h" + +struct mail_user_settings; + +/* <settings checks> */ +enum imap_client_workarounds { + WORKAROUND_DELAY_NEWMAIL = 0x01, + WORKAROUND_TB_EXTRA_MAILBOX_SEP = 0x08, + WORKAROUND_TB_LSUB_FLAGS = 0x10 +}; + +enum imap_client_fetch_failure { + IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_IMMEDIATELY, + IMAP_CLIENT_FETCH_FAILURE_DISCONNECT_AFTER, + IMAP_CLIENT_FETCH_FAILURE_NO_AFTER, +}; +/* </settings checks> */ + +struct imap_settings { + bool verbose_proctitle; + const char *rawlog_dir; + + /* imap: */ + uoff_t imap_max_line_length; + unsigned int imap_idle_notify_interval; + const char *imap_capability; + const char *imap_client_workarounds; + const char *imap_logout_format; + const char *imap_id_send; + const char *imap_id_log; + const char *imap_fetch_failure; + bool imap_metadata; + bool imap_literal_minus; + unsigned int imap_hibernate_timeout; + + /* imap urlauth: */ + const char *imap_urlauth_host; + in_port_t imap_urlauth_port; + + enum imap_client_workarounds parsed_workarounds; + enum imap_client_fetch_failure parsed_fetch_failure; +}; + +extern const struct setting_parser_info imap_setting_parser_info; + +#endif diff --git a/src/imap/imap-state.c b/src/imap/imap-state.c new file mode 100644 index 0000000..2b064ec --- /dev/null +++ b/src/imap/imap-state.c @@ -0,0 +1,897 @@ +/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "crc32.h" +#include "numpack.h" +#include "net.h" +#include "ostream.h" +#include "str.h" +#include "str-sanitize.h" +#include "imap-util.h" +#include "mail-search-build.h" +#include "mail-storage-private.h" +#include "mailbox-recent-flags.h" +#include "imap-client.h" +#include "imap-feature.h" +#include "imap-fetch.h" +#include "imap-search-args.h" +#include "imap-state.h" + +enum imap_state_type_public { + IMAP_STATE_TYPE_MAILBOX = 'B', + IMAP_STATE_TYPE_ENABLED_FEATURE = 'F', + IMAP_STATE_TYPE_SEARCHRES = '1', +}; + +enum imap_state_type_internal { + IMAP_STATE_TYPE_ID_LOGGED = 'I', + IMAP_STATE_TYPE_TLS_COMPRESSION = 'C', +}; + +struct mailbox_import_state { + const char *vname; + guid_128_t mailbox_guid; + bool examined; + uint32_t keywords_count, keywords_crc32, uids_crc32; + uint32_t uidvalidity, uidnext, messages; + uint64_t highest_modseq; + ARRAY_TYPE(seq_range) recent_uids; +}; + +static void +export_seq_range(buffer_t *dest, const ARRAY_TYPE(seq_range) *range) +{ + const struct seq_range *uids; + unsigned int i, count; + uint32_t next_uid; + + uids = array_get(range, &count); + numpack_encode(dest, count); + next_uid = 1; + for (i = 0; i < count; i++) { + i_assert(uids[i].seq1 >= next_uid); + if (uids[i].seq1 == uids[i].seq2) { + numpack_encode(dest, (uids[i].seq1 - next_uid) << 1); + } else { + numpack_encode(dest, 1 | ((uids[i].seq1 - next_uid) << 1)); + numpack_encode(dest, uids[i].seq2 - uids[i].seq1 - 1); + } + next_uid = uids[i].seq2 + 1; + } +} + +static int +import_seq_range(const unsigned char **data, const unsigned char *end, + ARRAY_TYPE(seq_range) *range) +{ + uint32_t i, count, next_uid, num, uid1, uid2; + + if (numpack_decode32(data, end, &count) < 0) + return -1; + next_uid = 1; + + for (i = 0; i < count; i++) { + if (numpack_decode32(data, end, &num) < 0) + return -1; + uid1 = next_uid + (num >> 1); + if ((num & 1) == 0) { + uid2 = uid1; + seq_range_array_add(range, uid1); + } else { + if (numpack_decode32(data, end, &num) < 0) + return -1; + uid2 = uid1 + num + 1; + seq_range_array_add_range(range, uid1, uid2); + } + next_uid = uid2 + 1; + } + return 0; +} + +int imap_state_export_internal(struct client *client, buffer_t *dest, + const char **error_r) +{ + /* the only IMAP command we allow running is IDLE or X-STATE */ + if (client->command_queue_size > 1) { + *error_r = "Multiple commands in progress"; + return 0; + } + if (client->command_queue == NULL || + strcasecmp(client->command_queue->name, "IDLE") != 0) { + /* this would require saving the seq <-> uid mapping + and restore it on import. quite a lot of trouble if + messages have been expunged in the mean time. */ + *error_r = "Non-IDLE connections not supported currently"; + return 0; + } + return client->v.state_export(client, TRUE, dest, error_r); +} + +int imap_state_export_external(struct client *client, buffer_t *dest, + const char **error_r) +{ + if (client->command_queue_size > 1) { + *error_r = "Multiple commands in progress"; + return 0; + } + + i_assert(client->command_queue_size == 1); + i_assert(strcmp(client->command_queue->name, "X-STATE") == 0); + return client->v.state_export(client, FALSE, dest, error_r); +} + +static int +imap_state_import(struct client *client, bool internal, + const unsigned char *data, size_t size, const char **error_r) +{ + ssize_t ret; + + while (size > 0) { + ret = client->v.state_import(client, internal, + data, size, error_r); + if (ret <= 0) { + i_assert(*error_r != NULL); + return ret < 0 ? -1 : 0; + } + i_assert((size_t)ret <= size); + data += ret; + size -= ret; + } + return 1; +} + +int imap_state_import_internal(struct client *client, + const unsigned char *data, size_t size, + const char **error_r) +{ + return imap_state_import(client, TRUE, data, size, error_r); +} + +int imap_state_import_external(struct client *client, + const unsigned char *data, size_t size, + const char **error_r) +{ + return imap_state_import(client, FALSE, data, size, error_r); +} + +static int +imap_state_export_mailbox_mails(buffer_t *dest, struct mailbox *box, + const char **error_r) +{ + struct mailbox_transaction_context *trans; + struct mail_search_args *search_args; + struct mail_search_context *search_ctx; + struct mail *mail; + ARRAY_TYPE(seq_range) recent_uids; + uint32_t crc = 0; + int ret = 1; + + search_args = mail_search_build_init(); + mail_search_build_add_all(search_args); + + trans = mailbox_transaction_begin(box, 0, + "unhibernate imap_state_export_mailbox_mails"); + search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL); + mail_search_args_unref(&search_args); + + t_array_init(&recent_uids, 8); + while (mailbox_search_next(search_ctx, &mail)) { + crc = crc32_data_more(crc, &mail->uid, sizeof(mail->uid)); + if ((mail_get_flags(mail) & MAIL_RECENT) != 0) + seq_range_array_add(&recent_uids, mail->uid); + } + if (mailbox_search_deinit(&search_ctx) < 0) { + *error_r = mailbox_get_last_internal_error(box, NULL); + ret = -1; + } + (void)mailbox_transaction_commit(&trans); + + numpack_encode(dest, crc); + export_seq_range(dest, &recent_uids); + return ret; +} + +static uint32_t +mailbox_status_keywords_crc32(const struct mailbox_status *status) +{ + const char *str; + uint32_t crc = 0; + + array_foreach_elem(status->keywords, str) + crc = crc32_str(str); + return crc; +} + +static int +imap_state_export_mailbox(buffer_t *dest, struct client *client, + struct mailbox *box, const char **error_r) +{ + struct mailbox_status status; + struct mailbox_metadata metadata; + const char *vname = mailbox_get_vname(box); + enum mail_error mail_error; + + mailbox_get_open_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT | + STATUS_MESSAGES | STATUS_HIGHESTMODSEQ | + STATUS_KEYWORDS, + &status); + if (status.nonpermanent_modseqs) { + *error_r = "Nonpermanent modseqs"; + return 0; + } + + if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) { + *error_r = mailbox_get_last_internal_error(box, &mail_error); + /* if the selected mailbox can't have a GUID, fail silently */ + return mail_error == MAIL_ERROR_NOTPOSSIBLE ? 0 : -1; + } + + buffer_append_c(dest, IMAP_STATE_TYPE_MAILBOX); + buffer_append(dest, vname, strlen(vname)+1); + buffer_append(dest, metadata.guid, sizeof(metadata.guid)); + + buffer_append_c(dest, client->mailbox_examined ? 1 : 0); + numpack_encode(dest, status.uidvalidity); + numpack_encode(dest, status.uidnext); + numpack_encode(dest, status.messages); + if (client_has_enabled(client, imap_feature_qresync) && + !client->nonpermanent_modseqs) + numpack_encode(dest, client->sync_last_full_modseq); + else + numpack_encode(dest, status.highest_modseq); + + /* keywords count + CRC32 should be enough to figure out if it + needs to be resent */ + numpack_encode(dest, array_count(status.keywords)); + numpack_encode(dest, mailbox_status_keywords_crc32(&status)); + + /* we're now basically done, but just in case there's a bug add a + checksum of the currently existing UIDs and verify it when + importing. this also writes the list of recent UIDs. */ + return imap_state_export_mailbox_mails(dest, box, error_r); +} + +int imap_state_export_base(struct client *client, bool internal, + buffer_t *dest, const char **error_r) +{ + int ret; + + str_append(dest, "base\n"); + if (array_is_created(&client->search_updates) && + array_count(&client->search_updates) > 0) { + /* these could be tricky */ + *error_r = "CONTEXT=SEARCH updates not supported currently"; + return 0; + } + if (client->notify_ctx != NULL) { + /* FIXME: this really should be supported. also IDLE wouldn't + be needed if NOTIFY allows sending EXPUNGEs to selected + mailbox. */ + *error_r = "NOTIFY not supported currently"; + return 0; + } + + if (client->mailbox != NULL) { + ret = imap_state_export_mailbox(dest, client, + client->mailbox, error_r); + if (ret <= 0) + return ret; + } + + /* IMAP features */ + const char *const *features = client_enabled_features(client); + if (features != NULL) { + for (unsigned int i = 0; features[i] != NULL; i++) { + buffer_append_c(dest, IMAP_STATE_TYPE_ENABLED_FEATURE); + buffer_append(dest, features[i], strlen(features[i])+1); + } + } + if (internal) { + if (client->id_logged) + buffer_append_c(dest, IMAP_STATE_TYPE_ID_LOGGED); + if (client->tls_compression) + buffer_append_c(dest, IMAP_STATE_TYPE_TLS_COMPRESSION); + } + + /* IMAP SEARCHRES extension */ + if (array_is_created(&client->search_saved_uidset) && + array_count(&client->search_saved_uidset) > 0) { + buffer_append_c(dest, IMAP_STATE_TYPE_SEARCHRES); + export_seq_range(dest, &client->search_saved_uidset); + } + return 1; +} + +static int +import_string(const unsigned char **data, const unsigned char *end, + const char **str_r) +{ + const unsigned char *p; + + p = memchr(*data, '\0', end - *data); + if (p == NULL) + return -1; + *str_r = (const void *)*data; + *data = p + 1; + return 0; +} + +static int +import_send_expunges(struct client *client, + const struct mailbox_import_state *state, + unsigned int *expunge_count_r, + const char **error_r) +{ + struct mailbox_transaction_context *trans; + struct mail_search_args *search_args; + struct mail_search_context *search_ctx; + struct mail *mail; + uint32_t crc = 0, seq, expunged_uid; + ARRAY_TYPE(seq_range) uids_filter, expunged_uids; + ARRAY_TYPE(uint32_t) expunged_seqs; + struct seq_range_iter iter; + const uint32_t *seqs; + unsigned int i, expunge_count, n = 0; + string_t *str; + int ret = 0; + + *expunge_count_r = 0; + + if (state->messages == 0) { + /* the mailbox was empty originally - there couldn't be any + pending expunges. */ + return 0; + } + if (state->uidnext <= 1) { + *error_r = "Invalid UIDNEXT"; + return -1; + } + + /* get all the message UIDs expunged since the last known modseq */ + t_array_init(&uids_filter, 1); + t_array_init(&expunged_uids, 128); + seq_range_array_add_range(&uids_filter, 1, state->uidnext-1); + if (!mailbox_get_expunged_uids(client->mailbox, state->highest_modseq, + &uids_filter, &expunged_uids)) { + *error_r = t_strdup_printf( + "Couldn't get recently expunged UIDs " + "(uidnext=%u highest_modseq=%"PRIu64")", + state->uidnext, state->highest_modseq); + return -1; + } + seq_range_array_iter_init(&iter, &expunged_uids); + + search_args = mail_search_build_init(); + mail_search_build_add_all(search_args); + + trans = mailbox_transaction_begin(client->mailbox, 0, + "unhibernate import_send_expunges"); + search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL); + mail_search_args_unref(&search_args); + + /* find sequence numbers for the expunged UIDs */ + t_array_init(&expunged_seqs, array_count(&expunged_uids)+1); seq = 0; + while (mailbox_search_next(search_ctx, &mail)) { + while (seq_range_array_iter_nth(&iter, n, &expunged_uid) && + expunged_uid < mail->uid && seq < state->messages) { + seq++; n++; + array_push_back(&expunged_seqs, &seq); + crc = crc32_data_more(crc, &expunged_uid, + sizeof(expunged_uid)); + } + if (seq == state->messages) + break; + crc = crc32_data_more(crc, &mail->uid, sizeof(mail->uid)); + if (++seq == state->messages) + break; + } + while (seq_range_array_iter_nth(&iter, n, &expunged_uid) && + seq < state->messages) { + seq++; n++; + array_push_back(&expunged_seqs, &seq); + crc = crc32_data_more(crc, &expunged_uid, + sizeof(expunged_uid)); + } + + if (mailbox_search_deinit(&search_ctx) < 0) { + *error_r = mailbox_get_last_internal_error(client->mailbox, NULL); + ret = -1; + } else if (seq != state->messages) { + *error_r = t_strdup_printf("Message count mismatch after " + "handling expunges (%u != %u)", + seq, state->messages); + ret = -1; + } + (void)mailbox_transaction_commit(&trans); + if (ret < 0) + return -1; + + seqs = array_get(&expunged_seqs, &expunge_count); + if (client->messages_count + expunge_count < state->messages) { + *error_r = t_strdup_printf("Message count too low after " + "handling expunges (%u < %u)", + client->messages_count + expunge_count, + state->messages); + return -1; + } + if (crc != state->uids_crc32) { + *error_r = t_strdup_printf("Message UIDs CRC32 mismatch (%u != %u)", + crc, state->uids_crc32); + return -1; + } + + if (!client_has_enabled(client, imap_feature_qresync)) { + str = t_str_new(32); + for (i = expunge_count; i > 0; i--) { + str_truncate(str, 0); + str_printfa(str, "* %u EXPUNGE", seqs[i-1]); + client_send_line(client, str_c(str)); + } + } else { + str = str_new(default_pool, 128); + str_append(str, "* VANISHED "); + imap_write_seq_range(str, &expunged_uids); + str_append(str, "\r\n"); + o_stream_nsend(client->output, str_data(str), str_len(str)); + str_free(&str); + } + *expunge_count_r = expunge_count; + return 0; +} + +static int +import_send_flag_changes(struct client *client, + const struct mailbox_import_state *state, + unsigned int *flag_change_count_r) +{ + struct imap_fetch_context *fetch_ctx; + struct mail_search_args *search_args; + ARRAY_TYPE(seq_range) old_uids; + pool_t pool; + int ret; + + *flag_change_count_r = 0; + if (state->messages == 0) + return 0; + + t_array_init(&old_uids, 1); + seq_range_array_add_range(&old_uids, 1, state->uidnext-1); + + search_args = mail_search_build_init(); + search_args->args = p_new(search_args->pool, struct mail_search_arg, 1); + search_args->args->type = SEARCH_UIDSET; + search_args->args->value.seqset = old_uids; + imap_search_add_changed_since(search_args, state->highest_modseq); + + pool = pool_alloconly_create("imap state flag changes", 1024); + fetch_ctx = imap_fetch_alloc(client, pool, "unhibernate"); + pool_unref(&pool); + + imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_flags_init); + if (client_has_enabled(client, imap_feature_qresync)) { + imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_uid_init); + imap_fetch_init_nofail_handler(fetch_ctx, imap_fetch_modseq_init); + } + + imap_fetch_begin(fetch_ctx, client->mailbox, search_args); + mail_search_args_unref(&search_args); + + /* FIXME: ideally do this asynchronously.. */ + while (imap_fetch_more_no_lock_update(fetch_ctx) == 0) ; + + ret = imap_fetch_end(fetch_ctx); + *flag_change_count_r = fetch_ctx->fetched_mails_count; + imap_fetch_free(&fetch_ctx); + return ret; +} + +static ssize_t +import_state_mailbox_struct(const unsigned char *data, size_t size, + struct mailbox_import_state *state_r, + const char **error_r) +{ + const unsigned char *p = data, *end = data + size; + + i_zero(state_r); + t_array_init(&state_r->recent_uids, 8); + + /* vname */ + if (import_string(&p, end, &state_r->vname) < 0) { + *error_r = "Mailbox state truncated at name"; + return 0; + } + + /* GUID */ + if (end-p < (int)sizeof(state_r->mailbox_guid)) { + *error_r = "Mailbox state truncated at GUID"; + return 0; + } + memcpy(state_r->mailbox_guid, p, sizeof(state_r->mailbox_guid)); + p += sizeof(state_r->mailbox_guid); + + if (guid_128_is_empty(state_r->mailbox_guid)) { + *error_r = "Empty GUID"; + return 0; + } + + /* EXAMINEd vs SELECTed */ + if (p == end) { + *error_r = "Mailbox state truncated at examined-flag"; + return 0; + } + state_r->examined = p[0] != 0; + p++; + + /* mailbox state */ + if (numpack_decode32(&p, end, &state_r->uidvalidity) < 0 || + numpack_decode32(&p, end, &state_r->uidnext) < 0 || + numpack_decode32(&p, end, &state_r->messages) < 0 || + numpack_decode(&p, end, &state_r->highest_modseq) < 0 || + numpack_decode32(&p, end, &state_r->keywords_count) < 0 || + numpack_decode32(&p, end, &state_r->keywords_crc32) < 0 || + numpack_decode32(&p, end, &state_r->uids_crc32) < 0 || + import_seq_range(&p, end, &state_r->recent_uids) < 0) { + *error_r = "Mailbox state truncated"; + return 0; + } + if (state_r->uidvalidity == 0) { + *error_r = "Empty UIDVALIDITY"; + return 0; + } + if (state_r->uidnext == 0) { + *error_r = "Empty UIDNEXT"; + return 0; + } + return p - data; +} + +static int +import_state_mailbox_open(struct client *client, + const struct mailbox_import_state *state, + const char **error_r) +{ + struct mail_namespace *ns; + struct mailbox *box; + struct mailbox_metadata metadata; + struct mailbox_status status; + const struct seq_range *range; + enum mailbox_flags flags = 0; + unsigned int expunge_count, new_mails_count = 0, flag_change_count = 0; + uint32_t uid; + int ret = 0; + + ns = mail_namespace_find(client->user->namespaces, state->vname); + if (ns == NULL) { + *error_r = "Namespace not found for mailbox"; + return -1; + } + + if (state->examined) + flags |= MAILBOX_FLAG_READONLY; + else + flags |= MAILBOX_FLAG_DROP_RECENT; + box = mailbox_alloc(ns->list, state->vname, flags); + if (mailbox_open(box) < 0) { + *error_r = t_strdup_printf("Couldn't open mailbox: %s", + mailbox_get_last_internal_error(box, NULL)); + mailbox_free(&box); + return -1; + } + + ret = mailbox_enable(box, client_enabled_mailbox_features(client)); + if (ret < 0 || mailbox_sync(box, 0) < 0) { + *error_r = t_strdup_printf("Couldn't sync mailbox: %s", + mailbox_get_last_internal_error(box, NULL)); + mailbox_free(&box); + return -1; + } + /* verify that this still looks like the same mailbox */ + if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) { + *error_r = mailbox_get_last_internal_error(box, NULL); + mailbox_free(&box); + return -1; + } + if (!guid_128_equals(metadata.guid, state->mailbox_guid)) { + *error_r = t_strdup_printf("Mailbox GUID has changed %s->%s", + guid_128_to_string(state->mailbox_guid), + guid_128_to_string(metadata.guid)); + mailbox_free(&box); + return -1; + } + mailbox_get_open_status(box, STATUS_UIDVALIDITY | STATUS_UIDNEXT | + STATUS_HIGHESTMODSEQ | STATUS_RECENT | + STATUS_KEYWORDS, &status); + if (status.uidvalidity != state->uidvalidity) { + *error_r = t_strdup_printf("Mailbox UIDVALIDITY has changed %u->%u", + state->uidvalidity, status.uidvalidity); + mailbox_free(&box); + return -1; + } + if (status.uidnext < state->uidnext) { + *error_r = t_strdup_printf("Mailbox UIDNEXT shrank %u -> %u", + state->uidnext, status.uidnext); + mailbox_free(&box); + return -1; + } + if (status.highest_modseq < state->highest_modseq) { + *error_r = t_strdup_printf("Mailbox HIGHESTMODSEQ shrank %"PRIu64" -> %"PRIu64, + state->highest_modseq, + status.highest_modseq); + mailbox_free(&box); + return -1; + } + + client->mailbox = box; + client->mailbox_examined = state->examined; + client->messages_count = status.messages; + client->uidvalidity = status.uidvalidity; + client->notify_uidnext = status.uidnext; + + if (import_send_expunges(client, state, &expunge_count, error_r) < 0) + return -1; + i_assert(expunge_count <= state->messages); + if (state->messages - expunge_count > client->messages_count) { + *error_r = t_strdup_printf("Mailbox message count shrank %u -> %u", + client->messages_count, + state->messages - expunge_count); + return -1; + } + + client_update_mailbox_flags(client, status.keywords); + array_foreach(&state->recent_uids, range) { + for (uid = range->seq1; uid <= range->seq2; uid++) { + uint32_t seq; + + if (mail_index_lookup_seq(box->view, uid, &seq)) + mailbox_recent_flags_set_uid_forced(box, uid); + } + } + client->recent_count = mailbox_recent_flags_count(box); + + if (state->messages - expunge_count < client->messages_count) { + /* new messages arrived */ + new_mails_count = client->messages_count - + (state->messages - expunge_count); + client_send_line(client, + t_strdup_printf("* %u EXISTS", client->messages_count)); + client_send_line(client, + t_strdup_printf("* %u RECENT", client->recent_count)); + } + + if (array_count(status.keywords) == state->keywords_count && + mailbox_status_keywords_crc32(&status) == state->keywords_crc32) { + /* no changes to keywords */ + client->keywords.announce_count = state->keywords_count; + } else { + client_send_mailbox_flags(client, TRUE); + } + if (import_send_flag_changes(client, state, &flag_change_count) < 0) { + *error_r = "Couldn't send flag changes"; + return -1; + } + if (client_has_enabled(client, imap_feature_qresync) && + !client->nonpermanent_modseqs && + status.highest_modseq != state->highest_modseq) { + client_send_line(client, t_strdup_printf( + "* OK [HIGHESTMODSEQ %"PRIu64"] Highest", + status.highest_modseq)); + client->sync_last_full_modseq = status.highest_modseq; + } + e_debug(client->event, + "Unhibernation sync: %u expunges, %u new messages, %u flag changes, %"PRIu64" modseq changes", + expunge_count, new_mails_count, flag_change_count, + status.highest_modseq - state->highest_modseq); + return 0; +} + +static ssize_t +import_state_mailbox(struct client *client, const unsigned char *data, + size_t size, const char **error_r) +{ + struct mailbox_import_state state; + ssize_t ret; + + if (client->mailbox != NULL) { + *error_r = "Duplicate mailbox state"; + return 0; + } + + ret = import_state_mailbox_struct(data, size, &state, error_r); + if (ret <= 0) { + i_assert(*error_r != NULL); + return ret; + } + if (import_state_mailbox_open(client, &state, error_r) < 0) { + *error_r = t_strdup_printf("Mailbox %s: %s", state.vname, *error_r); + return -1; + } + return ret; +} + +static ssize_t +import_state_enabled_feature(struct client *client, const unsigned char *data, + size_t size, const char **error_r) +{ + const unsigned char *p = data, *end = data + size; + const char *name; + unsigned int feature_idx; + + if (import_string(&p, end, &name) < 0) { + *error_r = "Mailbox state truncated at name"; + return 0; + } + if (!imap_feature_lookup(name, &feature_idx)) { + *error_r = t_strdup_printf("Unknown feature '%s'", name); + return 0; + } + client_enable(client, feature_idx); + return p - data; +} + +static ssize_t +import_state_searchres(struct client *client, const unsigned char *data, + size_t size, const char **error_r) +{ + const unsigned char *p = data; + + i_array_init(&client->search_saved_uidset, 128); + if (import_seq_range(&p, data+size, &client->search_saved_uidset) < 0) { + *error_r = "Invalid SEARCHRES seq-range"; + return 0; + } + return p - data; +} + +static ssize_t +import_state_id_logged(struct client *client, + const unsigned char *data ATTR_UNUSED, + size_t size ATTR_UNUSED, + const char **error_r ATTR_UNUSED) +{ + client->id_logged = TRUE; + return 0; +} + +static ssize_t +import_state_tls_compression(struct client *client, + const unsigned char *data ATTR_UNUSED, + size_t size ATTR_UNUSED, + const char **error_r ATTR_UNUSED) +{ + client->tls_compression = TRUE; + return 0; +} + +void imap_state_import_idle_cmd_tag(struct client *client, const char *tag) +{ + if (client->state_import_idle_continue) { + /* IDLE command continues */ + struct client_command_context *cmd; + struct command *command; + + cmd = client_command_alloc(client); + cmd->tag = p_strdup(cmd->pool, tag); + cmd->name = "IDLE"; + + command = command_find("IDLE"); + i_assert(command != NULL); + cmd->func = command->func; + cmd->cmd_flags = command->flags; + client_command_init_finished(cmd); + + if (command_exec(cmd)) { + /* IDLE terminated because of an external change, but + DONE was already buffered */ + client_command_free(&cmd); + client_add_missing_io(client); + } else { + i_assert(cmd->state == CLIENT_COMMAND_STATE_WAIT_INPUT || + cmd->state == CLIENT_COMMAND_STATE_WAIT_OUTPUT); + } + } else { + /* we're finishing IDLE command */ + client_send_line(client, t_strdup_printf( + "%s %s Idle completed.", tag, + client->state_import_bad_idle_done ? "BAD" : "OK")); + } +} + +static struct { + enum imap_state_type_public type; + ssize_t (*import)(struct client *client, const unsigned char *data, + size_t size, const char **error_r); +} imap_states_public[] = { + { IMAP_STATE_TYPE_MAILBOX, import_state_mailbox }, + { IMAP_STATE_TYPE_ENABLED_FEATURE, import_state_enabled_feature }, + { IMAP_STATE_TYPE_SEARCHRES, import_state_searchres } +}; + +static struct { + enum imap_state_type_internal type; + ssize_t (*import)(struct client *client, const unsigned char *data, + size_t size, const char **error_r); +} imap_states_internal[] = { + { IMAP_STATE_TYPE_ID_LOGGED, import_state_id_logged }, + { IMAP_STATE_TYPE_TLS_COMPRESSION, import_state_tls_compression } +}; + +static ssize_t +imap_state_try_import_public(struct client *client, const unsigned char *data, + size_t size, const char **error_r) +{ + unsigned int i; + ssize_t ret; + + i_assert(size > 0); + + for (i = 0; i < N_ELEMENTS(imap_states_public); i++) { + if (imap_states_public[i].type == data[0]) { + ret = imap_states_public[i]. + import(client, data+1, size-1, error_r); + return ret < 0 ? -1 : ret+1; + } + } + return -2; +} + +static ssize_t +imap_state_try_import_internal(struct client *client, const unsigned char *data, + size_t size, const char **error_r) +{ + unsigned int i; + ssize_t ret; + + i_assert(size > 0); + + for (i = 0; i < N_ELEMENTS(imap_states_internal); i++) { + if (imap_states_internal[i].type == data[0]) { + ret = imap_states_internal[i]. + import(client, data+1, size-1, error_r); + return ret < 0 ? -1 : ret+1; + } + } + return -2; +} + +ssize_t imap_state_import_base(struct client *client, bool internal, + const unsigned char *data, size_t size, + const char **error_r) +{ + const unsigned char *p; + ssize_t ret; + size_t pos; + + i_assert(client->mailbox == NULL); + + *error_r = NULL; + + if (size < 5 || memcmp(data, "base\n", 5) != 0) { + p = memchr(data, '\n', size); + if (p == NULL) + p = data + I_MIN(size, 20); + *error_r = t_strdup_printf("Unknown state block '%s'", + str_sanitize(t_strdup_until(data, p), 20)); + return 0; + } + + pos = 5; + while (pos < size) { + ret = imap_state_try_import_public(client, data+pos, + size-pos, error_r); + if (ret == -2 && internal) { + ret = imap_state_try_import_internal(client, data+pos, + size-pos, error_r); + } + if (ret < 0 || *error_r != NULL) { + if (ret == -2) { + *error_r = t_strdup_printf("Unknown type '%c'", + data[pos]); + } + i_assert(*error_r != NULL); + return ret < 0 ? -1 : 0; + } + i_assert(size - pos >= (size_t)ret); + pos += ret; + } + return pos; +} diff --git a/src/imap/imap-state.h b/src/imap/imap-state.h new file mode 100644 index 0000000..ee00376 --- /dev/null +++ b/src/imap/imap-state.h @@ -0,0 +1,30 @@ +#ifndef IMAP_STATE_H +#define IMAP_STATE_H + +/* Export the IMAP client state to the given buffer. Returns 1 if ok, + 0 if state couldn't be exported, -1 if temporary internal error error. */ +int imap_state_export_internal(struct client *client, buffer_t *dest, + const char **error_r); +int imap_state_export_external(struct client *client, buffer_t *dest, + const char **error_r); + +/* Returns 1 if ok, 0 if state was corrupted, -1 if other error. Internal state + comes from another Dovecot component, which can override IP addresses, + session IDs, etc. */ +int imap_state_import_internal(struct client *client, + const unsigned char *data, size_t size, + const char **error_r); +int imap_state_import_external(struct client *client, + const unsigned char *data, size_t size, + const char **error_r); + +/* INTERNAL API: Note that the "internal" flag specifies whether we're doing + the import/export from/to another Dovecot component or an untrusted + IMAP client. */ +int imap_state_export_base(struct client *client, bool internal, + buffer_t *dest, const char **error_r); +ssize_t imap_state_import_base(struct client *client, bool internal, + const unsigned char *data, size_t size, + const char **error_r); +void imap_state_import_idle_cmd_tag(struct client *client, const char *tag); +#endif diff --git a/src/imap/imap-status.c b/src/imap/imap-status.c new file mode 100644 index 0000000..341e537 --- /dev/null +++ b/src/imap/imap-status.c @@ -0,0 +1,172 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "hex-binary.h" +#include "str.h" +#include "imap-quote.h" +#include "imap-status.h" + +int imap_status_parse_items(struct client_command_context *cmd, + const struct imap_arg *args, + struct imap_status_items *items_r) +{ + enum imap_status_item_flags flags = 0; + const char *item; + + if (IMAP_ARG_IS_EOL(args)) { + client_send_command_error(cmd, "Empty status list."); + return -1; + } + + i_zero(items_r); + for (; !IMAP_ARG_IS_EOL(args); args++) { + if (!imap_arg_get_atom(args, &item)) { + /* list may contain only atoms */ + client_send_command_error(cmd, + "Status list contains non-atoms."); + return -1; + } + + item = t_str_ucase(item); + if (strcmp(item, "MESSAGES") == 0) + flags |= IMAP_STATUS_ITEM_MESSAGES; + else if (strcmp(item, "RECENT") == 0) + flags |= IMAP_STATUS_ITEM_RECENT; + else if (strcmp(item, "UIDNEXT") == 0) + flags |= IMAP_STATUS_ITEM_UIDNEXT; + else if (strcmp(item, "UIDVALIDITY") == 0) + flags |= IMAP_STATUS_ITEM_UIDVALIDITY; + else if (strcmp(item, "UNSEEN") == 0) + flags |= IMAP_STATUS_ITEM_UNSEEN; + else if (strcmp(item, "HIGHESTMODSEQ") == 0) + flags |= IMAP_STATUS_ITEM_HIGHESTMODSEQ; + else if (strcmp(item, "SIZE") == 0) + flags |= IMAP_STATUS_ITEM_SIZE; + else if (strcmp(item, "X-SIZE") == 0) + flags |= IMAP_STATUS_ITEM_X_SIZE; + else if (strcmp(item, "X-GUID") == 0) + flags |= IMAP_STATUS_ITEM_X_GUID; + else { + client_send_command_error(cmd, t_strconcat( + "Invalid status item ", item, NULL)); + return -1; + } + } + + items_r->flags = flags; + return 0; +} + +int imap_status_get_result(struct client *client, struct mailbox *box, + const struct imap_status_items *items, + struct imap_status_result *result_r) +{ + enum mailbox_status_items status = 0; + enum mailbox_metadata_items metadata = 0; + int ret; + + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_MESSAGES)) + status |= STATUS_MESSAGES; + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_RECENT)) + status |= STATUS_RECENT; + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UIDNEXT)) + status |= STATUS_UIDNEXT; + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UIDVALIDITY)) + status |= STATUS_UIDVALIDITY; + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UNSEEN)) + status |= STATUS_UNSEEN; + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_HIGHESTMODSEQ)) { + client_enable(client, imap_feature_condstore); + status |= STATUS_HIGHESTMODSEQ; + } + if (HAS_ANY_BITS(items->flags, IMAP_STATUS_ITEM_SIZE | + IMAP_STATUS_ITEM_X_SIZE)) + metadata |= MAILBOX_METADATA_VIRTUAL_SIZE; + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_X_GUID)) + metadata |= MAILBOX_METADATA_GUID; + + ret = mailbox_get_status(box, status, &result_r->status); + if (metadata != 0 && ret == 0) + ret = mailbox_get_metadata(box, metadata, &result_r->metadata); + + return ret; +} + +int imap_status_get(struct client_command_context *cmd, + struct mail_namespace *ns, const char *mailbox, + const struct imap_status_items *items, + struct imap_status_result *result_r) +{ + struct client *client = cmd->client; + struct mailbox *box; + const char *errstr; + int ret = 0; + + if (client->mailbox != NULL && + mailbox_equals(client->mailbox, ns, mailbox)) { + /* this mailbox is selected */ + box = client->mailbox; + } else { + /* open the mailbox */ + box = mailbox_alloc(ns->list, mailbox, MAILBOX_FLAG_READONLY); + (void)mailbox_enable(box, client_enabled_mailbox_features(client)); + } + + ret = imap_status_get_result(client, box, items, result_r); + if (ret < 0) { + errstr = mailbox_get_last_error(box, &result_r->error); + result_r->errstr = imap_get_error_string(cmd, errstr, + result_r->error); + } + if (box != client->mailbox) + mailbox_free(&box); + return ret; +} + +int imap_status_send(struct client *client, const char *mailbox_mutf7, + const struct imap_status_items *items, + const struct imap_status_result *result) +{ + const struct mailbox_status *status = &result->status; + string_t *str; + size_t prefix_len; + + str = t_str_new(128); + str_append(str, "* STATUS "); + imap_append_astring(str, mailbox_mutf7); + str_append(str, " ("); + + prefix_len = str_len(str); + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_MESSAGES)) + str_printfa(str, "MESSAGES %u ", status->messages); + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_RECENT)) + str_printfa(str, "RECENT %u ", status->recent); + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UIDNEXT)) + str_printfa(str, "UIDNEXT %u ", status->uidnext); + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UIDVALIDITY)) + str_printfa(str, "UIDVALIDITY %u ", status->uidvalidity); + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_UNSEEN)) + str_printfa(str, "UNSEEN %u ", status->unseen); + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_HIGHESTMODSEQ)) { + str_printfa(str, "HIGHESTMODSEQ %"PRIu64" ", + status->highest_modseq); + } + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_SIZE)) { + str_printfa(str, "SIZE %"PRIu64" ", + result->metadata.virtual_size); + } + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_X_SIZE)) { + str_printfa(str, "X-SIZE %"PRIu64" ", + result->metadata.virtual_size); + } + if (HAS_ALL_BITS(items->flags, IMAP_STATUS_ITEM_X_GUID)) { + str_printfa(str, "X-GUID %s ", + guid_128_to_string(result->metadata.guid)); + } + + if (str_len(str) != prefix_len) + str_truncate(str, str_len(str)-1); + str_append_c(str, ')'); + + return client_send_line_next(client, str_c(str)); +} diff --git a/src/imap/imap-status.h b/src/imap/imap-status.h new file mode 100644 index 0000000..58bd581 --- /dev/null +++ b/src/imap/imap-status.h @@ -0,0 +1,51 @@ +#ifndef IMAP_STATUS_H +#define IMAP_STATUS_H + +enum imap_status_item_flags { + IMAP_STATUS_ITEM_MESSAGES = BIT(0), + IMAP_STATUS_ITEM_RECENT = BIT(1), + IMAP_STATUS_ITEM_UIDNEXT = BIT(2), + IMAP_STATUS_ITEM_UIDVALIDITY = BIT(3), + IMAP_STATUS_ITEM_UNSEEN = BIT(4), + IMAP_STATUS_ITEM_HIGHESTMODSEQ = BIT(5), + IMAP_STATUS_ITEM_SIZE = BIT(6), + + IMAP_STATUS_ITEM_X_SIZE = BIT(16), /* to be deprecated */ + IMAP_STATUS_ITEM_X_GUID = BIT(17), +}; + +struct imap_status_items { + enum imap_status_item_flags flags; +}; + +struct imap_status_result { + struct mailbox_status status; + struct mailbox_metadata metadata; + enum mail_error error; + const char *errstr; +}; + +static inline bool +imap_status_items_is_empty(const struct imap_status_items *items) +{ + return (items->flags == 0); +} + +int imap_status_parse_items(struct client_command_context *cmd, + const struct imap_arg *args, + struct imap_status_items *items_r); + +int imap_status_get_result(struct client *client, struct mailbox *box, + const struct imap_status_items *items, + struct imap_status_result *result_r); +int imap_status_get(struct client_command_context *cmd, + struct mail_namespace *ns, const char *mailbox, + const struct imap_status_items *items, + struct imap_status_result *result_r); + +int imap_status_send(struct client *client, const char *mailbox_mutf7, + const struct imap_status_items *items, + const struct imap_status_result *result) + ATTR_NOWARN_UNUSED_RESULT; + +#endif diff --git a/src/imap/imap-sync-private.h b/src/imap/imap-sync-private.h new file mode 100644 index 0000000..b0b1715 --- /dev/null +++ b/src/imap/imap-sync-private.h @@ -0,0 +1,47 @@ +#ifndef IMAP_SYNC_PRIVATE_H +#define IMAP_SYNC_PRIVATE_H + +#include "imap-sync.h" + +struct imap_client_sync_context { + /* if multiple commands are in progress, we may need to wait for them + to finish before syncing mailbox. */ + unsigned int counter; + enum mailbox_sync_flags flags; + enum imap_sync_flags imap_flags; + const char *tagline; +}; + +struct imap_sync_context { + struct client *client; + struct mailbox *box; + enum imap_sync_flags imap_flags; + + struct mailbox_transaction_context *t; + struct mailbox_sync_context *sync_ctx; + struct mail *mail; + + struct mailbox_status status; + struct mailbox_sync_status sync_status; + + struct mailbox_sync_rec sync_rec; + ARRAY_TYPE(keywords) tmp_keywords; + ARRAY_TYPE(seq_range) expunges; + uint32_t seq; + + ARRAY_TYPE(seq_range) search_adds, search_removes; + unsigned int search_update_idx; + + unsigned int messages_count; + + /* Module-specific contexts. */ + ARRAY(union imap_module_context *) module_contexts; + + bool failed:1; + bool finished:1; + bool no_newmail:1; + bool have_new_mails:1; + bool search_update_notifying:1; +}; + +#endif diff --git a/src/imap/imap-sync.c b/src/imap/imap-sync.c new file mode 100644 index 0000000..fc49ae7 --- /dev/null +++ b/src/imap/imap-sync.c @@ -0,0 +1,841 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "str.h" +#include "ostream.h" +#include "mail-user.h" +#include "mail-storage.h" +#include "mail-search-build.h" +#include "imap-quote.h" +#include "imap-util.h" +#include "imap-fetch.h" +#include "imap-notify.h" +#include "imap-commands.h" +#include "imap-sync-private.h" + +static void uids_to_seqs(struct mailbox *box, ARRAY_TYPE(seq_range) *uids) +{ + T_BEGIN { + ARRAY_TYPE(seq_range) seqs; + const struct seq_range *range; + uint32_t seq1, seq2; + + t_array_init(&seqs, array_count(uids)); + array_foreach(uids, range) { + mailbox_get_seq_range(box, range->seq1, range->seq2, + &seq1, &seq2); + /* since we have to notify about expunged messages, + we expect that all the referenced UIDs exist */ + i_assert(seq1 != 0); + i_assert(seq2 - seq1 == range->seq2 - range->seq1); + + seq_range_array_add_range(&seqs, seq1, seq2); + } + /* replace uids with seqs */ + array_clear(uids); + array_append_array(uids, &seqs); + + } T_END; +} + +static int search_update_fetch_more(const struct imap_search_update *update) +{ + int ret; + + if ((ret = imap_fetch_more_no_lock_update(update->fetch_ctx)) == 0) + return 0; + /* finished the FETCH */ + if (imap_fetch_end(update->fetch_ctx) < 0) + return -1; + return ret; +} + +static int +imap_sync_send_fetch_to_search_update(struct imap_sync_context *ctx, + const struct imap_search_update *update) +{ + struct mail_search_args *search_args; + struct mail_search_arg *arg; + ARRAY_TYPE(seq_range) seqs; + + if (ctx->search_update_notifying) + return search_update_fetch_more(update); + + i_assert(!update->fetch_ctx->state.fetching); + + if (array_count(&ctx->search_adds) == 0 || !ctx->have_new_mails) + return 1; + + search_args = mail_search_build_init(); + arg = mail_search_build_add(search_args, SEARCH_UIDSET); + p_array_init(&arg->value.seqset, search_args->pool, 1); + + /* find the newly appended messages: ctx->messages_count is the message + count before new messages found by sync, client->messages_count is + the number of messages after. */ + t_array_init(&seqs, 1); + seq_range_array_add_range(&seqs, ctx->messages_count+1, + ctx->client->messages_count); + mailbox_get_uid_range(ctx->client->mailbox, &seqs, &arg->value.seqset); + /* remove messages not in the search_adds list */ + seq_range_array_intersect(&arg->value.seqset, &ctx->search_adds); + + imap_fetch_begin(update->fetch_ctx, ctx->client->mailbox, search_args); + mail_search_args_unref(&search_args); + return search_update_fetch_more(update); +} + +static int +imap_sync_send_search_update(struct imap_sync_context *ctx, + const struct imap_search_update *update, + bool removes_only) +{ + string_t *cmd; + int ret = 1; + + if (!ctx->search_update_notifying) { + mailbox_search_result_sync(update->result, &ctx->search_removes, + &ctx->search_adds); + } + if (array_count(&ctx->search_adds) == 0 && + array_count(&ctx->search_removes) == 0) + return 1; + + i_assert(array_count(&ctx->search_adds) == 0 || !removes_only); + if (update->fetch_ctx != NULL) { + ret = imap_sync_send_fetch_to_search_update(ctx, update); + if (ret == 0) { + ctx->search_update_notifying = TRUE; + return 0; + } + } + ctx->search_update_notifying = FALSE; + + cmd = t_str_new(256); + str_append(cmd, "* ESEARCH (TAG "); + imap_append_string(cmd, update->tag); + str_append_c(cmd, ')'); + if (update->return_uids) + str_append(cmd, " UID"); + else { + /* convert to sequences */ + uids_to_seqs(ctx->client->mailbox, &ctx->search_removes); + uids_to_seqs(ctx->client->mailbox, &ctx->search_adds); + } + + if (array_count(&ctx->search_removes) != 0) { + str_printfa(cmd, " REMOVEFROM (0 "); + imap_write_seq_range(cmd, &ctx->search_removes); + str_append_c(cmd, ')'); + } + if (array_count(&ctx->search_adds) != 0) { + str_printfa(cmd, " ADDTO (0 "); + imap_write_seq_range(cmd, &ctx->search_adds); + str_append_c(cmd, ')'); + } + str_append(cmd, "\r\n"); + o_stream_nsend(ctx->client->output, str_data(cmd), str_len(cmd)); + return ret; +} + +static int +imap_sync_send_search_updates(struct imap_sync_context *ctx, bool removes_only) +{ + const struct imap_search_update *updates; + unsigned int i, count; + int ret = 1; + + if (!array_is_created(&ctx->client->search_updates)) + return 1; + + if (!array_is_created(&ctx->search_removes)) { + i_array_init(&ctx->search_removes, 64); + i_array_init(&ctx->search_adds, 128); + } + + updates = array_get(&ctx->client->search_updates, &count); + for (i = ctx->search_update_idx; i < count; i++) { + T_BEGIN { + ret = imap_sync_send_search_update(ctx, &updates[i], + removes_only); + } T_END; + if (ret <= 0) + break; + } + ctx->search_update_idx = i; + return ret; +} + +struct imap_sync_context * +imap_sync_init(struct client *client, struct mailbox *box, + enum imap_sync_flags imap_flags, enum mailbox_sync_flags flags) +{ + struct imap_sync_context *ctx; + + i_assert(client->mailbox == box); + + if (client->notify_immediate_expunges) { + /* NOTIFY enabled without SELECTED-DELAYED */ + flags &= ENUM_NEGATE(MAILBOX_SYNC_FLAG_NO_EXPUNGES); + } + + ctx = i_new(struct imap_sync_context, 1); + ctx->client = client; + ctx->box = box; + ctx->imap_flags = imap_flags; + i_array_init(&ctx->module_contexts, 5); + + /* make sure user can't DoS the system by causing Dovecot to create + tons of useless namespaces. */ + mail_user_drop_useless_namespaces(client->user); + + ctx->sync_ctx = mailbox_sync_init(box, flags); + ctx->t = mailbox_transaction_begin(box, 0, "Mailbox sync"); + ctx->mail = mail_alloc(ctx->t, MAIL_FETCH_FLAGS, NULL); + ctx->messages_count = client->messages_count; + i_array_init(&ctx->tmp_keywords, client->keywords.announce_count + 8); + + if (client_has_enabled(client, imap_feature_qresync)) { + i_array_init(&ctx->expunges, 128); + /* always send UIDs in FETCH replies */ + ctx->imap_flags |= IMAP_SYNC_FLAG_SEND_UID; + } + + client_send_mailbox_flags(client, FALSE); + /* send search updates the first time after sync is initialized. + it now contains expunged messages that must be sent before + EXPUNGE replies. */ + if (imap_sync_send_search_updates(ctx, TRUE) == 0) + i_unreached(); + ctx->search_update_idx = 0; + return ctx; +} + +static void +imap_sync_send_highestmodseq(struct imap_sync_context *ctx, + struct client_command_context *sync_cmd) +{ + struct client *client = ctx->client; + uint64_t send_modseq = 0; + + if (ctx->sync_status.sync_delayed_expunges && + client->highest_fetch_modseq > client->sync_last_full_modseq) { + /* if client updates highest-modseq using returned MODSEQs + it loses expunges. try to avoid this by sending it a lower + pre-expunge HIGHESTMODSEQ reply. */ + send_modseq = client->sync_last_full_modseq; + } else if (!ctx->sync_status.sync_delayed_expunges && + ctx->status.highest_modseq > client->sync_last_full_modseq && + ctx->status.highest_modseq > client->highest_fetch_modseq) { + /* we've probably sent some VANISHED or EXISTS replies which + increased the highest-modseq. notify the client about + this. */ + send_modseq = ctx->status.highest_modseq; + } + + if (send_modseq == 0) { + /* no sending */ + } else if (sync_cmd->sync != NULL && /* IDLE doesn't have ->sync */ + sync_cmd->sync->tagline != NULL && /* NOTIFY doesn't have tagline */ + str_begins(sync_cmd->sync->tagline, "OK ") && + sync_cmd->sync->tagline[3] != '[') { + /* modify the tagged reply directly */ + sync_cmd->sync->tagline = p_strdup_printf(sync_cmd->pool, + "OK [HIGHESTMODSEQ %"PRIu64"] %s", + send_modseq, sync_cmd->sync->tagline + 3); + } else { + /* send an untagged OK reply */ + client_send_line(client, t_strdup_printf( + "* OK [HIGHESTMODSEQ %"PRIu64"] Highest", + send_modseq)); + } + + if (!ctx->sync_status.sync_delayed_expunges) { + /* no delayed expunges, remember this for future */ + client->sync_last_full_modseq = ctx->status.highest_modseq; + } + client->highest_fetch_modseq = 0; +} + +static int imap_sync_finish(struct imap_sync_context *ctx, bool aborting) +{ + struct client *client = ctx->client; + int ret = ctx->failed ? -1 : 0; + + if (ctx->finished) + return ret; + ctx->finished = TRUE; + + mail_free(&ctx->mail); + /* the transaction is used only for fetching modseqs/flags. + it can't really fail.. */ + (void)mailbox_transaction_commit(&ctx->t); + + if (array_is_created(&ctx->expunges)) + array_free(&ctx->expunges); + + if (mailbox_sync_deinit(&ctx->sync_ctx, &ctx->sync_status) < 0 || + ctx->failed) { + ctx->failed = TRUE; + ret = -1; + } + mailbox_get_open_status(ctx->box, STATUS_UIDVALIDITY | + STATUS_MESSAGES | STATUS_RECENT | + STATUS_HIGHESTMODSEQ, &ctx->status); + + if (ctx->status.uidvalidity != client->uidvalidity) { + /* most clients would get confused by this. disconnect them. */ + client_disconnect_with_error(client, + "Mailbox UIDVALIDITY changed"); + } + if (mailbox_is_inconsistent(ctx->box)) { + client_disconnect_with_error(client, + "IMAP session state is inconsistent, please relogin."); + /* we can't trust status information anymore, so don't try to + sync message counts. */ + return -1; + } + if (!ctx->no_newmail && !aborting) { + if (ctx->status.messages < ctx->messages_count) + i_panic("Message count decreased"); + if (ctx->status.messages != ctx->messages_count && + client->notify_count_changes) { + client_send_line(client, + t_strdup_printf("* %u EXISTS", ctx->status.messages)); + ctx->have_new_mails = TRUE; + } + if (ctx->status.recent != client->recent_count && + client->notify_count_changes) { + client_send_line(client, + t_strdup_printf("* %u RECENT", ctx->status.recent)); + } + client->messages_count = ctx->status.messages; + client->recent_count = ctx->status.recent; + } + return ret; +} + +static int imap_sync_notify_more(struct imap_sync_context *ctx) +{ + int ret = 1; + + if (ctx->have_new_mails && ctx->client->notify_ctx != NULL) { + /* send FETCH replies for the new mails */ + if ((ret = imap_client_notify_newmails(ctx->client)) == 0) + return 0; + if (ret < 0) + ctx->failed = TRUE; + } + + /* send search updates the second time after syncing in done. + now it contains added/removed messages. */ + if ((ret = imap_sync_send_search_updates(ctx, FALSE)) < 0) + ctx->failed = TRUE; + + if (ret > 0) + ret = ctx->client->v.sync_notify_more(ctx); + return ret; +} + +int imap_sync_deinit(struct imap_sync_context *ctx, + struct client_command_context *sync_cmd) +{ + int ret; + + ret = imap_sync_finish(ctx, TRUE); + imap_client_notify_finished(ctx->client); + + if (client_has_enabled(ctx->client, imap_feature_qresync) && + !ctx->client->nonpermanent_modseqs) + imap_sync_send_highestmodseq(ctx, sync_cmd); + + if (array_is_created(&ctx->search_removes)) { + array_free(&ctx->search_removes); + array_free(&ctx->search_adds); + } + + array_free(&ctx->tmp_keywords); + array_free(&ctx->module_contexts); + i_free(ctx); + return ret; +} + +static void imap_sync_add_modseq(struct imap_sync_context *ctx, string_t *str) +{ + uint64_t modseq; + + modseq = mail_get_modseq(ctx->mail); + if (ctx->client->highest_fetch_modseq < modseq) + ctx->client->highest_fetch_modseq = modseq; + str_printfa(str, "MODSEQ (%"PRIu64")", modseq); +} + +static int imap_sync_send_flags(struct imap_sync_context *ctx, string_t *str) +{ + enum mail_flags flags; + const char *const *keywords; + + mail_set_seq(ctx->mail, ctx->seq); + flags = mail_get_flags(ctx->mail); + keywords = client_get_keyword_names(ctx->client, &ctx->tmp_keywords, + mail_get_keyword_indexes(ctx->mail)); + + if ((flags & MAIL_DELETED) != 0) + ctx->client->sync_seen_deletes = TRUE; + + str_truncate(str, 0); + str_printfa(str, "* %u FETCH (", ctx->seq); + if ((ctx->imap_flags & IMAP_SYNC_FLAG_SEND_UID) != 0) + str_printfa(str, "UID %u ", ctx->mail->uid); + if (client_has_enabled(ctx->client, imap_feature_condstore) && + !ctx->client->nonpermanent_modseqs) { + imap_sync_add_modseq(ctx, str); + str_append_c(str, ' '); + } + str_append(str, "FLAGS ("); + imap_write_flags(str, flags, keywords); + str_append(str, "))"); + return client_send_line_next(ctx->client, str_c(str)); +} + +static int imap_sync_send_modseq(struct imap_sync_context *ctx, string_t *str) +{ + mail_set_seq(ctx->mail, ctx->seq); + + str_truncate(str, 0); + str_printfa(str, "* %u FETCH (", ctx->seq); + if ((ctx->imap_flags & IMAP_SYNC_FLAG_SEND_UID) != 0) + str_printfa(str, "UID %u ", ctx->mail->uid); + imap_sync_add_modseq(ctx, str); + str_append_c(str, ')'); + return client_send_line_next(ctx->client, str_c(str)); +} + +static void imap_sync_vanished(struct imap_sync_context *ctx) +{ + const struct seq_range *seqs; + unsigned int i, count; + string_t *line; + uint32_t seq, prev_uid, start_uid; + bool comma = FALSE; + + /* Convert expunge sequences to UIDs and send them in VANISHED line. */ + seqs = array_get(&ctx->expunges, &count); + if (count == 0) + return; + + line = t_str_new(256); + str_append(line, "* VANISHED "); + for (i = 0; i < count; i++) { + start_uid = 0; prev_uid = 0; + for (seq = seqs[i].seq1; seq <= seqs[i].seq2; seq++) { + mail_set_seq(ctx->mail, seq); + if (prev_uid == 0 || prev_uid + 1 != ctx->mail->uid) { + if (start_uid != 0) { + if (!comma) + comma = TRUE; + else + str_append_c(line, ','); + str_printfa(line, "%u", start_uid); + if (start_uid != prev_uid) { + str_printfa(line, ":%u", + prev_uid); + } + } + start_uid = ctx->mail->uid; + } + prev_uid = ctx->mail->uid; + } + if (!comma) + comma = TRUE; + else + str_append_c(line, ','); + str_printfa(line, "%u", start_uid); + if (start_uid != prev_uid) + str_printfa(line, ":%u", prev_uid); + } + str_append(line, "\r\n"); + o_stream_nsend(ctx->client->output, str_data(line), str_len(line)); +} + +static int imap_sync_send_expunges(struct imap_sync_context *ctx, string_t *str) +{ + int ret = 1; + + if (!ctx->client->notify_count_changes) { + /* NOTIFY: MessageEvent not specified for selected mailbox */ + return 1; + } + + if (array_is_created(&ctx->expunges)) { + /* Use a single VANISHED line */ + seq_range_array_add_range(&ctx->expunges, + ctx->sync_rec.seq1, + ctx->sync_rec.seq2); + return 1; + } + if (ctx->seq == 0) + ctx->seq = ctx->sync_rec.seq2; + for (; ctx->seq >= ctx->sync_rec.seq1; ctx->seq--) { + if (ret == 0) { + /* buffer full, continue later */ + return 0; + } + + str_truncate(str, 0); + str_printfa(str, "* %u EXPUNGE", ctx->seq); + ret = client_send_line_next(ctx->client, str_c(str)); + } + return 1; +} + +int imap_sync_more(struct imap_sync_context *ctx) +{ + string_t *str; + int ret = 1; + + if (ctx->finished) + return imap_sync_notify_more(ctx); + + /* finish syncing even when client has disconnected. otherwise our + internal state (ctx->messages_count) can get messed up and unless + we immediately stop handling all commands and syncs we could end up + assert-crashing. */ + str = t_str_new(256); + for (;;) { + if (ctx->seq == 0) { + /* get next one */ + if (!mailbox_sync_next(ctx->sync_ctx, &ctx->sync_rec)) { + /* finished */ + ret = 1; + break; + } + } + + if (ctx->sync_rec.seq2 > ctx->messages_count) { + /* don't send change notifications of messages we + haven't even announced to client yet */ + if (ctx->sync_rec.seq1 > ctx->messages_count) { + ctx->seq = 0; + continue; + } + ctx->sync_rec.seq2 = ctx->messages_count; + } + + /* EXPUNGEs must come last */ + i_assert(!array_is_created(&ctx->expunges) || + array_count(&ctx->expunges) == 0 || + ctx->sync_rec.type == MAILBOX_SYNC_TYPE_EXPUNGE); + switch (ctx->sync_rec.type) { + case MAILBOX_SYNC_TYPE_FLAGS: + if (!ctx->client->notify_flag_changes) { + /* NOTIFY: FlagChange not specified for + selected mailbox */ + break; + } + if (ctx->seq == 0) + ctx->seq = ctx->sync_rec.seq1; + + ret = 1; + for (; ctx->seq <= ctx->sync_rec.seq2; ctx->seq++) { + if (ret == 0) + break; + + ret = imap_sync_send_flags(ctx, str); + } + break; + case MAILBOX_SYNC_TYPE_EXPUNGE: + ret = imap_sync_send_expunges(ctx, str); + if (ret > 0) { + /* update only after we're finished, so that + the seq2 > messages_count check above + doesn't break */ + ctx->messages_count -= + ctx->sync_rec.seq2 - + ctx->sync_rec.seq1 + 1; + } + break; + case MAILBOX_SYNC_TYPE_MODSEQ: + if (!client_has_enabled(ctx->client, imap_feature_condstore)) + break; + if (!ctx->client->notify_flag_changes) { + /* NOTIFY: FlagChange not specified for + selected mailbox. The RFC doesn't explicitly + specify MODSEQ changes, but they're close + enough to flag changes. */ + break; + } + + if (ctx->seq == 0) + ctx->seq = ctx->sync_rec.seq1; + + ret = 1; + for (; ctx->seq <= ctx->sync_rec.seq2; ctx->seq++) { + if (ret == 0) + break; + + ret = imap_sync_send_modseq(ctx, str); + } + break; + } + if (ret == 0) { + /* buffer full */ + break; + } + + ctx->seq = 0; + } + if (ret > 0) { + if (array_is_created(&ctx->expunges)) + imap_sync_vanished(ctx); + if (imap_sync_finish(ctx, FALSE) < 0) + return -1; + return imap_sync_more(ctx); + } + return ret; +} + +bool imap_sync_is_allowed(struct client *client) +{ + if (client->syncing) + return FALSE; + + if (client->mailbox != NULL && + mailbox_transaction_get_count(client->mailbox) > 0) + return FALSE; + + return TRUE; +} + +static bool cmd_finish_sync(struct client_command_context *cmd) +{ + if (cmd->sync->tagline != NULL) + client_send_tagline(cmd, cmd->sync->tagline); + return TRUE; +} + +static bool cmd_sync_continue(struct client_command_context *sync_cmd) +{ + struct client_command_context *cmd, *prev; + struct client *client = sync_cmd->client; + struct imap_sync_context *ctx = sync_cmd->context; + int ret; + + i_assert(ctx->client == client); + + if ((ret = imap_sync_more(ctx)) == 0) + return FALSE; + if (ret < 0) + ctx->failed = TRUE; + + client->syncing = FALSE; + if (imap_sync_deinit(ctx, sync_cmd) < 0) { + client_send_untagged_storage_error(client, + mailbox_get_storage(client->mailbox)); + } + sync_cmd->context = NULL; + + /* Finish all commands that waited for this sync. Go through the queue + backwards, so that tagged replies are sent in the same order as + they were received. This fixes problems with clients that rely on + this (Apple Mail 3.2) */ + for (cmd = client->command_queue; cmd->next != NULL; cmd = cmd->next) ; + for (; cmd != NULL; cmd = prev) { + prev = cmd->prev; + + if (cmd->state == CLIENT_COMMAND_STATE_WAIT_SYNC && + cmd != sync_cmd && + cmd->sync->counter+1 == client->sync_counter) { + cmd_finish_sync(cmd); + client_command_free(&cmd); + } + } + cmd_finish_sync(sync_cmd); + return TRUE; +} + +static void get_common_sync_flags(struct client *client, + enum mailbox_sync_flags *flags_r, + enum imap_sync_flags *imap_flags_r) +{ + struct client_command_context *cmd; + unsigned int count = 0, fast_count = 0, noexpunges_count = 0; + + *flags_r = 0; + *imap_flags_r = 0; + + for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) { + if (cmd->sync != NULL && + cmd->sync->counter == client->sync_counter) { + if ((cmd->sync->flags & MAILBOX_SYNC_FLAG_FAST) != 0) + fast_count++; + if ((cmd->sync->flags & MAILBOX_SYNC_FLAG_NO_EXPUNGES) != 0) + noexpunges_count++; + *flags_r |= cmd->sync->flags; + *imap_flags_r |= cmd->sync->imap_flags; + count++; + } + } + i_assert(noexpunges_count == 0 || noexpunges_count == count); + if (fast_count != count) + *flags_r &= ENUM_NEGATE(MAILBOX_SYNC_FLAG_FAST); + + i_assert((*flags_r & MAILBOX_SYNC_FLAG_FIX_INCONSISTENT) == 0); +} + +static bool cmd_sync_client(struct client_command_context *sync_cmd) +{ + struct client *client = sync_cmd->client; + struct imap_sync_context *ctx; + enum mailbox_sync_flags flags; + enum imap_sync_flags imap_flags; + bool no_newmail; + + /* there may be multiple commands waiting. use their combined flags */ + get_common_sync_flags(client, &flags, &imap_flags); + client->sync_counter++; + + no_newmail = (client->set->parsed_workarounds & WORKAROUND_DELAY_NEWMAIL) != 0 && + client->notify_ctx == NULL && /* always disabled with NOTIFY */ + (imap_flags & IMAP_SYNC_FLAG_SAFE) == 0; + if (no_newmail) { + /* expunges might break the client just as badly as new mail + notifications. */ + flags |= MAILBOX_SYNC_FLAG_NO_EXPUNGES; + } + + client->syncing = TRUE; + + ctx = imap_sync_init(client, client->mailbox, imap_flags, flags); + ctx->no_newmail = no_newmail; + + /* handle the syncing using sync_cmd. it doesn't actually matter which + one of the pending commands it is. */ + sync_cmd->func = cmd_sync_continue; + sync_cmd->context = ctx; + sync_cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT; + if (!cmd_sync_continue(sync_cmd)) { + o_stream_set_flush_pending(client->output, TRUE); + return FALSE; + } + + client_command_free(&sync_cmd); + cmd_sync_delayed(client); + return TRUE; +} + +bool cmd_sync(struct client_command_context *cmd, enum mailbox_sync_flags flags, + enum imap_sync_flags imap_flags, const char *tagline) +{ + struct client *client = cmd->client; + + i_assert(client->output_cmd_lock == NULL); + + if (cmd->cancel) + return TRUE; + + cmd->stats.last_run_timeval = ioloop_timeval; + if (client->mailbox == NULL) { + /* no mailbox selected, no point in delaying the sync */ + if (tagline != NULL) + client_send_tagline(cmd, tagline); + return TRUE; + } + cmd->tagline_reply = p_strdup(cmd->pool, tagline); + + cmd->sync = p_new(cmd->pool, struct imap_client_sync_context, 1); + cmd->sync->counter = client->sync_counter; + cmd->sync->flags = flags; + cmd->sync->imap_flags = imap_flags; + cmd->sync->tagline = cmd->tagline_reply; + cmd->state = CLIENT_COMMAND_STATE_WAIT_SYNC; + + cmd->func = NULL; + cmd->context = NULL; + + if (client->input_lock == cmd) + client->input_lock = NULL; + return FALSE; +} + +static bool cmd_sync_drop_fast(struct client *client) +{ + struct client_command_context *cmd, *prev; + bool ret = FALSE; + + if (client->command_queue == NULL) + return FALSE; + + for (cmd = client->command_queue; cmd->next != NULL; cmd = cmd->next) ; + for (; cmd != NULL; cmd = prev) { + prev = cmd->next; + + if (cmd->state != CLIENT_COMMAND_STATE_WAIT_SYNC) + continue; + + i_assert(cmd->sync != NULL); + if ((cmd->sync->flags & MAILBOX_SYNC_FLAG_FAST) != 0) { + cmd_finish_sync(cmd); + client_command_free(&cmd); + ret = TRUE; + } + } + return ret; +} + +static bool cmd_sync_delayed_real(struct client *client) +{ + struct client_command_context *cmd, *first_expunge, *first_nonexpunge; + + if (client->output_cmd_lock != NULL) { + /* wait until we can send output to client */ + return FALSE; + } + + if (!imap_sync_is_allowed(client)) { + /* wait until mailbox can be synced */ + return cmd_sync_drop_fast(client); + } + + /* separate syncs that can send expunges from those that can't */ + first_expunge = first_nonexpunge = NULL; + for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) { + if (cmd->sync != NULL && + cmd->sync->counter == client->sync_counter) { + if ((cmd->sync->flags & MAILBOX_SYNC_FLAG_NO_EXPUNGES) != 0) { + if (first_nonexpunge == NULL) + first_nonexpunge = cmd; + } else { + if (first_expunge == NULL) + first_expunge = cmd; + } + } + } + if (first_expunge != NULL && first_nonexpunge != NULL) { + /* sync expunges after nonexpunges */ + for (cmd = first_expunge; cmd != NULL; cmd = cmd->next) { + if (cmd->sync != NULL && + cmd->sync->counter == client->sync_counter && + (cmd->sync->flags & + MAILBOX_SYNC_FLAG_NO_EXPUNGES) == 0) + cmd->sync->counter++; + } + first_expunge = NULL; + } + cmd = first_nonexpunge != NULL ? first_nonexpunge : first_expunge; + + if (cmd == NULL) + return cmd_sync_drop_fast(client); + i_assert(client->mailbox != NULL); + return cmd_sync_client(cmd); +} + +bool cmd_sync_delayed(struct client *client) +{ + bool ret; + + T_BEGIN { + ret = cmd_sync_delayed_real(client); + } T_END; + return ret; +} diff --git a/src/imap/imap-sync.h b/src/imap/imap-sync.h new file mode 100644 index 0000000..a1c5198 --- /dev/null +++ b/src/imap/imap-sync.h @@ -0,0 +1,25 @@ +#ifndef IMAP_SYNC_H +#define IMAP_SYNC_H + +enum imap_sync_flags { + IMAP_SYNC_FLAG_SEND_UID = 0x01, + IMAP_SYNC_FLAG_SAFE = 0x02 +}; + +struct client; + +struct imap_sync_context * +imap_sync_init(struct client *client, struct mailbox *box, + enum imap_sync_flags imap_flags, enum mailbox_sync_flags flags); +int imap_sync_deinit(struct imap_sync_context *ctx, + struct client_command_context *sync_cmd); +int imap_sync_more(struct imap_sync_context *ctx); + +/* Returns TRUE if syncing would be allowed currently. */ +bool imap_sync_is_allowed(struct client *client); + +bool cmd_sync(struct client_command_context *cmd, enum mailbox_sync_flags flags, + enum imap_sync_flags imap_flags, const char *tagline); +bool cmd_sync_delayed(struct client *client) ATTR_NOWARN_UNUSED_RESULT; + +#endif diff --git a/src/imap/mail-storage-callbacks.c b/src/imap/mail-storage-callbacks.c new file mode 100644 index 0000000..16c2d51 --- /dev/null +++ b/src/imap/mail-storage-callbacks.c @@ -0,0 +1,45 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "ostream.h" +#include "mail-storage.h" +#include "imap-commands-util.h" + +static void notify_ok(struct mailbox *mailbox ATTR_UNUSED, + const char *text, void *context) +{ + struct client *client = context; + + if (o_stream_get_buffer_used_size(client->output) != 0) + return; + + T_BEGIN { + const char *str; + + str = t_strconcat("* OK ", text, "\r\n", NULL); + o_stream_nsend_str(client->output, str); + (void)o_stream_flush(client->output); + } T_END; +} + +static void notify_no(struct mailbox *mailbox ATTR_UNUSED, + const char *text, void *context) +{ + struct client *client = context; + + if (o_stream_get_buffer_used_size(client->output) != 0) + return; + + T_BEGIN { + const char *str; + + str = t_strconcat("* NO ", text, "\r\n", NULL); + o_stream_nsend_str(client->output, str); + (void)o_stream_flush(client->output); + } T_END; +} + +struct mail_storage_callbacks mail_storage_callbacks = { + notify_ok, + notify_no +}; diff --git a/src/imap/main.c b/src/imap/main.c new file mode 100644 index 0000000..8efcfd4 --- /dev/null +++ b/src/imap/main.c @@ -0,0 +1,580 @@ +/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */ + +#include "imap-common.h" +#include "ioloop.h" +#include "istream.h" +#include "ostream.h" +#include "path-util.h" +#include "str.h" +#include "base64.h" +#include "process-title.h" +#include "randgen.h" +#include "restrict-access.h" +#include "write-full.h" +#include "settings-parser.h" +#include "master-interface.h" +#include "master-service.h" +#include "master-login.h" +#include "mail-user.h" +#include "mail-storage-service.h" +#include "smtp-submit-settings.h" +#include "imap-master-client.h" +#include "imap-resp-code.h" +#include "imap-commands.h" +#include "imap-feature.h" +#include "imap-fetch.h" + +#include <stdio.h> +#include <unistd.h> + +#define IS_STANDALONE() \ + (getenv(MASTER_IS_PARENT_ENV) == NULL) + +#define IMAP_DIE_IDLE_SECS 10 + +static bool verbose_proctitle = FALSE; +static struct mail_storage_service_ctx *storage_service; +static struct master_login *master_login = NULL; +static struct timeout *to_proctitle; + +imap_client_created_func_t *hook_client_created = NULL; +bool imap_debug = FALSE; + +struct event_category event_category_imap = { + .name = "imap", +}; + +imap_client_created_func_t * +imap_client_created_hook_set(imap_client_created_func_t *new_hook) +{ + imap_client_created_func_t *old_hook = hook_client_created; + + hook_client_created = new_hook; + return old_hook; +} + +static void imap_refresh_proctitle_callback(void *context ATTR_UNUSED) +{ + timeout_remove(&to_proctitle); + imap_refresh_proctitle(); +} + +void imap_refresh_proctitle_delayed(void) +{ + if (to_proctitle == NULL) + to_proctitle = timeout_add_short(0, + imap_refresh_proctitle_callback, NULL); +} + +void imap_refresh_proctitle(void) +{ +#define IMAP_PROCTITLE_PREFERRED_LEN 80 + struct client *client; + struct client_command_context *cmd; + string_t *title = t_str_new(128); + bool wait_output; + + if (!verbose_proctitle) + return; + + str_append_c(title, '['); + switch (imap_client_count) { + case 0: + str_append(title, "idling"); + break; + case 1: + client = imap_clients; + str_append(title, client->user->username); + if (client->user->conn.remote_ip != NULL) { + str_append_c(title, ' '); + str_append(title, + net_ip2addr(client->user->conn.remote_ip)); + } + wait_output = FALSE; + for (cmd = client->command_queue; cmd != NULL; cmd = cmd->next) { + if (cmd->name == NULL) + continue; + + if (str_len(title) < IMAP_PROCTITLE_PREFERRED_LEN) { + str_append_c(title, ' '); + str_append(title, cmd->name); + } + if (cmd->state == CLIENT_COMMAND_STATE_WAIT_OUTPUT) + wait_output = TRUE; + } + if (wait_output) { + str_printfa(title, " - %zu bytes waiting", + o_stream_get_buffer_used_size(client->output)); + if (o_stream_is_corked(client->output)) + str_append(title, " corked"); + } + if (client->destroyed) + str_append(title, " (deinit)"); + break; + default: + str_printfa(title, "%u connections", imap_client_count); + break; + } + str_append_c(title, ']'); + process_title_set(str_c(title)); +} + +static void client_kill_idle(struct client *client) +{ + if (client->output_cmd_lock != NULL) + return; + + mail_storage_service_io_activate_user(client->service_user); + client_send_line(client, "* BYE Server shutting down."); + client_destroy(client, "Server shutting down."); +} + +static void imap_die(void) +{ + struct client *client, *next; + time_t last_io, now = time(NULL); + time_t stop_timestamp = now - IMAP_DIE_IDLE_SECS; + unsigned int stop_msecs; + + for (client = imap_clients; client != NULL; client = next) { + next = client->next; + + last_io = I_MAX(client->last_input, client->last_output); + if (last_io <= stop_timestamp) + client_kill_idle(client); + else { + timeout_remove(&client->to_idle); + stop_msecs = (last_io - stop_timestamp) * 1000; + client->to_idle = timeout_add(stop_msecs, + client_kill_idle, client); + } + } +} + +struct imap_login_request { + const char *tag; + + const unsigned char *input; + size_t input_size; + bool send_untagged_capability; +}; + +static void +client_parse_imap_login_request(const unsigned char *data, size_t len, + struct imap_login_request *input_r) +{ + size_t taglen; + + i_zero(input_r); + if (len == 0) + return; + + if (data[0] == '1') + input_r->send_untagged_capability = TRUE; + data++; len--; + + input_r->tag = t_strndup(data, len); + taglen = strlen(input_r->tag) + 1; + + if (len > taglen) { + input_r->input = data + taglen; + input_r->input_size = len - taglen; + } +} + +static void +client_send_login_reply(struct ostream *output, const char *capability_string, + const char *preauth_username, + const struct imap_login_request *request) +{ + string_t *reply = t_str_new(256); + + /* cork/uncork around the OK reply to minimize latency */ + o_stream_cork(output); + if (request->tag == NULL) { + str_printfa(reply, "* PREAUTH [CAPABILITY %s] Logged in as %s\r\n", + capability_string, preauth_username); + } else if (capability_string == NULL) { + /* Client initialization failed. There's no need to send + capabilities. Just send the tagged OK so the client knows + the login itself succeeded, followed by a BYE. */ + str_printfa(reply, "%s OK Logged in, but initialization failed.\r\n", + request->tag); + str_append(reply, "* BYE "MAIL_ERRSTR_CRITICAL_MSG"\r\n"); + } else if (request->send_untagged_capability) { + /* client doesn't seem to understand tagged capabilities. send + untagged instead and hope that it works. */ + str_printfa(reply, "* CAPABILITY %s\r\n", capability_string); + str_printfa(reply, "%s OK Logged in\r\n", request->tag); + } else { + str_printfa(reply, "%s OK [CAPABILITY %s] Logged in\r\n", + request->tag, capability_string); + } + o_stream_nsend(output, str_data(reply), str_len(reply)); + if (o_stream_uncork_flush(output) < 0 && + output->stream_errno != EPIPE) + i_error("write(client) failed: %s", o_stream_get_error(output)); +} + +static void +client_add_input_finalize(struct client *client) +{ + struct ostream *output; + + /* try to condense any responses into as few packets as possible */ + output = client->output; + o_stream_ref(output); + o_stream_cork(output); + (void)client_handle_input(client); + o_stream_uncork(output); + o_stream_unref(&output); + + /* we could have already handled LOGOUT, or we might need to continue + pending ambiguous commands. */ + client_continue_pending_input(client); +} + +int client_create_from_input(const struct mail_storage_service_input *input, + int fd_in, int fd_out, + struct client **client_r, const char **error_r) +{ + struct mail_storage_service_input service_input; + struct mail_storage_service_user *user; + struct mail_user *mail_user; + struct client *client; + struct imap_settings *imap_set; + struct smtp_submit_settings *smtp_set; + struct event *event; + const char *errstr; + + event = event_create(NULL); + event_add_category(event, &event_category_imap); + event_add_fields(event, (const struct event_add_field []){ + { .key = "user", .value = input->username }, + { .key = NULL } + }); + if (input->local_ip.family != 0) + event_add_str(event, "local_ip", net_ip2addr(&input->local_ip)); + if (input->local_port != 0) + event_add_int(event, "local_port", input->local_port); + if (input->remote_ip.family != 0) + event_add_str(event, "remote_ip", net_ip2addr(&input->remote_ip)); + if (input->remote_port != 0) + event_add_int(event, "remote_port", input->remote_port); + + service_input = *input; + service_input.event_parent = event; + if (mail_storage_service_lookup_next(storage_service, &service_input, + &user, &mail_user, error_r) <= 0) { + event_unref(&event); + return -1; + } + /* Add the session only after creating the user, because + input->session_id may be NULL */ + event_add_str(event, "session", mail_user->session_id); + + restrict_access_allow_coredumps(TRUE); + + smtp_set = mail_storage_service_user_get_set(user)[1]; + imap_set = mail_storage_service_user_get_set(user)[2]; + if (imap_set->verbose_proctitle) + verbose_proctitle = TRUE; + + if (settings_var_expand(&smtp_submit_setting_parser_info, smtp_set, + mail_user->pool, mail_user_var_expand_table(mail_user), + &errstr) <= 0 || + settings_var_expand(&imap_setting_parser_info, imap_set, + mail_user->pool, mail_user_var_expand_table(mail_user), + &errstr) <= 0) { + *error_r = t_strdup_printf("Failed to expand settings: %s", errstr); + mail_user_deinit(&mail_user); + mail_storage_service_user_unref(&user); + event_unref(&event); + return -1; + } + + client = client_create(fd_in, fd_out, + event, mail_user, user, imap_set, smtp_set); + client->userdb_fields = input->userdb_fields == NULL ? NULL : + p_strarray_dup(client->pool, input->userdb_fields); + event_unref(&event); + *client_r = client; + return 0; +} + +static void main_stdio_run(const char *username) +{ + struct client *client; + struct mail_storage_service_input input; + struct imap_login_request request; + const char *value, *error, *input_base64; + + i_zero(&input); + input.module = input.service = "imap"; + input.username = username != NULL ? username : getenv("USER"); + if (input.username == NULL && IS_STANDALONE()) + input.username = getlogin(); + if (input.username == NULL) + i_fatal("USER environment missing"); + if ((value = getenv("IP")) != NULL) + (void)net_addr2ip(value, &input.remote_ip); + if ((value = getenv("LOCAL_IP")) != NULL) + (void)net_addr2ip(value, &input.local_ip); + + if (client_create_from_input(&input, STDIN_FILENO, STDOUT_FILENO, + &client, &error) < 0) + i_fatal("%s", error); + + input_base64 = getenv("CLIENT_INPUT"); + if (input_base64 == NULL) { + /* IMAPLOGINTAG environment is compatible with mailfront */ + i_zero(&request); + request.tag = getenv("IMAPLOGINTAG"); + } else { + const buffer_t *input_buf = t_base64_decode_str(input_base64); + client_parse_imap_login_request(input_buf->data, input_buf->used, + &request); + if (request.input_size > 0) { + client_add_istream_prefix(client, request.input, + request.input_size); + } + } + + client_create_finish_io(client); + client_send_login_reply(client->output, + str_c(client->capability_string), + client->user->username, &request); + if (client_create_finish(client, &error) < 0) + i_fatal("%s", error); + client_add_input_finalize(client); + /* client may be destroyed now */ +} + +static void +login_client_connected(const struct master_login_client *login_client, + const char *username, const char *const *extra_fields) +{ +#define MSG_BYE_INTERNAL_ERROR "* BYE "MAIL_ERRSTR_CRITICAL_MSG"\r\n" + struct mail_storage_service_input input; + struct client *client; + struct imap_login_request request; + enum mail_auth_request_flags flags = login_client->auth_req.flags; + const char *error; + + i_zero(&input); + input.module = input.service = "imap"; + input.local_ip = login_client->auth_req.local_ip; + input.remote_ip = login_client->auth_req.remote_ip; + input.local_port = login_client->auth_req.local_port; + input.remote_port = login_client->auth_req.remote_port; + input.username = username; + input.userdb_fields = extra_fields; + input.session_id = login_client->session_id; + if ((flags & MAIL_AUTH_REQUEST_FLAG_CONN_SECURED) != 0) + input.conn_secured = TRUE; + if ((flags & MAIL_AUTH_REQUEST_FLAG_CONN_SSL_SECURED) != 0) + input.conn_ssl_secured = TRUE; + + client_parse_imap_login_request(login_client->data, + login_client->auth_req.data_size, + &request); + + if (client_create_from_input(&input, login_client->fd, login_client->fd, + &client, &error) < 0) { + int fd = login_client->fd; + struct ostream *output = + o_stream_create_fd_autoclose(&fd, IO_BLOCK_SIZE); + client_send_login_reply(output, NULL, NULL, &request); + o_stream_destroy(&output); + + i_error("%s", error); + master_service_client_connection_destroyed(master_service); + return; + } + if ((flags & MAIL_AUTH_REQUEST_FLAG_TLS_COMPRESSION) != 0) + client->tls_compression = TRUE; + if (request.input_size > 0) { + client_add_istream_prefix(client, request.input, + request.input_size); + } + + /* The order here is important: + 1. Finish setting up rawlog, so all input/output is written there. + 2. Send tagged reply to login before any potentially long-running + work (during which client could disconnect due to timeout). + 3. Finish initializing user, which can potentially take a long time. + */ + client_create_finish_io(client); + client_send_login_reply(client->output, + str_c(client->capability_string), + NULL, &request); + if (client_create_finish(client, &error) < 0) { + if (write_full(login_client->fd, MSG_BYE_INTERNAL_ERROR, + strlen(MSG_BYE_INTERNAL_ERROR)) < 0) + if (errno != EAGAIN && errno != EPIPE) + e_error(client->event, + "write_full(client) failed: %m"); + + e_error(client->event, "%s", error); + client_destroy(client, error); + return; + } + + client_add_input_finalize(client); + /* client may be destroyed now */ +} + +static void login_client_failed(const struct master_login_client *client, + const char *errormsg) +{ + struct imap_login_request request; + const char *msg; + + client_parse_imap_login_request(client->data, + client->auth_req.data_size, &request); + msg = t_strdup_printf("%s NO ["IMAP_RESP_CODE_UNAVAILABLE"] %s\r\n", + request.tag, errormsg); + if (write(client->fd, msg, strlen(msg)) < 0) { + /* ignored */ + } +} + +static void client_connected(struct master_service_connection *conn) +{ + /* when running standalone, we shouldn't even get here */ + i_assert(master_login != NULL); + + master_service_client_connection_accept(conn); + if (strcmp(conn->name, "imap-master") == 0) { + /* restoring existing IMAP connection (e.g. from imap-idle) */ + imap_master_client_create(conn->fd); + } else { + master_login_add(master_login, conn->fd); + } +} + +int main(int argc, char *argv[]) +{ + static const struct setting_parser_info *set_roots[] = { + &smtp_submit_setting_parser_info, + &imap_setting_parser_info, + NULL + }; + struct master_login_settings login_set; + enum master_service_flags service_flags = 0; + enum mail_storage_service_flags storage_service_flags = + MAIL_STORAGE_SERVICE_FLAG_NO_SSL_CA | + /* + * We include MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES so + * that the mail_user initialization is fast and we can + * quickly send back the OK response to LOGIN/AUTHENTICATE. + * Otherwise we risk a very slow namespace initialization to + * cause client timeouts on login. + */ + MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES; + const char *username = NULL, *auth_socket_path = "auth-master"; + int c; + + i_zero(&login_set); + login_set.postlogin_timeout_secs = MASTER_POSTLOGIN_TIMEOUT_DEFAULT; + login_set.request_auth_token = TRUE; + + if (IS_STANDALONE() && getuid() == 0 && + net_getpeername(1, NULL, NULL) == 0) { + printf("* BAD [ALERT] imap binary must not be started from " + "inetd, use imap-login instead.\n"); + return 1; + } + + if (IS_STANDALONE()) { + service_flags |= MASTER_SERVICE_FLAG_STANDALONE | + MASTER_SERVICE_FLAG_STD_CLIENT; + } else { + service_flags |= MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN; + } + + master_service = master_service_init("imap", service_flags, + &argc, &argv, "a:Dt:u:"); + while ((c = master_getopt(master_service)) > 0) { + switch (c) { + case 'a': + auth_socket_path = optarg; + break; + case 't': + if (str_to_uint(optarg, &login_set.postlogin_timeout_secs) < 0 || + login_set.postlogin_timeout_secs == 0) + i_fatal("Invalid -t parameter: %s", optarg); + break; + case 'u': + storage_service_flags |= + MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP; + username = optarg; + break; + case 'D': + imap_debug = TRUE; + break; + default: + return FATAL_DEFAULT; + } + } + + master_service_set_die_callback(master_service, imap_die); + + /* plugins may want to add commands, so this needs to be called early */ + commands_init(); + imap_fetch_handlers_init(); + imap_features_init(); + clients_init(); + imap_master_clients_init(); + + const char *error; + if (t_abspath(auth_socket_path, &login_set.auth_socket_path, &error) < 0) + i_fatal("t_abspath(%s) failed: %s", auth_socket_path, error); + + if (argv[optind] != NULL) { + if (t_abspath(argv[optind], &login_set.postlogin_socket_path, &error) < 0) + i_fatal("t_abspath(%s) failed: %s", argv[optind], error); + } + login_set.callback = login_client_connected; + login_set.failure_callback = login_client_failed; + + if (!IS_STANDALONE()) + master_login = master_login_init(master_service, &login_set); + + storage_service = + mail_storage_service_init(master_service, + set_roots, storage_service_flags); + master_service_init_finish(master_service); + /* NOTE: login_set.*_socket_path are now invalid due to data stack + having been freed */ + + /* fake that we're running, so we know if client was destroyed + while handling its initial input */ + io_loop_set_running(current_ioloop); + + if (IS_STANDALONE()) { + T_BEGIN { + main_stdio_run(username); + } T_END; + } else { + io_loop_set_running(current_ioloop); + } + + if (io_loop_is_running(current_ioloop)) + master_service_run(master_service, client_connected); + clients_destroy_all(); + + if (master_login != NULL) + master_login_deinit(&master_login); + mail_storage_service_deinit(&storage_service); + + imap_fetch_handlers_deinit(); + imap_features_deinit(); + + commands_deinit(); + imap_master_clients_deinit(); + + timeout_remove(&to_proctitle); + master_service_deinit(&master_service); + return 0; +} diff --git a/src/imap/test-imap-client-hibernate.c b/src/imap/test-imap-client-hibernate.c new file mode 100644 index 0000000..6136019 --- /dev/null +++ b/src/imap/test-imap-client-hibernate.c @@ -0,0 +1,291 @@ +/* Copyright (c) 2020 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "test-common.h" +#include "test-subprocess.h" +#include "istream.h" +#include "istream-unix.h" +#include "strescape.h" +#include "path-util.h" +#include "unlink-directory.h" +#include "settings-parser.h" +#include "master-service.h" +#include "smtp-submit.h" +#include "mail-storage-service.h" +#include "mail-storage-private.h" +#include "imap-common.h" +#include "imap-settings.h" +#include "imap-client.h" + +#include <sys/stat.h> + +#define TEMP_DIRNAME ".test-imap-client-hibernate" + +#define EVILSTR "\t\r\n\001" + +struct test_imap_client_hibernate { + struct client *client; + int fd_listen; + bool has_mailbox; + const char *reply; +}; + +imap_client_created_func_t *hook_client_created = NULL; +bool imap_debug = FALSE; + +static const char *tmpdir; +static struct mail_storage_service_ctx *storage_service; + +void imap_refresh_proctitle(void) { } +void imap_refresh_proctitle_delayed(void) { } +int client_create_from_input(const struct mail_storage_service_input *input ATTR_UNUSED, + int fd_in ATTR_UNUSED, int fd_out ATTR_UNUSED, + struct client **client_r ATTR_UNUSED, + const char **error_r ATTR_UNUSED) { return -1; } + +static int imap_hibernate_server(struct test_imap_client_hibernate *ctx) +{ + i_set_failure_prefix("SERVER: "); + + int fd = net_accept(ctx->fd_listen, NULL, NULL); + i_assert(fd > 0); + struct istream *input = i_stream_create_unix(fd, SIZE_MAX); + i_stream_unix_set_read_fd(input); + + /* send handshake */ + const char *str = "VERSION\timap-hibernate\t1\t0\n"; + if (write(fd, str, strlen(str)) != (ssize_t)strlen(str)) + i_fatal("write(imap-hibernate client handshake) failed: %m"); + + /* read handshake */ + const char *line; + if ((line = i_stream_read_next_line(input)) == NULL) + i_fatal("read(imap-hibernate client handshake) failed: %s", + i_stream_get_error(input)); + if (strcmp(line, "VERSION\timap-hibernate\t1\t0") != 0) + i_fatal("VERSION not received"); + /* read command */ + if ((line = i_stream_read_next_line(input)) == NULL) + i_fatal("read(imap-hibernate client command) failed: %s", + i_stream_get_error(input)); + int fd2 = i_stream_unix_get_read_fd(input); + test_assert(fd2 != -1); + i_close_fd(&fd2); + const char *const *args = t_strsplit_tabescaped(line); + + /* write reply */ + if (write(fd, ctx->reply, strlen(ctx->reply)) != (ssize_t)strlen(ctx->reply)) + i_fatal("write(imap-hibernate client command) failed: %m"); + + if (ctx->has_mailbox) { + /* read mailbox notify fd */ + i_stream_unix_set_read_fd(input); + if (i_stream_read_next_line(input) == NULL) + i_fatal("read(imap-hibernate notify fd) failed: %s", + i_stream_get_error(input)); + + fd2 = i_stream_unix_get_read_fd(input); + test_assert(fd2 != -1); + i_close_fd(&fd2); + + if (write(fd, "+\n", 2) != 2) + i_fatal("write(imap-hibernate client command) failed: %m"); + } + + unsigned int i = 0; + test_assert_strcmp(args[i++], EVILSTR"testuser"); + test_assert_strcmp(args[i++], EVILSTR"%u"); + test_assert_strcmp(args[i++], "idle_notify_interval=120"); + test_assert(str_begins(args[i++], "peer_dev_major=")); + test_assert(str_begins(args[i++], "peer_dev_minor=")); + test_assert(str_begins(args[i++], "peer_ino=")); + test_assert_strcmp(args[i++], "session="EVILSTR"session"); + test_assert(str_begins(args[i++], "session_created=")); + test_assert_strcmp(args[i++], "lip=127.0.0.1"); + test_assert_strcmp(args[i++], "lport=1234"); + test_assert_strcmp(args[i++], "rip=127.0.0.2"); + test_assert_strcmp(args[i++], "rport=5678"); + test_assert(str_begins(args[i++], "uid=")); + test_assert(str_begins(args[i++], "gid=")); + if (ctx->has_mailbox) + test_assert_strcmp(args[i++], "mailbox="EVILSTR"mailbox"); + test_assert_strcmp(args[i++], "tag="EVILSTR"tag"); + test_assert(str_begins(args[i++], "stats=")); + test_assert_strcmp(args[i++], "idle-cmd"); + if (ctx->has_mailbox) + test_assert_strcmp(args[i++], "notify_fd"); + test_assert(str_begins(args[i++], "state=")); + test_assert(args[i] == NULL); + + i_stream_unref(&input); + i_close_fd(&ctx->fd_listen); + i_close_fd(&fd); + + ctx->client->hibernated = TRUE; /* prevent disconnect Info message */ + client_destroy(ctx->client, NULL); + + mail_storage_service_deinit(&storage_service); + master_service_deinit_forked(&master_service); + return 0; +} + +static void +mailbox_notify_callback(struct mailbox *box ATTR_UNUSED, + struct client *client ATTR_UNUSED) +{ +} + +static void test_imap_client_hibernate(void) +{ + struct client *client; + struct smtp_submit_settings smtp_set; + struct mail_storage_service_user *service_user; + struct mail_user *mail_user; + struct test_imap_client_hibernate ctx; + const char *error; + + storage_service = mail_storage_service_init(master_service, NULL, + MAIL_STORAGE_SERVICE_FLAG_ALLOW_ROOT | + MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT | + MAIL_STORAGE_SERVICE_FLAG_NO_CHDIR | + MAIL_STORAGE_SERVICE_FLAG_NO_RESTRICT_ACCESS); + + const char *const input_userdb[] = { + "mailbox_list_index=no", + t_strdup_printf("mail=mbox:%s/mbox", tmpdir), + NULL + }; + struct mail_storage_service_input input = { + .username = EVILSTR"testuser", + .local_port = 1234, + .remote_port = 5678, + .userdb_fields = input_userdb, + }; + test_assert(net_addr2ip("127.0.0.1", &input.local_ip) == 0); + test_assert(net_addr2ip("127.0.0.2", &input.remote_ip) == 0); + test_assert(mail_storage_service_lookup_next(storage_service, &input, + &service_user, &mail_user, &error) == 1); + mail_user->set->base_dir = tmpdir; + mail_user->set->mail_log_prefix = EVILSTR"%u"; + mail_user->session_id = EVILSTR"session"; + i_zero(&smtp_set); + i_zero(&ctx); + + struct event *event = event_create(NULL); + int client_fd = dup(dev_null_fd); + client = client_create(client_fd, client_fd, event, + mail_user, service_user, + imap_setting_parser_info.defaults, &smtp_set); + ctx.client = client; + + /* can't hibernate without IDLE */ + test_begin("imap client hibernate: non-IDLE"); + test_assert(!imap_client_hibernate(&client, &error)); + test_assert_strcmp(error, "Non-IDLE connections not supported currently"); + test_end(); + + struct client_command_context *cmd = client_command_alloc(client); + cmd->tag = EVILSTR"tag"; + cmd->name = "IDLE"; + event_unref(&event); + + /* imap-hibernate socket doesn't exist */ + test_begin("imap client hibernate: socket not found"); + test_expect_error_string("/"TEMP_DIRNAME"/imap-hibernate) failed: No such file or directory"); + test_assert(!imap_client_hibernate(&client, &error)); + test_expect_no_more_errors(); + test_assert(strstr(error, "net_connect_unix") != NULL); + test_end(); + + /* imap-hibernate socket times out */ + const char *socket_path = t_strdup_printf("%s/imap-hibernate", tmpdir); + ctx.fd_listen = net_listen_unix(socket_path, 1); + if (ctx.fd_listen == -1) + i_fatal("net_listen_unix(%s) failed: %m", socket_path); + fd_set_nonblock(ctx.fd_listen, FALSE); + + /* imap-hibernate socket returns failure */ + test_begin("imap client hibernate: error returned"); + ctx.reply = "-notgood\n"; + test_subprocess_fork(imap_hibernate_server, &ctx, FALSE); + + test_expect_error_string(TEMP_DIRNAME"/imap-hibernate returned failure: notgood"); + test_assert(!imap_client_hibernate(&client, &error)); + test_expect_no_more_errors(); + test_assert(strstr(error, "notgood") != NULL); + test_end(); + + /* create and open evil mailbox */ + client->mailbox = mailbox_alloc(client->user->namespaces->list, + "testbox", 0); + struct mailbox_update update = { + .uid_validity = 12345678, + }; + memset(update.mailbox_guid, 0x12, sizeof(update.mailbox_guid)); + test_assert(mailbox_create(client->mailbox, &update, FALSE) == 0); + test_assert(mailbox_open(client->mailbox) == 0); + client->mailbox->vname = EVILSTR"mailbox"; + + /* successful hibernation */ + test_begin("imap client hibernate: success"); + ctx.reply = "+\n"; + ctx.has_mailbox = TRUE; + test_subprocess_fork(imap_hibernate_server, &ctx, FALSE); + /* start notification only after forking or we'll have trouble + deinitializing cleanly */ + mailbox_notify_changes(client->mailbox, mailbox_notify_callback, client); + test_assert(imap_client_hibernate(&client, &error)); + test_end(); + + i_close_fd(&ctx.fd_listen); + mail_storage_service_deinit(&storage_service); +} + +static void test_cleanup(void) +{ + const char *error; + + if (unlink_directory(tmpdir, UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0) + i_error("unlink_directory() failed: %s", error); +} + +static void test_init(void) +{ + const char *cwd, *error; + + test_assert(t_get_working_dir(&cwd, &error) == 0); + tmpdir = t_strconcat(cwd, "/"TEMP_DIRNAME, NULL); + + test_cleanup(); + if (mkdir(tmpdir, 0700) < 0) + i_fatal("mkdir() failed: %m"); + + test_subprocesses_init(FALSE); +} + +int main(int argc, char *argv[]) +{ + const enum master_service_flags service_flags = + MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS | + MASTER_SERVICE_FLAG_STANDALONE | + MASTER_SERVICE_FLAG_STD_CLIENT | + MASTER_SERVICE_FLAG_DONT_SEND_STATS; + int ret; + + master_service = master_service_init("test-imap-client-hibernate", + service_flags, &argc, &argv, "D"); + + master_service_init_finish(master_service); + test_init(); + + static void (*const test_functions[])(void) = { + test_imap_client_hibernate, + NULL + }; + ret = test_run(test_functions); + + test_subprocesses_deinit(); + test_cleanup(); + master_service_deinit(&master_service); + return ret; +} |