diff options
Diffstat (limited to 'src/plugins/quota')
-rw-r--r-- | src/plugins/quota/Makefile.am | 144 | ||||
-rw-r--r-- | src/plugins/quota/Makefile.in | 1179 | ||||
-rw-r--r-- | src/plugins/quota/doveadm-quota.c | 165 | ||||
-rw-r--r-- | src/plugins/quota/quota-count.c | 400 | ||||
-rw-r--r-- | src/plugins/quota/quota-dict.c | 269 | ||||
-rw-r--r-- | src/plugins/quota/quota-dirsize.c | 232 | ||||
-rw-r--r-- | src/plugins/quota/quota-fs.c | 970 | ||||
-rw-r--r-- | src/plugins/quota/quota-fs.h | 51 | ||||
-rw-r--r-- | src/plugins/quota/quota-imapc.c | 494 | ||||
-rw-r--r-- | src/plugins/quota/quota-maildir.c | 953 | ||||
-rw-r--r-- | src/plugins/quota/quota-plugin.c | 31 | ||||
-rw-r--r-- | src/plugins/quota/quota-plugin.h | 36 | ||||
-rw-r--r-- | src/plugins/quota/quota-private.h | 230 | ||||
-rw-r--r-- | src/plugins/quota/quota-status-settings.c | 37 | ||||
-rw-r--r-- | src/plugins/quota/quota-status-settings.h | 10 | ||||
-rw-r--r-- | src/plugins/quota/quota-status.c | 360 | ||||
-rw-r--r-- | src/plugins/quota/quota-storage.c | 780 | ||||
-rw-r--r-- | src/plugins/quota/quota-util.c | 465 | ||||
-rw-r--r-- | src/plugins/quota/quota.c | 1543 | ||||
-rw-r--r-- | src/plugins/quota/quota.h | 147 | ||||
-rw-r--r-- | src/plugins/quota/rquota.x | 139 | ||||
-rw-r--r-- | src/plugins/quota/test-quota-util.c | 96 |
22 files changed, 8731 insertions, 0 deletions
diff --git a/src/plugins/quota/Makefile.am b/src/plugins/quota/Makefile.am new file mode 100644 index 0000000..e8bad8c --- /dev/null +++ b/src/plugins/quota/Makefile.am @@ -0,0 +1,144 @@ +doveadm_moduledir = $(moduledir)/doveadm + +pkglibexecdir = $(libexecdir)/dovecot +pkglibexec_PROGRAMS = quota-status + +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-test \ + -I$(top_srcdir)/src/lib-master \ + -I$(top_srcdir)/src/lib-dict \ + -I$(top_srcdir)/src/lib-index \ + -I$(top_srcdir)/src/lib-mail \ + -I$(top_srcdir)/src/lib-smtp \ + -I$(top_srcdir)/src/lib-imap \ + -I$(top_srcdir)/src/lib-imap-client \ + -I$(top_srcdir)/src/lib-settings \ + -I$(top_srcdir)/src/lib-ssl-iostream \ + -I$(top_srcdir)/src/lib-storage \ + -I$(top_srcdir)/src/lib-storage/index \ + -I$(top_srcdir)/src/lib-storage/index/imapc \ + -I$(top_srcdir)/src/lib-storage/index/maildir \ + -I$(top_srcdir)/src/lib-program-client \ + -I$(top_srcdir)/src/doveadm \ + $(LIBTIRPC_CFLAGS) + +NOPLUGIN_LDFLAGS = +lib10_doveadm_quota_plugin_la_LDFLAGS = -module -avoid-version +lib10_quota_plugin_la_LDFLAGS = -module -avoid-version + +module_LTLIBRARIES = \ + lib10_quota_plugin.la + +quota_dist_sources = \ + quota.c \ + quota-count.c \ + quota-fs.c \ + quota-dict.c \ + quota-dirsize.c \ + quota-imapc.c \ + quota-maildir.c \ + quota-plugin.c \ + quota-storage.c \ + quota-util.c + +quota_common_objects = \ + quota.lo \ + quota-count.lo \ + quota-fs.lo \ + quota-dict.lo \ + quota-dirsize.lo \ + quota-imapc.lo \ + quota-maildir.lo \ + quota-plugin.lo \ + quota-storage.lo \ + quota-util.lo \ + $(RQUOTA_XDR_LO) + +lib10_quota_plugin_la_SOURCES = $(quota_dist_sources) +nodist_lib10_quota_plugin_la_SOURCES = $(RQUOTA_XDR) +lib10_quota_plugin_la_LIBADD = $(QUOTA_LIBS) + +doveadm_module_LTLIBRARIES = \ + lib10_doveadm_quota_plugin.la + +lib10_doveadm_quota_plugin_la_SOURCES = \ + doveadm-quota.c + +quota_status_SOURCES = \ + quota-status.c \ + quota-status-settings.c + +quota_status_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS) +quota_status_LDADD = \ + $(quota_common_objects) \ + $(LIBDOVECOT_STORAGE) \ + $(LIBDOVECOT) \ + $(QUOTA_LIBS) \ + $(BINARY_LDFLAGS) +quota_status_DEPENDENCIES = \ + $(quota_common_objects) \ + $(LIBDOVECOT_STORAGE_DEPS) \ + $(LIBDOVECOT_DEPS) + +if HAVE_RQUOTA +RQUOTA_XDR = rquota_xdr.c +RQUOTA_XDR_LO = rquota_xdr.lo +#RQUOTA_X = /usr/include/rpcsvc/rquota.x +RQUOTA_X = $(srcdir)/rquota.x +rquota_xdr.c: Makefile rquota.h + if [ "$(top_srcdir)" != "$(top_builddir)" ]; then \ + cp $(RQUOTA_X) $(top_builddir)/src/plugins/quota/; \ + fi; \ + (echo '#include "lib.h"'; \ + echo '#undef FALSE'; \ + echo '#undef TRUE'; \ + echo '#include <rpc/rpc.h>'; \ + $(RPCGEN) -c $(top_builddir)/src/plugins/quota/rquota.x | \ + sed -e 's/IXDR_PUT/(void)IXDR_PUT/g' \ + -e 's,!xdr_,0 == xdr_,' \ + -e 's,/usr/include/rpcsvc/rquota.h,rquota.h,' \ + -e 's/int32_t \*buf/int32_t *buf ATTR_UNUSED/' \ + -e 's/^static char rcsid.*//' ) > rquota_xdr.c + +rquota.h: Makefile $(RQUOTA_X) + $(RPCGEN) -h $(RQUOTA_X) > rquota.h + +quota-fs.lo: rquota.h + +endif + +pkginc_libdir=$(pkgincludedir) +pkginc_lib_HEADERS = \ + quota.h \ + quota-fs.h \ + quota-plugin.h \ + quota-private.h +noinst_HEADERS = \ + quota-status-settings.h + +EXTRA_DIST = rquota.x + +clean-generic: + if [ "$(top_srcdir)" != "$(top_builddir)" ]; then \ + rm -f $(top_builddir)/src/plugins/quota/rquota.x; \ + fi; \ + rm -f rquota_xdr.c rquota.h + +test_programs = \ + test-quota-util +noinst_PROGRAMS = $(test_programs) + +test_libs = \ + ../../lib-test/libtest.la \ + ../../lib/liblib.la +test_deps = $(noinst_LTLIBRARIES) $(test_libs) + +test_quota_util_SOURCES = test-quota-util.c +test_quota_util_LDADD = quota-util.lo $(test_libs) +test_quota_util_DEPENDENCIES = quota-util.lo $(test_deps) + +check-local: + for bin in $(test_programs); do \ + if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \ + done diff --git a/src/plugins/quota/Makefile.in b/src/plugins/quota/Makefile.in new file mode 100644 index 0000000..a8a824f --- /dev/null +++ b/src/plugins/quota/Makefile.in @@ -0,0 +1,1179 @@ +# 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 = quota-status$(EXEEXT) +noinst_PROGRAMS = $(am__EXEEXT_1) +subdir = src/plugins/quota +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/m4/ac_checktype2.m4 \ + $(top_srcdir)/m4/ac_typeof.m4 $(top_srcdir)/m4/arc4random.m4 \ + $(top_srcdir)/m4/blockdev.m4 $(top_srcdir)/m4/c99_vsnprintf.m4 \ + $(top_srcdir)/m4/clock_gettime.m4 $(top_srcdir)/m4/crypt.m4 \ + $(top_srcdir)/m4/crypt_xpg6.m4 $(top_srcdir)/m4/dbqlk.m4 \ + $(top_srcdir)/m4/dirent_dtype.m4 $(top_srcdir)/m4/dovecot.m4 \ + $(top_srcdir)/m4/fd_passing.m4 $(top_srcdir)/m4/fdatasync.m4 \ + $(top_srcdir)/m4/flexible_array_member.m4 \ + $(top_srcdir)/m4/glibc.m4 $(top_srcdir)/m4/gmtime_max.m4 \ + $(top_srcdir)/m4/gmtime_tm_gmtoff.m4 \ + $(top_srcdir)/m4/ioloop.m4 $(top_srcdir)/m4/iovec.m4 \ + $(top_srcdir)/m4/ipv6.m4 $(top_srcdir)/m4/libcap.m4 \ + $(top_srcdir)/m4/libtool.m4 $(top_srcdir)/m4/libwrap.m4 \ + $(top_srcdir)/m4/linux_mremap.m4 $(top_srcdir)/m4/ltoptions.m4 \ + $(top_srcdir)/m4/ltsugar.m4 $(top_srcdir)/m4/ltversion.m4 \ + $(top_srcdir)/m4/lt~obsolete.m4 $(top_srcdir)/m4/mmap_write.m4 \ + $(top_srcdir)/m4/mntctl.m4 $(top_srcdir)/m4/modules.m4 \ + $(top_srcdir)/m4/notify.m4 $(top_srcdir)/m4/nsl.m4 \ + $(top_srcdir)/m4/off_t_max.m4 $(top_srcdir)/m4/pkg.m4 \ + $(top_srcdir)/m4/pr_set_dumpable.m4 \ + $(top_srcdir)/m4/q_quotactl.m4 $(top_srcdir)/m4/quota.m4 \ + $(top_srcdir)/m4/random.m4 $(top_srcdir)/m4/rlimit.m4 \ + $(top_srcdir)/m4/sendfile.m4 $(top_srcdir)/m4/size_t_signed.m4 \ + $(top_srcdir)/m4/sockpeercred.m4 $(top_srcdir)/m4/sql.m4 \ + $(top_srcdir)/m4/ssl.m4 $(top_srcdir)/m4/st_tim.m4 \ + $(top_srcdir)/m4/static_array.m4 $(top_srcdir)/m4/test_with.m4 \ + $(top_srcdir)/m4/time_t.m4 $(top_srcdir)/m4/typeof.m4 \ + $(top_srcdir)/m4/typeof_dev_t.m4 \ + $(top_srcdir)/m4/uoff_t_max.m4 $(top_srcdir)/m4/vararg.m4 \ + $(top_srcdir)/m4/want_apparmor.m4 \ + $(top_srcdir)/m4/want_bsdauth.m4 \ + $(top_srcdir)/m4/want_bzlib.m4 \ + $(top_srcdir)/m4/want_cassandra.m4 \ + $(top_srcdir)/m4/want_cdb.m4 \ + $(top_srcdir)/m4/want_checkpassword.m4 \ + $(top_srcdir)/m4/want_clucene.m4 $(top_srcdir)/m4/want_db.m4 \ + $(top_srcdir)/m4/want_gssapi.m4 $(top_srcdir)/m4/want_icu.m4 \ + $(top_srcdir)/m4/want_ldap.m4 $(top_srcdir)/m4/want_lua.m4 \ + $(top_srcdir)/m4/want_lz4.m4 $(top_srcdir)/m4/want_lzma.m4 \ + $(top_srcdir)/m4/want_mysql.m4 $(top_srcdir)/m4/want_pam.m4 \ + $(top_srcdir)/m4/want_passwd.m4 $(top_srcdir)/m4/want_pgsql.m4 \ + $(top_srcdir)/m4/want_prefetch.m4 \ + $(top_srcdir)/m4/want_shadow.m4 \ + $(top_srcdir)/m4/want_sodium.m4 $(top_srcdir)/m4/want_solr.m4 \ + $(top_srcdir)/m4/want_sqlite.m4 \ + $(top_srcdir)/m4/want_stemmer.m4 \ + $(top_srcdir)/m4/want_systemd.m4 \ + $(top_srcdir)/m4/want_textcat.m4 \ + $(top_srcdir)/m4/want_unwind.m4 $(top_srcdir)/m4/want_zlib.m4 \ + $(top_srcdir)/m4/want_zstd.m4 $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +DIST_COMMON = $(srcdir)/Makefile.am $(noinst_HEADERS) \ + $(pkginc_lib_HEADERS) $(am__DIST_COMMON) +mkinstalldirs = $(install_sh) -d +CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +am__EXEEXT_1 = test-quota-util$(EXEEXT) +am__installdirs = "$(DESTDIR)$(pkglibexecdir)" \ + "$(DESTDIR)$(doveadm_moduledir)" "$(DESTDIR)$(moduledir)" \ + "$(DESTDIR)$(pkginc_libdir)" +PROGRAMS = $(noinst_PROGRAMS) $(pkglibexec_PROGRAMS) +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__uninstall_files_from_dir = { \ + test -z "$$files" \ + || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ + || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ + $(am__cd) "$$dir" && rm -f $$files; }; \ + } +LTLIBRARIES = $(doveadm_module_LTLIBRARIES) $(module_LTLIBRARIES) +lib10_doveadm_quota_plugin_la_LIBADD = +am_lib10_doveadm_quota_plugin_la_OBJECTS = doveadm-quota.lo +lib10_doveadm_quota_plugin_la_OBJECTS = \ + $(am_lib10_doveadm_quota_plugin_la_OBJECTS) +AM_V_lt = $(am__v_lt_@AM_V@) +am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@) +am__v_lt_0 = --silent +am__v_lt_1 = +lib10_doveadm_quota_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) \ + $(lib10_doveadm_quota_plugin_la_LDFLAGS) $(LDFLAGS) -o $@ +am__DEPENDENCIES_1 = +lib10_quota_plugin_la_DEPENDENCIES = $(am__DEPENDENCIES_1) +am__objects_1 = quota.lo quota-count.lo quota-fs.lo quota-dict.lo \ + quota-dirsize.lo quota-imapc.lo quota-maildir.lo \ + quota-plugin.lo quota-storage.lo quota-util.lo +am_lib10_quota_plugin_la_OBJECTS = $(am__objects_1) +@HAVE_RQUOTA_TRUE@am__objects_2 = rquota_xdr.lo +nodist_lib10_quota_plugin_la_OBJECTS = $(am__objects_2) +lib10_quota_plugin_la_OBJECTS = $(am_lib10_quota_plugin_la_OBJECTS) \ + $(nodist_lib10_quota_plugin_la_OBJECTS) +lib10_quota_plugin_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CCLD) \ + $(AM_CFLAGS) $(CFLAGS) $(lib10_quota_plugin_la_LDFLAGS) \ + $(LDFLAGS) -o $@ +am_quota_status_OBJECTS = quota_status-quota-status.$(OBJEXT) \ + quota_status-quota-status-settings.$(OBJEXT) +quota_status_OBJECTS = $(am_quota_status_OBJECTS) +am_test_quota_util_OBJECTS = test-quota-util.$(OBJEXT) +test_quota_util_OBJECTS = $(am_test_quota_util_OBJECTS) +AM_V_P = $(am__v_P_@AM_V@) +am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) +am__v_P_0 = false +am__v_P_1 = : +AM_V_GEN = $(am__v_GEN_@AM_V@) +am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) +am__v_GEN_0 = @echo " GEN " $@; +am__v_GEN_1 = +AM_V_at = $(am__v_at_@AM_V@) +am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) +am__v_at_0 = @ +am__v_at_1 = +DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir) +depcomp = $(SHELL) $(top_srcdir)/depcomp +am__maybe_remake_depfiles = depfiles +am__depfiles_remade = ./$(DEPDIR)/doveadm-quota.Plo \ + ./$(DEPDIR)/quota-count.Plo ./$(DEPDIR)/quota-dict.Plo \ + ./$(DEPDIR)/quota-dirsize.Plo ./$(DEPDIR)/quota-fs.Plo \ + ./$(DEPDIR)/quota-imapc.Plo ./$(DEPDIR)/quota-maildir.Plo \ + ./$(DEPDIR)/quota-plugin.Plo ./$(DEPDIR)/quota-storage.Plo \ + ./$(DEPDIR)/quota-util.Plo ./$(DEPDIR)/quota.Plo \ + ./$(DEPDIR)/quota_status-quota-status-settings.Po \ + ./$(DEPDIR)/quota_status-quota-status.Po \ + ./$(DEPDIR)/rquota_xdr.Plo ./$(DEPDIR)/test-quota-util.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 = $(lib10_doveadm_quota_plugin_la_SOURCES) \ + $(lib10_quota_plugin_la_SOURCES) \ + $(nodist_lib10_quota_plugin_la_SOURCES) \ + $(quota_status_SOURCES) $(test_quota_util_SOURCES) +DIST_SOURCES = $(lib10_doveadm_quota_plugin_la_SOURCES) \ + $(lib10_quota_plugin_la_SOURCES) $(quota_status_SOURCES) \ + $(test_quota_util_SOURCES) +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +HEADERS = $(noinst_HEADERS) $(pkginc_lib_HEADERS) +am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) +# Read a list of newline-separated strings from the standard input, +# and print each of them once, without duplicates. Input order is +# *not* preserved. +am__uniquify_input = $(AWK) '\ + BEGIN { nonempty = 0; } \ + { items[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in items) print i; }; } \ +' +# Make sure the list of sources is unique. This is necessary because, +# e.g., the same source file might be shared among _SOURCES variables +# for different programs/libraries. +am__define_uniq_tagged_files = \ + list='$(am__tagged_files)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | $(am__uniquify_input)` +ETAGS = etags +CTAGS = ctags +am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +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 = +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@ +doveadm_moduledir = $(moduledir)/doveadm +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/lib \ + -I$(top_srcdir)/src/lib-test \ + -I$(top_srcdir)/src/lib-master \ + -I$(top_srcdir)/src/lib-dict \ + -I$(top_srcdir)/src/lib-index \ + -I$(top_srcdir)/src/lib-mail \ + -I$(top_srcdir)/src/lib-smtp \ + -I$(top_srcdir)/src/lib-imap \ + -I$(top_srcdir)/src/lib-imap-client \ + -I$(top_srcdir)/src/lib-settings \ + -I$(top_srcdir)/src/lib-ssl-iostream \ + -I$(top_srcdir)/src/lib-storage \ + -I$(top_srcdir)/src/lib-storage/index \ + -I$(top_srcdir)/src/lib-storage/index/imapc \ + -I$(top_srcdir)/src/lib-storage/index/maildir \ + -I$(top_srcdir)/src/lib-program-client \ + -I$(top_srcdir)/src/doveadm \ + $(LIBTIRPC_CFLAGS) + +lib10_doveadm_quota_plugin_la_LDFLAGS = -module -avoid-version +lib10_quota_plugin_la_LDFLAGS = -module -avoid-version +module_LTLIBRARIES = \ + lib10_quota_plugin.la + +quota_dist_sources = \ + quota.c \ + quota-count.c \ + quota-fs.c \ + quota-dict.c \ + quota-dirsize.c \ + quota-imapc.c \ + quota-maildir.c \ + quota-plugin.c \ + quota-storage.c \ + quota-util.c + +quota_common_objects = \ + quota.lo \ + quota-count.lo \ + quota-fs.lo \ + quota-dict.lo \ + quota-dirsize.lo \ + quota-imapc.lo \ + quota-maildir.lo \ + quota-plugin.lo \ + quota-storage.lo \ + quota-util.lo \ + $(RQUOTA_XDR_LO) + +lib10_quota_plugin_la_SOURCES = $(quota_dist_sources) +nodist_lib10_quota_plugin_la_SOURCES = $(RQUOTA_XDR) +lib10_quota_plugin_la_LIBADD = $(QUOTA_LIBS) +doveadm_module_LTLIBRARIES = \ + lib10_doveadm_quota_plugin.la + +lib10_doveadm_quota_plugin_la_SOURCES = \ + doveadm-quota.c + +quota_status_SOURCES = \ + quota-status.c \ + quota-status-settings.c + +quota_status_CPPFLAGS = $(AM_CPPFLAGS) $(BINARY_CFLAGS) +quota_status_LDADD = \ + $(quota_common_objects) \ + $(LIBDOVECOT_STORAGE) \ + $(LIBDOVECOT) \ + $(QUOTA_LIBS) \ + $(BINARY_LDFLAGS) + +quota_status_DEPENDENCIES = \ + $(quota_common_objects) \ + $(LIBDOVECOT_STORAGE_DEPS) \ + $(LIBDOVECOT_DEPS) + +@HAVE_RQUOTA_TRUE@RQUOTA_XDR = rquota_xdr.c +@HAVE_RQUOTA_TRUE@RQUOTA_XDR_LO = rquota_xdr.lo +#RQUOTA_X = /usr/include/rpcsvc/rquota.x +@HAVE_RQUOTA_TRUE@RQUOTA_X = $(srcdir)/rquota.x +pkginc_libdir = $(pkgincludedir) +pkginc_lib_HEADERS = \ + quota.h \ + quota-fs.h \ + quota-plugin.h \ + quota-private.h + +noinst_HEADERS = \ + quota-status-settings.h + +EXTRA_DIST = rquota.x +test_programs = \ + test-quota-util + +test_libs = \ + ../../lib-test/libtest.la \ + ../../lib/liblib.la + +test_deps = $(noinst_LTLIBRARIES) $(test_libs) +test_quota_util_SOURCES = test-quota-util.c +test_quota_util_LDADD = quota-util.lo $(test_libs) +test_quota_util_DEPENDENCIES = quota-util.lo $(test_deps) +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/plugins/quota/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/plugins/quota/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 + +install-doveadm_moduleLTLIBRARIES: $(doveadm_module_LTLIBRARIES) + @$(NORMAL_INSTALL) + @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_moduledir)" || list=; \ + list2=; for p in $$list; do \ + if test -f $$p; then \ + list2="$$list2 $$p"; \ + else :; fi; \ + done; \ + test -z "$$list2" || { \ + echo " $(MKDIR_P) '$(DESTDIR)$(doveadm_moduledir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(doveadm_moduledir)" || exit 1; \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(doveadm_moduledir)'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(doveadm_moduledir)"; \ + } + +uninstall-doveadm_moduleLTLIBRARIES: + @$(NORMAL_UNINSTALL) + @list='$(doveadm_module_LTLIBRARIES)'; test -n "$(doveadm_moduledir)" || list=; \ + for p in $$list; do \ + $(am__strip_dir) \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(doveadm_moduledir)/$$f'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(doveadm_moduledir)/$$f"; \ + done + +clean-doveadm_moduleLTLIBRARIES: + -test -z "$(doveadm_module_LTLIBRARIES)" || rm -f $(doveadm_module_LTLIBRARIES) + @list='$(doveadm_module_LTLIBRARIES)'; \ + locs=`for p in $$list; do echo $$p; done | \ + sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \ + sort -u`; \ + test -z "$$locs" || { \ + echo rm -f $${locs}; \ + rm -f $${locs}; \ + } + +install-moduleLTLIBRARIES: $(module_LTLIBRARIES) + @$(NORMAL_INSTALL) + @list='$(module_LTLIBRARIES)'; test -n "$(moduledir)" || list=; \ + list2=; for p in $$list; do \ + if test -f $$p; then \ + list2="$$list2 $$p"; \ + else :; fi; \ + done; \ + test -z "$$list2" || { \ + echo " $(MKDIR_P) '$(DESTDIR)$(moduledir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(moduledir)" || exit 1; \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(moduledir)'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(moduledir)"; \ + } + +uninstall-moduleLTLIBRARIES: + @$(NORMAL_UNINSTALL) + @list='$(module_LTLIBRARIES)'; test -n "$(moduledir)" || list=; \ + for p in $$list; do \ + $(am__strip_dir) \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(moduledir)/$$f'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(moduledir)/$$f"; \ + done + +clean-moduleLTLIBRARIES: + -test -z "$(module_LTLIBRARIES)" || rm -f $(module_LTLIBRARIES) + @list='$(module_LTLIBRARIES)'; \ + locs=`for p in $$list; do echo $$p; done | \ + sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \ + sort -u`; \ + test -z "$$locs" || { \ + echo rm -f $${locs}; \ + rm -f $${locs}; \ + } + +lib10_doveadm_quota_plugin.la: $(lib10_doveadm_quota_plugin_la_OBJECTS) $(lib10_doveadm_quota_plugin_la_DEPENDENCIES) $(EXTRA_lib10_doveadm_quota_plugin_la_DEPENDENCIES) + $(AM_V_CCLD)$(lib10_doveadm_quota_plugin_la_LINK) -rpath $(doveadm_moduledir) $(lib10_doveadm_quota_plugin_la_OBJECTS) $(lib10_doveadm_quota_plugin_la_LIBADD) $(LIBS) + +lib10_quota_plugin.la: $(lib10_quota_plugin_la_OBJECTS) $(lib10_quota_plugin_la_DEPENDENCIES) $(EXTRA_lib10_quota_plugin_la_DEPENDENCIES) + $(AM_V_CCLD)$(lib10_quota_plugin_la_LINK) -rpath $(moduledir) $(lib10_quota_plugin_la_OBJECTS) $(lib10_quota_plugin_la_LIBADD) $(LIBS) + +quota-status$(EXEEXT): $(quota_status_OBJECTS) $(quota_status_DEPENDENCIES) $(EXTRA_quota_status_DEPENDENCIES) + @rm -f quota-status$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(quota_status_OBJECTS) $(quota_status_LDADD) $(LIBS) + +test-quota-util$(EXEEXT): $(test_quota_util_OBJECTS) $(test_quota_util_DEPENDENCIES) $(EXTRA_test_quota_util_DEPENDENCIES) + @rm -f test-quota-util$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(test_quota_util_OBJECTS) $(test_quota_util_LDADD) $(LIBS) + +mostlyclean-compile: + -rm -f *.$(OBJEXT) + +distclean-compile: + -rm -f *.tab.c + +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/doveadm-quota.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-count.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-dict.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-dirsize.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-fs.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-imapc.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-maildir.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-plugin.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-storage.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota-util.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota_status-quota-status-settings.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/quota_status-quota-status.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rquota_xdr.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/test-quota-util.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 $@ $< + +quota_status-quota-status.o: quota-status.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT quota_status-quota-status.o -MD -MP -MF $(DEPDIR)/quota_status-quota-status.Tpo -c -o quota_status-quota-status.o `test -f 'quota-status.c' || echo '$(srcdir)/'`quota-status.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/quota_status-quota-status.Tpo $(DEPDIR)/quota_status-quota-status.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='quota-status.c' object='quota_status-quota-status.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o quota_status-quota-status.o `test -f 'quota-status.c' || echo '$(srcdir)/'`quota-status.c + +quota_status-quota-status.obj: quota-status.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT quota_status-quota-status.obj -MD -MP -MF $(DEPDIR)/quota_status-quota-status.Tpo -c -o quota_status-quota-status.obj `if test -f 'quota-status.c'; then $(CYGPATH_W) 'quota-status.c'; else $(CYGPATH_W) '$(srcdir)/quota-status.c'; fi` +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/quota_status-quota-status.Tpo $(DEPDIR)/quota_status-quota-status.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='quota-status.c' object='quota_status-quota-status.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o quota_status-quota-status.obj `if test -f 'quota-status.c'; then $(CYGPATH_W) 'quota-status.c'; else $(CYGPATH_W) '$(srcdir)/quota-status.c'; fi` + +quota_status-quota-status-settings.o: quota-status-settings.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT quota_status-quota-status-settings.o -MD -MP -MF $(DEPDIR)/quota_status-quota-status-settings.Tpo -c -o quota_status-quota-status-settings.o `test -f 'quota-status-settings.c' || echo '$(srcdir)/'`quota-status-settings.c +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/quota_status-quota-status-settings.Tpo $(DEPDIR)/quota_status-quota-status-settings.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='quota-status-settings.c' object='quota_status-quota-status-settings.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o quota_status-quota-status-settings.o `test -f 'quota-status-settings.c' || echo '$(srcdir)/'`quota-status-settings.c + +quota_status-quota-status-settings.obj: quota-status-settings.c +@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT quota_status-quota-status-settings.obj -MD -MP -MF $(DEPDIR)/quota_status-quota-status-settings.Tpo -c -o quota_status-quota-status-settings.obj `if test -f 'quota-status-settings.c'; then $(CYGPATH_W) 'quota-status-settings.c'; else $(CYGPATH_W) '$(srcdir)/quota-status-settings.c'; fi` +@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/quota_status-quota-status-settings.Tpo $(DEPDIR)/quota_status-quota-status-settings.Po +@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='quota-status-settings.c' object='quota_status-quota-status-settings.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(quota_status_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o quota_status-quota-status-settings.obj `if test -f 'quota-status-settings.c'; then $(CYGPATH_W) 'quota-status-settings.c'; else $(CYGPATH_W) '$(srcdir)/quota-status-settings.c'; fi` + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +install-pkginc_libHEADERS: $(pkginc_lib_HEADERS) + @$(NORMAL_INSTALL) + @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \ + if test -n "$$list"; then \ + echo " $(MKDIR_P) '$(DESTDIR)$(pkginc_libdir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(pkginc_libdir)" || exit 1; \ + fi; \ + for p in $$list; do \ + if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ + echo "$$d$$p"; \ + done | $(am__base_list) | \ + while read files; do \ + echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(pkginc_libdir)'"; \ + $(INSTALL_HEADER) $$files "$(DESTDIR)$(pkginc_libdir)" || exit $$?; \ + done + +uninstall-pkginc_libHEADERS: + @$(NORMAL_UNINSTALL) + @list='$(pkginc_lib_HEADERS)'; test -n "$(pkginc_libdir)" || list=; \ + files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \ + dir='$(DESTDIR)$(pkginc_libdir)'; $(am__uninstall_files_from_dir) + +ID: $(am__tagged_files) + $(am__define_uniq_tagged_files); mkid -fID $$unique +tags: tags-am +TAGS: tags + +tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + set x; \ + here=`pwd`; \ + $(am__define_uniq_tagged_files); \ + shift; \ + if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ + test -n "$$unique" || unique=$$empty_fix; \ + if test $$# -gt 0; then \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + "$$@" $$unique; \ + else \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + $$unique; \ + fi; \ + fi +ctags: ctags-am + +CTAGS: ctags +ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + $(am__define_uniq_tagged_files); \ + test -z "$(CTAGS_ARGS)$$unique" \ + || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ + $$unique + +GTAGS: + here=`$(am__cd) $(top_builddir) && pwd` \ + && $(am__cd) $(top_srcdir) \ + && gtags -i $(GTAGS_ARGS) "$$here" +cscopelist: cscopelist-am + +cscopelist-am: $(am__tagged_files) + list='$(am__tagged_files)'; \ + case "$(srcdir)" in \ + [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \ + *) sdir=$(subdir)/$(srcdir) ;; \ + esac; \ + for i in $$list; do \ + if test -f "$$i"; then \ + echo "$(subdir)/$$i"; \ + else \ + echo "$$sdir/$$i"; \ + fi; \ + done >> $(top_builddir)/cscope.files + +distclean-tags: + -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags + +distdir: $(BUILT_SOURCES) + $(MAKE) $(AM_MAKEFLAGS) distdir-am + +distdir-am: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done +check-am: all-am + $(MAKE) $(AM_MAKEFLAGS) check-local +check: check-am +all-am: Makefile $(PROGRAMS) $(LTLIBRARIES) $(HEADERS) +installdirs: + for dir in "$(DESTDIR)$(pkglibexecdir)" "$(DESTDIR)$(doveadm_moduledir)" "$(DESTDIR)$(moduledir)" "$(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: + +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-doveadm_moduleLTLIBRARIES clean-generic clean-libtool \ + clean-moduleLTLIBRARIES clean-noinstPROGRAMS \ + clean-pkglibexecPROGRAMS mostlyclean-am + +distclean: distclean-am + -rm -f ./$(DEPDIR)/doveadm-quota.Plo + -rm -f ./$(DEPDIR)/quota-count.Plo + -rm -f ./$(DEPDIR)/quota-dict.Plo + -rm -f ./$(DEPDIR)/quota-dirsize.Plo + -rm -f ./$(DEPDIR)/quota-fs.Plo + -rm -f ./$(DEPDIR)/quota-imapc.Plo + -rm -f ./$(DEPDIR)/quota-maildir.Plo + -rm -f ./$(DEPDIR)/quota-plugin.Plo + -rm -f ./$(DEPDIR)/quota-storage.Plo + -rm -f ./$(DEPDIR)/quota-util.Plo + -rm -f ./$(DEPDIR)/quota.Plo + -rm -f ./$(DEPDIR)/quota_status-quota-status-settings.Po + -rm -f ./$(DEPDIR)/quota_status-quota-status.Po + -rm -f ./$(DEPDIR)/rquota_xdr.Plo + -rm -f ./$(DEPDIR)/test-quota-util.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-doveadm_moduleLTLIBRARIES \ + install-moduleLTLIBRARIES 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)/doveadm-quota.Plo + -rm -f ./$(DEPDIR)/quota-count.Plo + -rm -f ./$(DEPDIR)/quota-dict.Plo + -rm -f ./$(DEPDIR)/quota-dirsize.Plo + -rm -f ./$(DEPDIR)/quota-fs.Plo + -rm -f ./$(DEPDIR)/quota-imapc.Plo + -rm -f ./$(DEPDIR)/quota-maildir.Plo + -rm -f ./$(DEPDIR)/quota-plugin.Plo + -rm -f ./$(DEPDIR)/quota-storage.Plo + -rm -f ./$(DEPDIR)/quota-util.Plo + -rm -f ./$(DEPDIR)/quota.Plo + -rm -f ./$(DEPDIR)/quota_status-quota-status-settings.Po + -rm -f ./$(DEPDIR)/quota_status-quota-status.Po + -rm -f ./$(DEPDIR)/rquota_xdr.Plo + -rm -f ./$(DEPDIR)/test-quota-util.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-doveadm_moduleLTLIBRARIES \ + uninstall-moduleLTLIBRARIES 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-doveadm_moduleLTLIBRARIES \ + clean-generic clean-libtool clean-moduleLTLIBRARIES \ + 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-doveadm_moduleLTLIBRARIES install-dvi \ + install-dvi-am install-exec install-exec-am install-html \ + install-html-am install-info install-info-am install-man \ + install-moduleLTLIBRARIES 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-doveadm_moduleLTLIBRARIES \ + uninstall-moduleLTLIBRARIES uninstall-pkginc_libHEADERS \ + uninstall-pkglibexecPROGRAMS + +.PRECIOUS: Makefile + +@HAVE_RQUOTA_TRUE@rquota_xdr.c: Makefile rquota.h +@HAVE_RQUOTA_TRUE@ if [ "$(top_srcdir)" != "$(top_builddir)" ]; then \ +@HAVE_RQUOTA_TRUE@ cp $(RQUOTA_X) $(top_builddir)/src/plugins/quota/; \ +@HAVE_RQUOTA_TRUE@ fi; \ +@HAVE_RQUOTA_TRUE@ (echo '#include "lib.h"'; \ +@HAVE_RQUOTA_TRUE@ echo '#undef FALSE'; \ +@HAVE_RQUOTA_TRUE@ echo '#undef TRUE'; \ +@HAVE_RQUOTA_TRUE@ echo '#include <rpc/rpc.h>'; \ +@HAVE_RQUOTA_TRUE@ $(RPCGEN) -c $(top_builddir)/src/plugins/quota/rquota.x | \ +@HAVE_RQUOTA_TRUE@ sed -e 's/IXDR_PUT/(void)IXDR_PUT/g' \ +@HAVE_RQUOTA_TRUE@ -e 's,!xdr_,0 == xdr_,' \ +@HAVE_RQUOTA_TRUE@ -e 's,/usr/include/rpcsvc/rquota.h,rquota.h,' \ +@HAVE_RQUOTA_TRUE@ -e 's/int32_t \*buf/int32_t *buf ATTR_UNUSED/' \ +@HAVE_RQUOTA_TRUE@ -e 's/^static char rcsid.*//' ) > rquota_xdr.c + +@HAVE_RQUOTA_TRUE@rquota.h: Makefile $(RQUOTA_X) +@HAVE_RQUOTA_TRUE@ $(RPCGEN) -h $(RQUOTA_X) > rquota.h + +@HAVE_RQUOTA_TRUE@quota-fs.lo: rquota.h + +clean-generic: + if [ "$(top_srcdir)" != "$(top_builddir)" ]; then \ + rm -f $(top_builddir)/src/plugins/quota/rquota.x; \ + fi; \ + rm -f rquota_xdr.c rquota.h + +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/plugins/quota/doveadm-quota.c b/src/plugins/quota/doveadm-quota.c new file mode 100644 index 0000000..8a42b22 --- /dev/null +++ b/src/plugins/quota/doveadm-quota.c @@ -0,0 +1,165 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "module-dir.h" +#include "quota-plugin.h" +#include "quota-private.h" +#include "doveadm-print.h" +#include "doveadm-mail.h" + +const char *doveadm_quota_plugin_version = DOVECOT_ABI_VERSION; + +void doveadm_quota_plugin_init(struct module *module); +void doveadm_quota_plugin_deinit(void); + +static int cmd_quota_get_root(struct quota_root *root) +{ + const char *const *res; + const char *error; + uint64_t value, limit; + enum quota_get_result qret; + int ret = 0; + + res = quota_root_get_resources(root); + for (; *res != NULL; res++) { + qret = quota_get_resource(root, "", *res, &value, &limit, &error); + doveadm_print(root->set->name); + doveadm_print(*res); + if (qret == QUOTA_GET_RESULT_LIMITED) { + doveadm_print_num(value); + doveadm_print_num(limit); + if (limit > 0) + doveadm_print_num(value*100 / limit); + else + doveadm_print("0"); + } else if (qret == QUOTA_GET_RESULT_UNLIMITED) { + doveadm_print_num(value); + doveadm_print("-"); + doveadm_print("0"); + } else { + i_error("Failed to get quota resource %s: %s", + *res, error); + doveadm_print("error"); + doveadm_print("error"); + doveadm_print("error"); + ret = -1; + } + } + return ret; +} + +static int +cmd_quota_get_run(struct doveadm_mail_cmd_context *ctx, + struct mail_user *user) +{ + struct quota_user *quser = QUOTA_USER_CONTEXT(user); + struct quota_root *const *root; + + if (quser == NULL) { + i_error("Quota not enabled"); + doveadm_mail_failed_error(ctx, MAIL_ERROR_NOTFOUND); + return -1; + } + + int ret = 0; + array_foreach(&quser->quota->roots, root) + if (cmd_quota_get_root(*root) < 0) + ret = -1; + if (ret < 0) + doveadm_mail_failed_error(ctx, MAIL_ERROR_TEMP); + return ret; +} + +static void cmd_quota_get_init(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED, + const char *const args[] ATTR_UNUSED) +{ + doveadm_print_header("root", "Quota name", 0); + doveadm_print_header("type", "Type", 0); + doveadm_print_header("value", "Value", + DOVEADM_PRINT_HEADER_FLAG_RIGHT_JUSTIFY); + doveadm_print_header("limit", "Limit", + DOVEADM_PRINT_HEADER_FLAG_RIGHT_JUSTIFY); + doveadm_print_header("percent", "%", + DOVEADM_PRINT_HEADER_FLAG_RIGHT_JUSTIFY); +} + +static struct doveadm_mail_cmd_context * +cmd_quota_get_alloc(void) +{ + struct doveadm_mail_cmd_context *ctx; + + ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context); + ctx->v.run = cmd_quota_get_run; + ctx->v.init = cmd_quota_get_init; + doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE); + return ctx; +} + +static int +cmd_quota_recalc_run(struct doveadm_mail_cmd_context *ctx ATTR_UNUSED, + struct mail_user *user) +{ + struct quota_user *quser = QUOTA_USER_CONTEXT(user); + struct quota_root *const *root; + struct quota_transaction_context trans; + + if (quser == NULL) { + i_error("Quota not enabled"); + doveadm_mail_failed_error(ctx, MAIL_ERROR_NOTFOUND); + return -1; + } + + i_zero(&trans); + trans.quota = quser->quota; + trans.recalculate = QUOTA_RECALCULATE_FORCED; + + array_foreach(&quser->quota->roots, root) { + const char *error; + if ((*root)->backend.v.update(*root, &trans, &error) < 0) + i_error("Recalculating quota failed: %s", error); + if ((*root)->backend.v.flush != NULL) + (*root)->backend.v.flush(*root); + } + return 0; +} + +static struct doveadm_mail_cmd_context * +cmd_quota_recalc_alloc(void) +{ + struct doveadm_mail_cmd_context *ctx; + + ctx = doveadm_mail_cmd_alloc(struct doveadm_mail_cmd_context); + ctx->v.run = cmd_quota_recalc_run; + return ctx; +} + +static struct doveadm_cmd_ver2 quota_commands[] = { + { + .name = "quota get", + .usage = "", + .mail_cmd = cmd_quota_get_alloc, +DOVEADM_CMD_PARAMS_START +DOVEADM_CMD_MAIL_COMMON +DOVEADM_CMD_PARAMS_END + }, + { + .name = "quota recalc", + .usage = "", + .mail_cmd = cmd_quota_recalc_alloc, +DOVEADM_CMD_PARAMS_START +DOVEADM_CMD_MAIL_COMMON +DOVEADM_CMD_PARAMS_END + } +}; + +void doveadm_quota_plugin_init(struct module *module ATTR_UNUSED) +{ + unsigned int i; + + for (i = 0; i < N_ELEMENTS(quota_commands); i++) + doveadm_cmd_register_ver2("a_commands[i]); +} + +void doveadm_quota_plugin_deinit(void) +{ +} diff --git a/src/plugins/quota/quota-count.c b/src/plugins/quota/quota-count.c new file mode 100644 index 0000000..00e25e6 --- /dev/null +++ b/src/plugins/quota/quota-count.c @@ -0,0 +1,400 @@ +/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "mailbox-list-iter.h" +#include "quota-private.h" + +struct count_quota_root { + struct quota_root root; + + struct timeval cache_timeval; + uint64_t cached_bytes, cached_count; +}; + +struct quota_mailbox_iter { + struct quota_root *root; + struct mail_namespace *ns; + unsigned int ns_idx; + struct mailbox_list_iterate_context *iter; + struct mailbox_info info; + const char *error; +}; + +extern struct quota_backend quota_backend_count; + +static int +quota_count_mailbox(struct quota_root *root, struct mail_namespace *ns, + const char *vname, uint64_t *bytes, uint64_t *count, + enum quota_get_result *error_result_r, + const char **error_r) +{ + struct quota_rule *rule; + struct mailbox *box; + struct mailbox_metadata metadata; + struct mailbox_status status; + enum mail_error error; + const char *errstr; + int ret; + + rule = quota_root_rule_find(root->set, vname); + if (rule != NULL && rule->ignore) { + /* mailbox not included in quota */ + return 0; + } + + box = mailbox_alloc(ns->list, vname, MAILBOX_FLAG_READONLY); + if ((box->storage->class_flags & MAIL_STORAGE_CLASS_FLAG_NOQUOTA) != 0) { + /* quota doesn't exist for this mailbox/storage */ + ret = 0; + } else if (mailbox_get_metadata(box, root->quota->set->vsizes ? + MAILBOX_METADATA_VIRTUAL_SIZE : + MAILBOX_METADATA_PHYSICAL_SIZE, + &metadata) < 0 || + mailbox_get_status(box, STATUS_MESSAGES, &status) < 0) { + errstr = mailbox_get_last_internal_error(box, &error); + if (error == MAIL_ERROR_TEMP) { + *error_r = t_strdup_printf( + "Couldn't get size of mailbox %s: %s", + vname, errstr); + *error_result_r = QUOTA_GET_RESULT_INTERNAL_ERROR; + ret = -1; + } else if (error == MAIL_ERROR_INUSE) { + /* started on background. don't log an error. */ + *error_r = t_strdup_printf( + "Ongoing quota calculation blocked getting size of %s: %s", + vname, errstr); + *error_result_r = QUOTA_GET_RESULT_BACKGROUND_CALC; + ret = -1; + } else { + /* non-temporary error, e.g. ACLs denied access. */ + ret = 0; + } + } else { + ret = 0; + *bytes += root->quota->set->vsizes ? + metadata.virtual_size : metadata.physical_size; + *count += status.messages; + } + mailbox_free(&box); + return ret; +} + +static struct quota_mailbox_iter * +quota_mailbox_iter_begin(struct quota_root *root) +{ + struct quota_mailbox_iter *iter; + + iter = i_new(struct quota_mailbox_iter, 1); + iter->root = root; + iter->error = ""; + return iter; +} + +static int +quota_mailbox_iter_deinit(struct quota_mailbox_iter **_iter, + const char **error_r) +{ + struct quota_mailbox_iter *iter = *_iter; + int ret = *iter->error != '\0' ? -1 : 0; + + *_iter = NULL; + + const char *error2 = ""; + if (iter->iter != NULL) { + if (mailbox_list_iter_deinit(&iter->iter) < 0) { + error2 = t_strdup_printf( + "Listing namespace '%s' failed: %s", + iter->ns->prefix, + mailbox_list_get_last_internal_error(iter->ns->list, NULL)); + ret = -1; + } + } + if (ret < 0) { + const char *separator = + *iter->error != '\0' && *error2 != '\0' ? " and " : ""; + *error_r = t_strdup_printf("%s%s%s", + iter->error, separator, error2); + } + i_free(iter); + return ret; +} + +static const struct mailbox_info * +quota_mailbox_iter_next(struct quota_mailbox_iter *iter) +{ + struct mail_namespace *const *namespaces; + const struct mailbox_info *info; + unsigned int count; + + if (iter->iter == NULL) { + namespaces = array_get(&iter->root->quota->namespaces, &count); + do { + if (iter->ns_idx >= count) + return NULL; + + iter->ns = namespaces[iter->ns_idx++]; + } while (!quota_root_is_namespace_visible(iter->root, iter->ns)); + iter->iter = mailbox_list_iter_init(iter->ns->list, "*", + MAILBOX_LIST_ITER_SKIP_ALIASES | + MAILBOX_LIST_ITER_RETURN_NO_FLAGS | + MAILBOX_LIST_ITER_NO_AUTO_BOXES); + } + while ((info = mailbox_list_iter_next(iter->iter)) != NULL) { + if ((info->flags & (MAILBOX_NONEXISTENT | + MAILBOX_NOSELECT)) == 0) + return info; + } + if (mailbox_list_iter_deinit(&iter->iter) < 0) { + iter->error = t_strdup_printf( + "Listing namespace '%s' failed: %s", + iter->ns->prefix, + mailbox_list_get_last_internal_error(iter->ns->list, NULL)); + } + if (iter->ns->prefix_len > 0 && + (iter->ns->prefix_len != 6 || + strncasecmp(iter->ns->prefix, "INBOX", 5) != 0)) { + /* if the namespace prefix itself exists, count it also */ + iter->info.ns = iter->ns; + iter->info.vname = t_strndup(iter->ns->prefix, + iter->ns->prefix_len-1); + return &iter->info; + } + /* try the next namespace */ + return quota_mailbox_iter_next(iter); +} + +int quota_count(struct quota_root *root, uint64_t *bytes_r, uint64_t *count_r, + enum quota_get_result *error_result_r, const char **error_r) +{ + struct quota_mailbox_iter *iter; + const struct mailbox_info *info; + const char *error1 = "", *error2 = ""; + int ret = 1; + + *bytes_r = *count_r = 0; + if (root->recounting) + return 0; + root->recounting = TRUE; + + struct event_reason *reason = event_reason_begin("quota:count"); + + iter = quota_mailbox_iter_begin(root); + while ((info = quota_mailbox_iter_next(iter)) != NULL) { + if (quota_count_mailbox(root, info->ns, info->vname, + bytes_r, count_r, error_result_r, + &error1) < 0) { + ret = -1; + break; + } + } + if (quota_mailbox_iter_deinit(&iter, &error2) < 0) { + *error_result_r = QUOTA_GET_RESULT_INTERNAL_ERROR; + ret = -1; + } + if (ret < 0) { + const char *separator = + *error1 != '\0' && *error2 != '\0' ? " and " : ""; + *error_r = t_strconcat(error1, separator, error2, NULL); + } + event_reason_end(&reason); + root->recounting = FALSE; + return ret; +} + +static enum quota_get_result +quota_count_cached(struct count_quota_root *root, + uint64_t *bytes_r, uint64_t *count_r, + const char **error_r) +{ + int ret; + + if (root->cache_timeval.tv_usec == ioloop_timeval.tv_usec && + root->cache_timeval.tv_sec == ioloop_timeval.tv_sec && + ioloop_timeval.tv_sec != 0) { + *bytes_r = root->cached_bytes; + *count_r = root->cached_count; + return QUOTA_GET_RESULT_LIMITED; + } + + enum quota_get_result error_res; + ret = quota_count(&root->root, bytes_r, count_r, &error_res, error_r); + if (ret < 0) { + return error_res; + } else if (ret > 0) { + root->cache_timeval = ioloop_timeval; + root->cached_bytes = *bytes_r; + root->cached_count = *count_r; + } + return QUOTA_GET_RESULT_LIMITED; +} + +static struct quota_root *count_quota_alloc(void) +{ + struct count_quota_root *root; + + root = i_new(struct count_quota_root, 1); + return &root->root; +} + +static int count_quota_init(struct quota_root *root, const char *args, + const char **error_r) +{ + if (!root->quota->set->vsizes) { + *error_r = "quota count backend requires quota_vsizes=yes"; + return -1; + } + event_set_append_log_prefix(root->backend.event, "quota-count: "); + + root->auto_updating = TRUE; + return quota_root_default_init(root, args, error_r); +} + +static void count_quota_deinit(struct quota_root *_root) +{ + i_free(_root); +} + +static const char *const * +count_quota_root_get_resources(struct quota_root *root ATTR_UNUSED) +{ + static const char *resources[] = { + QUOTA_NAME_STORAGE_KILOBYTES, QUOTA_NAME_MESSAGES, NULL + }; + return resources; +} + +static enum quota_get_result +count_quota_get_resource(struct quota_root *_root, + const char *name, uint64_t *value_r, + const char **error_r) +{ + struct count_quota_root *root = (struct count_quota_root *)_root; + uint64_t bytes, count; + enum quota_get_result ret; + + ret = quota_count_cached(root, &bytes, &count, error_r); + if (ret <= QUOTA_GET_RESULT_INTERNAL_ERROR) + return ret; + + if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) + *value_r = bytes; + else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0) + *value_r = count; + else { + *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING; + return QUOTA_GET_RESULT_UNKNOWN_RESOURCE; + } + return QUOTA_GET_RESULT_LIMITED; +} + +static int quota_count_recalculate_box(struct mailbox *box, + const char **error_r) +{ + struct mail_index_transaction *trans; + struct mailbox_metadata metadata; + struct mailbox_index_vsize vsize_hdr; + const char *errstr; + enum mail_error error; + + if (mailbox_open(box) < 0) { + errstr = mailbox_get_last_internal_error(box, &error); + if (error != MAIL_ERROR_TEMP) { + /* non-temporary error, e.g. ACLs denied access. */ + return 0; + } + *error_r = t_strdup_printf( + "Couldn't open mailbox %s: %s", box->vname, errstr); + return -1; + } + + /* reset the vsize header first */ + trans = mail_index_transaction_begin(box->view, + MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL); + i_zero(&vsize_hdr); + mail_index_update_header_ext(trans, box->vsize_hdr_ext_id, + 0, &vsize_hdr, sizeof(vsize_hdr)); + if (mail_index_transaction_commit(&trans) < 0) { + *error_r = t_strdup_printf( + "Couldn't commit mail index transaction for %s: %s", + box->vname, + mail_index_get_error_message(box->view->index)); + return -1; + } + /* getting the vsize now forces its recalculation */ + if (mailbox_get_metadata(box, MAILBOX_METADATA_VIRTUAL_SIZE, + &metadata) < 0) { + *error_r = t_strdup_printf( + "Couldn't get mailbox %s vsize: %s", box->vname, + mailbox_get_last_internal_error(box, NULL)); + return -1; + } + /* call sync to write the change to mailbox list index */ + if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FAST) < 0) { + *error_r = t_strdup_printf( + "Couldn't sync mailbox %s: %s", box->vname, + mailbox_get_last_internal_error(box, NULL)); + return -1; + } + return 0; +} + +static int quota_count_recalculate(struct quota_root *root, + const char **error_r) +{ + struct event_reason *reason; + struct quota_mailbox_iter *iter; + const struct mailbox_info *info; + struct mailbox *box; + int ret = 0; + const char *error1 = "", *error2 = ""; + + reason = event_reason_begin("quota:recalculate"); + + iter = quota_mailbox_iter_begin(root); + while ((info = quota_mailbox_iter_next(iter)) != NULL) { + box = mailbox_alloc(info->ns->list, info->vname, 0); + if (quota_count_recalculate_box(box, &error1) < 0) + ret = -1; + mailbox_free(&box); + } + if (quota_mailbox_iter_deinit(&iter, &error2) < 0) + ret = -1; + if (ret < 0) { + const char *separator = + *error1 != '\0' && *error2 != '\0' ? " and " : ""; + *error_r = t_strdup_printf( + "quota-count: recalculate failed: %s%s%s", + error1, separator, error2); + } + event_reason_end(&reason); + return ret; +} + +static int +count_quota_update(struct quota_root *root, + struct quota_transaction_context *ctx, + const char **error_r) +{ + struct count_quota_root *croot = (struct count_quota_root *)root; + + croot->cache_timeval.tv_sec = 0; + if (ctx->recalculate == QUOTA_RECALCULATE_FORCED) { + if (quota_count_recalculate(root, error_r) < 0) + return -1; + } + return 0; +} + +struct quota_backend quota_backend_count = { + .name = "count", + + .v = { + .alloc = count_quota_alloc, + .init = count_quota_init, + .deinit = count_quota_deinit, + .get_resources = count_quota_root_get_resources, + .get_resource = count_quota_get_resource, + .update = count_quota_update, + } +}; diff --git a/src/plugins/quota/quota-dict.c b/src/plugins/quota/quota-dict.c new file mode 100644 index 0000000..02e444a --- /dev/null +++ b/src/plugins/quota/quota-dict.c @@ -0,0 +1,269 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "ioloop.h" +#include "str.h" +#include "dict.h" +#include "mail-user.h" +#include "mail-namespace.h" +#include "quota-private.h" + + +#define DICT_QUOTA_CURRENT_PATH DICT_PATH_PRIVATE"quota/" +#define DICT_QUOTA_CURRENT_BYTES_PATH DICT_QUOTA_CURRENT_PATH"storage" +#define DICT_QUOTA_CURRENT_COUNT_PATH DICT_QUOTA_CURRENT_PATH"messages" + +struct dict_quota_root { + struct quota_root root; + struct dict *dict; + struct timeout *to_update; + bool disable_unset; +}; + +extern struct quota_backend quota_backend_dict; + +static struct quota_root *dict_quota_alloc(void) +{ + struct dict_quota_root *root; + + root = i_new(struct dict_quota_root, 1); + return &root->root; +} + +static void handle_nounset_param(struct quota_root *_root, const char *param_value ATTR_UNUSED) +{ + ((struct dict_quota_root *)_root)->disable_unset = TRUE; +} + +static int dict_quota_init(struct quota_root *_root, const char *args, + const char **error_r) +{ + struct dict_quota_root *root = (struct dict_quota_root *)_root; + struct dict_settings set; + const char *username, *p, *error; + + event_set_append_log_prefix(_root->backend.event, "quota-dict: "); + + const struct quota_param_parser dict_params[] = { + {.param_name = "no-unset", .param_handler = handle_nounset_param}, + quota_param_hidden, quota_param_ignoreunlimited, quota_param_noenforcing, quota_param_ns, + {.param_name = NULL} + }; + + p = args == NULL ? NULL : strchr(args, ':'); + if (p == NULL) { + *error_r = "URI missing from parameters"; + return -1; + } + + username = t_strdup_until(args, p); + args = p+1; + + if (quota_parse_parameters(_root, &args, error_r, dict_params, FALSE) < 0) + i_unreached(); + + if (*username == '\0') + username = _root->quota->user->username; + + e_debug(_root->backend.event, "user=%s, uri=%s, noenforcing=%d", + username, args, _root->no_enforcing ? 1 : 0); + + /* FIXME: we should use 64bit integer as datatype instead but before + it can actually be used don't bother */ + i_zero(&set); + set.base_dir = _root->quota->user->set->base_dir; + set.event_parent = _root->quota->user->event; + if (dict_init(args, &set, &root->dict, &error) < 0) { + *error_r = t_strdup_printf("dict_init(%s) failed: %s", args, error); + return -1; + } + return 0; +} + +static void dict_quota_deinit(struct quota_root *_root) +{ + struct dict_quota_root *root = (struct dict_quota_root *)_root; + + i_assert(root->to_update == NULL); + + if (root->dict != NULL) { + dict_wait(root->dict); + dict_deinit(&root->dict); + } + i_free(root); +} + +static const char *const * +dict_quota_root_get_resources(struct quota_root *root ATTR_UNUSED) +{ + static const char *resources[] = { + QUOTA_NAME_STORAGE_KILOBYTES, QUOTA_NAME_MESSAGES, NULL + }; + + return resources; +} + +static enum quota_get_result +dict_quota_count(struct dict_quota_root *root, + bool want_bytes, uint64_t *value_r, + const char **error_r) +{ + struct dict_transaction_context *dt; + struct event_reason *reason; + uint64_t bytes, count; + enum quota_get_result error_res; + const struct dict_op_settings *set; + + reason = event_reason_begin("quota:recalculate"); + int ret = quota_count(&root->root, &bytes, &count, &error_res, error_r); + event_reason_end(&reason); + if (ret < 0) + return error_res; + + set = mail_user_get_dict_op_settings(root->root.quota->user); + dt = dict_transaction_begin(root->dict, set); + /* these unsets are mainly necessary for pgsql, because its + trigger otherwise increases quota without deleting it. + but some people with other databases want to store the + quota usage among other data in the same row, which + shouldn't be deleted. */ + if (!root->disable_unset) { + dict_unset(dt, DICT_QUOTA_CURRENT_BYTES_PATH); + dict_unset(dt, DICT_QUOTA_CURRENT_COUNT_PATH); + } + dict_set(dt, DICT_QUOTA_CURRENT_BYTES_PATH, dec2str(bytes)); + dict_set(dt, DICT_QUOTA_CURRENT_COUNT_PATH, dec2str(count)); + + e_debug(root->root.backend.event, "Quota recalculated: " + "count=%"PRIu64" bytes=%"PRIu64, count, bytes); + + dict_transaction_commit_async_nocallback(&dt); + *value_r = want_bytes ? bytes : count; + return QUOTA_GET_RESULT_LIMITED; +} + +static enum quota_get_result +dict_quota_get_resource(struct quota_root *_root, + const char *name, uint64_t *value_r, + const char **error_r) +{ + struct dict_quota_root *root = (struct dict_quota_root *)_root; + bool want_bytes; + int ret; + const struct dict_op_settings *set; + + if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) + want_bytes = TRUE; + else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0) + want_bytes = FALSE; + else { + *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING; + return QUOTA_GET_RESULT_UNKNOWN_RESOURCE; + } + + set = mail_user_get_dict_op_settings(root->root.quota->user); + const char *key, *value, *error; + key = want_bytes ? DICT_QUOTA_CURRENT_BYTES_PATH : + DICT_QUOTA_CURRENT_COUNT_PATH; + ret = dict_lookup(root->dict, set, unsafe_data_stack_pool, + key, &value, &error); + if (ret < 0) { + *error_r = t_strdup_printf( + "dict_lookup(%s) failed: %s", key, error); + *value_r = 0; + return QUOTA_GET_RESULT_INTERNAL_ERROR; + } + + intmax_t tmp; + /* recalculate quota if it's negative or if it wasn't found */ + if (ret == 0 || str_to_intmax(value, &tmp) < 0) + tmp = -1; + if (tmp >= 0) + *value_r = tmp; + else + return dict_quota_count(root, want_bytes, value_r, error_r); + return QUOTA_GET_RESULT_LIMITED; +} + +static void dict_quota_recalc_timeout(struct dict_quota_root *root) +{ + uint64_t value; + const char *error; + + timeout_remove(&root->to_update); + if (dict_quota_count(root, TRUE, &value, &error) + <= QUOTA_GET_RESULT_INTERNAL_ERROR) + e_error(root->root.backend.event, + "Recalculation failed: %s", error); +} + +static void dict_quota_update_callback(const struct dict_commit_result *result, + struct dict_quota_root *root) +{ + if (result->ret == 0) { + /* row doesn't exist, need to recalculate it */ + if (root->to_update == NULL) + root->to_update = timeout_add_short(0, dict_quota_recalc_timeout, root); + } else if (result->ret < 0) { + e_error(root->root.backend.event, + "Quota update failed: %s " + "- Quota is now desynced", result->error); + } +} + +static int +dict_quota_update(struct quota_root *_root, + struct quota_transaction_context *ctx, + const char **error_r) +{ + struct dict_quota_root *root = (struct dict_quota_root *) _root; + struct dict_transaction_context *dt; + uint64_t value; + const struct dict_op_settings *set; + + if (ctx->recalculate != QUOTA_RECALCULATE_DONT) { + if (dict_quota_count(root, TRUE, &value, error_r) + <= QUOTA_GET_RESULT_INTERNAL_ERROR) + return -1; + } else { + set = mail_user_get_dict_op_settings(root->root.quota->user); + dt = dict_transaction_begin(root->dict, set); + if (ctx->bytes_used != 0) { + dict_atomic_inc(dt, DICT_QUOTA_CURRENT_BYTES_PATH, + ctx->bytes_used); + } + if (ctx->count_used != 0) { + dict_atomic_inc(dt, DICT_QUOTA_CURRENT_COUNT_PATH, + ctx->count_used); + } + dict_transaction_no_slowness_warning(dt); + dict_transaction_commit_async(&dt, dict_quota_update_callback, + root); + } + return 0; +} + +static void dict_quota_flush(struct quota_root *_root) +{ + struct dict_quota_root *root = (struct dict_quota_root *)_root; + + dict_wait(root->dict); + if (root->to_update != NULL) { + dict_quota_recalc_timeout(root); + dict_wait(root->dict); + } +} + +struct quota_backend quota_backend_dict = { + .name = "dict", + + .v = { + .alloc = dict_quota_alloc, + .init = dict_quota_init, + .deinit = dict_quota_deinit, + .get_resources = dict_quota_root_get_resources, + .get_resource = dict_quota_get_resource, + .update = dict_quota_update, + .flush = dict_quota_flush, + } +}; diff --git a/src/plugins/quota/quota-dirsize.c b/src/plugins/quota/quota-dirsize.c new file mode 100644 index 0000000..a8305d8 --- /dev/null +++ b/src/plugins/quota/quota-dirsize.c @@ -0,0 +1,232 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +/* Quota reporting based on simply summing sizes of all files in mailbox + together. */ + +#include "lib.h" +#include "array.h" +#include "str.h" +#include "quota-private.h" + +#include <unistd.h> +#include <dirent.h> +#include <sys/stat.h> + +struct quota_count_path { + const char *path; + bool is_file; +}; +ARRAY_DEFINE_TYPE(quota_count_path, struct quota_count_path); + +extern struct quota_backend quota_backend_dirsize; + +static struct quota_root *dirsize_quota_alloc(void) +{ + return i_new(struct quota_root, 1); +} + +static int dirsize_quota_init(struct quota_root *root, const char *args, + const char **error_r) +{ + root->auto_updating = TRUE; + event_set_append_log_prefix(root->backend.event, "quota-dirsize: "); + return quota_root_default_init(root, args, error_r); +} + +static void dirsize_quota_deinit(struct quota_root *_root) +{ + i_free(_root); +} + +static const char *const * +dirsize_quota_root_get_resources(struct quota_root *root ATTR_UNUSED) +{ + static const char *resources[] = { QUOTA_NAME_STORAGE_KILOBYTES, NULL }; + + return resources; +} + +static int get_dir_usage(const char *dir, uint64_t *value, + const char **error_r) +{ + DIR *dirp; + string_t *path; + struct dirent *d; + struct stat st; + unsigned int path_pos; + int ret; + + dirp = opendir(dir); + if (dirp == NULL) { + if (errno == ENOENT) + return 0; + + *error_r = t_strdup_printf("opendir(%s) failed: %m", dir); + return -1; + } + + path = t_str_new(128); + str_append(path, dir); + str_append_c(path, '/'); + path_pos = str_len(path); + + ret = 0; + while ((d = readdir(dirp)) != NULL) { + if (d->d_name[0] == '.' && + (d->d_name[1] == '\0' || + (d->d_name[1] == '.' && d->d_name[2] == '\0'))) { + /* skip . and .. */ + continue; + } + + str_truncate(path, path_pos); + str_append(path, d->d_name); + + if (lstat(str_c(path), &st) < 0) { + if (errno == ENOENT) + continue; + + *error_r = t_strdup_printf("lstat(%s) failed: %m", dir); + ret = -1; + break; + } else if (S_ISDIR(st.st_mode)) { + if (get_dir_usage(str_c(path), value, error_r) < 0) { + ret = -1; + break; + } + } else { + *value += st.st_size; + } + } + + (void)closedir(dirp); + return ret; +} + +static int get_usage(const char *path, bool is_file, uint64_t *value_r, + const char **error_r) +{ + struct stat st; + + if (is_file) { + if (lstat(path, &st) < 0) { + if (errno == ENOENT) + return 0; + + *error_r = t_strdup_printf("lstat(%s) failed: %m", path); + return -1; + } + *value_r += st.st_size; + } else { + if (get_dir_usage(path, value_r, error_r) < 0) + return -1; + } + return 0; +} + +static void quota_count_path_add(ARRAY_TYPE(quota_count_path) *paths, + const char *path, bool is_file) +{ + struct quota_count_path *count_path; + unsigned int i, count; + size_t path_len; + + path_len = strlen(path); + count_path = array_get_modifiable(paths, &count); + for (i = 0; i < count; ) { + if (strncmp(count_path[i].path, path, + strlen(count_path[i].path)) == 0) { + /* this path has already been counted */ + return; + } + if (strncmp(count_path[i].path, path, path_len) == 0 && + count_path[i].path[path_len] == '/') { + /* the new path contains the existing path. + drop it and see if there are more to drop. */ + array_delete(paths, i, 1); + count_path = array_get_modifiable(paths, &count); + } else { + i++; + } + } + + count_path = array_append_space(paths); + count_path->path = t_strdup(path); + count_path->is_file = is_file; +} + +static int +get_quota_root_usage(struct quota_root *root, uint64_t *value_r, + const char **error_r) +{ + struct mail_namespace *const *namespaces; + ARRAY_TYPE(quota_count_path) paths; + const struct quota_count_path *count_paths; + unsigned int i, count; + const char *path; + bool is_file; + + t_array_init(&paths, 8); + namespaces = array_get(&root->quota->namespaces, &count); + for (i = 0; i < count; i++) { + if (!quota_root_is_namespace_visible(root, namespaces[i])) + continue; + + is_file = mail_storage_is_mailbox_file(namespaces[i]->storage); + if (mailbox_list_get_root_path(namespaces[i]->list, + MAILBOX_LIST_PATH_TYPE_DIR, &path)) + quota_count_path_add(&paths, path, FALSE); + + /* INBOX may be in different path. */ + if (mailbox_list_get_path(namespaces[i]->list, "INBOX", + MAILBOX_LIST_PATH_TYPE_MAILBOX, &path) > 0) + quota_count_path_add(&paths, path, is_file); + } + + /* now sum up the found paths */ + *value_r = 0; + count_paths = array_get(&paths, &count); + for (i = 0; i < count; i++) { + if (get_usage(count_paths[i].path, count_paths[i].is_file, + value_r, error_r) < 0) + return -1; + } + return 0; +} + +static enum quota_get_result +dirsize_quota_get_resource(struct quota_root *_root, const char *name, + uint64_t *value_r, const char **error_r) +{ + int ret; + + if (strcasecmp(name, QUOTA_NAME_STORAGE_BYTES) != 0) { + *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING; + return QUOTA_GET_RESULT_UNKNOWN_RESOURCE; + } + + ret = get_quota_root_usage(_root, value_r, error_r); + + return ret < 0 ? QUOTA_GET_RESULT_INTERNAL_ERROR : QUOTA_GET_RESULT_LIMITED; +} + +static int +dirsize_quota_update(struct quota_root *root ATTR_UNUSED, + struct quota_transaction_context *ctx ATTR_UNUSED, + const char **error_r ATTR_UNUSED) +{ + return 0; +} + +struct quota_backend quota_backend_dirsize = { + .name = "dirsize", + + .v = { + .alloc = dirsize_quota_alloc, + .init = dirsize_quota_init, + .deinit = dirsize_quota_deinit, + .get_resources = dirsize_quota_root_get_resources, + .get_resource = dirsize_quota_get_resource, + .update = dirsize_quota_update, + } +}; diff --git a/src/plugins/quota/quota-fs.c b/src/plugins/quota/quota-fs.c new file mode 100644 index 0000000..57620c1 --- /dev/null +++ b/src/plugins/quota/quota-fs.c @@ -0,0 +1,970 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +/* Only for reporting filesystem quota */ + +#include "lib.h" +#include "array.h" +#include "str.h" +#include "hostpid.h" +#include "mountpoint.h" +#include "quota-private.h" +#include "quota-fs.h" + +#ifdef HAVE_FS_QUOTA + +#include <stdio.h> +#include <fcntl.h> +#include <unistd.h> +#include <sys/stat.h> +#ifdef HAVE_LINUX_DQBLK_XFS_H +# include <linux/dqblk_xfs.h> +# define HAVE_XFS_QUOTA +#elif defined (HAVE_XFS_XQM_H) +# include <xfs/xqm.h> /* CentOS 4.x at least uses this */ +# define HAVE_XFS_QUOTA +#endif + +#ifdef HAVE_RQUOTA +# include "rquota.h" +# define RQUOTA_GETQUOTA_TIMEOUT_SECS 10 +#endif + +#ifndef DEV_BSIZE +# ifdef DQBSIZE +# define DEV_BSIZE DQBSIZE /* AIX */ +# else +# define DEV_BSIZE 512 +# endif +#endif + +#ifdef HAVE_STRUCT_DQBLK_CURSPACE +# define dqb_curblocks dqb_curspace +#endif + +/* Very old sys/quota.h doesn't define _LINUX_QUOTA_VERSION at all, which means + it supports only v1 quota. However, new sys/quota.h (glibc 2.25) removes + support for v1 entirely and again it doesn't define it. I guess we can just + assume v2 now, and if someone still wants v1 support they can add + -D_LINUX_QUOTA_VERSION=1 to CFLAGS. */ +#ifndef _LINUX_QUOTA_VERSION +# define _LINUX_QUOTA_VERSION 2 +#endif + +#define mount_type_is_nfs(mount) \ + (strcmp((mount)->type, "nfs") == 0 || \ + strcmp((mount)->type, "nfs4") == 0) + +struct fs_quota_mountpoint { + int refcount; + + char *mount_path; + char *device_path; + char *type; + unsigned int block_size; + +#ifdef FS_QUOTA_SOLARIS + int fd; + char *path; +#endif +}; + +struct fs_quota_root { + struct quota_root root; + char *storage_mount_path; + + uid_t uid; + gid_t gid; + struct fs_quota_mountpoint *mount; + + bool inode_per_mail:1; + bool user_disabled:1; + bool group_disabled:1; +#ifdef FS_QUOTA_NETBSD + struct quotahandle *qh; +#endif +}; + +extern struct quota_backend quota_backend_fs; + +static struct quota_root *fs_quota_alloc(void) +{ + struct fs_quota_root *root; + + root = i_new(struct fs_quota_root, 1); + root->uid = geteuid(); + root->gid = getegid(); + + return &root->root; +} + +static void handle_user_param(struct quota_root *_root, const char *param_value ATTR_UNUSED) +{ + ((struct fs_quota_root *)_root)->group_disabled = TRUE; +} + +static void handle_group_param(struct quota_root *_root, const char *param_value ATTR_UNUSED) +{ + ((struct fs_quota_root *)_root)->user_disabled = TRUE; +} + +static void handle_inode_param(struct quota_root *_root, const char *param_value ATTR_UNUSED) +{ + ((struct fs_quota_root *)_root)->inode_per_mail = TRUE; +} + +static void handle_mount_param(struct quota_root *_root, const char *param_value) +{ + struct fs_quota_root *root = (struct fs_quota_root *)_root; + i_free(root->storage_mount_path); + root->storage_mount_path = i_strdup(param_value); +} + +static int fs_quota_init(struct quota_root *_root, const char *args, + const char **error_r) +{ + const struct quota_param_parser fs_params[] = { + {.param_name = "user", .param_handler = handle_user_param}, + {.param_name = "group", .param_handler = handle_group_param}, + {.param_name = "mount=", .param_handler = handle_mount_param}, + {.param_name = "inode_per_mail", .param_handler = handle_inode_param}, + quota_param_hidden, quota_param_noenforcing, quota_param_ns, + {.param_name = NULL} + }; + + event_set_append_log_prefix(_root->backend.event, "quota-fs: "); + + if (quota_parse_parameters(_root, &args, error_r, fs_params, TRUE) < 0) + return -1; + _root->auto_updating = TRUE; + return 0; +} + +static void fs_quota_mountpoint_free(struct fs_quota_mountpoint *mount) +{ + if (--mount->refcount > 0) + return; + +#ifdef FS_QUOTA_SOLARIS + i_close_fd_path(&mount->fd, mount->path); + i_free(mount->path); +#endif + + i_free(mount->device_path); + i_free(mount->mount_path); + i_free(mount->type); + i_free(mount); +} + +static void fs_quota_deinit(struct quota_root *_root) +{ + struct fs_quota_root *root = (struct fs_quota_root *)_root; + + if (root->mount != NULL) + fs_quota_mountpoint_free(root->mount); + i_free(root->storage_mount_path); + i_free(root); +} + +static struct fs_quota_mountpoint *fs_quota_mountpoint_get(const char *dir) +{ + struct fs_quota_mountpoint *mount; + struct mountpoint point; + int ret; + + ret = mountpoint_get(dir, default_pool, &point); + if (ret <= 0) + return NULL; + + mount = i_new(struct fs_quota_mountpoint, 1); + mount->refcount = 1; + mount->device_path = point.device_path; + mount->mount_path = point.mount_path; + mount->type = point.type; + mount->block_size = point.block_size; +#ifdef FS_QUOTA_SOLARIS + mount->fd = -1; +#endif + + if (mount_type_is_nfs(mount)) { + if (strchr(mount->device_path, ':') == NULL) { + e_error(quota_backend_fs.event, + "%s is not a valid NFS device path", + mount->device_path); + fs_quota_mountpoint_free(mount); + return NULL; + } + } + return mount; +} + +#define QUOTA_ROOT_MATCH(root, mount) \ + ((root)->root.backend.name == quota_backend_fs.name && \ + ((root)->storage_mount_path == NULL || \ + strcmp((root)->storage_mount_path, (mount)->mount_path) == 0)) + +static struct fs_quota_root * +fs_quota_root_find_mountpoint(struct quota *quota, + const struct fs_quota_mountpoint *mount) +{ + struct quota_root *const *roots; + struct fs_quota_root *empty = NULL; + unsigned int i, count; + + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) { + struct fs_quota_root *root = (struct fs_quota_root *)roots[i]; + if (QUOTA_ROOT_MATCH(root, mount)) { + if (root->mount == NULL) + empty = root; + else if (strcmp(root->mount->mount_path, + mount->mount_path) == 0) + return root; + } + } + return empty; +} + +static void +fs_quota_mount_init(struct fs_quota_root *root, + struct fs_quota_mountpoint *mount, const char *dir) +{ + struct quota_root *const *roots; + unsigned int i, count; + +#ifdef FS_QUOTA_SOLARIS +#ifdef HAVE_RQUOTA + if (mount_type_is_nfs(mount)) { + /* using rquota for this mount */ + } else +#endif + if (mount->path == NULL) { + mount->path = i_strconcat(mount->mount_path, "/quotas", NULL); + mount->fd = open(mount->path, O_RDONLY); + if (mount->fd == -1 && errno != ENOENT) + e_error(root->root.backend.event, + "open(%s) failed: %m", mount->path); + } +#endif + root->mount = mount; + + e_debug(root->root.backend.event, "fs quota add mailbox dir = %s", dir); + e_debug(root->root.backend.event, "fs quota block device = %s", mount->device_path); + e_debug(root->root.backend.event, "fs quota mount point = %s", mount->mount_path); + e_debug(root->root.backend.event, "fs quota mount type = %s", mount->type); + + /* if there are more unused quota roots, copy this mount to them */ + roots = array_get(&root->root.quota->roots, &count); + for (i = 0; i < count; i++) { + root = (struct fs_quota_root *)roots[i]; + if (QUOTA_ROOT_MATCH(root, mount) && root->mount == NULL) { + mount->refcount++; + root->mount = mount; + } + } +} + +static void fs_quota_add_missing_mounts(struct quota *quota) +{ + struct fs_quota_mountpoint *mount; + struct quota_root *const *roots; + unsigned int i, count; + + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) { + struct fs_quota_root *root = (struct fs_quota_root *)roots[i]; + + if (root->root.backend.name != quota_backend_fs.name || + root->storage_mount_path == NULL || root->mount != NULL) + continue; + + mount = fs_quota_mountpoint_get(root->storage_mount_path); + if (mount != NULL) { + fs_quota_mount_init(root, mount, + root->storage_mount_path); + } + } +} + +static void fs_quota_namespace_added(struct quota *quota, + struct mail_namespace *ns) +{ + struct fs_quota_mountpoint *mount; + struct fs_quota_root *root; + const char *dir; + + if (!mailbox_list_get_root_path(ns->list, MAILBOX_LIST_PATH_TYPE_MAILBOX, + &dir)) + mount = NULL; + else + mount = fs_quota_mountpoint_get(dir); + if (mount != NULL) { + root = fs_quota_root_find_mountpoint(quota, mount); + if (root != NULL && root->mount == NULL) + fs_quota_mount_init(root, mount, dir); + else + fs_quota_mountpoint_free(mount); + } + + /* we would actually want to do this only once after all quota roots + are created, but there's no way to do this right now */ + fs_quota_add_missing_mounts(quota); +} + +static const char *const * +fs_quota_root_get_resources(struct quota_root *_root) +{ + struct fs_quota_root *root = (struct fs_quota_root *)_root; + static const char *resources_kb[] = { + QUOTA_NAME_STORAGE_KILOBYTES, + NULL + }; + static const char *resources_kb_messages[] = { + QUOTA_NAME_STORAGE_KILOBYTES, + QUOTA_NAME_MESSAGES, + NULL + }; + + return root->inode_per_mail ? resources_kb_messages : resources_kb; +} + +#if defined(FS_QUOTA_LINUX) || defined(FS_QUOTA_BSDAIX) || \ + defined(FS_QUOTA_NETBSD) || defined(HAVE_RQUOTA) +static void fs_quota_root_disable(struct fs_quota_root *root, bool group) +{ + if (group) + root->group_disabled = TRUE; + else + root->user_disabled = TRUE; +} +#endif + +#ifdef HAVE_RQUOTA +static void +rquota_get_result(const rquota *rq, + uint64_t *bytes_value_r, uint64_t *bytes_limit_r, + uint64_t *count_value_r, uint64_t *count_limit_r) +{ + /* use soft limits if they exist, fallback to hard limits */ + + /* convert the results from blocks to bytes */ + *bytes_value_r = (uint64_t)rq->rq_curblocks * + (uint64_t)rq->rq_bsize; + if (rq->rq_bsoftlimit != 0) { + *bytes_limit_r = (uint64_t)rq->rq_bsoftlimit * + (uint64_t)rq->rq_bsize; + } else { + *bytes_limit_r = (uint64_t)rq->rq_bhardlimit * + (uint64_t)rq->rq_bsize; + } + + *count_value_r = rq->rq_curfiles; + if (rq->rq_fsoftlimit != 0) + *count_limit_r = rq->rq_fsoftlimit; + else + *count_limit_r = rq->rq_fhardlimit; +} + +static int +do_rquota_user(struct fs_quota_root *root, + uint64_t *bytes_value_r, uint64_t *bytes_limit_r, + uint64_t *count_value_r, uint64_t *count_limit_r, + const char **error_r) +{ + struct getquota_rslt result; + struct getquota_args args; + struct timeval timeout; + enum clnt_stat call_status; + CLIENT *cl; + struct fs_quota_mountpoint *mount = root->mount; + const char *host; + char *path; + + path = strchr(mount->device_path, ':'); + i_assert(path != NULL); + + host = t_strdup_until(mount->device_path, path); + path++; + + /* For NFSv4, we send the filesystem path without initial /. Server + prepends proper NFS pseudoroot automatically and uses this for + detection of NFSv4 mounts. */ + if (strcmp(root->mount->type, "nfs4") == 0) { + while (*path == '/') + path++; + } + + e_debug(root->root.backend.event, "host=%s, path=%s, uid=%s", + host, path, dec2str(root->uid)); + + /* clnt_create() polls for a while to establish a connection */ + cl = clnt_create(host, RQUOTAPROG, RQUOTAVERS, "udp"); + if (cl == NULL) { + *error_r = t_strdup_printf( + "could not contact RPC service on %s", host); + return -1; + } + + /* Establish some RPC credentials */ + auth_destroy(cl->cl_auth); + cl->cl_auth = authunix_create_default(); + + /* make the rquota call on the remote host */ + args.gqa_pathp = path; + args.gqa_uid = root->uid; + + timeout.tv_sec = RQUOTA_GETQUOTA_TIMEOUT_SECS; + timeout.tv_usec = 0; + call_status = clnt_call(cl, RQUOTAPROC_GETQUOTA, + (xdrproc_t)xdr_getquota_args, (char *)&args, + (xdrproc_t)xdr_getquota_rslt, (char *)&result, + timeout); + + /* the result has been deserialized, let the client go */ + auth_destroy(cl->cl_auth); + clnt_destroy(cl); + + if (call_status != RPC_SUCCESS) { + const char *rpc_error_msg = clnt_sperrno(call_status); + + *error_r = t_strdup_printf( + "remote rquota call failed: %s", + rpc_error_msg); + return -1; + } + + switch (result.status) { + case Q_OK: { + rquota_get_result(&result.getquota_rslt_u.gqr_rquota, + bytes_value_r, bytes_limit_r, + count_value_r, count_limit_r); + e_debug(root->root.backend.event, "uid=%s, bytes=%"PRIu64"/%"PRIu64" " + "files=%"PRIu64"/%"PRIu64, + dec2str(root->uid), + *bytes_value_r, *bytes_limit_r, + *count_value_r, *count_limit_r); + return 1; + } + case Q_NOQUOTA: + e_debug(root->root.backend.event, "uid=%s, limit=unlimited", + dec2str(root->uid)); + fs_quota_root_disable(root, FALSE); + return 0; + case Q_EPERM: + *error_r = "permission denied to rquota service"; + return -1; + default: + *error_r = t_strdup_printf( + "unrecognized status code (%d) from rquota service", + result.status); + return -1; + } +} + +static int +do_rquota_group(struct fs_quota_root *root ATTR_UNUSED, + uint64_t *bytes_value_r ATTR_UNUSED, + uint64_t *bytes_limit_r ATTR_UNUSED, + uint64_t *count_value_r ATTR_UNUSED, + uint64_t *count_limit_r ATTR_UNUSED, + const char **error_r) +{ +#if defined(EXT_RQUOTAVERS) && defined(GRPQUOTA) + struct getquota_rslt result; + ext_getquota_args args; + struct timeval timeout; + enum clnt_stat call_status; + CLIENT *cl; + struct fs_quota_mountpoint *mount = root->mount; + const char *host; + char *path; + + path = strchr(mount->device_path, ':'); + i_assert(path != NULL); + + host = t_strdup_until(mount->device_path, path); + path++; + + e_debug(root->root.backend.event, "host=%s, path=%s, gid=%s", + host, path, dec2str(root->gid)); + + /* clnt_create() polls for a while to establish a connection */ + cl = clnt_create(host, RQUOTAPROG, EXT_RQUOTAVERS, "udp"); + if (cl == NULL) { + *error_r = t_strdup_printf( + "could not contact RPC service on %s (group)", host); + return -1; + } + + /* Establish some RPC credentials */ + auth_destroy(cl->cl_auth); + cl->cl_auth = authunix_create_default(); + + /* make the rquota call on the remote host */ + args.gqa_pathp = path; + args.gqa_id = root->gid; + args.gqa_type = GRPQUOTA; + timeout.tv_sec = RQUOTA_GETQUOTA_TIMEOUT_SECS; + timeout.tv_usec = 0; + + call_status = clnt_call(cl, RQUOTAPROC_GETQUOTA, + (xdrproc_t)xdr_ext_getquota_args, (char *)&args, + (xdrproc_t)xdr_getquota_rslt, (char *)&result, + timeout); + + /* the result has been deserialized, let the client go */ + auth_destroy(cl->cl_auth); + clnt_destroy(cl); + + if (call_status != RPC_SUCCESS) { + const char *rpc_error_msg = clnt_sperrno(call_status); + + *error_r = t_strdup_printf( + "remote ext rquota call failed: %s", rpc_error_msg); + return -1; + } + + switch (result.status) { + case Q_OK: { + rquota_get_result(&result.getquota_rslt_u.gqr_rquota, + bytes_value_r, bytes_limit_r, + count_value_r, count_limit_r); + e_debug(root->root.backend.event, "gid=%s, bytes=%"PRIu64"/%"PRIu64" " + "files=%"PRIu64"/%"PRIu64, + dec2str(root->gid), + *bytes_value_r, *bytes_limit_r, + *count_value_r, *count_limit_r); + return 1; + } + case Q_NOQUOTA: + e_debug(root->root.backend.event, "gid=%s, limit=unlimited", + dec2str(root->gid)); + fs_quota_root_disable(root, TRUE); + return 0; + case Q_EPERM: + *error_r = "permission denied to ext rquota service"; + return -1; + default: + *error_r = t_strdup_printf( + "unrecognized status code (%d) from ext rquota service", + result.status); + return -1; + } +#else + *error_r = "rquota not compiled with group support"; + return -1; +#endif +} +#endif + +#ifdef FS_QUOTA_LINUX +static int +fs_quota_get_linux(struct fs_quota_root *root, bool group, + uint64_t *bytes_value_r, uint64_t *bytes_limit_r, + uint64_t *count_value_r, uint64_t *count_limit_r, + const char **error_r) +{ + struct dqblk dqblk; + int type, id; + + type = group ? GRPQUOTA : USRQUOTA; + id = group ? root->gid : root->uid; + +#ifdef HAVE_XFS_QUOTA + if (strcmp(root->mount->type, "xfs") == 0) { + struct fs_disk_quota xdqblk; + + if (quotactl(QCMD(Q_XGETQUOTA, type), + root->mount->device_path, + id, (caddr_t)&xdqblk) < 0) { + if (errno == ESRCH) { + fs_quota_root_disable(root, group); + return 0; + } + *error_r = t_strdup_printf( + "errno=%d, quotactl(Q_XGETQUOTA, %s) failed: %m", + errno, root->mount->device_path); + return -1; + } + + /* values always returned in 512 byte blocks */ + *bytes_value_r = xdqblk.d_bcount * 512ULL; + *bytes_limit_r = xdqblk.d_blk_softlimit * 512ULL; + if (*bytes_limit_r == 0) { + *bytes_limit_r = xdqblk.d_blk_hardlimit * 512ULL; + } + *count_value_r = xdqblk.d_icount; + *count_limit_r = xdqblk.d_ino_softlimit; + if (*count_limit_r == 0) { + *count_limit_r = xdqblk.d_ino_hardlimit; + } + } else +#endif + { + /* ext2, ext3 */ + if (quotactl(QCMD(Q_GETQUOTA, type), + root->mount->device_path, + id, (caddr_t)&dqblk) < 0) { + if (errno == ESRCH) { + fs_quota_root_disable(root, group); + return 0; + } + *error_r = t_strdup_printf( + "quotactl(Q_GETQUOTA, %s) failed: %m", + root->mount->device_path); + if (errno == EINVAL) { + *error_r = t_strdup_printf("%s, " + "Dovecot was compiled with Linux quota " + "v%d support, try changing it " + "(CPPFLAGS=-D_LINUX_QUOTA_VERSION=%d configure)", + *error_r, + _LINUX_QUOTA_VERSION, + _LINUX_QUOTA_VERSION == 1 ? 2 : 1); + } + return -1; + } + +#if _LINUX_QUOTA_VERSION == 1 + *bytes_value_r = dqblk.dqb_curblocks * 1024ULL; +#else + *bytes_value_r = dqblk.dqb_curblocks; +#endif + *bytes_limit_r = dqblk.dqb_bsoftlimit * 1024ULL; + if (*bytes_limit_r == 0) { + *bytes_limit_r = dqblk.dqb_bhardlimit * 1024ULL; + } + *count_value_r = dqblk.dqb_curinodes; + *count_limit_r = dqblk.dqb_isoftlimit; + if (*count_limit_r == 0) { + *count_limit_r = dqblk.dqb_ihardlimit; + } + } + return 1; +} +#endif + +#ifdef FS_QUOTA_BSDAIX +static int +fs_quota_get_bsdaix(struct fs_quota_root *root, bool group, + uint64_t *bytes_value_r, uint64_t *bytes_limit_r, + uint64_t *count_value_r, uint64_t *count_limit_r, + const char **error_r) +{ + struct dqblk dqblk; + int type, id; + + type = group ? GRPQUOTA : USRQUOTA; + id = group ? root->gid : root->uid; + + if (quotactl(root->mount->mount_path, QCMD(Q_GETQUOTA, type), + id, (void *)&dqblk) < 0) { + if (errno == ESRCH) { + fs_quota_root_disable(root, group); + return 0; + } + *error_r = t_strdup_printf( + "quotactl(Q_GETQUOTA, %s) failed: %m", + root->mount->mount_path); + return -1; + } + *bytes_value_r = (uint64_t)dqblk.dqb_curblocks * DEV_BSIZE; + *bytes_limit_r = (uint64_t)dqblk.dqb_bsoftlimit * DEV_BSIZE; + if (*bytes_limit_r == 0) { + *bytes_limit_r = (uint64_t)dqblk.dqb_bhardlimit * DEV_BSIZE; + } + *count_value_r = dqblk.dqb_curinodes; + *count_limit_r = dqblk.dqb_isoftlimit; + if (*count_limit_r == 0) { + *count_limit_r = dqblk.dqb_ihardlimit; + } + return 1; +} +#endif + +#ifdef FS_QUOTA_NETBSD +static int +fs_quota_get_netbsd(struct fs_quota_root *root, bool group, + uint64_t *bytes_value_r, uint64_t *bytes_limit_r, + uint64_t *count_value_r, uint64_t *count_limit_r, + const char **error_r) +{ + struct quotakey qk; + struct quotaval qv; + struct quotahandle *qh; + int ret; + + if ((qh = quota_open(root->mount->mount_path)) == NULL) { + *error_r = t_strdup_printf("cannot open quota for %s: %m", + root->mount->mount_path); + fs_quota_root_disable(root, group); + return 0; + } + + qk.qk_idtype = group ? QUOTA_IDTYPE_GROUP : QUOTA_IDTYPE_USER; + qk.qk_id = group ? root->gid : root->uid; + + for (int i = 0; i < 2; i++) { + qk.qk_objtype = i == 0 ? QUOTA_OBJTYPE_BLOCKS : QUOTA_OBJTYPE_FILES; + + if (quota_get(qh, &qk, &qv) != 0) { + if (errno == ESRCH) { + fs_quota_root_disable(root, group); + return 0; + } + *error_r = t_strdup_printf( + "quotactl(Q_GETQUOTA, %s) failed: %m", + root->mount->mount_path); + ret = -1; + break; + } + if (i == 0) { + *bytes_value_r = qv.qv_usage * DEV_BSIZE; + *bytes_limit_r = qv.qv_softlimit * DEV_BSIZE; + } else { + *count_value_r = qv.qv_usage; + *count_limit_r = qv.qv_softlimit; + } + ret = 1; + } + quota_close(qh); + return ret; +} +#endif + +#ifdef FS_QUOTA_HPUX +static int +fs_quota_get_hpux(struct fs_quota_root *root, + uint64_t *bytes_value_r, uint64_t *bytes_limit_r, + uint64_t *count_value_r, uint64_t *count_limit_r, + const char **error_r) +{ + struct dqblk dqblk; + + if (quotactl(Q_GETQUOTA, root->mount->device_path, + root->uid, &dqblk) < 0) { + if (errno == ESRCH) { + root->user_disabled = TRUE; + return 0; + } + *error_r = t_strdup_printf( + "quotactl(Q_GETQUOTA, %s) failed: %m", + root->mount->device_path); + return -1; + } + + *bytes_value_r = (uint64_t)dqblk.dqb_curblocks * + root->mount->block_size; + *bytes_limit_r = (uint64_t)dqblk.dqb_bsoftlimit * + root->mount->block_size; + if (*bytes_limit_r == 0) { + *bytes_limit_r = (uint64_t)dqblk.dqb_bhardlimit * + root->mount->block_size; + } + *count_value_r = dqblk.dqb_curfiles; + *count_limit_r = dqblk.dqb_fsoftlimit; + if (*count_limit_r == 0) { + *count_limit_r = dqblk.dqb_fhardlimit; + } + return 1; +} +#endif + +#ifdef FS_QUOTA_SOLARIS +static int +fs_quota_get_solaris(struct fs_quota_root *root, + uint64_t *bytes_value_r, uint64_t *bytes_limit_r, + uint64_t *count_value_r, uint64_t *count_limit_r, + const char **error_r) +{ + struct dqblk dqblk; + struct quotctl ctl; + + if (root->mount->fd == -1) + return 0; + + ctl.op = Q_GETQUOTA; + ctl.uid = root->uid; + ctl.addr = (caddr_t)&dqblk; + if (ioctl(root->mount->fd, Q_QUOTACTL, &ctl) < 0) { + *error_r = t_strdup_printf( + "ioctl(%s, Q_QUOTACTL) failed: %m", + root->mount->path); + return -1; + } + *bytes_value_r = (uint64_t)dqblk.dqb_curblocks * DEV_BSIZE; + *bytes_limit_r = (uint64_t)dqblk.dqb_bsoftlimit * DEV_BSIZE; + if (*bytes_limit_r == 0) { + *bytes_limit_r = (uint64_t)dqblk.dqb_bhardlimit * DEV_BSIZE; + } + *count_value_r = dqblk.dqb_curfiles; + *count_limit_r = dqblk.dqb_fsoftlimit; + if (*count_limit_r == 0) { + *count_limit_r = dqblk.dqb_fhardlimit; + } + return 1; +} +#endif + +static int +fs_quota_get_resources(struct fs_quota_root *root, bool group, + uint64_t *bytes_value_r, uint64_t *bytes_limit_r, + uint64_t *count_value_r, uint64_t *count_limit_r, + const char **error_r) +{ + if (group) { + if (root->group_disabled) + return 0; + } else { + if (root->user_disabled) + return 0; + } +#ifdef FS_QUOTA_LINUX + return fs_quota_get_linux(root, group, bytes_value_r, bytes_limit_r, + count_value_r, count_limit_r, error_r); +#elif defined (FS_QUOTA_NETBSD) + return fs_quota_get_netbsd(root, group, bytes_value_r, bytes_limit_r, + count_value_r, count_limit_r, error_r); +#elif defined (FS_QUOTA_BSDAIX) + return fs_quota_get_bsdaix(root, group, bytes_value_r, bytes_limit_r, + count_value_r, count_limit_r, error_r); +#else + if (group) { + /* not supported */ + return 0; + } +#ifdef FS_QUOTA_HPUX + return fs_quota_get_hpux(root, bytes_value_r, bytes_limit_r, + count_value_r, count_limit_r, error_r); +#else + return fs_quota_get_solaris(root, bytes_value_r, bytes_limit_r, + count_value_r, count_limit_r, error_r); +#endif +#endif +} + +static bool fs_quota_match_box(struct quota_root *_root, struct mailbox *box) +{ + struct fs_quota_root *root = (struct fs_quota_root *)_root; + struct stat mst, rst; + const char *mailbox_path; + bool match; + + if (root->storage_mount_path == NULL) + return TRUE; + + if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX, + &mailbox_path) <= 0) + return FALSE; + if (stat(mailbox_path, &mst) < 0) { + if (errno != ENOENT) + e_error(_root->backend.event, + "stat(%s) failed: %m", mailbox_path); + return FALSE; + } + if (stat(root->storage_mount_path, &rst) < 0) { + e_debug(_root->backend.event, "stat(%s) failed: %m", + root->storage_mount_path); + return FALSE; + } + match = CMP_DEV_T(mst.st_dev, rst.st_dev); + e_debug(_root->backend.event, "box=%s mount=%s match=%s", mailbox_path, + root->storage_mount_path, match ? "yes" : "no"); + return match; +} + +static enum quota_get_result +fs_quota_get_resource(struct quota_root *_root, const char *name, + uint64_t *value_r, const char **error_r) +{ + struct fs_quota_root *root = (struct fs_quota_root *)_root; + uint64_t bytes_value, count_value; + uint64_t bytes_limit = 0, count_limit = 0; + int ret; + + *value_r = 0; + + if (root->mount == NULL) { + if (root->storage_mount_path != NULL) + *error_r = t_strdup_printf( + "Mount point unknown for path %s", + root->storage_mount_path); + else + *error_r = "Mount point unknown"; + return QUOTA_GET_RESULT_INTERNAL_ERROR; + } + if (strcasecmp(name, QUOTA_NAME_STORAGE_BYTES) != 0 && + strcasecmp(name, QUOTA_NAME_MESSAGES) != 0) { + *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING; + return QUOTA_GET_RESULT_UNKNOWN_RESOURCE; + } + +#ifdef HAVE_RQUOTA + if (mount_type_is_nfs(root->mount)) { + ret = root->user_disabled ? 0 : + do_rquota_user(root, &bytes_value, &bytes_limit, + &count_value, &count_limit, error_r); + if (ret == 0 && !root->group_disabled) + ret = do_rquota_group(root, &bytes_value, + &bytes_limit, &count_value, + &count_limit, error_r); + } else +#endif + { + ret = fs_quota_get_resources(root, FALSE, &bytes_value, + &bytes_limit, &count_value, + &count_limit, error_r); + if (ret == 0) { + /* fallback to group quota */ + ret = fs_quota_get_resources(root, TRUE, &bytes_value, + &bytes_limit, &count_value, + &count_limit, error_r); + } + } + if (ret < 0) + return QUOTA_GET_RESULT_INTERNAL_ERROR; + else if (ret == 0) + return QUOTA_GET_RESULT_LIMITED; + + if (strcasecmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) + *value_r = bytes_value; + else + *value_r = count_value; + if (_root->bytes_limit != (int64_t)bytes_limit || + _root->count_limit != (int64_t)count_limit) { + /* update limit */ + _root->bytes_limit = bytes_limit; + _root->count_limit = count_limit; + + /* limits have changed, so we'll need to recalculate + relative quota rules */ + quota_root_recalculate_relative_rules(_root->set, bytes_limit, count_limit); + } + return QUOTA_GET_RESULT_LIMITED; +} + +static int +fs_quota_update(struct quota_root *root ATTR_UNUSED, + struct quota_transaction_context *ctx ATTR_UNUSED, + const char **error_r ATTR_UNUSED) +{ + return 0; +} + +struct quota_backend quota_backend_fs = { + .name = "fs", + + .v = { + .alloc = fs_quota_alloc, + .init = fs_quota_init, + .deinit = fs_quota_deinit, + + .namespace_added = fs_quota_namespace_added, + + .get_resources = fs_quota_root_get_resources, + .get_resource = fs_quota_get_resource, + .update = fs_quota_update, + + .match_box = fs_quota_match_box, + } +}; + +#endif diff --git a/src/plugins/quota/quota-fs.h b/src/plugins/quota/quota-fs.h new file mode 100644 index 0000000..8508cdf --- /dev/null +++ b/src/plugins/quota/quota-fs.h @@ -0,0 +1,51 @@ +#ifndef QUOTA_FS_H +#define QUOTA_FS_H + +#if defined (HAVE_STRUCT_DQBLK_CURBLOCKS) || \ + defined (HAVE_STRUCT_DQBLK_CURSPACE) +# define HAVE_FS_QUOTA +#endif + +#ifdef HAVE_QUOTA_OPEN +/* absolute path to avoid confusion with ./quota.h */ +# include "/usr/include/quota.h" /* NetBSD with libquota */ +#endif + +#ifdef HAVE_SYS_QUOTA_H +# include <sys/quota.h> /* Linux, HP-UX */ +#elif defined(HAVE_SYS_FS_UFS_QUOTA_H) +# include <sys/fs/ufs_quota.h> /* Solaris */ +#elif defined(HAVE_UFS_UFS_QUOTA_H) +# include <ufs/ufs/quota.h> /* BSDs */ +#elif defined(HAVE_JFS_QUOTA_H) +# include <jfs/quota.h> /* AIX */ +# ifdef HAVE_SYS_FS_QUOTA_COMMON_H +# include <sys/fs/quota_common.h> /* quotactl() */ +# endif +#else +# undef HAVE_FS_QUOTA +#endif + +#ifdef HAVE_QUOTACTL +# ifdef HAVE_SYS_QUOTA_H +# ifndef _HPUX_SOURCE +# define FS_QUOTA_LINUX +# else +# define FS_QUOTA_HPUX +# endif +# else +# define FS_QUOTA_BSDAIX +# endif +#elif defined (HAVE_Q_QUOTACTL) +# define FS_QUOTA_SOLARIS +#else +# undef HAVE_FS_QUOTA +#endif + +#ifdef HAVE_QUOTA_OPEN /* NetBSD with libquota */ +# define FS_QUOTA_NETBSD +# define HAVE_FS_QUOTA +# undef FS_QUOTA_LINUX /* obtained because we also have <sys/quota.h> */ +#endif + +#endif diff --git a/src/plugins/quota/quota-imapc.c b/src/plugins/quota/quota-imapc.c new file mode 100644 index 0000000..d5931db --- /dev/null +++ b/src/plugins/quota/quota-imapc.c @@ -0,0 +1,494 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "ioloop.h" +#include "imap-arg.h" +#include "imapc-storage.h" +#include "mailbox-list-private.h" +#include "quota-private.h" + +struct imapc_quota_refresh_root { + const char *name; + unsigned int order; + + uint64_t bytes_cur, count_cur; + uint64_t bytes_limit, count_limit; +}; + +struct imapc_quota_refresh { + pool_t pool; + const char *box_name; + ARRAY(struct imapc_quota_refresh_root) roots; +}; + +struct imapc_quota_root { + struct quota_root root; + const char *box_name, *root_name; + + struct mail_namespace *imapc_ns; + struct imapc_storage_client *client; + bool initialized; + + uint64_t bytes_last, count_last; + + struct timeval last_refresh; + struct imapc_quota_refresh refresh; +}; + +extern struct quota_backend quota_backend_imapc; + +static struct quota_root *imapc_quota_alloc(void) +{ + struct imapc_quota_root *root; + + root = i_new(struct imapc_quota_root, 1); + return &root->root; +} + +static void handle_box_param(struct quota_root *_root, const char *param_value) +{ + ((struct imapc_quota_root *)_root)->box_name = p_strdup(_root->pool, param_value); +} + +static void handle_root_param(struct quota_root *_root, const char *param_value) +{ + ((struct imapc_quota_root *)_root)->root_name = p_strdup(_root->pool, param_value); +} + +static int imapc_quota_init(struct quota_root *_root, const char *args, + const char **error_r) +{ + struct imapc_quota_root *root = (struct imapc_quota_root *)_root; + const struct quota_param_parser imapc_params[] = { + {.param_name = "box=", .param_handler = handle_box_param}, + {.param_name = "root=", .param_handler = handle_root_param}, + quota_param_ns, + {.param_name = NULL} + }; + + _root->auto_updating = TRUE; + event_set_append_log_prefix(root->root.backend.event, "quota-imapc: "); + + if (quota_parse_parameters(_root, &args, error_r, imapc_params, TRUE) < 0) + return -1; + + if (root->box_name == NULL && root->root_name == NULL) + root->box_name = "INBOX"; + /* we'll never try to enforce the quota - it's just a lot of + unnecessary remote GETQUOTA calls. */ + _root->no_enforcing = TRUE; + return 0; +} + +static void imapc_quota_deinit(struct quota_root *_root) +{ + i_free(_root); +} + +static void +imapc_quota_root_namespace_added(struct quota_root *_root, + struct mail_namespace *ns) +{ + struct imapc_quota_root *root = (struct imapc_quota_root *)_root; + + if (root->imapc_ns == NULL) + root->imapc_ns = ns; +} + +static struct imapc_quota_refresh * +imapc_quota_root_refresh_find(struct imapc_storage_client *client) +{ + struct imapc_storage *storage = client->_storage; + struct quota *quota; + struct quota_root *const *rootp; + + i_assert(storage != NULL); + quota = quota_get_mail_user_quota(storage->storage.user); + i_assert(quota != NULL); + + /* find the quota root that is being refreshed */ + array_foreach("a->roots, rootp) { + if ((*rootp)->backend.name == quota_backend_imapc.name) { + struct imapc_quota_root *root = + (struct imapc_quota_root *)*rootp; + + if (root->refresh.pool != NULL) + return &root->refresh; + } + } + return NULL; +} + +static struct imapc_quota_refresh_root * +imapc_quota_refresh_root_get(struct imapc_quota_refresh *refresh, + const char *root_name) +{ + struct imapc_quota_refresh_root *refresh_root; + + array_foreach_modifiable(&refresh->roots, refresh_root) { + if (strcmp(refresh_root->name, root_name) == 0) + return refresh_root; + } + + refresh_root = array_append_space(&refresh->roots); + refresh_root->order = UINT_MAX; + refresh_root->name = p_strdup(refresh->pool, root_name); + refresh_root->bytes_limit = (uint64_t)-1; + refresh_root->count_limit = (uint64_t)-1; + return refresh_root; +} + +static void imapc_untagged_quotaroot(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client) +{ + struct imapc_quota_refresh *refresh; + struct imapc_quota_refresh_root *refresh_root; + const char *mailbox_name, *root_name; + unsigned int i; + + if (!imap_arg_get_astring(&reply->args[0], &mailbox_name)) + return; + + if ((refresh = imapc_quota_root_refresh_find(client)) == NULL || + refresh->box_name == NULL || + strcmp(refresh->box_name, mailbox_name) != 0) { + /* unsolicited QUOTAROOT reply - ignore */ + return; + } + if (array_count(&refresh->roots) > 0) { + /* duplicate QUOTAROOT reply - ignore */ + return; + } + + i = 1; + while (imap_arg_get_astring(&reply->args[i], &root_name)) { + refresh_root = imapc_quota_refresh_root_get(refresh, root_name); + refresh_root->order = i; + i++; + } +} + +static void imapc_untagged_quota(const struct imapc_untagged_reply *reply, + struct imapc_storage_client *client) +{ + const struct imap_arg *list; + struct imapc_quota_refresh *refresh; + struct imapc_quota_refresh_root *refresh_root; + const char *root_name, *resource, *value_str, *limit_str; + uint64_t value, limit; + unsigned int i; + + if (!imap_arg_get_astring(&reply->args[0], &root_name) || + !imap_arg_get_list(&reply->args[1], &list)) + return; + + if ((refresh = imapc_quota_root_refresh_find(client)) == NULL) { + /* unsolicited QUOTA reply - ignore */ + return; + } + refresh_root = imapc_quota_refresh_root_get(refresh, root_name); + + for (i = 0; list[i].type != IMAP_ARG_EOL; i += 3) { + if (!imap_arg_get_atom(&list[i], &resource) || + !imap_arg_get_atom(&list[i+1], &value_str) || + !imap_arg_get_atom(&list[i+2], &limit_str) || + /* RFC2087 uses 32bit number, but be ready for future */ + str_to_uint64(value_str, &value) < 0 || + str_to_uint64(limit_str, &limit) < 0) + return; + + if (strcasecmp(resource, QUOTA_NAME_STORAGE_KILOBYTES) == 0) { + refresh_root->bytes_cur = value * 1024; + refresh_root->bytes_limit = limit * 1024; + } else if (strcasecmp(resource, QUOTA_NAME_MESSAGES) == 0) { + refresh_root->count_cur = value; + refresh_root->count_limit = limit; + } + } +} + +static bool imapc_quota_client_init(struct imapc_quota_root *root) +{ + struct mailbox_list *list; + struct mail_storage *storage; + + if (root->initialized) + return root->client != NULL; + root->initialized = TRUE; + + list = root->imapc_ns->list; + if (mailbox_list_get_storage(&list, "", &storage) == 0 && + strcmp(storage->name, IMAPC_STORAGE_NAME) != 0) { + /* non-imapc namespace, skip */ + if ((storage->class_flags & + MAIL_STORAGE_CLASS_FLAG_NOQUOTA) == 0) { + e_warning(root->root.backend.event, + "Namespace '%s' is not imapc, " + "skipping for imapc quota", + root->imapc_ns->prefix); + } + return FALSE; + } + root->client = ((struct imapc_storage *)storage)->client; + + imapc_storage_client_register_untagged(root->client, "QUOTAROOT", + imapc_untagged_quotaroot); + imapc_storage_client_register_untagged(root->client, "QUOTA", + imapc_untagged_quota); + return TRUE; +} + +static void imapc_quota_refresh_init(struct imapc_quota_refresh *refresh) +{ + i_assert(refresh->pool == NULL); + + refresh->pool = pool_alloconly_create("imapc quota refresh", 256); + p_array_init(&refresh->roots, refresh->pool, 4); +} + +static void +imapc_quota_refresh_update(struct quota *quota, + struct imapc_quota_refresh *refresh) +{ + struct quota_root *const *rootp; + const struct imapc_quota_refresh_root *refresh_root; + + if (array_count(&refresh->roots) == 0) { + e_error(quota_backend_imapc.event, + "imapc didn't return any QUOTA results"); + return; + } + /* use the first quota root for everything */ + refresh_root = array_front(&refresh->roots); + + array_foreach("a->roots, rootp) { + if ((*rootp)->backend.name == quota_backend_imapc.name) { + struct imapc_quota_root *root = + (struct imapc_quota_root *)*rootp; + + root->bytes_last = refresh_root->bytes_cur; + root->count_last = refresh_root->count_cur; + + /* If limits are higher than what dovecot can handle + consider them unlimited. */ + if (refresh_root->bytes_limit > INT64_MAX) + root->root.bytes_limit = 0; + else + root->root.bytes_limit = refresh_root->bytes_limit; + if (refresh_root->count_limit > INT64_MAX) + root->root.count_limit = 0; + else + root->root.count_limit = refresh_root->count_limit; + } + } +} + +static void +imapc_quota_refresh_deinit(struct quota *quota, + struct imapc_quota_refresh *refresh, bool success) +{ + if (success) + imapc_quota_refresh_update(quota, refresh); + pool_unref(&refresh->pool); + i_zero(refresh); +} + +static int +imapc_quota_refresh_root_order_cmp(const struct imapc_quota_refresh_root *root1, + const struct imapc_quota_refresh_root *root2) +{ + if (root1->order < root2->order) + return -1; + else if (root1->order > root2->order) + return 1; + else + return 0; +} + +static int imapc_quota_refresh_mailbox(struct imapc_quota_root *root, + const char **error_r) +{ + struct imapc_simple_context sctx; + struct imapc_command *cmd; + + i_assert(root->box_name != NULL); + + /* ask quotas for the configured mailbox */ + imapc_quota_refresh_init(&root->refresh); + root->refresh.box_name = root->box_name; + + imapc_simple_context_init(&sctx, root->client); + cmd = imapc_client_cmd(root->client->client, + imapc_simple_callback, &sctx); + imapc_command_sendf(cmd, "GETQUOTAROOT %s", root->box_name); + imapc_simple_run(&sctx, &cmd); + + /* if there are multiple quota roots, use the first one returned by + the QUOTAROOT */ + array_sort(&root->refresh.roots, imapc_quota_refresh_root_order_cmp); + imapc_quota_refresh_deinit(root->root.quota, &root->refresh, + sctx.ret == 0); + if (sctx.ret < 0) + *error_r = t_strdup_printf( + "GETQUOTAROOT %s failed: %s", + root->box_name, + mail_storage_get_last_internal_error( + &root->client->_storage->storage, NULL)); + + return sctx.ret; +} + +static int imapc_quota_refresh_root(struct imapc_quota_root *root, + const char **error_r) +{ + struct imapc_simple_context sctx; + struct imapc_command *cmd; + + i_assert(root->root_name != NULL); + + /* ask quotas for the configured quota root */ + imapc_quota_refresh_init(&root->refresh); + + imapc_simple_context_init(&sctx, root->client); + cmd = imapc_client_cmd(root->client->client, + imapc_simple_callback, &sctx); + imapc_command_sendf(cmd, "GETQUOTA %s", root->root_name); + imapc_simple_run(&sctx, &cmd); + + /* there shouldn't be more than one QUOTA reply, but ignore anyway + anything we didn't expect. */ + while (array_count(&root->refresh.roots) > 0) { + const struct imapc_quota_refresh_root *refresh_root = + array_front(&root->refresh.roots); + if (strcmp(refresh_root->name, root->root_name) == 0) + break; + array_pop_front(&root->refresh.roots); + } + imapc_quota_refresh_deinit(root->root.quota, &root->refresh, + sctx.ret == 0); + if (sctx.ret < 0) + *error_r = t_strdup_printf( + "GETQUOTA %s failed: %s", + root->root_name, + mail_storage_get_last_internal_error( + &root->client->_storage->storage, NULL)); + return sctx.ret; +} + +static int imapc_quota_refresh(struct imapc_quota_root *root, + const char **error_r) +{ + enum imapc_capability capa; + int ret; + + if (root->imapc_ns == NULL) { + /* imapc namespace is missing - disable this quota backend */ + return 0; + } + if (root->last_refresh.tv_sec == ioloop_timeval.tv_sec && + root->last_refresh.tv_usec == ioloop_timeval.tv_usec) + return 0; + if (!imapc_quota_client_init(root)) + return 0; + + if (imapc_client_get_capabilities(root->client->client, &capa) < 0) { + *error_r = "Failed to get server capabilities"; + return -1; + } + if ((capa & IMAPC_CAPABILITY_QUOTA) == 0) { + /* no QUOTA capability - disable quota */ + e_warning(root->root.backend.event, + "Remote IMAP server doesn't support QUOTA - disabling"); + root->client = NULL; + return 0; + } + + if (root->root_name == NULL) + ret = imapc_quota_refresh_mailbox(root, error_r); + else + ret = imapc_quota_refresh_root(root, error_r); + + /* set the last_refresh only after the refresh, because it changes + ioloop_timeval. */ + root->last_refresh = ioloop_timeval; + return ret; +} + +static int imapc_quota_init_limits(struct quota_root *_root, + const char **error_r) +{ + struct imapc_quota_root *root = (struct imapc_quota_root *)_root; + + return imapc_quota_refresh(root, error_r); +} + +static void +imapc_quota_namespace_added(struct quota *quota, struct mail_namespace *ns) +{ + struct quota_root **roots; + unsigned int i, count; + + roots = array_get_modifiable("a->roots, &count); + for (i = 0; i < count; i++) { + if (roots[i]->backend.name == quota_backend_imapc.name && + ((roots[i]->ns_prefix == NULL && + ns->type == MAIL_NAMESPACE_TYPE_PRIVATE) || + roots[i]->ns == ns)) + imapc_quota_root_namespace_added(roots[i], ns); + } +} + +static const char *const * +imapc_quota_root_get_resources(struct quota_root *root ATTR_UNUSED) +{ + static const char *resources_both[] = { + QUOTA_NAME_STORAGE_KILOBYTES, + QUOTA_NAME_MESSAGES, + NULL + }; + return resources_both; +} + +static enum quota_get_result +imapc_quota_get_resource(struct quota_root *_root, const char *name, + uint64_t *value_r, const char **error_r) +{ + struct imapc_quota_root *root = (struct imapc_quota_root *)_root; + + if (imapc_quota_refresh(root, error_r) < 0) + return QUOTA_GET_RESULT_INTERNAL_ERROR; + + if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) + *value_r = root->bytes_last; + else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0) + *value_r = root->count_last; + else { + *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING; + return QUOTA_GET_RESULT_UNKNOWN_RESOURCE; + } + return QUOTA_GET_RESULT_LIMITED; +} + +static int +imapc_quota_update(struct quota_root *root ATTR_UNUSED, + struct quota_transaction_context *ctx ATTR_UNUSED, + const char **error_r ATTR_UNUSED) +{ + return 0; +} + +struct quota_backend quota_backend_imapc = { + .name = "imapc", + + .v = { + .alloc = imapc_quota_alloc, + .init = imapc_quota_init, + .deinit = imapc_quota_deinit, + .init_limits = imapc_quota_init_limits, + .namespace_added = imapc_quota_namespace_added, + .get_resources = imapc_quota_root_get_resources, + .get_resource = imapc_quota_get_resource, + .update = imapc_quota_update, + } +}; diff --git a/src/plugins/quota/quota-maildir.c b/src/plugins/quota/quota-maildir.c new file mode 100644 index 0000000..f4fd3a7 --- /dev/null +++ b/src/plugins/quota/quota-maildir.c @@ -0,0 +1,953 @@ +/* Copyright (c) 2006-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "ioloop.h" +#include "nfs-workarounds.h" +#include "safe-mkstemp.h" +#include "mkdir-parents.h" +#include "read-full.h" +#include "write-full.h" +#include "str.h" +#include "maildir-storage.h" +#include "mailbox-list-private.h" +#include "quota-private.h" + +#include <stdio.h> +#include <dirent.h> +#include <sys/stat.h> + +#define MAILDIRSIZE_FILENAME "maildirsize" +#define MAILDIRSIZE_STALE_SECS (60*15) + +struct maildir_quota_root { + struct quota_root root; + + struct mail_namespace *maildirsize_ns; + const char *maildirsize_path; + + uint64_t total_bytes; + uint64_t total_count; + + int fd; + time_t recalc_last_stamp; + off_t last_size; + + bool limits_initialized:1; +}; + +struct maildir_list_context { + struct mailbox_list *list; + struct maildir_quota_root *root; + struct mailbox_list_iterate_context *iter; + const struct mailbox_info *info; + + string_t *path; + int state; +}; + +extern struct quota_backend quota_backend_maildir; + +static struct dotlock_settings dotlock_settings = { + .timeout = 0, + .stale_timeout = 30 +}; + +static int maildir_sum_dir(const char *dir, uint64_t *total_bytes, + uint64_t *total_count, const char **error_r) +{ + DIR *dirp; + struct dirent *dp; + string_t *path; + const char *p; + size_t len; + uoff_t num; + int ret = 0; + + dirp = opendir(dir); + if (dirp == NULL) { + if (errno == ENOENT || errno == ESTALE) + return 0; + *error_r = t_strdup_printf("opendir(%s) failed: %m", dir); + return -1; + } + + path = t_str_new(256); + str_append(path, dir); + str_append_c(path, '/'); + + len = str_len(path); + while ((dp = readdir(dirp)) != NULL) { + if (dp->d_name[0] == '.' && + (dp->d_name[1] == '\0' || dp->d_name[1] == '.')) + continue; + + p = strstr(dp->d_name, ",S="); + num = UOFF_T_MAX; + if (p != NULL) { + /* ,S=nnnn[:,] */ + p += 3; + for (num = 0; *p >= '0' && *p <= '9'; p++) + num = num * 10 + (*p - '0'); + + if (*p != ':' && *p != '\0' && *p != ',') { + /* not in expected format, fallback to stat() */ + num = UOFF_T_MAX; + } else { + *total_bytes += num; + *total_count += 1; + } + } + if (num == UOFF_T_MAX) { + struct stat st; + + str_truncate(path, len); + str_append(path, dp->d_name); + if (stat(str_c(path), &st) == 0) { + *total_bytes += st.st_size; + *total_count += 1; + } else if (errno != ENOENT && errno != ESTALE) { + *error_r = t_strdup_printf( + "stat(%s) failed: %m", str_c(path)); + ret = -1; + } + } + } + + if (closedir(dirp) < 0) { + *error_r = t_strdup_printf("closedir(%s) failed: %m", dir); + return -1; + } + return ret; +} + +static struct maildir_list_context * +maildir_list_init(struct maildir_quota_root *root, struct mailbox_list *list) +{ + struct maildir_list_context *ctx; + + ctx = i_new(struct maildir_list_context, 1); + ctx->root = root; + ctx->path = str_new(default_pool, 512); + ctx->list = list; + ctx->iter = mailbox_list_iter_init(list, "*", + MAILBOX_LIST_ITER_SKIP_ALIASES | + MAILBOX_LIST_ITER_RETURN_NO_FLAGS); + return ctx; +} + +static bool maildir_set_next_path(struct maildir_list_context *ctx) +{ + const char *path, *storage_name; + + str_truncate(ctx->path, 0); + + storage_name = mailbox_list_get_storage_name( + ctx->info->ns->list, ctx->info->vname); + if (mailbox_list_get_path(ctx->list, storage_name, + MAILBOX_LIST_PATH_TYPE_MAILBOX, + &path) > 0) { + str_append(ctx->path, path); + str_append(ctx->path, ctx->state == 0 ? + "/new" : "/cur"); + } + + return str_len(ctx->path) > 0; +} + +static const char * +maildir_list_next(struct maildir_list_context *ctx, time_t *mtime_r) +{ + struct quota_rule *rule; + struct stat st; + + for (;;) { + if (ctx->state == 0) { + ctx->info = mailbox_list_iter_next(ctx->iter); + if (ctx->info == NULL) + return NULL; + + rule = quota_root_rule_find(ctx->root->root.set, + ctx->info->vname); + if (rule != NULL && rule->ignore) { + /* mailbox not included in quota */ + continue; + } + } + + if (!maildir_set_next_path(ctx)) { + ctx->state = 0; + continue; + } + + if (++ctx->state == 2) + ctx->state = 0; + + if (stat(str_c(ctx->path), &st) == 0) + break; + /* ignore if the directory got lost, stale or if it was + actually a file and not a directory */ + if (errno != ENOENT && errno != ESTALE && errno != ENOTDIR) { + e_error(ctx->root->root.backend.event, + "stat(%s) failed: %m", str_c(ctx->path)); + ctx->state = 0; + } + } + + *mtime_r = st.st_mtime; + return str_c(ctx->path); +} + +static int maildir_list_deinit(struct maildir_list_context *ctx, + const char **error_r) +{ + int ret = mailbox_list_iter_deinit(&ctx->iter); + if (ret < 0) + *error_r = t_strdup_printf( + "Listing mailboxes failed: %s", + mailbox_list_get_last_internal_error(ctx->list, NULL)); + + str_free(&ctx->path); + i_free(ctx); + return ret; +} + +static int +maildirs_check_have_changed(struct maildir_quota_root *root, + struct mail_namespace *ns, time_t latest_mtime, + const char **error_r) +{ + struct maildir_list_context *ctx; + time_t mtime; + int ret = 0; + + ctx = maildir_list_init(root, ns->list); + while (maildir_list_next(ctx, &mtime) != NULL) { + if (mtime > latest_mtime) { + ret = 1; + break; + } + } + if (maildir_list_deinit(ctx, error_r) < 0) + return -1; + return ret; +} + +static int maildirsize_write(struct maildir_quota_root *root, const char *path) +{ + const struct mail_storage_settings *set = + root->maildirsize_ns->mail_set; + struct quota_root *_root = &root->root; + struct mail_namespace *const *namespaces; + unsigned int i, count; + struct mailbox_permissions perm; + const char *p, *dir; + string_t *str, *temp_path; + int fd; + + i_assert(root->fd == -1); + + /* figure out what permissions we should use for maildirsize. + use the inbox namespace's permissions if possible. */ + perm.file_create_mode = 0600; perm.dir_create_mode = 0700; + perm.file_create_gid = (gid_t)-1; + perm.file_create_gid_origin = "default"; + namespaces = array_get(&root->root.quota->namespaces, &count); + i_assert(count > 0); + for (i = 0; i < count; i++) { + if ((namespaces[i]->flags & NAMESPACE_FLAG_INBOX_USER) == 0) + continue; + + mailbox_list_get_root_permissions(namespaces[i]->list, + &perm); + break; + } + + dotlock_settings.use_excl_lock = set->dotlock_use_excl; + dotlock_settings.nfs_flush = set->mail_nfs_storage; + + temp_path = t_str_new(128); + str_append(temp_path, path); + fd = safe_mkstemp_hostpid_group(temp_path, perm.file_create_mode, + perm.file_create_gid, + perm.file_create_gid_origin); + if (fd == -1 && errno == ENOENT) { + /* the control directory doesn't exist yet? create it */ + p = strrchr(path, '/'); + dir = t_strdup_until(path, p); + if (mkdir_parents_chgrp(dir, perm.dir_create_mode, + perm.file_create_gid, + perm.file_create_gid_origin) < 0 && + errno != EEXIST) { + e_error(root->root.backend.event, + "mkdir_parents(%s) failed: %m", dir); + return -1; + } + fd = safe_mkstemp_hostpid_group(temp_path, + perm.file_create_mode, + perm.file_create_gid, + perm.file_create_gid_origin); + } + if (fd == -1) { + e_error(root->root.backend.event, + "safe_mkstemp(%s) failed: %m", path); + return -1; + } + + str = t_str_new(128); + /* if we have no limits, write 0S instead of an empty line */ + if (_root->bytes_limit != 0 || _root->count_limit == 0) { + str_printfa(str, "%"PRId64"S", _root->bytes_limit); + } + if (_root->count_limit != 0) { + if (str_len(str) > 0) + str_append_c(str, ','); + str_printfa(str, "%"PRIu64"C", _root->count_limit); + } + str_printfa(str, "\n%"PRIu64" %"PRIu64"\n", + root->total_bytes, root->total_count); + if (write_full(fd, str_data(str), str_len(str)) < 0) { + e_error(root->root.backend.event, + "write_full(%s) failed: %m", str_c(temp_path)); + i_close_fd(&fd); + i_unlink(str_c(temp_path)); + return -1; + } + i_close_fd(&fd); + + if (rename(str_c(temp_path), path) < 0) { + e_error(root->root.backend.event, + "rename(%s, %s) failed: %m", str_c(temp_path), path); + i_unlink_if_exists(str_c(temp_path)); + return -1; + } + return 0; +} + +static void maildirsize_recalculate_init(struct maildir_quota_root *root) +{ + root->total_bytes = root->total_count = 0; + root->recalc_last_stamp = 0; +} + +static int maildirsize_recalculate_namespace(struct maildir_quota_root *root, + struct mail_namespace *ns, + const char **error_r) +{ + struct maildir_list_context *ctx; + const char *dir; + time_t mtime; + int ret = 0; + + ctx = maildir_list_init(root, ns->list); + while ((dir = maildir_list_next(ctx, &mtime)) != NULL) { + if (mtime > root->recalc_last_stamp) + root->recalc_last_stamp = mtime; + + if (maildir_sum_dir(dir, &root->total_bytes, + &root->total_count, error_r) < 0) + ret = -1; + } + if (maildir_list_deinit(ctx, error_r) < 0) + ret = -1; + + return ret; +} + +static void maildirsize_rebuild_later(struct maildir_quota_root *root) +{ + if (!root->root.set->force_default_rule) { + /* FIXME: can't unlink(), because the limits would be lost. */ + return; + } + + if (unlink(root->maildirsize_path) < 0 && + errno != ENOENT && errno != ESTALE) + e_error(root->root.backend.event, + "unlink(%s) failed: %m", root->maildirsize_path); +} + +static int maildirsize_recalculate_finish(struct maildir_quota_root *root, + int ret, const char **error_r) +{ + if (ret == 0) { + /* maildir didn't change, we can write the maildirsize file */ + if ((ret = maildirsize_write(root, root->maildirsize_path)) < 0) + *error_r = "failed to write maildirsize"; + } + if (ret != 0) + maildirsize_rebuild_later(root); + + return ret; +} + +static int maildirsize_recalculate(struct maildir_quota_root *root, + const char **error_r) +{ + struct mail_namespace *const *namespaces; + struct event_reason *reason; + unsigned int i, count; + int ret = 0; + + reason = event_reason_begin("quota:recalculate"); + maildirsize_recalculate_init(root); + + /* count mails from all namespaces */ + namespaces = array_get(&root->root.quota->namespaces, &count); + for (i = 0; i < count; i++) { + if (!quota_root_is_namespace_visible(&root->root, namespaces[i])) + continue; + + if (maildirsize_recalculate_namespace(root, namespaces[i], error_r) < 0) { + ret = -1; + break; + } + } + + if (ret == 0) { + /* check if any of the directories have changed */ + for (i = 0; i < count; i++) { + if (!quota_root_is_namespace_visible(&root->root, + namespaces[i])) + continue; + + ret = maildirs_check_have_changed(root, namespaces[i], + root->recalc_last_stamp, + error_r); + if (ret != 0) + break; + } + } + + ret = maildirsize_recalculate_finish(root, ret, error_r); + event_reason_end(&reason); + return ret; +} + +static bool +maildir_parse_limit(const char *str, uint64_t *bytes_r, uint64_t *count_r) +{ + const char *const *limit; + unsigned long long value; + const char *pos; + bool ret = TRUE; + + *bytes_r = 0; + *count_r = 0; + + /* 0 values mean unlimited */ + for (limit = t_strsplit(str, ","); *limit != NULL; limit++) { + if (str_parse_ullong(*limit, &value, &pos) < 0) { + ret = FALSE; + continue; + } + if (pos[0] != '\0' && pos[1] == '\0') { + switch (pos[0]) { + case 'C': + if (value != 0) + *count_r = value; + break; + case 'S': + if (value != 0) + *bytes_r = value; + break; + default: + ret = FALSE; + break; + } + } else { + ret = FALSE; + } + } + return ret; +} + +static int maildirsize_parse(struct maildir_quota_root *root, + int fd, const char *const *lines) +{ + struct quota_root *_root = &root->root; + uint64_t message_bytes_limit, message_count_limit; + long long bytes_diff, total_bytes; + int count_diff, total_count; + unsigned int line_count = 0; + + if (*lines == NULL) + return -1; + + /* first line contains the limits */ + (void)maildir_parse_limit(lines[0], &message_bytes_limit, + &message_count_limit); + + /* truncate too high limits to signed 64bit int range */ + if (message_bytes_limit >= (1ULL << 63)) + message_bytes_limit = (1ULL << 63) - 1; + if (message_count_limit >= (1ULL << 63)) + message_count_limit = (1ULL << 63) - 1; + + if (root->root.bytes_limit == (int64_t)message_bytes_limit && + root->root.count_limit == (int64_t)message_count_limit) { + /* limits haven't changed */ + } else if (root->root.set->force_default_rule) { + /* we know the limits and they've changed. + the file must be rewritten. */ + return 0; + } else { + /* we're using limits from the file. */ + root->root.bytes_limit = message_bytes_limit; + root->root.count_limit = message_count_limit; + quota_root_recalculate_relative_rules(root->root.set, + message_bytes_limit, + message_count_limit); + } + + if (*lines == NULL) { + /* no quota lines. rebuild it. */ + return 0; + } + + /* rest of the lines contains <bytes> <count> diffs */ + total_bytes = 0; total_count = 0; + for (lines++; *lines != NULL; lines++, line_count++) { + if (sscanf(*lines, "%lld %d", &bytes_diff, &count_diff) != 2) + return -1; + + total_bytes += bytes_diff; + total_count += count_diff; + } + + if (total_bytes < 0 || total_count < 0) { + /* corrupted */ + return -1; + } + + if ((total_bytes > _root->bytes_limit && _root->bytes_limit != 0) || + (total_count > _root->count_limit && _root->count_limit != 0)) { + /* we're over quota. don't trust these values if the file + contains more than the initial summary line, or if the file + is older than 15 minutes. */ + struct stat st; + + if (line_count > 1) + return 0; + + if (fstat(fd, &st) < 0 || + st.st_mtime < ioloop_time - MAILDIRSIZE_STALE_SECS) + return 0; + } + root->total_bytes = (uint64_t)total_bytes; + root->total_count = (uint64_t)total_count; + return 1; +} + +static int maildirsize_open(struct maildir_quota_root *root, + const char **error_r) +{ + i_close_fd_path(&root->fd, root->maildirsize_path); + + root->fd = nfs_safe_open(root->maildirsize_path, O_RDWR | O_APPEND); + if (root->fd == -1) { + if (errno == ENOENT) + return 0; + *error_r = t_strdup_printf( + "open(%s) failed: %m", root->maildirsize_path); + return -1; + } + return 1; +} + +static bool maildirsize_has_changed(struct maildir_quota_root *root) +{ + struct stat st1, st2; + + if (dotlock_settings.nfs_flush) { + nfs_flush_file_handle_cache(root->maildirsize_path); + nfs_flush_attr_cache_unlocked(root->maildirsize_path); + } + + if (root->fd == -1) + return TRUE; + + if (stat(root->maildirsize_path, &st1) < 0) + return TRUE; + if (fstat(root->fd, &st2) < 0) + return TRUE; + + return root->last_size != st2.st_size || st1.st_ino != st2.st_ino || + !CMP_DEV_T(st1.st_dev, st2.st_dev); +} + +static int maildirsize_read(struct maildir_quota_root *root, bool *retry, + const char **error_r) +{ + char buf[5120+1]; + unsigned int i, size; + bool retry_estale = *retry; + int ret; + + *retry = FALSE; + + if (!maildirsize_has_changed(root)) + return 1; + + if ((ret = maildirsize_open(root, error_r)) <= 0) + return ret; + + /* @UNSAFE */ + size = 0; + while ((ret = read(root->fd, buf + size, sizeof(buf)-1 - size)) != 0) { + if (ret < 0) { + if (errno == ESTALE && retry_estale) { + *retry = TRUE; + break; + } + *error_r = t_strdup_printf( + "read(%s) failed: %m", root->maildirsize_path); + break; + } + size += ret; + if (size >= sizeof(buf)-1) { + /* we'll need to recalculate the quota */ + break; + } + } + + /* try to use the file even if we ran into some error. if we don't have + forced limits, we'll need to read the header to get them */ + root->total_bytes = root->total_count = 0; + root->last_size = size; + + /* skip the last line if there's no LF at the end. Remove the last LF + so we don't get one empty line in the strsplit. */ + while (size > 0 && buf[size-1] != '\n') size--; + if (size > 0) size--; + buf[size] = '\0'; + + if (ret < 0 && size == 0) { + /* the read failed and there's no usable header, fail. */ + i_close_fd(&root->fd); + return -1; + } + + /* If there are any NUL bytes, the file is broken. */ + for (i = 0; i < size; i++) { + if (buf[i] == '\0') + break; + } + + if (i == size && + maildirsize_parse(root, root->fd, t_strsplit(buf, "\n")) > 0 && + ret == 0) + ret = 1; + else { + /* broken file / need recalculation */ + i_close_fd(&root->fd); + ret = 0; + } + return ret; +} + +static bool maildirquota_limits_init(struct maildir_quota_root *root) +{ + struct mailbox_list *list; + struct mail_storage *storage; + const char *control_dir; + + if (root->limits_initialized) + return root->maildirsize_path != NULL; + root->limits_initialized = TRUE; + + if (root->maildirsize_ns == NULL) { + i_assert(root->maildirsize_path == NULL); + return FALSE; + } + + list = root->maildirsize_ns->list; + if (mailbox_list_get_storage(&list, "", &storage) == 0 && + strcmp(storage->name, MAILDIR_STORAGE_NAME) != 0) { + /* non-maildir namespace, skip */ + if ((storage->class_flags & + MAIL_STORAGE_CLASS_FLAG_NOQUOTA) == 0) { + e_warning(root->root.backend.event, + "Namespace '%s' is not Maildir, " + "skipping for Maildir++ quota", + root->maildirsize_ns->prefix); + } + root->maildirsize_path = NULL; + return FALSE; + } + if (root->maildirsize_path == NULL) { + if (!mailbox_list_get_root_path(list, MAILBOX_LIST_PATH_TYPE_CONTROL, + &control_dir)) + i_unreached(); + root->maildirsize_path = + p_strconcat(root->root.pool, control_dir, + "/"MAILDIRSIZE_FILENAME, NULL); + } + return TRUE; +} + +static int maildirquota_read_limits(struct maildir_quota_root *root, + const char **error_r) +{ + bool retry = TRUE; + int ret, n = 0; + + if (!maildirquota_limits_init(root)) + return 1; + + do { + if (n == NFS_ESTALE_RETRY_COUNT) + retry = FALSE; + ret = maildirsize_read(root, &retry, error_r); + n++; + } while (ret == -1 && retry); + return ret; +} + +static int +maildirquota_refresh(struct maildir_quota_root *root, bool *recalculated_r, + const char **error_r) +{ + int ret; + + *recalculated_r = FALSE; + + ret = maildirquota_read_limits(root, error_r); + if (ret == 0) { + if (root->root.bytes_limit == 0 && + root->root.count_limit == 0 && + root->root.set->default_rule.bytes_limit == 0 && + root->root.set->default_rule.count_limit == 0) { + /* no quota */ + if (!root->root.set->force_default_rule) + return 0; + /* explicitly specified 0 as quota. keep the quota + updated even if it's not enforced. */ + } + + ret = maildirsize_recalculate(root, error_r); + if (ret == 0) + *recalculated_r = TRUE; + } + return ret < 0 ? -1 : 0; +} + +static int maildirsize_update(struct maildir_quota_root *root, + int count_diff, int64_t bytes_diff) +{ + char str[MAX_INT_STRLEN * 2 + 2]; + int ret = 0; + + if (count_diff == 0 && bytes_diff == 0) + return 0; + + /* We rely on O_APPEND working in here. That isn't NFS-safe, but it + isn't necessarily that bad because the file is recreated once in + a while, and sooner if corruption causes calculations to go + over quota. This is also how Maildir++ spec specifies it should be + done.. */ + if (i_snprintf(str, sizeof(str), "%lld %d\n", + (long long)bytes_diff, count_diff) < 0) + i_unreached(); + if (write_full(root->fd, str, strlen(str)) < 0) { + ret = -1; + if (errno == ESTALE) { + /* deleted/replaced already, ignore */ + } else { + e_error(root->root.backend.event, + "write_full(%s) failed: %m", + root->maildirsize_path); + } + } else { + /* close the file to force a flush with NFS */ + if (close(root->fd) < 0) { + ret = -1; + if (errno != ESTALE) + e_error(root->root.backend.event, + "close(%s) failed: %m", root->maildirsize_path); + } + root->fd = -1; + } + return ret; +} + +static struct quota_root *maildir_quota_alloc(void) +{ + struct maildir_quota_root *root; + + root = i_new(struct maildir_quota_root, 1); + root->fd = -1; + return &root->root; +} + +static int maildir_quota_init(struct quota_root *_root, const char *args, + const char **error_r) +{ + event_set_append_log_prefix(_root->backend.event, "quota-maildir: "); + return quota_root_default_init(_root, args, error_r); +} + +static void maildir_quota_deinit(struct quota_root *_root) +{ + struct maildir_quota_root *root = (struct maildir_quota_root *)_root; + + i_close_fd(&root->fd); + i_free(root); +} + +static bool +maildir_quota_parse_rule(struct quota_root_settings *root_set ATTR_UNUSED, + struct quota_rule *rule, + const char *str, const char **error_r) +{ + uint64_t bytes, count; + + if (strcmp(str, "NOQUOTA") == 0) { + bytes = 0; + count = 0; + } else if (!maildir_parse_limit(str, &bytes, &count)) { + *error_r = t_strdup_printf( + "quota-maildir: Invalid Maildir++ quota rule \"%s\"", + str); + return FALSE; + } + + rule->bytes_limit = bytes; + rule->count_limit = count; + return TRUE; +} + +static int maildir_quota_init_limits(struct quota_root *_root, + const char **error_r) +{ + struct maildir_quota_root *root = (struct maildir_quota_root *)_root; + const char *error; + + if (maildirquota_read_limits(root, &error) < 0) { + *error_r = t_strdup_printf( + "quota-maildir: Failed to read limits: %s", error); + return -1; + } + return 0; +} + +static void +maildir_quota_root_namespace_added(struct quota_root *_root, + struct mail_namespace *ns) +{ + struct maildir_quota_root *root = (struct maildir_quota_root *)_root; + + if (root->maildirsize_ns == NULL) + root->maildirsize_ns = ns; +} + +static void +maildir_quota_namespace_added(struct quota *quota, struct mail_namespace *ns) +{ + struct quota_root **roots; + unsigned int i, count; + + roots = array_get_modifiable("a->roots, &count); + for (i = 0; i < count; i++) { + if (roots[i]->backend.name == quota_backend_maildir.name && + ((roots[i]->ns_prefix == NULL && + ns->type == MAIL_NAMESPACE_TYPE_PRIVATE) || + roots[i]->ns == ns)) + maildir_quota_root_namespace_added(roots[i], ns); + } +} + +static const char *const * +maildir_quota_root_get_resources(struct quota_root *root ATTR_UNUSED) +{ + static const char *resources_both[] = { + QUOTA_NAME_STORAGE_KILOBYTES, + QUOTA_NAME_MESSAGES, + NULL + }; + + return resources_both; +} + +static enum quota_get_result +maildir_quota_get_resource(struct quota_root *_root, const char *name, + uint64_t *value_r, const char **error_r) +{ + struct maildir_quota_root *root = (struct maildir_quota_root *)_root; + bool recalculated; + const char *error; + + if (maildirquota_refresh(root, &recalculated, &error) < 0) { + *error_r = t_strdup_printf("Failed to get %s: %s", name, error); + return QUOTA_GET_RESULT_INTERNAL_ERROR; + } + + if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) { + *value_r = root->total_bytes; + } else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0) { + *value_r = root->total_count; + } else { + *error_r = QUOTA_UNKNOWN_RESOURCE_ERROR_STRING; + return QUOTA_GET_RESULT_UNKNOWN_RESOURCE; + } + return QUOTA_GET_RESULT_LIMITED; +} + +static int +maildir_quota_update(struct quota_root *_root, + struct quota_transaction_context *ctx, + const char **error_r) +{ + struct maildir_quota_root *root = (struct maildir_quota_root *)_root; + bool recalculated; + const char *error; + + if (!maildirquota_limits_init(root)) { + /* no limits */ + return 0; + } + + /* even though we don't really care about the limits in here ourself, + we do want to make sure the header gets updated if the limits have + changed. also this makes sure the maildirsize file is created if + it doesn't exist. */ + if (maildirquota_refresh(root, &recalculated, &error) < 0) { + *error_r = t_strdup_printf( + "Could not update storage usage data: %s", + error); + return -1; + } + + if (recalculated) { + /* quota was just recalculated and it already contains the changes + we wanted to do. */ + } else if (root->fd == -1) { + if (maildirsize_recalculate(root, &error) < 0) + e_error(root->root.backend.event, "%s", error); + } else if (ctx->recalculate != QUOTA_RECALCULATE_DONT) { + i_close_fd(&root->fd); + if (maildirsize_recalculate(root, &error) < 0) + e_error(root->root.backend.event, "%s", error); + } else if (maildirsize_update(root, ctx->count_used, ctx->bytes_used) < 0) { + i_close_fd(&root->fd); + maildirsize_rebuild_later(root); + } + + return 0; +} + +struct quota_backend quota_backend_maildir = { + .name = "maildir", + + .v = { + .alloc = maildir_quota_alloc, + .init = maildir_quota_init, + .deinit = maildir_quota_deinit, + .parse_rule = maildir_quota_parse_rule, + .init_limits = maildir_quota_init_limits, + .namespace_added = maildir_quota_namespace_added, + .get_resources = maildir_quota_root_get_resources, + .get_resource = maildir_quota_get_resource, + .update = maildir_quota_update, + } +}; diff --git a/src/plugins/quota/quota-plugin.c b/src/plugins/quota/quota-plugin.c new file mode 100644 index 0000000..507a329 --- /dev/null +++ b/src/plugins/quota/quota-plugin.c @@ -0,0 +1,31 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "mail-user.h" +#include "mail-storage-hooks.h" +#include "quota-plugin.h" + +void quota_backends_register(void); +void quota_backends_unregister(void); + +const char *quota_plugin_version = DOVECOT_ABI_VERSION; + +static struct mail_storage_hooks quota_mail_storage_hooks = { + .mail_user_created = quota_mail_user_created, + .mail_namespaces_created = quota_mail_namespaces_created, + .mailbox_list_created = quota_mailbox_list_created, + .mailbox_allocated = quota_mailbox_allocated, + .mail_allocated = quota_mail_allocated +}; + +void quota_plugin_init(struct module *module) +{ + mail_storage_hooks_add(module, "a_mail_storage_hooks); + quota_backends_register(); +} + +void quota_plugin_deinit(void) +{ + mail_storage_hooks_remove("a_mail_storage_hooks); + quota_backends_unregister(); +} diff --git a/src/plugins/quota/quota-plugin.h b/src/plugins/quota/quota-plugin.h new file mode 100644 index 0000000..927d428 --- /dev/null +++ b/src/plugins/quota/quota-plugin.h @@ -0,0 +1,36 @@ +#ifndef QUOTA_PLUGIN_H +#define QUOTA_PLUGIN_H + +#include "module-context.h" +#include "mail-user.h" + +struct module; +struct mailbox; +struct mailbox_list; +struct mail; + +#define QUOTA_USER_CONTEXT(obj) \ + MODULE_CONTEXT(obj, quota_user_module) +#define QUOTA_USER_CONTEXT_REQUIRE(obj) \ + MODULE_CONTEXT_REQUIRE(obj, quota_user_module) + +struct quota_user { + union mail_user_module_context module_ctx; + + struct quota *quota; +}; + +struct mail_storage; + +extern MODULE_CONTEXT_DEFINE(quota_user_module, &mail_user_module_register); + +void quota_mail_user_created(struct mail_user *user); +void quota_mailbox_list_created(struct mailbox_list *list); +void quota_mail_namespaces_created(struct mail_namespace *namespaces); +void quota_mailbox_allocated(struct mailbox *box); +void quota_mail_allocated(struct mail *mail); + +void quota_plugin_init(struct module *module); +void quota_plugin_deinit(void); + +#endif diff --git a/src/plugins/quota/quota-private.h b/src/plugins/quota/quota-private.h new file mode 100644 index 0000000..ac091d3 --- /dev/null +++ b/src/plugins/quota/quota-private.h @@ -0,0 +1,230 @@ +#ifndef QUOTA_PRIVATE_H +#define QUOTA_PRIVATE_H + +#include "mail-storage-private.h" +#include "mail-namespace.h" +#include "quota.h" + +/* Modules should use do "my_id = quota_module_id++" and + use quota_module_contexts[id] for their own purposes. */ +extern unsigned int quota_module_id; + +struct quota { + struct mail_user *user; + struct quota_settings *set; + struct event *event; + + ARRAY(struct quota_root *) roots; + ARRAY(struct mail_namespace *) namespaces; + struct mail_namespace *unwanted_ns; +}; + +struct quota_settings { + pool_t pool; + + ARRAY(struct quota_root_settings *) root_sets; + struct event *event; + enum quota_alloc_result (*test_alloc)( + struct quota_transaction_context *ctx, uoff_t size, + const char **error_r); + + uoff_t max_mail_size; + const char *quota_exceeded_msg; + bool debug:1; + bool initialized:1; + bool vsizes:1; +}; + +struct quota_rule { + const char *mailbox_mask; + + int64_t bytes_limit, count_limit; + /* relative to default_rule */ + int bytes_percent, count_percent; + + /* Don't include this mailbox in quota */ + bool ignore:1; +}; + +struct quota_warning_rule { + struct quota_rule rule; + const char *command; + bool reverse:1; +}; + +struct quota_backend_vfuncs { + struct quota_root *(*alloc)(void); + int (*init)(struct quota_root *root, const char *args, + const char **error_r); + void (*deinit)(struct quota_root *root); + + bool (*parse_rule)(struct quota_root_settings *root_set, + struct quota_rule *rule, + const char *str, const char **error_r); + int (*init_limits)(struct quota_root *root, const char **error_r); + + /* called once for each namespace */ + void (*namespace_added)(struct quota *quota, + struct mail_namespace *ns); + + const char *const *(*get_resources)(struct quota_root *root); + /* Backends return success as QUOTA_GET_RESULT_LIMITED, and returning + QUOTA_GET_RESULT_UNLIMITED is prohibited by quota_get_resource(), + which is the only caller of this vfunc. */ + enum quota_get_result (*get_resource)(struct quota_root *root, + const char *name, + uint64_t *value_r, + const char **error_r); + + int (*update)(struct quota_root *root, + struct quota_transaction_context *ctx, + const char **error_r); + bool (*match_box)(struct quota_root *root, struct mailbox *box); + void (*flush)(struct quota_root *root); +}; + +struct quota_backend { + /* quota backends equal if backend1.name == backend2.name */ + const char *name; + struct event *event; + struct quota_backend_vfuncs v; +}; + +struct quota_root_settings { + /* Unique quota root name. */ + const char *name; + /* Name in settings, e.g. "quota", "quota2", .. */ + const char *set_name; + + struct quota_settings *set; + const char *args; + + const struct quota_backend *backend; + struct quota_rule default_rule; + ARRAY(struct quota_rule) rules; + ARRAY(struct quota_warning_rule) warning_rules; + const char *limit_set; + + /* If user is under quota before saving a mail, allow the last mail to + bring the user over quota by this many bytes. */ + uint64_t last_mail_max_extra_bytes; + struct quota_rule grace_rule; + + /* Limits in default_rule override backend's quota limits */ + bool force_default_rule:1; + /* TRUE if any of the warning_rules have reverse==TRUE */ + bool have_reverse_warnings:1; +}; + +struct quota_root { + pool_t pool; + + struct quota_root_settings *set; + struct quota *quota; + struct quota_backend backend; + struct dict *limit_set_dict; + + /* this quota root applies only to this namespace. it may also be + a public namespace without an owner. */ + struct mail_namespace *ns; + /* this is set in quota init(), because namespaces aren't known yet. + when accessing shared users the ns_prefix may be non-NULL but + ns=NULL, so when checking if quota root applies only to a specific + namespace use the ns_prefix!=NULL check. */ + const char *ns_prefix; + + /* initially the same as set->default_rule.*_limit, but some backends + may change these by reading the limits elsewhere (e.g. Maildir++, + FS quota) */ + int64_t bytes_limit, count_limit; + + /* Module-specific contexts. See quota_module_id. */ + ARRAY(void) quota_module_contexts; + + /* don't enforce quota when saving */ + bool no_enforcing:1; + /* quota is automatically updated. update() should be called but the + bytes won't be changed. count is still changed, because it's cheap + to do and it's internally used to figure out whether there have + been some changes and that quota_warnings should be checked. */ + bool auto_updating:1; + /* If user has unlimited quota, disable quota tracking */ + bool disable_unlimited_tracking:1; + /* Set while quota is being recalculated to avoid recursion. */ + bool recounting:1; + /* Quota root is hidden (to e.g. IMAP GETQUOTAROOT) */ + bool hidden:1; + /* Did we already check quota_over_flag correctness? */ + bool quota_over_flag_checked:1; +}; + +struct quota_transaction_context { + union mailbox_transaction_module_context module_ctx; + + struct quota *quota; + struct mailbox *box; + + int64_t bytes_used, count_used; + /* how many bytes/mails can be saved until limit is reached. + (set once, not updated by bytes_used/count_used). + + if last_mail_max_extra_bytes>0, the bytes_ceil is initially + increased by that much, while bytes_ceil2 contains the real ceiling. + after the first allocation is done, bytes_ceil is set to + bytes_ceil2. */ + uint64_t bytes_ceil, bytes_ceil2, count_ceil; + /* How many bytes/mails we are over quota. Like *_ceil, these are set + only once and not updated by bytes_used/count_used. (Either *_ceil + or *_over is always zero.) */ + uint64_t bytes_over, count_over; + + struct mail *tmp_mail; + enum quota_recalculate recalculate; + + bool limits_set:1; + bool failed:1; + bool sync_transaction:1; + /* TRUE if all roots have auto_updating=TRUE */ + bool auto_updating:1; + /* Quota doesn't need to be updated within this transaction. */ + bool no_quota_updates:1; +}; + +/* Register storage to all user's quota roots. */ +void quota_add_user_namespace(struct quota *quota, struct mail_namespace *ns); +void quota_remove_user_namespace(struct mail_namespace *ns); + +int quota_root_default_init(struct quota_root *root, const char *args, + const char **error_r); +struct quota *quota_get_mail_user_quota(struct mail_user *user); + +bool quota_root_is_namespace_visible(struct quota_root *root, + struct mail_namespace *ns); +struct quota_rule * +quota_root_rule_find(struct quota_root_settings *root_set, const char *name); + +void quota_root_recalculate_relative_rules(struct quota_root_settings *root_set, + int64_t bytes_limit, + int64_t count_limit); +/* Returns 1 if values were returned successfully, 0 if we're recursing into + the same function, -1 if error. */ +int quota_count(struct quota_root *root, uint64_t *bytes_r, uint64_t *count_r, + enum quota_get_result *error_result_r, const char **error_r); + +int quota_root_parse_grace(struct quota_root_settings *root_set, + const char *value, const char **error_r); +bool quota_warning_match(const struct quota_warning_rule *w, + uint64_t bytes_before, uint64_t bytes_current, + uint64_t count_before, uint64_t count_current, + const char **reason_r); +bool quota_transaction_is_over(struct quota_transaction_context *ctx, uoff_t size); +int quota_transaction_set_limits(struct quota_transaction_context *ctx, + enum quota_get_result *error_result_r, + const char **error_r); + +void quota_backend_register(const struct quota_backend *backend); +void quota_backend_unregister(const struct quota_backend *backend); + +#define QUOTA_UNKNOWN_RESOURCE_ERROR_STRING "Unknown quota resource" + +#endif diff --git a/src/plugins/quota/quota-status-settings.c b/src/plugins/quota/quota-status-settings.c new file mode 100644 index 0000000..e98cc09 --- /dev/null +++ b/src/plugins/quota/quota-status-settings.c @@ -0,0 +1,37 @@ +/* Copyright (c) 2017-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "settings-parser.h" +#include "service-settings.h" +#include "mail-storage-settings.h" +#include "quota-status-settings.h" + +#undef DEF +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct quota_status_settings) + +static const struct setting_define quota_status_setting_defines[] = { + DEF(STR, recipient_delimiter), + + SETTING_DEFINE_LIST_END +}; + +static const struct quota_status_settings quota_status_default_settings = { + .recipient_delimiter = "+", +}; + +static const struct setting_parser_info *quota_status_setting_dependencies[] = { + NULL +}; + +const struct setting_parser_info quota_status_setting_parser_info = { + .module_name = "mail", + .defines = quota_status_setting_defines, + .defaults = "a_status_default_settings, + + .type_offset = SIZE_MAX, + .struct_size = sizeof(struct quota_status_settings), + + .parent_offset = SIZE_MAX, + .dependencies = quota_status_setting_dependencies +}; diff --git a/src/plugins/quota/quota-status-settings.h b/src/plugins/quota/quota-status-settings.h new file mode 100644 index 0000000..e69a0aa --- /dev/null +++ b/src/plugins/quota/quota-status-settings.h @@ -0,0 +1,10 @@ +#ifndef QUOTA_STATUS_SETTINGS_H +#define QUOTA_STATUS_SETTINGS_H 1 + +struct quota_status_settings { + const char *recipient_delimiter; +}; + +extern const struct setting_parser_info quota_status_setting_parser_info; + +#endif diff --git a/src/plugins/quota/quota-status.c b/src/plugins/quota/quota-status.c new file mode 100644 index 0000000..7db0cb4 --- /dev/null +++ b/src/plugins/quota/quota-status.c @@ -0,0 +1,360 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "str.h" +#include "str-sanitize.h" +#include "ostream.h" +#include "connection.h" +#include "restrict-access.h" +#include "settings-parser.h" +#include "master-service.h" +#include "master-service-settings.h" +#include "mail-namespace.h" +#include "mail-storage.h" +#include "mail-storage-settings.h" +#include "mail-storage-service.h" +#include "smtp-address.h" +#include "quota-private.h" +#include "quota-plugin.h" +#include "quota-status-settings.h" + +enum quota_protocol { + QUOTA_PROTOCOL_UNKNOWN = 0, + QUOTA_PROTOCOL_POSTFIX +}; + +struct quota_client { + struct connection conn; + + struct event *event; + + char *state; + char *recipient; + uoff_t size; + + bool warned_bad_state:1; +}; + +static struct event_category event_category_quota_status = { + .name = "quota-status" +}; + +static struct quota_status_settings *quota_status_settings; +static pool_t quota_status_pool; +static enum quota_protocol protocol; +static struct mail_storage_service_ctx *storage_service; +static struct connection_list *clients; +static char *nouser_reply; + +static void client_connected(struct master_service_connection *conn) +{ + struct quota_client *client; + + client = i_new(struct quota_client, 1); + + client->event = event_create(NULL); + client->conn.event_parent = client->event; + event_add_category(client->event, &event_category_quota_status); + connection_init_server(clients, &client->conn, + "quota-client", conn->fd, conn->fd); + master_service_client_connection_accept(conn); + + e_debug(client->event, "Client connected"); +} + +static void client_reset(struct quota_client *client) +{ + i_free(client->state); + i_free(client->recipient); +} + +static enum quota_alloc_result +quota_check(struct mail_user *user, uoff_t mail_size, const char **error_r) +{ + struct quota_user *quser = QUOTA_USER_CONTEXT(user); + struct mail_namespace *ns; + struct mailbox *box; + struct quota_transaction_context *ctx; + enum quota_alloc_result ret; + + if (quser == NULL) { + /* no quota for user */ + e_debug(user->event, "User has no quota"); + return QUOTA_ALLOC_RESULT_OK; + } + + ns = mail_namespace_find_inbox(user->namespaces); + box = mailbox_alloc(ns->list, "INBOX", MAILBOX_FLAG_POST_SESSION); + + ctx = quota_transaction_begin(box); + const char *internal_error; + ret = quota_test_alloc(ctx, I_MAX(1, mail_size), &internal_error); + if (ret == QUOTA_ALLOC_RESULT_TEMPFAIL) + e_error(user->event, "quota check failed: %s", internal_error); + *error_r = quota_alloc_result_errstr(ret, ctx); + quota_transaction_rollback(&ctx); + + mailbox_free(&box); + return ret; +} + +static int client_check_mta_state(struct quota_client *client) +{ + if (client->state == NULL || strcasecmp(client->state, "RCPT") == 0) + return 0; + + if (!client->warned_bad_state) { + e_warning(client->event, + "Received policy query from MTA in unexpected state %s " + "(service can only be used for recipient restrictions)", + client->state); + } + client->warned_bad_state = TRUE; + return -1; +} + +static void client_handle_request(struct quota_client *client) +{ + struct mail_storage_service_input input; + struct mail_storage_service_user *service_user; + struct mail_user *user; + struct smtp_address *rcpt; + const char *value = NULL, *error; + const char *detail ATTR_UNUSED; + char delim ATTR_UNUSED; + string_t *resp; + int ret; + + if (client_check_mta_state(client) < 0 || client->recipient == NULL) { + e_debug(client->event, "Response: action=DUNNO"); + o_stream_nsend_str(client->conn.output, "action=DUNNO\n\n"); + return; + } + + if (smtp_address_parse_path(pool_datastack_create(), client->recipient, + SMTP_ADDRESS_PARSE_FLAG_ALLOW_LOCALPART | + SMTP_ADDRESS_PARSE_FLAG_BRACKETS_OPTIONAL | + SMTP_ADDRESS_PARSE_FLAG_ALLOW_BAD_LOCALPART, + &rcpt, &error) < 0) { + e_error(client->event, + "Client sent invalid recipient address `%s': " + "%s", str_sanitize(client->recipient, 256), error); + e_debug(client->event, "Response: action=DUNNO"); + o_stream_nsend_str(client->conn.output, "action=DUNNO\n\n"); + return; + } + + i_zero(&input); + input.event_parent = client->event; + smtp_address_detail_parse_temp(quota_status_settings->recipient_delimiter, + rcpt, &input.username, &delim, + &detail); + ret = mail_storage_service_lookup_next(storage_service, &input, + &service_user, &user, &error); + restrict_access_allow_coredumps(TRUE); + if (ret == 0) { + e_debug(client->event, "User `%s' not found", input.username); + value = nouser_reply; + } else if (ret > 0) { + enum quota_alloc_result qret = quota_check(user, client->size, + &error); + if (qret == QUOTA_ALLOC_RESULT_OK) { + e_debug(client->event, + "Message is acceptable"); + } else { + e_debug(client->event, + "Quota check failed: %s", error); + } + + switch (qret) { + case QUOTA_ALLOC_RESULT_OK: /* under quota */ + value = mail_user_plugin_getenv(user, + "quota_status_success"); + if (value == NULL) + value = "OK"; + break; + case QUOTA_ALLOC_RESULT_OVER_MAXSIZE: + /* even over maximum quota */ + case QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT: + value = mail_user_plugin_getenv(user, + "quota_status_toolarge"); + /* fall through */ + case QUOTA_ALLOC_RESULT_OVER_QUOTA: + if (value == NULL) + value = mail_user_plugin_getenv(user, + "quota_status_overquota"); + if (value == NULL) + value = t_strdup_printf("554 5.2.2 %s", error); + break; + case QUOTA_ALLOC_RESULT_TEMPFAIL: + case QUOTA_ALLOC_RESULT_BACKGROUND_CALC: + ret = -1; + break; + } + value = t_strdup(value); /* user's pool is being freed */ + mail_user_deinit(&user); + mail_storage_service_user_unref(&service_user); + } else { + e_error(client->event, + "Failed to lookup user %s: %s", input.username, error); + error = "Temporary internal error"; + } + + resp = t_str_new(256); + if (ret < 0) { + /* temporary failure */ + str_append(resp, "action=DEFER_IF_PERMIT "); + str_append(resp, error); + } else { + str_append(resp, "action="); + str_append(resp, value); + } + + e_debug(client->event, "Response: %s", str_c(resp)); + str_append(resp, "\n\n"); + o_stream_nsend_str(client->conn.output, str_c(resp)); +} + +static int client_input_line(struct connection *conn, const char *line) +{ + struct quota_client *client = (struct quota_client *)conn; + + e_debug(client->event, "Request: %s", str_sanitize(line, 1024)); + + if (*line == '\0') { + o_stream_cork(conn->output); + client_handle_request(client); + o_stream_uncork(conn->output); + client_reset(client); + return 1; + } + if (str_begins(line, "recipient=")) { + if (client->recipient == NULL) + client->recipient = i_strdup(line + 10); + } else if (str_begins(line, "size=")) { + if (str_to_uoff(line+5, &client->size) < 0) + client->size = 0; + } else if (str_begins(line, "protocol_state=")) { + if (client->state == NULL) + client->state = i_strdup(line + 15); + } + return 1; +} + +static void client_destroy(struct connection *conn) +{ + struct quota_client *client = (struct quota_client *)conn; + + e_debug(client->event, "Client disconnected"); + + connection_deinit(&client->conn); + client_reset(client); + event_unref(&client->event); + i_free(client); + + master_service_client_connection_destroyed(master_service); +} + +static struct connection_settings client_set = { + .input_max_size = SIZE_MAX, + .output_max_size = SIZE_MAX, + .client = FALSE +}; + +static const struct connection_vfuncs client_vfuncs = { + .destroy = client_destroy, + .input_line = client_input_line +}; + +static void main_preinit(void) +{ + restrict_access_by_env(RESTRICT_ACCESS_FLAG_ALLOW_ROOT, NULL); + restrict_access_allow_coredumps(TRUE); +} + +static void main_init(void) +{ + static const struct setting_parser_info *set_roots[] = { + "a_status_setting_parser_info, + NULL + }; + struct mail_storage_service_input input; + const struct setting_parser_info *user_info; + const struct setting_parser_context *set_parser; + const struct mail_user_settings *user_set; + const struct quota_status_settings *set; + const char *value, *error; + pool_t pool; + + clients = connection_list_init(&client_set, &client_vfuncs); + storage_service = mail_storage_service_init(master_service, set_roots, + MAIL_STORAGE_SERVICE_FLAG_ALLOW_ROOT | + MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP | + MAIL_STORAGE_SERVICE_FLAG_TEMP_PRIV_DROP | + MAIL_STORAGE_SERVICE_FLAG_ENABLE_CORE_DUMPS | + MAIL_STORAGE_SERVICE_FLAG_NO_CHDIR); + + i_zero(&input); + input.service = "quota-status"; + input.module = "mail"; + input.username = ""; + + quota_status_pool = pool_alloconly_create("quota status settings", 512); + pool = pool_alloconly_create("service all settings", 4096); + if (mail_storage_service_read_settings(storage_service, &input, pool, + &user_info, &set_parser, + &error) < 0) + i_fatal("%s", error); + user_set = master_service_settings_parser_get_others(master_service, + set_parser)[0]; + set = master_service_settings_get_others(master_service)[1]; + + quota_status_settings = settings_dup("a_status_setting_parser_info, set, + quota_status_pool); + value = mail_user_set_plugin_getenv(user_set, "quota_status_nouser"); + nouser_reply = p_strdup(quota_status_pool, + value != NULL ? value : "REJECT Unknown user"); + pool_unref(&pool); +} + +static void main_deinit(void) +{ + pool_unref("a_status_pool); + connection_list_deinit(&clients); + mail_storage_service_deinit(&storage_service); +} + +int main(int argc, char *argv[]) +{ + enum master_service_flags service_flags = + MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN; + int c; + + protocol = QUOTA_PROTOCOL_UNKNOWN; + master_service = master_service_init("quota-status", service_flags, + &argc, &argv, "p:"); + while ((c = master_getopt(master_service)) > 0) { + switch (c) { + case 'p': + if (strcmp(optarg, "postfix") == 0) + protocol = QUOTA_PROTOCOL_POSTFIX; + else + i_fatal("Unknown -p parameter: '%s'", optarg); + break; + default: + return FATAL_DEFAULT; + } + } + if (protocol == QUOTA_PROTOCOL_UNKNOWN) + i_fatal("Missing -p parameter"); + + master_service_init_log(master_service); + main_preinit(); + + main_init(); + master_service_init_finish(master_service); + master_service_run(master_service, client_connected); + main_deinit(); + master_service_deinit(&master_service); + return 0; +} diff --git a/src/plugins/quota/quota-storage.c b/src/plugins/quota/quota-storage.c new file mode 100644 index 0000000..a1d08ee --- /dev/null +++ b/src/plugins/quota/quota-storage.c @@ -0,0 +1,780 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "istream.h" +#include "mail-search-build.h" +#include "mail-storage-private.h" +#include "mailbox-list-private.h" +#include "maildir-storage.h" +#include "index-mailbox-size.h" +#include "quota-private.h" +#include "quota-plugin.h" + +#include <sys/stat.h> + +#define QUOTA_CONTEXT(obj) \ + MODULE_CONTEXT(obj, quota_storage_module) +#define QUOTA_CONTEXT_REQUIRE(obj) \ + MODULE_CONTEXT_REQUIRE(obj, quota_storage_module) +#define QUOTA_MAIL_CONTEXT(obj) \ + MODULE_CONTEXT_REQUIRE(obj, quota_mail_module) +#define QUOTA_LIST_CONTEXT(obj) \ + MODULE_CONTEXT(obj, quota_mailbox_list_module) + +struct quota_mailbox_list { + union mailbox_list_module_context module_ctx; +}; + +struct quota_mailbox { + union mailbox_module_context module_ctx; + + struct mailbox_transaction_context *expunge_trans; + struct quota_transaction_context *expunge_qt; + ARRAY(uint32_t) expunge_uids; + ARRAY(uoff_t) expunge_sizes; + unsigned int prev_idx; + + bool recalculate:1; + bool sync_transaction_expunge:1; +}; + +struct quota_user_module quota_user_module = + MODULE_CONTEXT_INIT(&mail_user_module_register); + +static MODULE_CONTEXT_DEFINE_INIT(quota_storage_module, + &mail_storage_module_register); +static MODULE_CONTEXT_DEFINE_INIT(quota_mail_module, &mail_module_register); +static MODULE_CONTEXT_DEFINE_INIT(quota_mailbox_list_module, + &mailbox_list_module_register); + +static void quota_set_storage_error(struct quota_transaction_context *qt, + struct mailbox *box, + enum quota_alloc_result res, + const char *internal_err) +{ + const char *errstr = quota_alloc_result_errstr(res, qt); + struct mail_storage *storage = box->storage; + switch (res) { + case QUOTA_ALLOC_RESULT_OVER_MAXSIZE: + mail_storage_set_error(storage, MAIL_ERROR_LIMIT, errstr); + break; + case QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT: + case QUOTA_ALLOC_RESULT_OVER_QUOTA: + mail_storage_set_error(storage, MAIL_ERROR_NOQUOTA, errstr); + break; + case QUOTA_ALLOC_RESULT_TEMPFAIL: + case QUOTA_ALLOC_RESULT_BACKGROUND_CALC: + mailbox_set_critical(box, "quota: %s", internal_err); + break; + case QUOTA_ALLOC_RESULT_OK: + i_unreached(); + } +} + +static void quota_mail_expunge(struct mail *_mail) +{ + struct mail_private *mail = (struct mail_private *)_mail; + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(_mail->box); + struct quota_user *quser = QUOTA_USER_CONTEXT_REQUIRE(_mail->box->storage->user); + union mail_module_context *qmail = QUOTA_MAIL_CONTEXT(mail); + struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(_mail->transaction); + uoff_t size; + int ret; + + if (qt->auto_updating) { + qmail->super.expunge(_mail); + return; + } + + /* We need to handle the situation where multiple transactions expunged + the mail at the same time. In here we'll just save the message's + physical size and do the quota freeing later when the message was + known to be expunged. */ + if (quser->quota->set->vsizes) + ret = mail_get_virtual_size(_mail, &size); + else + ret = mail_get_physical_size(_mail, &size); + if (ret == 0) { + if (!array_is_created(&qbox->expunge_uids)) { + i_array_init(&qbox->expunge_uids, 64); + i_array_init(&qbox->expunge_sizes, 64); + } + array_push_back(&qbox->expunge_uids, &_mail->uid); + array_push_back(&qbox->expunge_sizes, &size); + if ((_mail->transaction->flags & MAILBOX_TRANSACTION_FLAG_SYNC) != 0) { + /* we're running dsync. if this brings the quota below + a negative quota warning, don't execute it, because + it probably was already executed by the replica. */ + qbox->sync_transaction_expunge = TRUE; + } else { + qbox->sync_transaction_expunge = FALSE; + } + } + + qmail->super.expunge(_mail); +} + +static int +quota_get_status(struct mailbox *box, enum mailbox_status_items items, + struct mailbox_status *status_r) +{ + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(box); + struct quota_transaction_context *qt; + int ret = 0; + + if ((items & STATUS_CHECK_OVER_QUOTA) != 0) { + qt = quota_transaction_begin(box); + const char *error; + enum quota_alloc_result qret = quota_test_alloc(qt, 0, &error); + if (qret != QUOTA_ALLOC_RESULT_OK) { + quota_set_storage_error(qt, box, qret, error); + ret = -1; + } + quota_transaction_rollback(&qt); + + if ((items & ENUM_NEGATE(STATUS_CHECK_OVER_QUOTA)) == 0) { + /* don't bother calling parent, it may unnecessarily + try to open the mailbox */ + return ret < 0 ? -1 : 0; + } + } + + if (qbox->module_ctx.super.get_status(box, items, status_r) < 0) + ret = -1; + return ret < 0 ? -1 : 0; +} + +static struct mailbox_transaction_context * +quota_mailbox_transaction_begin(struct mailbox *box, + enum mailbox_transaction_flags flags, + const char *reason) +{ + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(box); + struct mailbox_transaction_context *t; + struct quota_transaction_context *qt; + + t = qbox->module_ctx.super.transaction_begin(box, flags, reason); + qt = quota_transaction_begin(box); + qt->sync_transaction = (flags & MAILBOX_TRANSACTION_FLAG_SYNC) != 0; + + MODULE_CONTEXT_SET(t, quota_storage_module, qt); + return t; +} + +static int +quota_mailbox_transaction_commit(struct mailbox_transaction_context *ctx, + struct mail_transaction_commit_changes *changes_r) +{ + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(ctx->box); + struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(ctx); + + i_assert(qt->tmp_mail == NULL); + + if (qbox->module_ctx.super.transaction_commit(ctx, changes_r) < 0) { + quota_transaction_rollback(&qt); + return -1; + } else { + (void)quota_transaction_commit(&qt); + return 0; + } +} + +static void +quota_mailbox_transaction_rollback(struct mailbox_transaction_context *ctx) +{ + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(ctx->box); + struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(ctx); + + i_assert(qt->tmp_mail == NULL); + + qbox->module_ctx.super.transaction_rollback(ctx); + quota_transaction_rollback(&qt); +} + +void quota_mail_allocated(struct mail *_mail) +{ + struct quota_mailbox *qbox = QUOTA_CONTEXT(_mail->box); + struct mail_private *mail = (struct mail_private *)_mail; + struct mail_vfuncs *v = mail->vlast; + union mail_module_context *qmail; + + if (qbox == NULL) + return; + + qmail = p_new(mail->pool, union mail_module_context, 1); + qmail->super = *v; + mail->vlast = &qmail->super; + + v->expunge = quota_mail_expunge; + MODULE_CONTEXT_SET_SELF(mail, quota_mail_module, qmail); +} + +static bool +quota_move_requires_check(struct mailbox *dest_box, struct mailbox *src_box) +{ + struct mail_namespace *src_ns = src_box->list->ns; + struct mail_namespace *dest_ns = dest_box->list->ns; + struct quota_user *quser = QUOTA_USER_CONTEXT_REQUIRE(src_ns->user); + struct quota_root *const *rootp; + + array_foreach(&quser->quota->roots, rootp) { + bool have_src_quota, have_dest_quota; + + have_src_quota = quota_root_is_namespace_visible(*rootp, src_ns); + have_dest_quota = quota_root_is_namespace_visible(*rootp, dest_ns); + if (have_src_quota == have_dest_quota) { + /* Both/neither have this quota */ + } else if (have_dest_quota) { + /* Destination mailbox has a quota that doesn't exist + in source. We'll need to check if it's being + exceeded. */ + return TRUE; + } else { + /* Source mailbox has a quota root that doesn't exist + in destination. We're not increasing the source + quota, so ignore it. */ + } + } + return FALSE; +} + +static int quota_check(struct mail_save_context *ctx, struct mailbox *src_box) +{ + struct mailbox_transaction_context *t = ctx->transaction; + struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(t); + enum quota_alloc_result ret; + + i_assert(!ctx->moving || src_box != NULL); + + if (ctx->moving && + !quota_move_requires_check(ctx->transaction->box, src_box)) { + /* the mail is being moved. the quota won't increase (after + the following expunge), so allow this even if user is + currently over quota */ + quota_alloc(qt, ctx->dest_mail); + return 0; + } else if (qt->failed) { + return 0; + } + + const char *error; + ret = quota_try_alloc(qt, ctx->dest_mail, &error); + switch (ret) { + case QUOTA_ALLOC_RESULT_OK: + return 0; + case QUOTA_ALLOC_RESULT_TEMPFAIL: + /* Log the error, but allow saving anyway. */ + e_error(qt->quota->event, + "Failed to check if user is under quota: %s - " + "saving mail anyway", error); + return 0; + case QUOTA_ALLOC_RESULT_BACKGROUND_CALC: + /* Could not determine if there is enough space due to ongoing + background quota calculation, allow saving anyway. */ + e_warning(qt->quota->event, + "Failed to check if user is under quota: %s - " + "saving mail anyway", error); + return 0; + default: + quota_set_storage_error(qt, t->box, ret, error); + return -1; + } +} + +static int +quota_copy(struct mail_save_context *ctx, struct mail *mail) +{ + struct mailbox_transaction_context *t = ctx->transaction; + struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(t); + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(t->box); + + /* we always want to know the mail size */ + mail_add_temp_wanted_fields(ctx->dest_mail, MAIL_FETCH_PHYSICAL_SIZE, NULL); + + /* get quota before copying any mails. this avoids dovecot-vsize.lock + deadlocks with backends that lock mails for expunging/copying. */ + enum quota_get_result error_res; + const char *error; + if (quota_transaction_set_limits(qt, &error_res, &error) < 0) { + if (error_res == QUOTA_GET_RESULT_BACKGROUND_CALC) { + e_warning(qt->quota->event, + "%s - copying mail anyway", error); + } else { + e_error(qt->quota->event, + "%s - copying mail anyway", error); + } + } + + if (qbox->module_ctx.super.copy(ctx, mail) < 0) + return -1; + + if (ctx->copying_via_save) { + /* copying used saving internally, we already checked the + quota */ + return 0; + } + return quota_check(ctx, mail->box); +} + +static int +quota_save_begin(struct mail_save_context *ctx, struct istream *input) +{ + struct mailbox_transaction_context *t = ctx->transaction; + struct quota_transaction_context *qt = QUOTA_CONTEXT_REQUIRE(t); + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(t->box); + const char *error; + uoff_t size; + + if (!ctx->moving && i_stream_get_size(input, TRUE, &size) > 0 && + !qt->failed) { + /* Input size is known, check for quota immediately. This + check isn't perfect, especially because input stream's + linefeeds may contain CR+LFs while physical message would + only contain LFs. With mbox some headers might be skipped + entirely. + + I think these don't really matter though compared to the + benefit of giving "out of quota" error before sending the + full mail. */ + + enum quota_alloc_result qret = quota_test_alloc(qt, size, &error); + switch (qret) { + case QUOTA_ALLOC_RESULT_OK: + /* Great, there is space. */ + break; + case QUOTA_ALLOC_RESULT_TEMPFAIL: + /* Log the error, but allow saving anyway. */ + e_error(qt->quota->event, + "Failed to check if user is under quota: %s - " + "saving mail anyway", error); + break; + case QUOTA_ALLOC_RESULT_BACKGROUND_CALC: + /* Could not determine if there is enough space due to + * ongoing background quota calculation, allow saving + * anyway. */ + e_warning(qt->quota->event, + "Failed to check if user is under quota: %s - " + "saving mail anyway", error); + break; + default: + quota_set_storage_error(qt, t->box, qret, error); + return -1; + } + } + + /* we always want to know the mail size */ + mail_add_temp_wanted_fields(ctx->dest_mail, MAIL_FETCH_PHYSICAL_SIZE, NULL); + + /* get quota before copying any mails. this avoids dovecot-vsize.lock + deadlocks with backends that lock mails for expunging/copying. */ + enum quota_get_result error_res; + if (quota_transaction_set_limits(qt, &error_res, &error) < 0) { + if (error_res == QUOTA_GET_RESULT_BACKGROUND_CALC) + e_warning(qt->quota->event, + "%s - saving mail anyway", error); + else + e_error(qt->quota->event, + "%s - saving mail anyway", error); + } + + return qbox->module_ctx.super.save_begin(ctx, input); +} + +static int quota_save_finish(struct mail_save_context *ctx) +{ + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(ctx->transaction->box); + struct mailbox *src_box; + + if (qbox->module_ctx.super.save_finish(ctx) < 0) + return -1; + + src_box = ctx->copy_src_mail == NULL ? NULL : ctx->copy_src_mail->box; + return quota_check(ctx, src_box); +} + +static void quota_mailbox_sync_cleanup(struct quota_mailbox *qbox) +{ + if (array_is_created(&qbox->expunge_uids)) { + array_clear(&qbox->expunge_uids); + array_clear(&qbox->expunge_sizes); + } + + if (qbox->expunge_qt != NULL && qbox->expunge_qt->tmp_mail != NULL) { + mail_free(&qbox->expunge_qt->tmp_mail); + (void)mailbox_transaction_commit(&qbox->expunge_trans); + } + qbox->sync_transaction_expunge = FALSE; +} + +static void quota_mailbox_sync_commit(struct quota_mailbox *qbox) +{ + quota_mailbox_sync_cleanup(qbox); + if (qbox->expunge_qt != NULL) + (void)quota_transaction_commit(&qbox->expunge_qt); + qbox->recalculate = FALSE; +} + +static void quota_mailbox_sync_notify(struct mailbox *box, uint32_t uid, + enum mailbox_sync_type sync_type) +{ + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(box); + struct index_mailbox_context *ibox = INDEX_STORAGE_CONTEXT(box); + struct quota_user *quser = QUOTA_USER_CONTEXT_REQUIRE(box->storage->user); + const uint32_t *uids; + const uoff_t *sizep; + unsigned int i, count; + uoff_t size; + + if (qbox->module_ctx.super.sync_notify != NULL) + qbox->module_ctx.super.sync_notify(box, uid, sync_type); + + if (sync_type != MAILBOX_SYNC_TYPE_EXPUNGE || qbox->recalculate || + (box->flags & MAILBOX_FLAG_DELETE_UNSAFE) != 0) { + if (uid == 0) { + /* free the transaction before view syncing begins, + otherwise it'll crash. */ + quota_mailbox_sync_cleanup(qbox); + } + return; + } + + if (qbox->expunge_qt == NULL) { + qbox->expunge_qt = quota_transaction_begin(box); + qbox->expunge_qt->sync_transaction = + qbox->sync_transaction_expunge; + } + if (qbox->expunge_qt->auto_updating) { + /* even though backend doesn't care about size/count changes, + make sure count_used changes so quota_warnings are + executed */ + quota_free_bytes(qbox->expunge_qt, 0); + return; + } + + /* we're in the middle of syncing the mailbox, so it's a bad idea to + try and get the message sizes at this point. Rely on sizes that + we saved earlier, or recalculate the whole quota if we don't know + the size. */ + if (!array_is_created(&qbox->expunge_uids) || + array_is_empty(&qbox->expunge_uids)) { + i = count = 0; + } else { + uids = array_get(&qbox->expunge_uids, &count); + for (i = qbox->prev_idx; i < count; i++) { + if (uids[i] == uid) + break; + } + if (i >= count) { + for (i = 0; i < qbox->prev_idx; i++) { + if (uids[i] == uid) + break; + } + if (i == qbox->prev_idx) + i = count; + } + qbox->prev_idx = i; + } + + if (i != count) { + /* we already know the size */ + sizep = array_idx(&qbox->expunge_sizes, i); + quota_free_bytes(qbox->expunge_qt, *sizep); + /* FIXME: it's not ideal that we do the vsize update here, but + this is the easiest place for it for now.. maybe the mail + size checking code could be moved to lib-storage */ + if (ibox->vsize_update != NULL && quser->quota->set->vsizes) + index_mailbox_vsize_hdr_expunge(ibox->vsize_update, uid, *sizep); + return; + } + + /* try to look up the size. this works only if it's cached. */ + if (qbox->expunge_qt->tmp_mail == NULL) { + /* FIXME: ugly kludge to open the transaction for sync_view. + box->view may not have all the new messages that + sync_notify() notifies about, and those messages would + cause a quota recalculation. */ + struct mail_index_view *box_view = box->view; + if (box->tmp_sync_view != NULL) + box->view = box->tmp_sync_view; + qbox->expunge_trans = mailbox_transaction_begin(box, 0, "quota"); + box->view = box_view; + qbox->expunge_qt->tmp_mail = + mail_alloc(qbox->expunge_trans, + MAIL_FETCH_PHYSICAL_SIZE, NULL); + } + if (!mail_set_uid(qbox->expunge_qt->tmp_mail, uid)) + ; + else if (!quser->quota->set->vsizes) { + if (mail_get_physical_size(qbox->expunge_qt->tmp_mail, &size) == 0) { + quota_free_bytes(qbox->expunge_qt, size); + return; + } + } else if (mail_get_virtual_size(qbox->expunge_qt->tmp_mail, &size) == 0) { + quota_free_bytes(qbox->expunge_qt, size); + if (ibox->vsize_update != NULL) + index_mailbox_vsize_hdr_expunge(ibox->vsize_update, uid, size); + } else { + /* there's no way to get the size. recalculate the quota. */ + quota_recalculate(qbox->expunge_qt, QUOTA_RECALCULATE_MISSING_FREES); + qbox->recalculate = TRUE; + } +} + +static int quota_mailbox_sync_deinit(struct mailbox_sync_context *ctx, + struct mailbox_sync_status *status_r) +{ + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(ctx->box); + int ret; + + ret = qbox->module_ctx.super.sync_deinit(ctx, status_r); + /* update quota only after syncing is finished. the quota commit may + recalculate the quota and cause all mailboxes to be synced, + including the one we're already syncing. */ + quota_mailbox_sync_commit(qbox); + return ret; +} + +static void quota_roots_flush(struct quota *quota) +{ + struct quota_root *const *roots; + unsigned int i, count; + + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) { + if (roots[i]->backend.v.flush != NULL) + roots[i]->backend.v.flush(roots[i]); + } +} + +static void quota_mailbox_close(struct mailbox *box) +{ + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(box); + struct quota_user *quser = QUOTA_USER_CONTEXT_REQUIRE(box->storage->user); + + /* sync_notify() may be called outside sync_begin()..sync_deinit(). + make sure we apply changes at close time at latest. */ + quota_mailbox_sync_commit(qbox); + + /* make sure quota backend flushes all data. this could also be done + somewhat later, but user.deinit() is too late, since the flushing + can trigger quota recalculation which isn't safe to do anymore + at user.deinit() when most of the loaded plugins have already been + deinitialized. */ + quota_roots_flush(quser->quota); + + qbox->module_ctx.super.close(box); +} + +static void quota_mailbox_free(struct mailbox *box) +{ + struct quota_mailbox *qbox = QUOTA_CONTEXT_REQUIRE(box); + + if (array_is_created(&qbox->expunge_uids)) { + array_free(&qbox->expunge_uids); + array_free(&qbox->expunge_sizes); + } + i_assert(qbox->expunge_qt == NULL || + qbox->expunge_qt->tmp_mail == NULL); + + qbox->module_ctx.super.free(box); +} + +void quota_mailbox_allocated(struct mailbox *box) +{ + struct mailbox_vfuncs *v = box->vlast; + struct quota_mailbox *qbox; + + if (QUOTA_LIST_CONTEXT(box->list) == NULL) + return; + + if ((box->storage->class_flags & MAIL_STORAGE_CLASS_FLAG_NOQUOTA) != 0) + return; + + qbox = p_new(box->pool, struct quota_mailbox, 1); + qbox->module_ctx.super = *v; + box->vlast = &qbox->module_ctx.super; + + v->get_status = quota_get_status; + v->transaction_begin = quota_mailbox_transaction_begin; + v->transaction_commit = quota_mailbox_transaction_commit; + v->transaction_rollback = quota_mailbox_transaction_rollback; + v->save_begin = quota_save_begin; + v->save_finish = quota_save_finish; + v->copy = quota_copy; + v->sync_notify = quota_mailbox_sync_notify; + v->sync_deinit = quota_mailbox_sync_deinit; + v->close = quota_mailbox_close; + v->free = quota_mailbox_free; + MODULE_CONTEXT_SET(box, quota_storage_module, qbox); +} + +static void quota_mailbox_list_deinit(struct mailbox_list *list) +{ + struct quota_mailbox_list *qlist = QUOTA_LIST_CONTEXT(list); + + i_assert(qlist != NULL); + quota_remove_user_namespace(list->ns); + qlist->module_ctx.super.deinit(list); +} + +struct quota *quota_get_mail_user_quota(struct mail_user *user) +{ + struct quota_user *quser = QUOTA_USER_CONTEXT(user); + + return quser == NULL ? NULL : quser->quota; +} + +static void quota_user_deinit(struct mail_user *user) +{ + struct quota_user *quser = QUOTA_USER_CONTEXT_REQUIRE(user); + struct quota_settings *quota_set = quser->quota->set; + + quota_deinit(&quser->quota); + quser->module_ctx.super.deinit(user); + + quota_settings_deinit("a_set); +} + +void quota_mail_user_created(struct mail_user *user) +{ + struct mail_user_vfuncs *v = user->vlast; + struct quota_user *quser; + struct quota_settings *set; + struct quota *quota; + const char *error; + int ret; + + if ((ret = quota_user_read_settings(user, &set, &error)) > 0) { + if (quota_init(set, user, "a, &error) < 0) { + quota_settings_deinit(&set); + ret = -1; + } + } + + if (ret < 0) { + user->error = p_strdup_printf(user->pool, + "Failed to initialize quota: %s", error); + return; + } + if (ret > 0) { + quser = p_new(user->pool, struct quota_user, 1); + quser->module_ctx.super = *v; + user->vlast = &quser->module_ctx.super; + v->deinit = quota_user_deinit; + quser->quota = quota; + + MODULE_CONTEXT_SET(user, quota_user_module, quser); + } else { + e_debug(user->event, "quota: No quota setting - plugin disabled"); + } +} + +static struct quota_root * +quota_find_root_for_ns(struct quota *quota, struct mail_namespace *ns) +{ + struct quota_root *const *roots; + unsigned int i, count; + + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) { + if (roots[i]->ns_prefix != NULL && + strcmp(roots[i]->ns_prefix, ns->prefix) == 0) + return roots[i]; + } + return NULL; +} + +void quota_mailbox_list_created(struct mailbox_list *list) +{ + struct quota_mailbox_list *qlist; + struct quota *quota = NULL; + struct quota_root *root; + struct mail_user *quota_user; + bool add; + + /* see if we have a quota explicitly defined for this namespace */ + quota = quota_get_mail_user_quota(list->ns->user); + if (quota == NULL) + return; + root = quota_find_root_for_ns(quota, list->ns); + if (root != NULL) { + /* explicit quota root */ + root->ns = list->ns; + quota_user = list->ns->user; + } else { + quota_user = list->ns->owner != NULL ? + list->ns->owner : list->ns->user; + } + + if ((list->ns->flags & NAMESPACE_FLAG_NOQUOTA) != 0) + add = FALSE; + else if (list->ns->owner == NULL) { + /* public namespace - add quota only if namespace is + explicitly defined for it */ + add = root != NULL; + } else { + /* for shared namespaces add only if the owner has quota + enabled */ + add = QUOTA_USER_CONTEXT(quota_user) != NULL; + } + + if (add) { + struct mailbox_list_vfuncs *v = list->vlast; + + qlist = p_new(list->pool, struct quota_mailbox_list, 1); + qlist->module_ctx.super = *v; + list->vlast = &qlist->module_ctx.super; + v->deinit = quota_mailbox_list_deinit; + MODULE_CONTEXT_SET(list, quota_mailbox_list_module, qlist); + + quota = quota_get_mail_user_quota(quota_user); + i_assert(quota != NULL); + quota_add_user_namespace(quota, list->ns); + } +} + +static void quota_root_set_namespace(struct quota_root *root, + struct mail_namespace *namespaces) +{ + const struct quota_rule *rule; + const char *name; + struct mail_namespace *ns; + /* silence errors for autocreated (shared) users */ + bool silent_errors = namespaces->user->autocreated; + + if (root->ns_prefix != NULL && root->ns == NULL) { + root->ns = mail_namespace_find_prefix(namespaces, + root->ns_prefix); + if (root->ns == NULL && !silent_errors) { + e_error(root->quota->event, + "Unknown namespace: %s", + root->ns_prefix); + } + } + + array_foreach(&root->set->rules, rule) { + name = rule->mailbox_mask; + ns = mail_namespace_find(namespaces, name); + if ((ns->flags & NAMESPACE_FLAG_UNUSABLE) != 0 && + !silent_errors) + e_error(root->quota->event, + "Unknown namespace: %s", name); + } +} + +void quota_mail_namespaces_created(struct mail_namespace *namespaces) +{ + struct quota *quota; + struct quota_root *const *roots; + unsigned int i, count; + + quota = quota_get_mail_user_quota(namespaces->user); + if (quota == NULL) + return; + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) + quota_root_set_namespace(roots[i], namespaces); + + quota_over_flag_check_startup(quota); +} diff --git a/src/plugins/quota/quota-util.c b/src/plugins/quota/quota-util.c new file mode 100644 index 0000000..95ce369 --- /dev/null +++ b/src/plugins/quota/quota-util.c @@ -0,0 +1,465 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "wildcard-match.h" +#include "quota-private.h" + +#include <ctype.h> + +#define QUOTA_DEFAULT_GRACE "10%" + +#define RULE_NAME_DEFAULT_FORCE "*" +#define RULE_NAME_DEFAULT_NONFORCE "?" + +struct quota_rule * +quota_root_rule_find(struct quota_root_settings *root_set, const char *name) +{ + struct quota_rule *rule; + + array_foreach_modifiable(&root_set->rules, rule) { + if (wildcard_match(name, rule->mailbox_mask)) + return rule; + } + return NULL; +} + +static struct quota_rule * +quota_root_rule_find_exact(struct quota_root_settings *root_set, + const char *name) +{ + struct quota_rule *rule; + + array_foreach_modifiable(&root_set->rules, rule) { + if (strcmp(rule->mailbox_mask, name) == 0) + return rule; + } + return NULL; +} + +static int +quota_rule_parse_percentage(struct quota_root_settings *root_set, + struct quota_rule *rule, + int64_t *limit, const char **error_r) +{ + int64_t percentage = *limit; + + if (percentage <= -100 || percentage >= UINT_MAX) { + *error_r = "Invalid percentage"; + return -1; + } + + if (rule == &root_set->default_rule) { + *error_r = "Default rule can't be a percentage"; + return -1; + } + + if (limit == &rule->bytes_limit) + rule->bytes_percent = percentage; + else if (limit == &rule->count_limit) + rule->count_percent = percentage; + else + i_unreached(); + return 0; +} + +static int quota_limit_parse(struct quota_root_settings *root_set, + struct quota_rule *rule, const char *unit, + uint64_t multiply, int64_t *limit, + const char **error_r) +{ + switch (i_toupper(*unit)) { + case '\0': + /* default */ + break; + case 'B': + multiply = 1; + break; + case 'K': + multiply = 1024; + break; + case 'M': + multiply = 1024*1024; + break; + case 'G': + multiply = 1024*1024*1024; + break; + case 'T': + multiply = 1024ULL*1024*1024*1024; + break; + case '%': + multiply = 0; + if (quota_rule_parse_percentage(root_set, rule, limit, + error_r) < 0) + return -1; + break; + default: + *error_r = t_strdup_printf("Unknown unit: %s", unit); + return -1; + } + *limit *= multiply; + return 0; +} + +static void +quota_rule_recalculate_relative_rules(struct quota_rule *rule, + int64_t bytes_limit, int64_t count_limit) +{ + if (rule->bytes_percent != 0) + rule->bytes_limit = bytes_limit * rule->bytes_percent / 100; + if (rule->count_percent != 0) + rule->count_limit = count_limit * rule->count_percent / 100; +} + +void quota_root_recalculate_relative_rules(struct quota_root_settings *root_set, + int64_t bytes_limit, + int64_t count_limit) +{ + struct quota_rule *rule; + struct quota_warning_rule *warning_rule; + + array_foreach_modifiable(&root_set->rules, rule) { + quota_rule_recalculate_relative_rules(rule, bytes_limit, + count_limit); + } + + array_foreach_modifiable(&root_set->warning_rules, warning_rule) { + quota_rule_recalculate_relative_rules(&warning_rule->rule, + bytes_limit, count_limit); + } + quota_rule_recalculate_relative_rules(&root_set->grace_rule, + bytes_limit, 0); + root_set->last_mail_max_extra_bytes = root_set->grace_rule.bytes_limit; + + if (root_set->set->initialized) { + e_debug(root_set->set->event, + "Quota root %s: Recalculated relative rules with " + "bytes=%lld count=%lld. Now grace=%"PRIu64, root_set->name, + (long long)bytes_limit, (long long)count_limit, + root_set->last_mail_max_extra_bytes); + } +} + +static int +quota_rule_parse_limits(struct quota_root_settings *root_set, + struct quota_rule *rule, const char *limits, + const char *full_rule_def, + bool relative_rule, const char **error_r) +{ + const char **args, *key, *value, *error, *p; + uint64_t multiply; + int64_t *limit; + + args = t_strsplit(limits, ":"); + for (; *args != NULL; args++) { + multiply = 1; + limit = NULL; + + key = *args; + value = strchr(key, '='); + if (value == NULL) + value = ""; + else + key = t_strdup_until(key, value++); + + if (*value == '+') { + if (!relative_rule) { + *error_r = "Rule limit cannot have '+'"; + return -1; + } + value++; + } else if (*value != '-' && relative_rule) { + e_warning(root_set->set->event, "quota root %s rule %s: " + "obsolete configuration for rule '%s' " + "should be changed to '%s=+%s'", + root_set->name, full_rule_def, + *args, key, value); + } + + if (strcmp(key, "storage") == 0) { + multiply = 1024; + limit = &rule->bytes_limit; + if (str_parse_int64(value, limit, &p) < 0) { + *error_r = p_strdup_printf(root_set->set->pool, + "Invalid storage limit: %s", value); + return -1; + } + } else if (strcmp(key, "bytes") == 0) { + limit = &rule->bytes_limit; + if (str_parse_int64(value, limit, &p) < 0) { + *error_r = p_strdup_printf(root_set->set->pool, + "Invalid bytes limit: %s", value); + return -1; + } + } else if (strcmp(key, "messages") == 0) { + limit = &rule->count_limit; + if (str_parse_int64(value, limit, &p) < 0) { + *error_r = p_strdup_printf(root_set->set->pool, + "Invalid bytes messages: %s", value); + return -1; + } + } else { + *error_r = p_strdup_printf(root_set->set->pool, + "Unknown rule limit name: %s", key); + return -1; + } + + if (quota_limit_parse(root_set, rule, p, multiply, + limit, &error) < 0) { + *error_r = p_strdup_printf(root_set->set->pool, + "Invalid rule limit value '%s': %s", + *args, error); + return -1; + } + } + if (!relative_rule) { + if (rule->bytes_limit < 0) { + *error_r = "Bytes limit can't be negative"; + return -1; + } + if (rule->count_limit < 0) { + *error_r = "Count limit can't be negative"; + return -1; + } + } + return 0; +} + +int quota_root_add_rule(struct quota_root_settings *root_set, + const char *rule_def, const char **error_r) +{ + struct quota_rule *rule; + const char *p, *mailbox_mask; + int ret = 0; + + p = strchr(rule_def, ':'); + if (p == NULL) { + *error_r = "Invalid rule"; + return -1; + } + + /* <mailbox mask>:<quota limits> */ + mailbox_mask = t_strdup_until(rule_def, p++); + + rule = quota_root_rule_find_exact(root_set, mailbox_mask); + if (rule == NULL) { + if (strcmp(mailbox_mask, RULE_NAME_DEFAULT_NONFORCE) == 0) + rule = &root_set->default_rule; + else if (strcmp(mailbox_mask, RULE_NAME_DEFAULT_FORCE) == 0) { + rule = &root_set->default_rule; + root_set->force_default_rule = TRUE; + } else { + rule = array_append_space(&root_set->rules); + rule->mailbox_mask = strcasecmp(mailbox_mask, "INBOX") == 0 ? "INBOX" : + p_strdup(root_set->set->pool, mailbox_mask); + } + } + + if (strcmp(p, "ignore") == 0) { + rule->ignore = TRUE; + e_debug(root_set->set->event, + "Quota rule: root=%s mailbox=%s ignored", + root_set->name, mailbox_mask); + return 0; + } + + if (str_begins(p, "backend=")) { + if (root_set->backend->v.parse_rule == NULL) { + *error_r = "backend rule not supported"; + ret = -1; + } else if (!root_set->backend->v.parse_rule(root_set, rule, + p + 8, error_r)) + ret = -1; + } else { + bool relative_rule = rule != &root_set->default_rule; + + if (quota_rule_parse_limits(root_set, rule, p, rule_def, + relative_rule, error_r) < 0) + ret = -1; + } + + quota_root_recalculate_relative_rules(root_set, + root_set->default_rule.bytes_limit, + root_set->default_rule.count_limit); + const char *rule_plus = + rule == &root_set->default_rule ? "" : "+"; + + e_debug(root_set->set->event, "Quota rule: root=%s mailbox=%s " + "bytes=%s%lld%s messages=%s%lld%s", + root_set->name, mailbox_mask, + rule->bytes_limit > 0 ? rule_plus : "", + (long long)rule->bytes_limit, + rule->bytes_percent == 0 ? "" : + t_strdup_printf(" (%u%%)", rule->bytes_percent), + rule->count_limit > 0 ? rule_plus : "", + (long long)rule->count_limit, + rule->count_percent == 0 ? "" : + t_strdup_printf(" (%u%%)", rule->count_percent)); + return ret; +} + +int quota_root_add_warning_rule(struct quota_root_settings *root_set, + const char *rule_def, const char **error_r) +{ + struct quota_warning_rule *warning; + struct quota_rule rule; + const char *p, *q; + int ret; + bool reverse = FALSE; + + p = strchr(rule_def, ' '); + if (p == NULL || p[1] == '\0') { + *error_r = "No command specified"; + return -1; + } + + if (*rule_def == '+') { + /* warn when exceeding quota */ + q = rule_def+1; + } else if (*rule_def == '-') { + /* warn when going below quota */ + q = rule_def+1; + reverse = TRUE; + } else { + /* default: same as '+' */ + q = rule_def; + } + + i_zero(&rule); + ret = quota_rule_parse_limits(root_set, &rule, t_strdup_until(q, p), + rule_def, FALSE, error_r); + if (ret < 0) + return -1; + + warning = array_append_space(&root_set->warning_rules); + warning->command = p_strdup(root_set->set->pool, p+1); + warning->rule = rule; + warning->reverse = reverse; + if (reverse) + root_set->have_reverse_warnings = TRUE; + + quota_root_recalculate_relative_rules(root_set, + root_set->default_rule.bytes_limit, + root_set->default_rule.count_limit); + e_debug(root_set->set->event, "Quota warning: bytes=%"PRId64"%s " + "messages=%"PRId64"%s reverse=%s command=%s", + warning->rule.bytes_limit, + warning->rule.bytes_percent == 0 ? "" : + t_strdup_printf(" (%u%%)", warning->rule.bytes_percent), + warning->rule.count_limit, + warning->rule.count_percent == 0 ? "" : + t_strdup_printf(" (%u%%)", warning->rule.count_percent), + warning->reverse ? "yes" : "no", + warning->command); + return 0; +} + +int quota_root_parse_grace(struct quota_root_settings *root_set, + const char *value, const char **error_r) +{ + const char *p; + + if (value == NULL) { + /* default */ + value = QUOTA_DEFAULT_GRACE; + } + + if (str_parse_int64(value, &root_set->grace_rule.bytes_limit, &p) < 0) + return -1; + if (quota_limit_parse(root_set, &root_set->grace_rule, p, 1, + &root_set->grace_rule.bytes_limit, error_r) < 0) + return -1; + quota_rule_recalculate_relative_rules(&root_set->grace_rule, + root_set->default_rule.bytes_limit, 0); + root_set->last_mail_max_extra_bytes = root_set->grace_rule.bytes_limit; + e_debug(root_set->set->event, "Quota grace: root=%s bytes=%lld%s", + root_set->name, (long long)root_set->grace_rule.bytes_limit, + root_set->grace_rule.bytes_percent == 0 ? "" : + t_strdup_printf(" (%u%%)", root_set->grace_rule.bytes_percent)); + return 0; +} + +bool quota_warning_match(const struct quota_warning_rule *w, + uint64_t bytes_before, uint64_t bytes_current, + uint64_t count_before, uint64_t count_current, + const char **reason_r) +{ +#define QUOTA_EXCEEDED(before, current, limit) \ + ((before) < (uint64_t)(limit) && (current) >= (uint64_t)(limit)) + if (!w->reverse) { + /* over quota (default) */ + if (QUOTA_EXCEEDED(bytes_before, bytes_current, w->rule.bytes_limit)) { + *reason_r = t_strdup_printf("bytes=%"PRIu64" -> %"PRIu64" over limit %"PRId64, + bytes_before, bytes_current, w->rule.bytes_limit); + return TRUE; + } + if (QUOTA_EXCEEDED(count_before, count_current, w->rule.count_limit)) { + *reason_r = t_strdup_printf("count=%"PRIu64" -> %"PRIu64" over limit %"PRId64, + count_before, count_current, w->rule.count_limit); + return TRUE; + } + } else { + if (QUOTA_EXCEEDED(bytes_current, bytes_before, w->rule.bytes_limit)) { + *reason_r = t_strdup_printf("bytes=%"PRIu64" -> %"PRIu64" below limit %"PRId64, + bytes_before, bytes_current, w->rule.bytes_limit); + return TRUE; + } + if (QUOTA_EXCEEDED(count_current, count_before, w->rule.count_limit)) { + *reason_r = t_strdup_printf("count=%"PRIu64" -> %"PRIu64" below limit %"PRId64, + count_before, count_current, w->rule.count_limit); + return TRUE; + } + } + return FALSE; +} + +bool quota_transaction_is_over(struct quota_transaction_context *ctx, + uoff_t size) +{ + if (ctx->count_used < 0) { + /* we've deleted some messages. we should be ok, unless we + were already over quota and still are after these + deletions. */ + const uint64_t count_deleted = (uint64_t)-ctx->count_used; + + if (ctx->count_over > 0) { + if (count_deleted - 1 < ctx->count_over) + return TRUE; + } + } else { + if (ctx->count_ceil < 1 || + ctx->count_ceil - 1 < (uint64_t)ctx->count_used) { + /* count limit reached */ + return TRUE; + } + } + + if (ctx->bytes_used < 0) { + const uint64_t bytes_deleted = (uint64_t)-ctx->bytes_used; + + /* we've deleted some messages. same logic as above. */ + if (ctx->bytes_over > 0) { + if (ctx->bytes_over > bytes_deleted) { + /* even after deletions we're over quota */ + return TRUE; + } + if (size > bytes_deleted - ctx->bytes_over) + return TRUE; + } else { + if (size > bytes_deleted && + size - bytes_deleted < ctx->bytes_ceil) + return TRUE; + } + } else if (size == 0) { + /* we need to explicitly test this case, since the generic + check would fail if user is already over quota */ + if (ctx->bytes_over > 0) + return TRUE; + } else { + if (ctx->bytes_ceil < size || + ctx->bytes_ceil - size < (uint64_t)ctx->bytes_used) { + /* bytes limit reached */ + return TRUE; + } + } + return FALSE; +} diff --git a/src/plugins/quota/quota.c b/src/plugins/quota/quota.c new file mode 100644 index 0000000..3d6d8e5 --- /dev/null +++ b/src/plugins/quota/quota.c @@ -0,0 +1,1543 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "str.h" +#include "ioloop.h" +#include "net.h" +#include "write-full.h" +#include "eacces-error.h" +#include "wildcard-match.h" +#include "dict.h" +#include "mailbox-list-private.h" +#include "quota-private.h" +#include "quota-fs.h" +#include "llist.h" +#include "program-client.h" +#include "settings-parser.h" + +#include <sys/wait.h> + +#define DEFAULT_QUOTA_EXCEEDED_MSG \ + "Quota exceeded (mailbox for user is full)" +#define QUOTA_LIMIT_SET_PATH DICT_PATH_PRIVATE"quota/limit/" + +/* How many seconds after the userdb lookup do we still want to execute the + quota_over_script. This applies to quota_over_flag_lazy_check=yes and also + after unhibernating IMAP connections. */ +#define QUOTA_OVER_FLAG_MAX_DELAY_SECS 10 + +struct quota_root_iter { + struct quota *quota; + struct mailbox *box; + + unsigned int i; +}; + +unsigned int quota_module_id = 0; + +extern struct quota_backend quota_backend_count; +extern struct quota_backend quota_backend_dict; +extern struct quota_backend quota_backend_dirsize; +extern struct quota_backend quota_backend_fs; +extern struct quota_backend quota_backend_imapc; +extern struct quota_backend quota_backend_maildir; + +static const struct quota_backend *quota_internal_backends[] = { +#ifdef HAVE_FS_QUOTA + "a_backend_fs, +#endif + "a_backend_count, + "a_backend_dict, + "a_backend_dirsize, + "a_backend_imapc, + "a_backend_maildir +}; + +static ARRAY(const struct quota_backend*) quota_backends; + +static void hidden_param_handler(struct quota_root *_root, const char *param_value); +static void ignoreunlim_param_handler(struct quota_root *_root, const char *param_value); +static void noenforcing_param_handler(struct quota_root *_root, const char *param_value); +static void ns_param_handler(struct quota_root *_root, const char *param_value); + +struct quota_param_parser quota_param_hidden = {.param_name = "hidden", .param_handler = hidden_param_handler}; +struct quota_param_parser quota_param_ignoreunlimited = {.param_name = "ignoreunlimited", .param_handler = ignoreunlim_param_handler}; +struct quota_param_parser quota_param_noenforcing = {.param_name = "noenforcing", .param_handler = noenforcing_param_handler}; +struct quota_param_parser quota_param_ns = {.param_name = "ns=", .param_handler = ns_param_handler}; + +static enum quota_alloc_result quota_default_test_alloc( + struct quota_transaction_context *ctx, uoff_t size, + const char **error_r); +static void quota_over_flag_check_root(struct quota_root *root); + +static const struct quota_backend *quota_backend_find(const char *name) +{ + const struct quota_backend *const *backend; + + array_foreach("a_backends, backend) { + if (strcmp((*backend)->name, name) == 0) + return *backend; + } + + return NULL; +} + +void quota_backend_register(const struct quota_backend *backend) +{ + i_assert(quota_backend_find(backend->name) == NULL); + array_push_back("a_backends, &backend); +} + +void quota_backend_unregister(const struct quota_backend *backend) +{ + for(unsigned int i = 0; i < array_count("a_backends); i++) { + const struct quota_backend *be = + array_idx_elem("a_backends, i); + if (strcmp(be->name, backend->name) == 0) { + array_delete("a_backends, i, 1); + return; + } + } + + i_unreached(); +} + +void quota_backends_register(void); +void quota_backends_unregister(void); + +void quota_backends_register(void) +{ + i_array_init("a_backends, 8); + array_append("a_backends, quota_internal_backends, + N_ELEMENTS(quota_internal_backends)); +} + +void quota_backends_unregister(void) +{ + for(size_t i = 0; i < N_ELEMENTS(quota_internal_backends); i++) { + quota_backend_unregister(quota_internal_backends[i]); + } + + i_assert(array_count("a_backends) == 0); + array_free("a_backends); + +} + +static int quota_root_add_rules(struct mail_user *user, const char *root_name, + struct quota_root_settings *root_set, + const char **error_r) +{ + const char *rule_name, *rule, *error; + unsigned int i; + + rule_name = t_strconcat(root_name, "_rule", NULL); + for (i = 2;; i++) { + rule = mail_user_plugin_getenv(user, rule_name); + if (rule == NULL) + break; + + if (quota_root_add_rule(root_set, rule, &error) < 0) { + *error_r = t_strdup_printf("Invalid rule %s: %s", + rule, error); + return -1; + } + rule_name = t_strdup_printf("%s_rule%d", root_name, i); + } + return 0; +} + +static int +quota_root_add_warning_rules(struct mail_user *user, const char *root_name, + struct quota_root_settings *root_set, + const char **error_r) +{ + const char *rule_name, *rule, *error; + unsigned int i; + + rule_name = t_strconcat(root_name, "_warning", NULL); + for (i = 2;; i++) { + rule = mail_user_plugin_getenv(user, rule_name); + if (rule == NULL) + break; + + if (quota_root_add_warning_rule(root_set, rule, &error) < 0) { + *error_r = t_strdup_printf("Invalid warning rule: %s", + rule); + return -1; + } + rule_name = t_strdup_printf("%s_warning%d", root_name, i); + } + return 0; +} + +static int +quota_root_parse_set(struct mail_user *user, const char *root_name, + struct quota_root_settings *root_set, + const char **error_r) +{ + const char *name, *value; + + name = t_strconcat(root_name, "_set", NULL); + value = mail_user_plugin_getenv(user, name); + if (value == NULL) + return 0; + + if (!str_begins(value, "dict:")) { + *error_r = t_strdup_printf("%s supports only dict backend", name); + return -1; + } + root_set->limit_set = p_strdup(root_set->set->pool, value+5); + return 0; +} + +static int +quota_root_settings_init(struct quota_settings *quota_set, const char *root_def, + struct quota_root_settings **set_r, + const char **error_r) +{ + struct quota_root_settings *root_set; + const struct quota_backend *backend; + const char *p, *args, *backend_name; + + /* <backend>[:<quota root name>[:<backend args>]] */ + p = strchr(root_def, ':'); + if (p == NULL) { + backend_name = root_def; + args = NULL; + } else { + backend_name = t_strdup_until(root_def, p); + args = p + 1; + } + + backend = quota_backend_find(backend_name); + if (backend == NULL) { + *error_r = t_strdup_printf("Unknown quota backend: %s", + backend_name); + return -1; + } + + root_set = p_new(quota_set->pool, struct quota_root_settings, 1); + root_set->set = quota_set; + root_set->backend = backend; + + if (args != NULL) { + /* save root's name */ + p = strchr(args, ':'); + if (p == NULL) { + root_set->name = p_strdup(quota_set->pool, args); + args = NULL; + } else { + root_set->name = + p_strdup_until(quota_set->pool, args, p); + args = p + 1; + } + } else { + root_set->name = ""; + } + root_set->args = p_strdup(quota_set->pool, args); + + e_debug(quota_set->event, "Quota root: name=%s backend=%s args=%s", + root_set->name, backend_name, args == NULL ? "" : args); + + p_array_init(&root_set->rules, quota_set->pool, 4); + p_array_init(&root_set->warning_rules, quota_set->pool, 4); + array_push_back("a_set->root_sets, &root_set); + *set_r = root_set; + return 0; +} + +static int +quota_root_add(struct quota_settings *quota_set, struct mail_user *user, + const char *env, const char *root_name, const char **error_r) +{ + struct quota_root_settings *root_set; + const char *set_name, *value; + + if (quota_root_settings_init(quota_set, env, &root_set, error_r) < 0) + return -1; + root_set->set_name = p_strdup(quota_set->pool, root_name); + if (quota_root_add_rules(user, root_name, root_set, error_r) < 0) + return -1; + if (quota_root_add_warning_rules(user, root_name, root_set, error_r) < 0) + return -1; + if (quota_root_parse_set(user, root_name, root_set, error_r) < 0) + return -1; + + set_name = t_strconcat(root_name, "_grace", NULL); + value = mail_user_plugin_getenv(user, set_name); + if (quota_root_parse_grace(root_set, value, error_r) < 0) { + *error_r = t_strdup_printf("Invalid %s value '%s': %s", + set_name, value, *error_r); + return -1; + } + return 0; +} + +const char *quota_alloc_result_errstr(enum quota_alloc_result res, + struct quota_transaction_context *qt) +{ + switch (res) { + case QUOTA_ALLOC_RESULT_OK: + return "OK"; + case QUOTA_ALLOC_RESULT_BACKGROUND_CALC: + return "Blocked by an ongoing background quota calculation"; + case QUOTA_ALLOC_RESULT_TEMPFAIL: + return "Internal quota calculation error"; + case QUOTA_ALLOC_RESULT_OVER_MAXSIZE: + return "Mail size is larger than the maximum size allowed by " + "server configuration"; + case QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT: + case QUOTA_ALLOC_RESULT_OVER_QUOTA: + return qt->quota->set->quota_exceeded_msg; + } + i_unreached(); +} + +int quota_user_read_settings(struct mail_user *user, + struct quota_settings **set_r, + const char **error_r) +{ + struct quota_settings *quota_set; + char root_name[5 + MAX_INT_STRLEN]; + const char *env, *error; + unsigned int i; + pool_t pool; + + pool = pool_alloconly_create("quota settings", 2048); + quota_set = p_new(pool, struct quota_settings, 1); + quota_set->pool = pool; + quota_set->event = event_create(user->event); + quota_set->test_alloc = quota_default_test_alloc; + quota_set->debug = user->mail_debug; + quota_set->quota_exceeded_msg = + mail_user_plugin_getenv(user, "quota_exceeded_message"); + if (quota_set->quota_exceeded_msg == NULL) + quota_set->quota_exceeded_msg = DEFAULT_QUOTA_EXCEEDED_MSG; + quota_set->vsizes = mail_user_plugin_getenv_bool(user, "quota_vsizes"); + + const char *max_size = mail_user_plugin_getenv(user, + "quota_max_mail_size"); + if (max_size != NULL) { + const char *error = NULL; + if (settings_get_size(max_size, "a_set->max_mail_size, + &error) < 0) { + *error_r = t_strdup_printf("quota_max_mail_size: %s", + error); + return -1; + } + } + + p_array_init("a_set->root_sets, pool, 4); + if (i_strocpy(root_name, "quota", sizeof(root_name)) < 0) + i_unreached(); + for (i = 2;; i++) { + env = mail_user_plugin_getenv(user, root_name); + if (env == NULL || *env == '\0') + break; + + if (quota_root_add(quota_set, user, env, root_name, + &error) < 0) { + *error_r = t_strdup_printf("Invalid quota root %s: %s", + root_name, error); + event_unref("a_set->event); + pool_unref(&pool); + return -1; + } + if (i_snprintf(root_name, sizeof(root_name), "quota%d", i) < 0) + i_unreached(); + } + if (quota_set->max_mail_size == 0 && + array_count("a_set->root_sets) == 0) { + event_unref("a_set->event); + pool_unref(&pool); + return 0; + } + + quota_set->initialized = TRUE; + *set_r = quota_set; + return 1; +} + +void quota_settings_deinit(struct quota_settings **_quota_set) +{ + struct quota_settings *quota_set = *_quota_set; + + *_quota_set = NULL; + + event_unref("a_set->event); + pool_unref("a_set->pool); +} + +static void quota_root_deinit(struct quota_root *root) +{ + pool_t pool = root->pool; + + if (root->limit_set_dict != NULL) + dict_deinit(&root->limit_set_dict); + event_unref(&root->backend.event); + root->backend.v.deinit(root); + pool_unref(&pool); +} + +int quota_root_default_init(struct quota_root *root, const char *args, + const char **error_r) +{ + const struct quota_param_parser default_params[] = { + quota_param_hidden, + quota_param_ignoreunlimited, + quota_param_noenforcing, + quota_param_ns, + {.param_name = NULL} + }; + return quota_parse_parameters(root, &args, error_r, default_params, TRUE); +} + +static int +quota_root_init(struct quota_root_settings *root_set, struct quota *quota, + struct quota_root **root_r, const char **error_r) +{ + struct quota_root *root; + + root = root_set->backend->v.alloc(); + root->pool = pool_alloconly_create("quota root", 512); + root->set = root_set; + root->quota = quota; + root->backend = *root_set->backend; + root->bytes_limit = root_set->default_rule.bytes_limit; + root->count_limit = root_set->default_rule.count_limit; + + array_create(&root->quota_module_contexts, root->pool, + sizeof(void *), 10); + + if (root->backend.v.init != NULL) { + root->backend.event = event_create(quota->event); + event_drop_parent_log_prefixes(root->backend.event, 1); + event_set_forced_debug(root->backend.event, root->quota->set->debug); + + if (root->backend.v.init(root, root_set->args, error_r) < 0) { + *error_r = t_strdup_printf("%s quota init failed: %s", + root->backend.name, *error_r); + + event_unref(&root->backend.event); + return -1; + } + } else { + if (quota_root_default_init(root, root_set->args, error_r) < 0) + return -1; + } + if (root_set->default_rule.bytes_limit == 0 && + root_set->default_rule.count_limit == 0 && + root->disable_unlimited_tracking) { + quota_root_deinit(root); + return 0; + } + *root_r = root; + return 1; +} + +int quota_init(struct quota_settings *quota_set, struct mail_user *user, + struct quota **quota_r, const char **error_r) +{ + struct quota *quota; + struct quota_root *root; + struct quota_root_settings *const *root_sets; + unsigned int i, count; + const char *error; + int ret; + + quota = i_new(struct quota, 1); + quota->event = event_create(user->event); + event_set_forced_debug(quota->event, quota_set->debug); + event_set_append_log_prefix(quota->event, "quota: "); + quota->user = user; + quota->set = quota_set; + i_array_init("a->roots, 8); + + root_sets = array_get("a_set->root_sets, &count); + i_array_init("a->namespaces, count); + for (i = 0; i < count; i++) { + ret = quota_root_init(root_sets[i], quota, &root, &error); + if (ret < 0) { + *error_r = t_strdup_printf("Quota root %s: %s", + root_sets[i]->name, error); + quota_deinit("a); + return -1; + } + if (ret > 0) + array_push_back("a->roots, &root); + } + *quota_r = quota; + return 0; +} + +void quota_deinit(struct quota **_quota) +{ + struct quota *quota = *_quota; + struct quota_root *const *roots; + unsigned int i, count; + + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) + quota_root_deinit(roots[i]); + + /* deinit quota roots before setting quser->quota=NULL */ + *_quota = NULL; + + array_free("a->roots); + array_free("a->namespaces); + event_unref("a->event); + i_free(quota); +} + +static int quota_root_get_rule_limits(struct quota_root *root, + const char *mailbox_name, + uint64_t *bytes_limit_r, + uint64_t *count_limit_r, + bool *ignored_r, + const char **error_r) +{ + struct quota_rule *rule; + int64_t bytes_limit, count_limit; + int ret; + + *ignored_r = FALSE; + + if (!root->set->force_default_rule) { + if (root->backend.v.init_limits != NULL) { + const char *error; + if (root->backend.v.init_limits(root, &error) < 0) { + *error_r = t_strdup_printf( + "Initializing limits failed for quota backend: %s", + error); + return -1; + } + } + } + + bytes_limit = root->bytes_limit; + count_limit = root->count_limit; + + /* if default rule limits are 0, user has unlimited quota. + ignore any specific quota rules */ + if (bytes_limit != 0 || count_limit != 0) { + (void)mail_namespace_find_unalias(root->quota->user->namespaces, + &mailbox_name); + rule = quota_root_rule_find(root->set, mailbox_name); + ret = 1; + } else { + rule = NULL; + ret = 0; + } + + if (rule != NULL) { + if (!rule->ignore) { + bytes_limit += rule->bytes_limit; + count_limit += rule->count_limit; + } else { + bytes_limit = 0; + count_limit = 0; + *ignored_r = TRUE; + } + } + + *bytes_limit_r = bytes_limit <= 0 ? 0 : bytes_limit; + *count_limit_r = count_limit <= 0 ? 0 : count_limit; + return ret; +} + +static bool +quota_is_duplicate_namespace(struct quota *quota, struct mail_namespace *ns) +{ + struct mail_namespace *const *namespaces; + unsigned int i, count; + const char *path, *path2; + + if (!mailbox_list_get_root_path(ns->list, + MAILBOX_LIST_PATH_TYPE_MAILBOX, &path)) + path = NULL; + + namespaces = array_get("a->namespaces, &count); + for (i = 0; i < count; i++) { + /* count namespace aliases only once. don't rely only on + alias_for != NULL, because the alias might have been + explicitly added as the wanted quota namespace. */ + if (ns->alias_for == namespaces[i] || + namespaces[i]->alias_for == ns) + continue; + + if (path != NULL && + mailbox_list_get_root_path(namespaces[i]->list, + MAILBOX_LIST_PATH_TYPE_MAILBOX, &path2) && + strcmp(path, path2) == 0) { + /* duplicate path */ + if ((ns->flags & NAMESPACE_FLAG_INBOX_USER) == 0) + return TRUE; + + /* this is inbox=yes namespace, but the earlier one + that had the same location was inbox=no. we need to + include the INBOX also in quota calculations, so we + can't just ignore this namespace. but since we've + already called backend's namespace_added(), we can't + just remove it either. so just mark the old one as + unwanted namespace. + + an alternative would be to do a bit larger change so + namespaces wouldn't be added until + mail_namespaces_created() hook is called */ + i_assert(quota->unwanted_ns == NULL); + quota->unwanted_ns = namespaces[i]; + return FALSE; + } + } + return FALSE; +} + +void quota_add_user_namespace(struct quota *quota, struct mail_namespace *ns) +{ + struct quota_root *const *roots; + struct quota_backend **backends; + unsigned int i, j, count; + + /* first check if there already exists a namespace with the exact same + path. we don't want to count them twice. */ + if (quota_is_duplicate_namespace(quota, ns)) + return; + + array_push_back("a->namespaces, &ns); + + roots = array_get("a->roots, &count); + /* @UNSAFE: get different backends into one array */ + backends = t_new(struct quota_backend *, count + 1); + for (i = 0; i < count; i++) { + for (j = 0; backends[j] != NULL; j++) { + if (backends[j]->name == roots[i]->backend.name) + break; + } + if (backends[j] == NULL) + backends[j] = &roots[i]->backend; + } + + for (i = 0; backends[i] != NULL; i++) { + if (backends[i]->v.namespace_added != NULL) + backends[i]->v.namespace_added(quota, ns); + } +} + +void quota_remove_user_namespace(struct mail_namespace *ns) +{ + struct quota *quota; + struct mail_namespace *const *namespaces; + unsigned int i, count; + + quota = ns->owner != NULL ? + quota_get_mail_user_quota(ns->owner) : + quota_get_mail_user_quota(ns->user); + if (quota == NULL) { + /* no quota for this namespace */ + return; + } + + namespaces = array_get("a->namespaces, &count); + for (i = 0; i < count; i++) { + if (namespaces[i] == ns) { + array_delete("a->namespaces, i, 1); + break; + } + } +} + +struct quota_root_iter * +quota_root_iter_init_user(struct mail_user *user) +{ + struct quota_root_iter *iter; + + iter = i_new(struct quota_root_iter, 1); + iter->quota = quota_get_mail_user_quota(user); + return iter; +} + +struct quota_root_iter * +quota_root_iter_init(struct mailbox *box) +{ + struct quota_root_iter *iter; + struct mail_user *user; + + user = box->list->ns->owner != NULL ? + box->list->ns->owner : box->list->ns->user; + iter = quota_root_iter_init_user(user); + iter->box = box; + return iter; +} + +bool quota_root_is_namespace_visible(struct quota_root *root, + struct mail_namespace *ns) +{ + struct mailbox_list *list = ns->list; + struct mail_storage *storage; + + /* this check works as long as there is only one storage per list */ + if (mailbox_list_get_storage(&list, "", &storage) == 0 && + (storage->class_flags & MAIL_STORAGE_CLASS_FLAG_NOQUOTA) != 0) + return FALSE; + if (root->quota->unwanted_ns == ns) + return FALSE; + + if (root->ns_prefix != NULL) { + if (root->ns != ns) + return FALSE; + } else { + if (ns->owner == NULL) + return FALSE; + } + return TRUE; +} + +static bool +quota_root_is_visible(struct quota_root *root, struct mailbox *box) +{ + if (!quota_root_is_namespace_visible(root, box->list->ns)) + return FALSE; + if (array_count(&root->quota->roots) == 1) { + /* a single quota root: don't bother checking further */ + return TRUE; + } + return root->backend.v.match_box == NULL ? TRUE : + root->backend.v.match_box(root, box); +} + +struct quota_root *quota_root_iter_next(struct quota_root_iter *iter) +{ + struct quota_root *const *roots, *root = NULL; + unsigned int count; + + if (iter->quota == NULL) + return NULL; + + roots = array_get(&iter->quota->roots, &count); + if (iter->i >= count) + return NULL; + + for (; iter->i < count; iter->i++) { + if (iter->box != NULL && + !quota_root_is_visible(roots[iter->i], iter->box)) + continue; + + root = roots[iter->i]; + break; + } + + iter->i++; + return root; +} + +void quota_root_iter_deinit(struct quota_root_iter **_iter) +{ + struct quota_root_iter *iter = *_iter; + + *_iter = NULL; + i_free(iter); +} + +struct quota_root *quota_root_lookup(struct mail_user *user, const char *name) +{ + struct quota *quota; + struct quota_root *const *roots; + unsigned int i, count; + + quota = quota_get_mail_user_quota(user); + if (quota == NULL) + return NULL; + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) { + if (strcmp(roots[i]->set->name, name) == 0) + return roots[i]; + } + return NULL; +} + +const char *quota_root_get_name(struct quota_root *root) +{ + return root->set->name; +} + +const char *const *quota_root_get_resources(struct quota_root *root) +{ + /* if we haven't checked the quota_over_flag yet, do it now */ + quota_over_flag_check_root(root); + + return root->backend.v.get_resources(root); +} + +bool quota_root_is_hidden(struct quota_root *root) +{ + return root->hidden; +} + +enum quota_get_result +quota_get_resource(struct quota_root *root, const char *mailbox_name, + const char *name, uint64_t *value_r, uint64_t *limit_r, + const char **error_r) +{ + const char *error; + uint64_t bytes_limit, count_limit; + bool ignored, kilobytes = FALSE; + enum quota_get_result ret; + + *value_r = *limit_r = 0; + + if (strcmp(name, QUOTA_NAME_STORAGE_KILOBYTES) == 0) { + name = QUOTA_NAME_STORAGE_BYTES; + kilobytes = TRUE; + } + + /* Get the value first. This call may also update quota limits if + they're defined externally. */ + ret = root->backend.v.get_resource(root, name, value_r, &error); + if (ret == QUOTA_GET_RESULT_UNLIMITED) + i_panic("Quota backend %s returned QUOTA_GET_RESULT_UNLIMITED " + "while getting resource %s from box %s", + root->backend.name, name, mailbox_name); + else if (ret != QUOTA_GET_RESULT_LIMITED) { + *error_r = t_strdup_printf( + "quota-%s: %s", root->set->backend->name, error); + return ret; + } + + if (quota_root_get_rule_limits(root, mailbox_name, + &bytes_limit, &count_limit, + &ignored, &error) < 0) { + *error_r = t_strdup_printf( + "Failed to get quota root rule limits for mailbox %s: %s", + mailbox_name, error); + return QUOTA_GET_RESULT_INTERNAL_ERROR; + } + + if (strcmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) + *limit_r = bytes_limit; + else if (strcmp(name, QUOTA_NAME_MESSAGES) == 0) + *limit_r = count_limit; + else + *limit_r = 0; + + if (kilobytes) { + *value_r = (*value_r + 1023) / 1024; + *limit_r = (*limit_r + 1023) / 1024; + } + return *limit_r == 0 ? QUOTA_GET_RESULT_UNLIMITED : QUOTA_GET_RESULT_LIMITED; +} + +int quota_set_resource(struct quota_root *root, const char *name, + uint64_t value, const char **client_error_r) +{ + struct dict_transaction_context *trans; + const char *key, *error; + const struct dict_op_settings *set; + + if (root->set->limit_set == NULL) { + *client_error_r = MAIL_ERRSTR_NO_PERMISSION; + return -1; + } + if (strcasecmp(name, QUOTA_NAME_STORAGE_KILOBYTES) == 0) + key = "storage"; + else if (strcasecmp(name, QUOTA_NAME_STORAGE_BYTES) == 0) + key = "bytes"; + else if (strcasecmp(name, QUOTA_NAME_MESSAGES) == 0) + key = "messages"; + else { + *client_error_r = t_strdup_printf( + "Unsupported resource name: %s", name); + return -1; + } + + if (root->limit_set_dict == NULL) { + struct dict_settings set; + + i_zero(&set); + set.base_dir = root->quota->user->set->base_dir; + set.event_parent = root->quota->user->event; + if (dict_init(root->set->limit_set, &set, + &root->limit_set_dict, &error) < 0) { + e_error(root->quota->event, + "dict_init() failed: %s", error); + *client_error_r = "Internal quota limit update error"; + return -1; + } + } + + set = mail_user_get_dict_op_settings(root->ns->user); + trans = dict_transaction_begin(root->limit_set_dict, set); + key = t_strdup_printf(QUOTA_LIMIT_SET_PATH"%s", key); + dict_set(trans, key, dec2str(value)); + if (dict_transaction_commit(&trans, &error) < 0) { + e_error(root->quota->event, + "dict_transaction_commit() failed: %s", error); + *client_error_r = "Internal quota limit update error"; + return -1; + } + return 0; +} + +struct quota_transaction_context *quota_transaction_begin(struct mailbox *box) +{ + struct quota_transaction_context *ctx; + struct quota_root *const *rootp; + const struct quota_rule *rule; + const char *mailbox_name; + + ctx = i_new(struct quota_transaction_context, 1); + ctx->quota = box->list->ns->owner != NULL ? + quota_get_mail_user_quota(box->list->ns->owner) : + quota_get_mail_user_quota(box->list->ns->user); + i_assert(ctx->quota != NULL); + + ctx->box = box; + ctx->bytes_ceil = (uint64_t)-1; + ctx->bytes_ceil2 = (uint64_t)-1; + ctx->count_ceil = (uint64_t)-1; + + mailbox_name = mailbox_get_vname(box); + (void)mail_namespace_find_unalias(box->storage->user->namespaces, + &mailbox_name); + + ctx->auto_updating = TRUE; + array_foreach(&ctx->quota->roots, rootp) { + if (!quota_root_is_visible(*rootp, ctx->box)) + continue; + + rule = quota_root_rule_find((*rootp)->set, mailbox_name); + if (rule != NULL && rule->ignore) { + /* This mailbox isn't included in quota. This means + it's also not included in quota_warnings, so make + sure it's fully ignored. */ + continue; + } + + /* If there are reverse quota_warnings, we'll need to track + how many bytes were expunged even with auto_updating roots. + (An alternative could be to get the current quota usage + before and after the expunges, but that's more complicated + and probably isn't any better.) */ + if (!(*rootp)->auto_updating || + (*rootp)->set->have_reverse_warnings) + ctx->auto_updating = FALSE; + } + + if (box->storage->user->dsyncing) { + /* ignore quota for dsync */ + ctx->limits_set = TRUE; + } + return ctx; +} + +int quota_transaction_set_limits(struct quota_transaction_context *ctx, + enum quota_get_result *error_result_r, + const char **error_r) +{ + struct quota_root *const *roots; + const char *mailbox_name, *error; + unsigned int i, count; + uint64_t bytes_limit, count_limit, current, limit, diff; + bool use_grace, ignored; + enum quota_get_result ret; + + if (ctx->limits_set) + return 0; + ctx->limits_set = TRUE; + mailbox_name = mailbox_get_vname(ctx->box); + /* use quota_grace only for LDA/LMTP */ + use_grace = (ctx->box->flags & MAILBOX_FLAG_POST_SESSION) != 0; + ctx->no_quota_updates = TRUE; + + /* find the lowest quota limits from all roots and use them */ + roots = array_get(&ctx->quota->roots, &count); + for (i = 0; i < count; i++) { + /* make sure variables get initialized */ + bytes_limit = count_limit = 0; + if (!quota_root_is_visible(roots[i], ctx->box)) + continue; + else if (roots[i]->no_enforcing) { + ignored = FALSE; + } else if (quota_root_get_rule_limits(roots[i], mailbox_name, + &bytes_limit, &count_limit, + &ignored, &error) < 0) { + ctx->failed = TRUE; + *error_result_r = QUOTA_GET_RESULT_INTERNAL_ERROR; + *error_r = t_strdup_printf( + "Failed to get quota root rule limits for %s: %s", + mailbox_name, error); + return -1; + } + if (!ignored) + ctx->no_quota_updates = FALSE; + + if (bytes_limit > 0) { + ret = quota_get_resource(roots[i], mailbox_name, + QUOTA_NAME_STORAGE_BYTES, + ¤t, &limit, &error); + if (ret == QUOTA_GET_RESULT_LIMITED) { + if (limit <= current) { + /* over quota */ + ctx->bytes_ceil = 0; + ctx->bytes_ceil2 = 0; + diff = current - limit; + if (ctx->bytes_over < diff) + ctx->bytes_over = diff; + } else { + diff = limit - current; + if (ctx->bytes_ceil2 > diff) + ctx->bytes_ceil2 = diff; + diff += !use_grace ? 0 : + roots[i]->set->last_mail_max_extra_bytes; + if (ctx->bytes_ceil > diff) + ctx->bytes_ceil = diff; + } + } else if (ret <= QUOTA_GET_RESULT_INTERNAL_ERROR) { + ctx->failed = TRUE; + *error_result_r = ret; + *error_r = t_strdup_printf( + "Failed to get quota resource " + QUOTA_NAME_STORAGE_BYTES" for %s: %s", + mailbox_name, error); + return -1; + } + } + + if (count_limit > 0) { + ret = quota_get_resource(roots[i], mailbox_name, + QUOTA_NAME_MESSAGES, + ¤t, &limit, &error); + if (ret == QUOTA_GET_RESULT_LIMITED) { + if (limit <= current) { + /* over quota */ + ctx->count_ceil = 0; + diff = current - limit; + if (ctx->count_over < diff) + ctx->count_over = diff; + } else { + diff = limit - current; + if (ctx->count_ceil > diff) + ctx->count_ceil = diff; + } + } else if (ret <= QUOTA_GET_RESULT_INTERNAL_ERROR) { + ctx->failed = TRUE; + *error_result_r = ret; + *error_r = t_strdup_printf( + "Failed to get quota resource " + QUOTA_NAME_MESSAGES" for %s: %s", + mailbox_name, error); + return -1; + } + } + } + return 0; +} + +static void quota_warning_execute(struct quota_root *root, const char *cmd, + const char *last_arg, const char *reason) +{ + const char *socket_path, *const *args, *error, *scheme, *ptr; + + struct program_client_settings set = { + .client_connect_timeout_msecs = 1000, + .debug = root->quota->user->mail_debug, + }; + struct program_client *pc; + + restrict_access_init(&set.restrict_set); + + e_debug(root->quota->event, "Executing warning: %s (because %s)", cmd, reason); + + args = t_strsplit_spaces(cmd, " "); + if (last_arg != NULL) { + unsigned int count = str_array_length(args); + const char **new_args = t_new(const char *, count + 2); + + memcpy(new_args, args, sizeof(const char *) * count); + new_args[count] = last_arg; + args = new_args; + } + socket_path = args[0]; + + if ((ptr = strchr(socket_path, ':')) != NULL) { + scheme = t_strcut(socket_path, ':'); + socket_path = ptr+1; + } else { + scheme = "unix"; + } + + if (*socket_path != '/' && + strcmp(scheme, "unix") == 0) + socket_path = + t_strconcat(root->quota->user->set->base_dir, + "/", socket_path, NULL); + + socket_path = t_strdup_printf("%s:%s", scheme, socket_path); + + args++; + + if (program_client_create(socket_path, args, &set, TRUE, + &pc, &error) < 0) { + e_error(root->quota->event, + "program_client_create(%s) failed: %s", socket_path, + error); + return; + } + + (void)program_client_run(pc); + + program_client_destroy(&pc); +} + +static void quota_warnings_execute(struct quota_transaction_context *ctx, + struct quota_root *root) +{ + struct quota_warning_rule *warnings; + unsigned int i, count; + uint64_t bytes_current, bytes_before, bytes_limit; + uint64_t count_current, count_before, count_limit; + const char *reason, *error; + + warnings = array_get_modifiable(&root->set->warning_rules, &count); + if (count == 0) + return; + + if (quota_get_resource(root, "", QUOTA_NAME_STORAGE_BYTES, + &bytes_current, &bytes_limit, &error) == QUOTA_GET_RESULT_INTERNAL_ERROR) { + e_error(root->quota->event, + "Failed to get quota resource "QUOTA_NAME_STORAGE_BYTES + ": %s", error); + return; + } + if (quota_get_resource(root, "", QUOTA_NAME_MESSAGES, + &count_current, &count_limit, &error) == QUOTA_GET_RESULT_INTERNAL_ERROR) { + e_error(root->quota->event, + "Failed to get quota resource "QUOTA_NAME_MESSAGES + ": %s", error); + return; + } + + if (ctx->bytes_used > 0 && bytes_current < (uint64_t)ctx->bytes_used) + bytes_before = 0; + else + bytes_before = (int64_t)bytes_current - ctx->bytes_used; + + if (ctx->count_used > 0 && count_current < (uint64_t)ctx->count_used) + count_before = 0; + else + count_before = (int64_t)count_current - ctx->count_used; + for (i = 0; i < count; i++) { + if (quota_warning_match(&warnings[i], + bytes_before, bytes_current, + count_before, count_current, + &reason)) { + quota_warning_execute(root, warnings[i].command, + NULL, reason); + break; + } + } +} + +int quota_transaction_commit(struct quota_transaction_context **_ctx) +{ + struct quota_transaction_context *ctx = *_ctx; + struct quota_rule *rule; + struct quota_root *const *roots; + unsigned int i, count; + const char *mailbox_name; + int ret = 0; + + *_ctx = NULL; + + if (ctx->failed) + ret = -1; + else if (ctx->bytes_used != 0 || ctx->count_used != 0 || + ctx->recalculate != QUOTA_RECALCULATE_DONT) T_BEGIN { + ARRAY(struct quota_root *) warn_roots; + + mailbox_name = mailbox_get_vname(ctx->box); + (void)mail_namespace_find_unalias( + ctx->box->storage->user->namespaces, &mailbox_name); + + roots = array_get(&ctx->quota->roots, &count); + t_array_init(&warn_roots, count); + for (i = 0; i < count; i++) { + if (!quota_root_is_visible(roots[i], ctx->box)) + continue; + + rule = quota_root_rule_find(roots[i]->set, + mailbox_name); + if (rule != NULL && rule->ignore) { + /* mailbox not included in quota */ + continue; + } + + const char *error; + if (roots[i]->backend.v.update(roots[i], ctx, &error) < 0) { + e_error(ctx->quota->event, + "Failed to update quota for %s: %s", + mailbox_name, error); + ret = -1; + } + else if (!ctx->sync_transaction) + array_push_back(&warn_roots, &roots[i]); + } + /* execute quota warnings after all updates. this makes it + work correctly regardless of whether backend.get_resource() + returns updated values before backend.update() or not. + warnings aren't executed when dsync bring the user over, + because the user probably already got the warning on the + other replica. */ + array_foreach(&warn_roots, roots) + quota_warnings_execute(ctx, *roots); + } T_END; + + i_free(ctx); + return ret; +} + +static bool quota_over_flag_init_root(struct quota_root *root, + const char **quota_over_script_r, + const char **quota_over_flag_r, + bool *status_r) +{ + const char *name, *flag_mask; + + *quota_over_flag_r = NULL; + *status_r = FALSE; + + name = t_strconcat(root->set->set_name, "_over_script", NULL); + *quota_over_script_r = mail_user_plugin_getenv(root->quota->user, name); + if (*quota_over_script_r == NULL) { + e_debug(root->quota->event, "quota_over_flag check: " + "%s unset - skipping", name); + return FALSE; + } + + /* e.g.: quota_over_flag_value=TRUE or quota_over_flag_value=* */ + name = t_strconcat(root->set->set_name, "_over_flag_value", NULL); + flag_mask = mail_user_plugin_getenv(root->quota->user, name); + if (flag_mask == NULL) { + e_debug(root->quota->event, "quota_over_flag check: " + "%s unset - skipping", name); + return FALSE; + } + + /* compare quota_over_flag's value (that comes from userdb) to + quota_over_flag_value and save the result. */ + name = t_strconcat(root->set->set_name, "_over_flag", NULL); + *quota_over_flag_r = mail_user_plugin_getenv(root->quota->user, name); + *status_r = *quota_over_flag_r != NULL && + wildcard_match_icase(*quota_over_flag_r, flag_mask); + return TRUE; +} + +static void quota_over_flag_check_root(struct quota_root *root) +{ + const char *quota_over_script, *quota_over_flag, *error; + const char *const *resources; + unsigned int i; + uint64_t value, limit; + bool cur_overquota = FALSE; + bool quota_over_status; + enum quota_get_result ret; + + if (root->quota_over_flag_checked) + return; + if (root->quota->user->session_create_time + + QUOTA_OVER_FLAG_MAX_DELAY_SECS < ioloop_time) { + /* userdb's quota_over_flag lookup is too old. */ + e_debug(root->quota->event, "quota_over_flag check: " + "Flag lookup time is too old - skipping"); + return; + } + if (root->quota->user->session_restored) { + /* we don't know whether the quota_over_script was executed + before hibernation. just assume that it was, so we don't + unnecessarily call it too often. */ + e_debug(root->quota->event, "quota_over_flag check: " + "Session was already hibernated - skipping"); + return; + } + root->quota_over_flag_checked = TRUE; + if (!quota_over_flag_init_root(root, "a_over_script, + "a_over_flag, "a_over_status)) + return; + + resources = quota_root_get_resources(root); + for (i = 0; resources[i] != NULL; i++) { + ret = quota_get_resource(root, "", resources[i], &value, + &limit, &error); + if (ret == QUOTA_GET_RESULT_INTERNAL_ERROR) { + /* can't reliably verify this */ + e_error(root->quota->event, "Quota %s lookup failed -" + "can't verify quota_over_flag: %s", + resources[i], error); + return; + } + e_debug(root->quota->event, "quota_over_flag check: %s ret=%d" + "value=%"PRIu64" limit=%"PRIu64, resources[i], ret, + value, limit); + if (ret == QUOTA_GET_RESULT_LIMITED && value >= limit) + cur_overquota = TRUE; + } + e_debug(root->quota->event, "quota_over_flag=%d(%s) vs currently overquota=%d", + quota_over_status ? 1 : 0, + quota_over_flag == NULL ? "(null)" : quota_over_flag, + cur_overquota ? 1 : 0); + if (cur_overquota != quota_over_status) { + quota_warning_execute(root, quota_over_script, quota_over_flag, + "quota_over_flag mismatch"); + } +} + +void quota_over_flag_check_startup(struct quota *quota) +{ + struct quota_root *const *roots; + unsigned int i, count; + const char *name; + + roots = array_get("a->roots, &count); + for (i = 0; i < count; i++) { + name = t_strconcat(roots[i]->set->set_name, "_over_flag_lazy_check", NULL); + if (!mail_user_plugin_getenv_bool(roots[i]->quota->user, name)) + quota_over_flag_check_root(roots[i]); + } +} + +void quota_transaction_rollback(struct quota_transaction_context **_ctx) +{ + struct quota_transaction_context *ctx = *_ctx; + + *_ctx = NULL; + i_free(ctx); +} + +static int quota_get_mail_size(struct quota_transaction_context *ctx, + struct mail *mail, uoff_t *size_r) +{ + if (ctx->quota->set->vsizes) + return mail_get_virtual_size(mail, size_r); + else + return mail_get_physical_size(mail, size_r); +} + +enum quota_alloc_result quota_try_alloc(struct quota_transaction_context *ctx, + struct mail *mail, const char **error_r) +{ + uoff_t size; + const char *error; + enum quota_get_result error_res; + + if (quota_transaction_set_limits(ctx, &error_res, error_r) < 0) { + if (error_res == QUOTA_GET_RESULT_BACKGROUND_CALC) + return QUOTA_ALLOC_RESULT_BACKGROUND_CALC; + return QUOTA_ALLOC_RESULT_TEMPFAIL; + } + + if (ctx->no_quota_updates) + return QUOTA_ALLOC_RESULT_OK; + + if (quota_get_mail_size(ctx, mail, &size) < 0) { + enum mail_error err; + error = mailbox_get_last_internal_error(mail->box, &err); + + if (err == MAIL_ERROR_EXPUNGED) { + /* mail being copied was already expunged. it'll fail, + so just return success for the quota allocated. */ + return QUOTA_ALLOC_RESULT_OK; + } + *error_r = t_strdup_printf( + "Failed to get mail size (box=%s, uid=%u): %s", + mail->box->vname, mail->uid, error); + return QUOTA_ALLOC_RESULT_TEMPFAIL; + } + + enum quota_alloc_result ret = quota_test_alloc(ctx, size, error_r); + if (ret != QUOTA_ALLOC_RESULT_OK) + return ret; + /* with quota_try_alloc() we want to keep track of how many bytes + we've been adding/removing, so disable auto_updating=TRUE + optimization. this of course doesn't work perfectly if + quota_alloc() or quota_free_bytes() was already used within the same + transaction, but that doesn't normally happen. */ + ctx->auto_updating = FALSE; + quota_alloc(ctx, mail); + return QUOTA_ALLOC_RESULT_OK; +} + +enum quota_alloc_result quota_test_alloc(struct quota_transaction_context *ctx, + uoff_t size, const char **error_r) +{ + if (ctx->failed) { + *error_r = "Quota transaction has failed earlier"; + return QUOTA_ALLOC_RESULT_TEMPFAIL; + } + + enum quota_get_result error_res; + if (quota_transaction_set_limits(ctx, &error_res, error_r) < 0) { + if (error_res == QUOTA_GET_RESULT_BACKGROUND_CALC) + return QUOTA_ALLOC_RESULT_BACKGROUND_CALC; + return QUOTA_ALLOC_RESULT_TEMPFAIL; + } + + uoff_t max_size = ctx->quota->set->max_mail_size; + if (max_size > 0 && size > max_size) { + *error_r = t_strdup_printf( + "Requested allocation size %"PRIuUOFF_T" exceeds max " + "mail size %"PRIuUOFF_T, size, max_size); + return QUOTA_ALLOC_RESULT_OVER_MAXSIZE; + } + + if (ctx->no_quota_updates) + return QUOTA_ALLOC_RESULT_OK; + /* this is a virtual function mainly for trash plugin and similar, + which may automatically delete mails to stay under quota. */ + return ctx->quota->set->test_alloc(ctx, size, error_r); +} + +static enum quota_alloc_result quota_default_test_alloc( + struct quota_transaction_context *ctx, uoff_t size, + const char **error_r) +{ + struct quota_root *const *roots; + unsigned int i, count; + bool ignore; + int ret; + + if (!quota_transaction_is_over(ctx, size)) + return QUOTA_ALLOC_RESULT_OK; + + /* limit reached. */ + roots = array_get(&ctx->quota->roots, &count); + for (i = 0; i < count; i++) { + uint64_t bytes_limit, count_limit; + + if (!quota_root_is_visible(roots[i], ctx->box) || + roots[i]->no_enforcing) + continue; + + const char *error; + ret = quota_root_get_rule_limits(roots[i], + mailbox_get_vname(ctx->box), + &bytes_limit, &count_limit, + &ignore, &error); + if (ret < 0) { + *error_r = t_strdup_printf( + "Failed to get quota root rule limits: %s", + error); + return QUOTA_ALLOC_RESULT_TEMPFAIL; + } + + /* if size is bigger than any limit, then + it is bigger than the lowest limit */ + if (bytes_limit > 0 && size > bytes_limit) { + *error_r = t_strdup_printf( + "Allocating %"PRIuUOFF_T" bytes would exceed quota limit", + size); + return QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT; + } + } + *error_r = t_strdup_printf( + "Allocating %"PRIuUOFF_T" bytes would exceed quota", size); + return QUOTA_ALLOC_RESULT_OVER_QUOTA; +} + +void quota_alloc(struct quota_transaction_context *ctx, struct mail *mail) +{ + uoff_t size; + + if (!ctx->auto_updating) { + if (quota_get_mail_size(ctx, mail, &size) == 0) + ctx->bytes_used += size; + } + + ctx->bytes_ceil = ctx->bytes_ceil2; + ctx->count_used++; +} + +void quota_free_bytes(struct quota_transaction_context *ctx, + uoff_t physical_size) +{ + i_assert(physical_size <= INT64_MAX); + ctx->bytes_used -= (int64_t)physical_size; + ctx->count_used--; +} + +void quota_recalculate(struct quota_transaction_context *ctx, + enum quota_recalculate recalculate) +{ + ctx->recalculate = recalculate; +} + +static void hidden_param_handler(struct quota_root *_root, const char *param_value ATTR_UNUSED) +{ + _root->hidden = TRUE; +} + +static void ignoreunlim_param_handler(struct quota_root *_root, const char *param_value ATTR_UNUSED) +{ + _root->disable_unlimited_tracking = TRUE; +} + +static void noenforcing_param_handler(struct quota_root *_root, const char *param_value ATTR_UNUSED) +{ + _root->no_enforcing = TRUE; +} + +static void ns_param_handler(struct quota_root *_root, const char *param_value) +{ + _root->ns_prefix = p_strdup(_root->pool, param_value); +} + +int quota_parse_parameters(struct quota_root *root, const char **args, const char **error_r, + const struct quota_param_parser *valid_params, bool fail_on_unknown) +{ + const char *tmp_param_name, *tmp_param_val; + size_t tmp_param_len; + + while (*args != NULL && (*args)[0] != '\0') { + for (; valid_params->param_name != NULL; ++valid_params) { + tmp_param_name = valid_params->param_name; + tmp_param_len = strlen(valid_params->param_name); + i_assert(*args != NULL); + if (strncmp(*args, tmp_param_name, tmp_param_len) == 0) { + tmp_param_val = NULL; + *args += tmp_param_len; + if (tmp_param_name[tmp_param_len - 1] == '=') { + const char *next_colon = strchr(*args, ':'); + tmp_param_val = (next_colon == NULL)? + t_strdup(*args): + t_strdup_until(*args, next_colon); + *args = (next_colon == NULL) ? NULL : next_colon + 1; + } + else if ((*args)[0] == '\0' || + (*args)[0] == ':') { + *args = ((*args)[0] == ':') ? *args + 1 : NULL; + /* in case parameter is a boolean second parameter + * string parameter value will be ignored by param_handler + * we just need some non-NULL value + * to indicate that argument is to be processed */ + tmp_param_val = ""; + } + if (tmp_param_val != NULL) { + valid_params->param_handler(root, tmp_param_val); + break; + } + } + } + if (valid_params->param_name == NULL) { + if (fail_on_unknown) { + *error_r = t_strdup_printf( + "Unknown parameter for backend %s: %s", + root->backend.name, *args); + return -1; + } + else { + break; + } + } + } + return 0; +} diff --git a/src/plugins/quota/quota.h b/src/plugins/quota/quota.h new file mode 100644 index 0000000..8de2d8e --- /dev/null +++ b/src/plugins/quota/quota.h @@ -0,0 +1,147 @@ +#ifndef QUOTA_H +#define QUOTA_H + +struct mail; +struct mailbox; +struct mail_user; + +/* Message storage size kilobytes. */ +#define QUOTA_NAME_STORAGE_KILOBYTES "STORAGE" +/* Message storage size bytes. This is used only internally. */ +#define QUOTA_NAME_STORAGE_BYTES "STORAGE_BYTES" +/* Number of messages. */ +#define QUOTA_NAME_MESSAGES "MESSAGE" + +struct quota; +struct quota_settings; +struct quota_root_settings; +struct quota_root; +struct quota_root_iter; +struct quota_transaction_context; + +struct quota_param_parser { + char *param_name; + void (* param_handler)(struct quota_root *_root, const char *param_value); +}; + +extern struct quota_param_parser quota_param_hidden; +extern struct quota_param_parser quota_param_ignoreunlimited; +extern struct quota_param_parser quota_param_noenforcing; +extern struct quota_param_parser quota_param_ns; + +enum quota_recalculate { + QUOTA_RECALCULATE_DONT = 0, + /* We may want to recalculate quota because we weren't able to call + quota_free*() correctly for all mails. Quota needs to be + recalculated unless the backend does the quota tracking + internally. */ + QUOTA_RECALCULATE_MISSING_FREES, + /* doveadm quota recalc called - make sure the quota is correct */ + QUOTA_RECALCULATE_FORCED +}; + +enum quota_alloc_result { + QUOTA_ALLOC_RESULT_OK, + QUOTA_ALLOC_RESULT_TEMPFAIL, + QUOTA_ALLOC_RESULT_OVER_MAXSIZE, + QUOTA_ALLOC_RESULT_OVER_QUOTA, + /* Mail size is larger than even the maximum allowed quota. */ + QUOTA_ALLOC_RESULT_OVER_QUOTA_LIMIT, + /* Blocked by ongoing background quota calculation. */ + QUOTA_ALLOC_RESULT_BACKGROUND_CALC, +}; + +/* Anything <= QUOTA_GET_RESULT_INTERNAL_ERROR is an error. */ +enum quota_get_result { + /* Ongoing background quota calculation */ + QUOTA_GET_RESULT_BACKGROUND_CALC, + /* Quota resource name doesn't exist */ + QUOTA_GET_RESULT_UNKNOWN_RESOURCE, + /* Internal error */ + QUOTA_GET_RESULT_INTERNAL_ERROR, + + /* Quota limit exists and was returned successfully */ + QUOTA_GET_RESULT_LIMITED, + /* Quota is unlimited, but its value was returned */ + QUOTA_GET_RESULT_UNLIMITED, +}; + +const char *quota_alloc_result_errstr(enum quota_alloc_result res, + struct quota_transaction_context *qt); + +int quota_user_read_settings(struct mail_user *user, + struct quota_settings **set_r, + const char **error_r); +void quota_settings_deinit(struct quota_settings **quota_set); + +/* Add a new rule too the quota root. Returns 0 if ok, -1 if rule is invalid. */ +int quota_root_add_rule(struct quota_root_settings *root_set, + const char *rule_def, const char **error_r); +/* Add a new warning rule for the quota root. Returns 0 if ok, -1 if rule is + invalid. */ +int quota_root_add_warning_rule(struct quota_root_settings *root_set, + const char *rule_def, const char **error_r); + +/* Initialize quota for the given user. Returns 0 and quota_r on success, + -1 and error_r on failure. */ +int quota_init(struct quota_settings *quota_set, struct mail_user *user, + struct quota **quota_r, const char **error_r); +void quota_deinit(struct quota **quota); + +/* List all visible quota roots. They don't need to be freed. */ +struct quota_root_iter *quota_root_iter_init_user(struct mail_user *user); +struct quota_root_iter *quota_root_iter_init(struct mailbox *box); +struct quota_root *quota_root_iter_next(struct quota_root_iter *iter); +void quota_root_iter_deinit(struct quota_root_iter **iter); + +/* Return quota root or NULL. */ +struct quota_root *quota_root_lookup(struct mail_user *user, const char *name); + +/* Returns name of the quota root. */ +const char *quota_root_get_name(struct quota_root *root); +/* Return a list of all resources set for the quota root. */ +const char *const *quota_root_get_resources(struct quota_root *root); +/* Returns TRUE if quota root is marked as hidden (so it shouldn't be visible + to users via IMAP GETQUOTAROOT command). */ +bool quota_root_is_hidden(struct quota_root *root); + +/* Returns 1 if values were successfully returned, 0 if resource name doesn't + exist or isn't enabled, -1 if error. */ +enum quota_get_result +quota_get_resource(struct quota_root *root, const char *mailbox_name, + const char *name, uint64_t *value_r, uint64_t *limit_r, + const char **error_r); +/* Returns 0 if OK, -1 if error (eg. permission denied, invalid name). */ +int quota_set_resource(struct quota_root *root, const char *name, + uint64_t value, const char **client_error_r); + +/* Start a new quota transaction. */ +struct quota_transaction_context *quota_transaction_begin(struct mailbox *box); +/* Commit quota transaction. Returns 0 if ok, -1 if failed. */ +int quota_transaction_commit(struct quota_transaction_context **ctx); +/* Rollback quota transaction changes. */ +void quota_transaction_rollback(struct quota_transaction_context **ctx); + +/* Allocate from quota if there's space. error_r is set when result is not + * QUOTA_ALLOC_RESULT_OK. */ +enum quota_alloc_result quota_try_alloc(struct quota_transaction_context *ctx, + struct mail *mail, const char **error_r); +/* Like quota_try_alloc(), but don't actually allocate anything. */ +enum quota_alloc_result quota_test_alloc(struct quota_transaction_context *ctx, + uoff_t size, const char **error_r); +/* Update quota by allocating/freeing space used by mail. */ +void quota_alloc(struct quota_transaction_context *ctx, struct mail *mail); +void quota_free_bytes(struct quota_transaction_context *ctx, + uoff_t physical_size); +/* Mark the quota to be recalculated */ +void quota_recalculate(struct quota_transaction_context *ctx, + enum quota_recalculate recalculate); + +/* Execute quota_over_scripts if needed. */ +void quota_over_flag_check_startup(struct quota *quota); + +/* Common quota parameters parsing loop */ +int quota_parse_parameters(struct quota_root *root, const char **args, const char **error_r, + const struct quota_param_parser *valid_params, bool fail_on_unknown); + +#endif diff --git a/src/plugins/quota/rquota.x b/src/plugins/quota/rquota.x new file mode 100644 index 0000000..3cd5c10 --- /dev/null +++ b/src/plugins/quota/rquota.x @@ -0,0 +1,139 @@ +/* @(#)rquota.x 2.1 88/08/01 4.0 RPCSRC */ +/* @(#)rquota.x 1.2 87/09/20 Copyr 1987 Sun Micro */ + +/* + * Remote quota protocol + * Requires unix authentication + */ + +const RQ_PATHLEN = 1024; + +struct sq_dqblk { + unsigned int rq_bhardlimit; /* absolute limit on disk blks alloc */ + unsigned int rq_bsoftlimit; /* preferred limit on disk blks */ + unsigned int rq_curblocks; /* current block count */ + unsigned int rq_fhardlimit; /* absolute limit on allocated files */ + unsigned int rq_fsoftlimit; /* preferred file limit */ + unsigned int rq_curfiles; /* current # allocated files */ + unsigned int rq_btimeleft; /* time left for excessive disk use */ + unsigned int rq_ftimeleft; /* time left for excessive files */ +}; + +struct getquota_args { + string gqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */ + int gqa_uid; /* Inquire about quota for uid */ +}; + +struct setquota_args { + int sqa_qcmd; + string sqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */ + int sqa_id; /* Set quota for uid */ + sq_dqblk sqa_dqblk; +}; + +struct ext_getquota_args { + string gqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */ + int gqa_type; /* Type of quota info is needed about */ + int gqa_id; /* Inquire about quota for id */ +}; + +struct ext_setquota_args { + int sqa_qcmd; + string sqa_pathp<RQ_PATHLEN>; /* path to filesystem of interest */ + int sqa_id; /* Set quota for id */ + int sqa_type; /* Type of quota to set */ + sq_dqblk sqa_dqblk; +}; + +/* + * remote quota structure + */ +struct rquota { + int rq_bsize; /* block size for block counts */ + bool rq_active; /* indicates whether quota is active */ + unsigned int rq_bhardlimit; /* absolute limit on disk blks alloc */ + unsigned int rq_bsoftlimit; /* preferred limit on disk blks */ + unsigned int rq_curblocks; /* current block count */ + unsigned int rq_fhardlimit; /* absolute limit on allocated files */ + unsigned int rq_fsoftlimit; /* preferred file limit */ + unsigned int rq_curfiles; /* current # allocated files */ + unsigned int rq_btimeleft; /* time left for excessive disk use */ + unsigned int rq_ftimeleft; /* time left for excessive files */ +}; + +enum qr_status { + Q_OK = 1, /* quota returned */ + Q_NOQUOTA = 2, /* noquota for uid */ + Q_EPERM = 3 /* no permission to access quota */ +}; + +union getquota_rslt switch (qr_status status) { +case Q_OK: + rquota gqr_rquota; /* valid if status == Q_OK */ +case Q_NOQUOTA: + void; +case Q_EPERM: + void; +}; + +union setquota_rslt switch (qr_status status) { +case Q_OK: + rquota sqr_rquota; /* valid if status == Q_OK */ +case Q_NOQUOTA: + void; +case Q_EPERM: + void; +}; + +program RQUOTAPROG { + version RQUOTAVERS { + /* + * Get all quotas + */ + getquota_rslt + RQUOTAPROC_GETQUOTA(getquota_args) = 1; + + /* + * Get active quotas only + */ + getquota_rslt + RQUOTAPROC_GETACTIVEQUOTA(getquota_args) = 2; + + /* + * Set all quotas + */ + setquota_rslt + RQUOTAPROC_SETQUOTA(setquota_args) = 3; + + /* + * Get active quotas only + */ + setquota_rslt + RQUOTAPROC_SETACTIVEQUOTA(setquota_args) = 4; + } = 1; + version EXT_RQUOTAVERS { + /* + * Get all quotas + */ + getquota_rslt + RQUOTAPROC_GETQUOTA(ext_getquota_args) = 1; + + /* + * Get active quotas only + */ + getquota_rslt + RQUOTAPROC_GETACTIVEQUOTA(ext_getquota_args) = 2; + + /* + * Set all quotas + */ + setquota_rslt + RQUOTAPROC_SETQUOTA(ext_setquota_args) = 3; + + /* + * Set active quotas only + */ + setquota_rslt + RQUOTAPROC_SETACTIVEQUOTA(ext_setquota_args) = 4; + } = 2; +} = 100011; diff --git a/src/plugins/quota/test-quota-util.c b/src/plugins/quota/test-quota-util.c new file mode 100644 index 0000000..682cffe --- /dev/null +++ b/src/plugins/quota/test-quota-util.c @@ -0,0 +1,96 @@ +/* Copyright (c) 2014-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "quota-private.h" +#include "test-common.h" + +struct test { + uint64_t limit, initial_size; + int64_t transaction_diff; + uint64_t new_size; + bool is_over; +}; + +static void test_quota_transaction_is_over(void) +{ +#define MAXU64 (uint64_t)-1 +#define MAXS64 9223372036854775807LL +#define MINS64 (-MAXS64 - 1LL) + static const struct test tests[] = { + /* first test only with new_size=1. these are used for both + count and bytes tests: */ + + /* limit, init, diff, new */ + { 1, 0, 0, 1, FALSE }, + { MAXU64, MAXU64, 0, 1, TRUE }, + { MAXU64, MAXU64-1, 0, 1, FALSE }, + { MAXU64, MAXU64-1, 1, 1, TRUE }, + { MAXU64-1, MAXU64-1, 0, 1, TRUE }, + { MAXU64-1, MAXU64-1, -1, 1, FALSE }, + { MAXU64-2, MAXU64-1, -1, 1, TRUE }, + { MAXU64-2, MAXU64-1, -2, 1, FALSE }, + + /* these are for bytes tests: */ + + /* limit, init, diff, new */ + { MAXU64, MAXU64, 0, 0, FALSE }, + { MAXU64, MAXU64-1, 1, 0, FALSE }, + { MAXU64-1, MAXU64, 1, 0, TRUE }, + { MAXU64-1, MAXU64, 0, 0, TRUE }, + { MAXU64-1, MAXU64, -1, 0, FALSE }, + { MAXU64, MAXU64, 0, 1, TRUE }, + { MAXU64, 0, 0, MAXU64, FALSE }, + { MAXU64, 1, 0, MAXU64, TRUE }, + { MAXU64, 0, 1, MAXU64, TRUE }, + { MAXU64-1, 0, 0, MAXU64, TRUE }, + { MAXU64-1, 0, 0, MAXU64-1, FALSE }, + { MAXU64-1, 1, 0, MAXU64-1, TRUE }, + { MAXU64-1, 1, -1, MAXU64-1, FALSE }, + { MAXU64, MAXU64, 0, MAXU64, TRUE }, + }; + struct quota_transaction_context ctx; + unsigned int i; + + test_begin("quota transaction is over (count)"); + for (i = 0; i < N_ELEMENTS(tests); i++) { + if (tests[i].new_size != 1) + continue; + + i_zero(&ctx); + ctx.count_used = tests[i].transaction_diff; + if (tests[i].initial_size > tests[i].limit) + ctx.count_over = tests[i].initial_size - tests[i].limit; + else { + ctx.count_ceil = tests[i].limit - tests[i].initial_size; + i_assert(ctx.count_used < 0 || + (uint64_t)ctx.count_used <= ctx.count_ceil); /* test is broken otherwise */ + } + test_assert_idx(quota_transaction_is_over(&ctx, 0) == tests[i].is_over, i); + } + test_end(); + + test_begin("quota transaction is over (bytes)"); + for (i = 0; i < N_ELEMENTS(tests); i++) { + i_zero(&ctx); + ctx.count_ceil = 1; + ctx.bytes_used = tests[i].transaction_diff; + if (tests[i].initial_size > tests[i].limit) + ctx.bytes_over = tests[i].initial_size - tests[i].limit; + else { + ctx.bytes_ceil = tests[i].limit - tests[i].initial_size; + i_assert(ctx.bytes_used < 0 || + (uint64_t)ctx.bytes_used <= ctx.bytes_ceil); /* test is broken otherwise */ + } + test_assert_idx(quota_transaction_is_over(&ctx, tests[i].new_size) == tests[i].is_over, i); + } + test_end(); +} + +int main(void) +{ + static void (*const test_functions[])(void) = { + test_quota_transaction_is_over, + NULL + }; + return test_run(test_functions); +} |