Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
240 KB
Referenced Files
None
Subscribers
None
diff --git a/Makefile.am b/Makefile.am
index 1337a7687..97ea53746 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,2062 +1,2063 @@
#
# @configure_input@
#
# Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# 3. The name "Carnegie Mellon University" must not be used to
# endorse or promote products derived from this software without
# prior written permission. For permission or any legal
# details, please contact
# Carnegie Mellon University
# Center for Technology Transfer and Enterprise Creation
# 4615 Forbes Avenue
# Suite 302
# Pittsburgh, PA 15213
# (412) 268-7393, fax: (412) 268-7395
# innovation@andrew.cmu.edu
#
# 4. Redistributions of any form whatsoever must retain the following
# acknowledgment:
# "This product includes software developed by Computing Services
# at Carnegie Mellon University (http://www.cmu.edu/computing/)."
#
# CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
# FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
# AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
# OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
ACLOCAL_AMFLAGS = -I cmulocal
AM_CFLAGS = @PERL_CCCDLFLAGS@ $(GCOV_CFLAGS)
AM_CXXFLAGS = $(GCOV_CXXFLAGS)
AM_CPPFLAGS = \
$(COM_ERR_CPPFLAGS) \
-I${top_builddir} \
-I${top_builddir}/lib \
-I${top_srcdir} \
-I${top_srcdir}/lib \
-DLIBEXEC_DIR=\"$(libexecdir)\" \
-DSBIN_DIR=\"$(sbindir)\" \
-DSYSCONF_DIR=\"$(sysconfdir)\" \
${DEFS} \
${LOCALDEFS} \
$(SASLFLAGS) \
$(SSL_CPPFLAGS) \
$(ICU_CFLAGS)
if HAVE_LDAP
AM_CPPFLAGS += $(LDAP_CPPFLAGS)
endif # HAVE_LDAP
AM_LDFLAGS = $(COV_LDFLAGS) $(ICU_LIBS)
# have distcheck try to build with all optional components enabled, to aid
# detection of missing files for these components
AM_DISTCHECK_CONFIGURE_FLAGS = \
--enable-http \
--enable-calalarmd \
--enable-replication \
--with-openssl=yes \
--enable-nntp \
--enable-murder \
--enable-idled \
--enable-sieve \
--enable-autocreate \
--enable-backup \
--enable-xapian \
--enable-jmap \
--with-ldap
BUILT_SOURCES = \
lib/imapopts.c \
lib/imapopts.h
CLEANFILES = \
lib/chartable.c \
lib/imapopts.c \
lib/imapopts.h
DISTCLEANFILES = \
com_err/et/compile_et \
imap/http_caldav_js.h \
imap/http_carddav_js.h \
imap/http_err.c \
imap/http_err.h \
imap/imap_err.c \
imap/imap_err.h \
imap/jmap_err.c \
imap/jmap_err.h \
imap/lmtp_err.c \
imap/lmtp_err.h \
imap/lmtpstats.c \
imap/lmtpstats.h \
imap/mupdate_err.c \
imap/mupdate_err.h \
imap/nntp_err.c \
imap/nntp_err.h \
imap/promdata.c \
imap/promdata.h \
imap/pushstats.c \
imap/pushstats.h \
imap/tz_err.c \
imap/tz_err.h \
perl/sieve/scripts/installsieve \
perl/sieve/scripts/sieveshell \
sieve/sieve_err.c \
sieve/sieve_err.h
MAINTAINERCLEANFILES = \
doc/legacy/murder.png \
doc/legacy/netnews.png \
man/imapd.conf.5 \
man/sieveshell.1 \
sieve/addr.h \
sieve/sieve.h
SUBDIRS = .
DIST_SUBDIRS = . perl/annotator perl/imap perl/sieve/managesieve
dist_sysconf_DATA =
lib_LTLIBRARIES = lib/libcyrus_min.la lib/libcyrus.la
check_PROGRAMS =
libexec_PROGRAMS =
sbin_PROGRAMS =
noinst_HEADERS =
noinst_LTLIBRARIES =
noinst_PROGRAMS =
if COM_ERR
COMPILE_ET_DEP = com_err/et/compile_et
BUILT_SOURCES += com_err/et/compile_et com_err/et/libcyrus_com_err.la
lib_LTLIBRARIES += com_err/et/libcyrus_com_err.la
endif # COM_ERR
bin_PROGRAMS = imtest/imtest
if SERVER
BUILT_SOURCES += \
imap/http_err.c \
imap/http_err.h \
imap/imap_err.c \
imap/imap_err.h \
imap/lmtp_err.c \
imap/lmtp_err.h \
imap/lmtpstats.c \
imap/lmtpstats.h \
imap/mupdate_err.c \
imap/mupdate_err.h \
imap/pushstats.c \
imap/pushstats.h \
imap/promdata.c \
imap/promdata.h \
lib/htmlchar.c \
lib/htmlchar.h \
imap/rfc822_header.c \
imap/rfc822_header.h \
imap/mailbox_header_cache.h
lib_LTLIBRARIES += imap/libcyrus_imap.la
libexec_PROGRAMS += \
master/master \
imap/imapd \
imap/lmtpd \
imap/pop3d \
imap/promstatsd \
imap/smmapd
sbin_PROGRAMS += \
imap/arbitron \
imap/chk_cyrus \
imap/ctl_conversationsdb \
imap/ctl_cyrusdb \
imap/ctl_deliver \
imap/ctl_mboxlist \
imap/cvt_cyrusdb \
imap/cyr_df \
imap/cyrdump \
imap/cyr_dbtool \
imap/cyr_deny \
imap/cyr_expire \
imap/cyr_info \
imap/cyr_buildinfo \
imap/cyr_sequence \
imap/cyr_synclog \
imap/cyr_userseen \
imap/cyr_virusscan \
imap/deliver \
imap/ipurge \
imap/mbexamine \
imap/mbpath \
imap/mbtool \
imap/quota \
imap/reconstruct \
imap/cvt_xlist_specialuse
noinst_PROGRAMS += \
imap/message_test \
imap/search_test
if USE_SQUAT
noinst_PROGRAMS += imap/squat_dump
endif
if SQUATTER
# Despite the name, the squatter program handles any search engine
sbin_PROGRAMS += imap/squatter
endif
if NNTPD
libexec_PROGRAMS += imap/nntpd
sbin_PROGRAMS += imap/fetchnews
BUILT_SOURCES += imap/nntp_err.c
endif # NNTPD
libexec_PROGRAMS += imap/fud
if IDLED
libexec_PROGRAMS += imap/idled
endif # IDLED
if MURDER
libexec_PROGRAMS += imap/mupdate
endif # MURDER
if CALALARMD
libexec_PROGRAMS += imap/calalarmd
endif
if HTTPD
AM_CPPFLAGS += $(HTTP_CPPFLAGS)
AM_LDFLAGS += $(HTTP_LIBS)
BUILT_SOURCES += \
imap/http_caldav_js.h \
imap/http_carddav_js.h \
imap/tz_err.c \
imap/tz_err.h
libexec_PROGRAMS += imap/httpd
check_PROGRAMS += imap/ical_apply_patch
sbin_PROGRAMS += \
imap/ctl_zoneinfo \
imap/dav_reconstruct
endif # HTTPD
if REPLICATION
libexec_PROGRAMS += imap/sync_server
sbin_PROGRAMS += imap/sync_client imap/sync_reset
endif # REPLICATION
if BACKUP
noinst_LTLIBRARIES += backup/libcyrus_backup.la
noinst_HEADERS += backup/backup.h
libexec_PROGRAMS += backup/backupd
sbin_PROGRAMS += backup/ctl_backups backup/cyr_backup backup/restore
endif # BACKUP
if HAVE_SSL
sbin_PROGRAMS += imap/tls_prune
endif # HAVE_SSL
sbin_PROGRAMS += imap/unexpunge
if SIEVE
check_PROGRAMS += notifyd/notifytest
libexec_PROGRAMS += notifyd/notifyd
endif # SIEVE
endif # SERVER
if CMULOCAL
dist_sysconf_DATA += depot/rc.local.imap depot/rc.local.ptclient
sbin_PROGRAMS += netnews/remotepurge
endif # CMULOCAL
if PTCLIENT
sbin_PROGRAMS += ptclient/ptdump ptclient/ptexpire
libexec_PROGRAMS += \
ptclient/ptloader
endif # PTCLIENT
if PERL
SUBDIRS += perl/annotator perl/imap
noinst_LTLIBRARIES += perl/libcyrus.la perl/libcyrus_min.la
endif # PERL
if SIEVE
if PERL
SUBDIRS += perl/sieve/managesieve
noinst_LTLIBRARIES += perl/sieve/lib/libisieve.la
endif # PERL
BUILT_SOURCES += sieve/addr.c sieve/sieve.c sieve/sieve_err.c
noinst_LTLIBRARIES += sieve/libcyrus_sieve_lex.la
lib_LTLIBRARIES += sieve/libcyrus_sieve.la
check_PROGRAMS += sieve/test sieve/test_mailbox
sbin_PROGRAMS += sieve/sievec sieve/sieved
if SERVER
libexec_PROGRAMS += timsieved/timsieved
endif # SERVER
endif # SIEVE
EXTRA_DIST = \
COPYING \
README.md \
VERSION \
com_err/et/et_c.awk \
com_err/et/et_h.awk \
com_err/et/test1.et \
com_err/et/test2.et \
com_err/et/test_et.c \
contrib/sieve-spamassassin \
contrib/fud-client.c \
contrib/deliver-notify-zephyr.patch \
contrib/add-cyrus-user \
contrib/README \
contrib/cyrus-graphtools.1.0 \
contrib/cyrus-graphtools.1.0/cgi-bin/cyrus_master.pl \
contrib/cyrus-graphtools.1.0/cgi-bin/graph_cyrus_db.pl \
contrib/cyrus-graphtools.1.0/cgi-bin/graph_cyrus_db-sum.pl \
contrib/cyrus-graphtools.1.0/html \
contrib/cyrus-graphtools.1.0/html/index.html \
contrib/cyrus-graphtools.1.0/README \
contrib/cyrus-graphtools.1.0/script \
contrib/cyrus-graphtools.1.0/script/cyrus.pl \
contrib/cyrus-graphtools.1.0/script/run \
contrib/cyrus-graphtools.1.0/script/cyrusrc \
contrib/cyrusv2.mc \
contrib/dkim_canon_ischedule.patch \
contrib/notify_unix/notify \
contrib/notify_unix/net-server-prefork-0.01.tgz \
contrib/notify_unix/README \
contrib/notify_unix/sql_notify.pl \
contrib/notify_unix/simple_notify.pl \
contrib/squatrunner.pl \
contrib/mupdate-test.pl \
contrib/squatrunner.txt \
cunit/cacert.pem \
cunit/cert.pem \
cunit/cunit.pl \
cunit/cunit-to-junit.pl \
cunit/key.pem \
cunit/vg.supp \
doc/legacy/murder.png \
doc/legacy/netnews.png \
doc \
docsrc \
imap/http_caldav.js \
imap/http_carddav.js \
imap/http_err.et \
imap/imap_err.et \
imap/jmap_err.et \
imap/lmtp_err.et \
imap/mailbox_header_cache.gperf \
imap/mupdate_err.et \
imap/nntp_err.et \
imap/promdata.p \
imap/rfc822_header.st \
imap/tz_err.et \
lib/charset/aliases.txt \
lib/charset/UnicodeData.txt \
lib/charset/unifix.txt \
lib/charset/us-ascii.t \
lib/htmlchar.st \
lib/imapoptions \
lib/test/cyrusdb.c \
lib/test/cyrusdb.INPUT \
lib/test/cyrusdblong.INPUT \
lib/test/cyrusdblong.OUTPUT \
lib/test/cyrusdb.OUTPUT \
lib/test/cyrusdbtxn.INPUT \
lib/test/cyrusdbtxn.OUTPUT \
lib/test/pool.c \
lib/test/rnddb.c \
master/CYRUS-MASTER.mib \
master/README \
netnews/inn.diffs \
perl/annotator/Daemon.pm \
perl/annotator/Makefile.PL \
perl/annotator/Makefile.PL.in \
perl/annotator/MANIFEST \
perl/annotator/MANIFEST.in \
perl/annotator/Message.pm \
perl/annotator/README \
perl/imap/Changes \
perl/imap/cyradm.sh \
perl/imap/cyrperl.h \
perl/imap/examples/auditmbox.pl \
perl/imap/examples/imapcollate.pl \
perl/imap/examples/imapdu.pl \
perl/imap/examples/test-imsp.pl \
perl/imap/IMAP/Admin.pm \
perl/imap/IMAP/IMSP.pm \
perl/imap/IMAP/Shell.pm \
perl/imap/IMAP.pm \
perl/imap/IMAP.xs \
perl/imap/Makefile.PL \
perl/imap/Makefile.PL.in \
perl/imap/MANIFEST \
perl/imap/MANIFEST.in \
perl/imap/README \
perl/imap/t/01-imclient.t \
perl/imap/t/02-admin.t \
perl/imap/typemap \
perl/imap/xsutil.c \
perl/sieve/managesieve/Makefile.PL \
perl/sieve/managesieve/Makefile.PL.in \
perl/sieve/managesieve/managesieve.h \
perl/sieve/managesieve/managesieve.pm \
perl/sieve/managesieve/managesieve.xs \
perl/sieve/managesieve/MANIFEST \
perl/sieve/managesieve/MANIFEST.in \
perl/sieve/managesieve/typemap \
ptclient/README \
ptclient/test.c \
ptclient/test2.c \
sieve/addr.h \
sieve/sieve.h \
sieve/sieve_err.et \
sieve/tests/testExtension \
sieve/tests/testExtension/uberExtensionTestScript.key \
sieve/tests/testExtension/testm \
sieve/tests/testExtension/testm/uetest-envelope \
sieve/tests/testExtension/testm/uetest-asub \
sieve/tests/testExtension/testm/uetest-areg \
sieve/tests/testExtension/testm/uetest-count \
sieve/tests/testExtension/testm/uetest-value \
sieve/tests/testExtension/testm/uetest-hreg \
sieve/tests/testExtension/serverm \
sieve/tests/testExtension/serverm/uetmail-hreg \
sieve/tests/testExtension/serverm/uetmail-value \
sieve/tests/testExtension/serverm/uetmail-count2 \
sieve/tests/testExtension/serverm/uetmail-envelope \
sieve/tests/testExtension/serverm/uetmail-asub \
sieve/tests/testExtension/serverm/uetmail-value2 \
sieve/tests/testExtension/serverm/uetmail-areg \
sieve/tests/testExtension/serverm/uetmail-count \
sieve/tests/testExtension/uberExtensionTestScript.s \
sieve/tests/README \
sieve/tests/action \
sieve/tests/action/testm \
sieve/tests/action/testm/uatest-keep \
sieve/tests/action/testm/uatest-redirect \
sieve/tests/action/testm/uatest-discard \
sieve/tests/action/testm/uatest-stop2 \
sieve/tests/action/testm/uatest-stop \
sieve/tests/action/serverm \
sieve/tests/action/serverm/uamail-stop2 \
sieve/tests/action/serverm/uamail-redirect \
sieve/tests/action/serverm/uamail-stop \
sieve/tests/action/serverm/uamail-keep \
sieve/tests/action/serverm/uamail-discard \
sieve/tests/action/uberActionScript.key \
sieve/tests/action/uberActionScript.s \
sieve/tests/test \
sieve/tests/test/uberTestScript.key \
sieve/tests/test/testm \
sieve/tests/test/testm/utest-header \
sieve/tests/test/testm/utest-address \
sieve/tests/test/serverm \
sieve/tests/test/serverm/utmail-address \
sieve/tests/test/serverm/utmail-header \
sieve/tests/test/uberTestScript.s \
sieve/tests/actionExtensions \
sieve/tests/actionExtensions/uberExtensionActionScript.s \
sieve/tests/actionExtensions/testm \
sieve/tests/actionExtensions/testm/ueatest-flag4 \
sieve/tests/actionExtensions/testm/ueatest-flag2 \
sieve/tests/actionExtensions/testm/ueatest-fileinto \
sieve/tests/actionExtensions/testm/ueatest-denotify \
sieve/tests/actionExtensions/testm/ueatest-vacation \
sieve/tests/actionExtensions/testm/ueatest-reject \
sieve/tests/actionExtensions/testm/ueatest-mark \
sieve/tests/actionExtensions/testm/ueatest-denotify2 \
sieve/tests/actionExtensions/testm/ueatest-flag5 \
sieve/tests/actionExtensions/testm/ueatest-notify2 \
sieve/tests/actionExtensions/testm/ueatest-notify \
sieve/tests/actionExtensions/testm/ueatest-flag1 \
sieve/tests/actionExtensions/testm/ueatest-flag3 \
sieve/tests/actionExtensions/testm/ueatest-unmark \
sieve/tests/actionExtensions/uberExtensionActionScript.key \
sieve/tests/actionExtensions/serverm \
sieve/tests/actionExtensions/serverm/ueamail-flag4 \
sieve/tests/actionExtensions/serverm/ueamail-denotify \
sieve/tests/actionExtensions/serverm/ueamail-mark \
sieve/tests/actionExtensions/serverm/ueamail-denotify2 \
sieve/tests/actionExtensions/serverm/ueamail-flag2 \
sieve/tests/actionExtensions/serverm/ueamail-unmark \
sieve/tests/actionExtensions/serverm/ueamail-reject \
sieve/tests/actionExtensions/serverm/ueamail-flag3 \
sieve/tests/actionExtensions/serverm/ueamail-fileinto \
sieve/tests/actionExtensions/serverm/ueamail-flag1 \
sieve/tests/actionExtensions/serverm/ueamail-notify \
sieve/tests/actionExtensions/serverm/ueamail-flag5 \
sieve/tests/actionExtensions/serverm/ueamail-notify2 \
sieve/tests/actionExtensions/serverm/ueamail-vacation \
timsieved/TODO
TEXINFO_TEX = com_err/et/texinfo.tex
dist_noinst_SCRIPTS = \
com_err/et/compile_et.sh \
com_err/et/config_script \
imap/lmtpstats.snmp \
imap/pushstats.snmp \
imap/promdatagen \
lib/mkchartable.pl \
lib/test/run \
perl/sieve/scripts/installsieve.pl \
perl/sieve/scripts/sieveshell.pl \
tools/arbitronsort.pl \
tools/compile_st.pl \
tools/config2header \
tools/config2rst \
tools/config2sample \
tools/fixsearchpath.pl \
tools/git-version.sh \
tools/jenkins-build.sh \
tools/masssievec \
tools/mkimap \
tools/mknewsgroups \
tools/perl2rst \
tools/rehash \
tools/translatesieve \
snmp/snmpgen
noinst_MAN = \
com_err/et/com_err.3 \
com_err/et/compile_et.1
noinst_TEXINFOS = com_err/et/com_err.texinfo
pkgconfigdir = $(libdir)/pkgconfig
pkgconfig_DATA = libcyrus_min.pc libcyrus.pc libcyrus_sieve.pc libcyrus_imap.pc
com_err_et_libcyrus_com_err_la_SOURCES = \
com_err/et/com_err.c \
com_err/et/com_err.h \
com_err/et/error_message.c \
com_err/et/error_table.h \
com_err/et/et_name.c \
com_err/et/init_et.c \
com_err/et/internal.h \
com_err/et/mit-sipb-copyright.h
com_err_et_libcyrus_com_err_la_CFLAGS = $(AM_CFLAGS) $(CFLAG_VISIBILITY)
com_err/et/compile_et: com_err/et/compile_et.sh com_err/et/config_script \
config.h
@${top_srcdir}/com_err/et/config_script ${top_srcdir}/com_err/et/compile_et.sh ${AWK} ${SED} > $@
@chmod 755 $@
# ---- Libraries ----
# BASIC is the libraries that every Cyrus program (except master) will
# need to link with.
#
# Note that several places in the code use -lcrypto, e.g. for SHA1 or
# MD5 algorithms, without needing SSL. Currently we have no way of
# minimally linking such code.
LD_BASIC_ADD = lib/libcyrus.la lib/libcyrus_min.la ${LIBS} \
${LIB_SASL} $(SSL_LIBS) $(GCOV_LIBS)
# UTILITY is the libraries that utility programs which use Cyrus'
# mailbox and message handling code need to link with.
LD_UTILITY_ADD = imap/libcyrus_imap.la $(LD_BASIC_ADD) $(COM_ERR_LIBS)
# SERVER is the libraries that network-facing servers need to link with
#
# Note that the code is horribly intertwingled e.g. in imap/global.c
# so that even utilities which never open a socket need to link against
# the SASL library.
LD_SERVER_ADD = $(LD_UTILITY_ADD) $(LIB_WRAP)
# SIEVE is the libraries that sieve-using components need to link with
#
# This is empty if sieve is not enabled, so it can be used unconditionally
# elsewhere.
if SIEVE
LD_SIEVE_ADD = sieve/libcyrus_sieve.la $(LD_BASIC_ADD)
else
LD_SIEVE_ADD =
endif
# ----
if USE_LIBCHARDET
AM_LDFLAGS += $(LIBCHARDET_LIBS)
AM_CPPFLAGS += $(LIBCHARDET_CFLAGS)
LD_SERVER_ADD += $(LIBCHARDET_LIBS)
endif
if USE_JANSSON
AM_LDFLAGS += $(JANSSON_LIBS)
AM_CPPFLAGS += $(JANSSON_CFLAGS)
LD_SERVER_ADD += $(JANSSON_LIBS)
endif
if CUNIT
CUNIT_PROJECT = cunit/default.cunit
BUILT_SOURCES += cunit/registers.h $(CUNIT_PROJECT)
CLEANFILES += cunit/registers.h $(CUNIT_PROJECT)
check_PROGRAMS += cunit/unit
cunit_FRAMEWORK = \
cunit/unit.c \
cunit/cyrunit.h \
cunit/syslog.c \
cunit/cunit-syslog.h \
cunit/timeout.c \
cunit/timeout.h \
cunit/timezones.c \
cunit/timezones.h \
cunit/timeofday.c \
cunit/timeofday.h
cunit_TESTS = \
cunit/aaa-db.testc \
cunit/annotate.testc \
cunit/backend.testc \
cunit/binhex.testc \
cunit/bitvector.testc \
cunit/buf.testc \
cunit/byteorder64.testc \
cunit/charset.testc \
cunit/command.testc \
cunit/conversations.testc \
cunit/cyr_qsort_r.testc \
cunit/crc32.testc \
cunit/dlist.testc \
cunit/duplicate.testc \
cunit/getxstring.testc \
cunit/glob.testc \
cunit/guid.testc \
cunit/hash.testc \
cunit/hashset.testc \
cunit/imapurl.testc \
cunit/imparse.testc \
cunit/libconfig.testc \
cunit/mboxname.testc \
cunit/md5.testc \
cunit/message.testc \
cunit/msgid.testc \
cunit/parseaddr.testc \
cunit/parse.testc \
cunit/prot.testc \
cunit/ptrarray.testc \
cunit/quota.testc \
cunit/rfc822tok.testc \
cunit/search_expr.testc \
cunit/seqset.testc
if SIEVE
cunit_TESTS += cunit/sieve.testc
endif
cunit_TESTS += \
cunit/spool.testc \
cunit/squat.testc \
cunit/strarray.testc \
cunit/strconcat.testc \
cunit/times.testc \
cunit/tok.testc \
cunit/vparse.testc
cunit_unit_SOURCES = $(cunit_FRAMEWORK) $(cunit_TESTS) \
imap/mutex_fake.c imap/spool.c
cunit_unit_LDADD = $(LD_SIEVE_ADD) $(LD_UTILITY_ADD) -lcunit
CUNIT_PL = $(top_srcdir)/cunit/cunit.pl --project $(CUNIT_PROJECT)
# don't discard the temporary source file if building with coverage
if HAVE_COVERAGE
CUNIT_RM = true
else
CUNIT_RM = $(RM)
endif
.testc.o:
$(AM_V_at)$(CUNIT_PL) --generate-wrapper $<
$(AM_V_CC)$(COMPILE) -c -o $@ $<-cunit.c
$(AM_V_at)$(CUNIT_RM) $<-cunit.c
$(CUNIT_PROJECT):
$(AM_V_at)$(RM) $@
$(AM_V_GEN)$(CUNIT_PL) --add-sources $(addprefix $(top_srcdir)/,$(cunit_TESTS))
cunit/registers.h: $(CUNIT_PROJECT)
$(AM_V_GEN)$(CUNIT_PL) --generate-register-function $@
# To run under Valgrind, do: make VG=1 check
VALGRIND = libtool --mode=execute valgrind --tool=memcheck --leak-check=full --suppressions=vg.supp
check-local:
@echo "Running unit tests"
@vg= ; \
test -z "$$VG" || vg="$(VALGRIND)" ; \
f="-v" ; \
test "x$$CUFORMAT" = xjunit && f="-x" ; \
cd cunit ; \
$$vg ./unit $$f ; \
retval=$$? ; \
if [ "x$$CUFORMAT" = xjunit ] ; then \
$(RM) -rf reports ; mkdir reports ; ./cunit-to-junit.pl ; \
fi ; \
exit $$retval
check-%: cunit/unit
@echo "Running unit tests for $*"
@vg= ; \
test -z "$$VG" || vg="$(VALGRIND)" ; \
cd cunit ; \
$$vg ./unit -v $* ; \
retval=$$? ; \
exit $$retval
endif
includedir=@includedir@/cyrus
include_HEADERS = \
lib/acl.h \
lib/arrayu64.h \
lib/assert.h \
lib/auth.h \
lib/auth_pts.h \
lib/bitvector.h \
lib/bloom.h \
lib/bsearch.h \
lib/bufarray.h \
lib/charset.h \
lib/chartable.h \
lib/command.h \
lib/crc32.h \
lib/crc32c.h \
lib/cyr_lock.h \
lib/cyr_qsort_r.h \
lib/cyrusdb.h \
lib/glob.h \
lib/gmtoff.h \
lib/hash.h \
lib/hashset.h \
lib/hashu64.h \
lib/imapurl.h \
lib/imclient.h \
lib/imparse.h \
lib/iostat.h \
lib/iptostring.h \
lib/libcyr_cfg.h \
lib/lsort.h \
lib/map.h \
lib/mappedfile.h \
lib/mkgmtime.h \
lib/mpool.h \
lib/murmurhash2.h \
lib/nonblock.h \
lib/parseaddr.h \
lib/retry.h \
lib/rfc822tok.h \
lib/signals.h \
lib/sqldb.h \
lib/strarray.h \
lib/strhash.h \
lib/stristr.h \
lib/times.h \
lib/tok.h \
lib/vparse.h \
lib/wildmat.h \
lib/xmalloc.h
nodist_include_HEADERS = \
lib/imapopts.h
nobase_include_HEADERS = sieve/sieve_interface.h
nobase_nodist_include_HEADERS = sieve/sieve_err.h
noinst_HEADERS += \
lib/byteorder64.h \
lib/gai.h \
lib/libconfig.h \
lib/md5.h \
lib/prot.h \
lib/ptrarray.h \
lib/util.h \
lib/xsha1.h \
lib/xstrlcat.h \
lib/xstrlcpy.h \
lib/xstrnchr.h
backup_libcyrus_backup_la_SOURCES = \
lib/gzuncat.c \
lib/gzuncat.h \
backup/lcb.c \
backup/lcb_append.c \
backup/lcb_backupdb.c \
backup/lcb_compact.c \
backup/lcb_indexr.c \
backup/lcb_indexw.c \
backup/lcb_internal.c \
backup/lcb_internal.h \
backup/lcb_partlist.c \
backup/lcb_read.c \
backup/lcb_sqlconsts.c \
backup/lcb_sqlconsts.h \
backup/lcb_verify.c
backup_libcyrus_backup_la_LIBADD = $(LD_BASIC_ADD)
backup_backupd_SOURCES = \
imap/mutex_fake.c \
imap/sync_support.c \
imap/sync_support.h \
master/service.c \
backup/backupd.c
backup_backupd_LDADD = backup/libcyrus_backup.la $(LD_SIEVE_ADD) $(LD_SERVER_ADD)
backup_ctl_backups_SOURCES = \
imap/mutex_fake.c \
imap/sync_support.c \
imap/sync_support.h \
backup/ctl_backups.c
backup_ctl_backups_LDADD = backup/libcyrus_backup.la $(LD_SIEVE_ADD) $(LD_UTILITY_ADD)
backup_cyr_backup_SOURCES = \
imap/json_support.h \
imap/mutex_fake.c \
backup/cyr_backup.c
backup_cyr_backup_LDADD = backup/libcyrus_backup.la $(LD_UTILITY_ADD)
backup_restore_SOURCES = \
imap/mutex_fake.c \
imap/sync_support.c \
imap/sync_support.h \
backup/restore.c
backup_restore_LDADD = backup/libcyrus_backup.la $(LD_SIEVE_ADD) $(LD_UTILITY_ADD)
imap_arbitron_SOURCES = imap/arbitron.c imap/cli_fatal.c imap/mutex_fake.c
imap_arbitron_LDADD = $(LD_UTILITY_ADD)
imap_chk_cyrus_SOURCES = imap/chk_cyrus.c imap/cli_fatal.c imap/mutex_fake.c
imap_chk_cyrus_LDADD = $(LD_UTILITY_ADD)
imap_ctl_conversationsdb_SOURCES = imap/ctl_conversationsdb.c imap/mutex_fake.c
imap_ctl_conversationsdb_LDADD = $(LD_UTILITY_ADD)
imap_ctl_cyrusdb_SOURCES = imap/cli_fatal.c imap/ctl_cyrusdb.c imap/mutex_fake.c
imap_ctl_cyrusdb_LDADD = $(LD_UTILITY_ADD)
imap_ctl_deliver_SOURCES = imap/cli_fatal.c imap/ctl_deliver.c imap/mutex_fake.c
imap_ctl_deliver_LDADD = $(LD_UTILITY_ADD)
imap_ctl_mboxlist_SOURCES = imap/cli_fatal.c imap/ctl_mboxlist.c imap/mutex_fake.c
imap_ctl_mboxlist_LDADD = $(LD_UTILITY_ADD)
imap_ctl_zoneinfo_SOURCES = imap/cli_fatal.c imap/ctl_zoneinfo.c imap/mutex_fake.c imap/zoneinfo_db.c
imap_ctl_zoneinfo_LDADD = $(LD_UTILITY_ADD)
imap_cvt_cyrusdb_SOURCES = imap/cli_fatal.c imap/cvt_cyrusdb.c imap/mutex_fake.c
imap_cvt_cyrusdb_LDADD = $(LD_UTILITY_ADD)
imap_cyrdump_SOURCES = imap/cli_fatal.c imap/cyrdump.c imap/mutex_fake.c
imap_cyrdump_LDADD = $(LD_UTILITY_ADD)
imap_cyr_dbtool_SOURCES = imap/cli_fatal.c imap/cyr_dbtool.c imap/mutex_fake.c
imap_cyr_dbtool_LDADD = $(LD_UTILITY_ADD)
imap_cyr_deny_SOURCES = imap/cli_fatal.c imap/cyr_deny.c imap/mutex_fake.c
imap_cyr_deny_LDADD = $(LD_UTILITY_ADD)
imap_cyr_df_SOURCES = imap/cli_fatal.c imap/cyr_df.c imap/mutex_fake.c
imap_cyr_df_LDADD = $(LD_UTILITY_ADD)
imap_cyr_expire_SOURCES = imap/cli_fatal.c imap/cyr_expire.c imap/mutex_fake.c
imap_cyr_expire_LDADD = $(LD_UTILITY_ADD)
imap_cyr_info_SOURCES = imap/cli_fatal.c imap/cyr_info.c imap/mutex_fake.c master/masterconf.c
imap_cyr_info_LDADD = $(LD_UTILITY_ADD)
imap_cyr_buildinfo_SOURCES = imap/cli_fatal.c imap/cyr_buildinfo.c imap/mutex_fake.c master/masterconf.c
imap_cyr_buildinfo_LDADD = $(LD_UTILITY_ADD)
imap_cyr_sequence_SOURCES = imap/cli_fatal.c imap/cyr_sequence.c imap/mutex_fake.c
imap_cyr_sequence_LDADD = $(LD_UTILITY_ADD)
imap_cyr_synclog_SOURCES = imap/cli_fatal.c imap/cyr_synclog.c imap/mutex_fake.c
imap_cyr_synclog_LDADD = $(LD_UTILITY_ADD)
imap_cyr_userseen_SOURCES = imap/cli_fatal.c imap/cyr_userseen.c imap/mutex_fake.c
imap_cyr_userseen_LDADD = $(LD_UTILITY_ADD)
imap_cyr_virusscan_SOURCES = imap/cli_fatal.c imap/cyr_virusscan.c imap/mutex_fake.c
imap_cyr_virusscan_CFLAGS = $(AM_CFLAGS) $(CLAMAV_CFLAGS) $(CFLAG_VISIBILITY)
imap_cyr_virusscan_LDADD = $(LD_UTILITY_ADD) $(CLAMAV_LIBS)
imap_deliver_SOURCES = \
imap/deliver.c \
imap/lmtpengine.c \
imap/mutex_fake.c \
imap/proxy.c \
imap/spool.c
nodist_imap_deliver_SOURCES = imap/lmtp_err.c imap/lmtpstats.c
imap_deliver_LDADD = $(LD_UTILITY_ADD)
imap_fetchnews_SOURCES = imap/cli_fatal.c imap/fetchnews.c imap/mutex_fake.c
imap_fetchnews_LDADD = $(LD_UTILITY_ADD)
imap_fud_SOURCES = imap/fud.c imap/mutex_fake.c master/service.c
imap_fud_LDADD = $(LD_SERVER_ADD)
imap_idled_SOURCES = imap/idled.c imap/mutex_fake.c
imap_idled_LDADD = $(LD_UTILITY_ADD)
imap_calalarmd_SOURCES = imap/calalarmd.c imap/mutex_fake.c
imap_calalarmd_LDADD = $(LD_SERVER_ADD)
imap_imapd_SOURCES = \
imap/imap_proxy.c \
imap/imap_proxy.h \
imap/imapd.c \
imap/imapd.h \
imap/mutex_fake.c \
imap/proxy.c \
imap/proxy.h \
imap/sync_support.c \
imap/sync_support.h \
master/service.c
nodist_imap_imapd_SOURCES = imap/pushstats.c
if AUTOCREATE
imap_imapd_SOURCES += \
imap/autocreate.h \
imap/autocreate.c
endif
imap_imapd_LDADD = $(LD_SIEVE_ADD) $(LD_SERVER_ADD)
imap_ipurge_SOURCES = imap/cli_fatal.c imap/ipurge.c imap/mutex_fake.c
imap_ipurge_LDADD = $(LD_UTILITY_ADD)
nodist_imap_libcyrus_imap_la_SOURCES = \
imap/http_err.c \
imap/http_err.h \
imap/imap_err.c \
imap/imap_err.h \
imap/mupdate_err.c \
imap/mupdate_err.h \
imap/promdata.c \
imap/promdata.h
imap_libcyrus_imap_la_LIBADD = \
$(COM_ERR_LIBS) \
$(LIB_UUID) \
$(GCOV_LIBS) \
lib/libcyrus_min.la \
lib/libcyrus.la
imap_libcyrus_imap_la_CFLAGS = $(AM_CFLAGS) $(CFLAG_VISIBILITY)
imap_libcyrus_imap_la_CXXFLAGS = $(AM_CXXFLAGS)
imap_libcyrus_imap_la_SOURCES = \
imap/annotate.c \
imap/annotate.h \
imap/append.c \
imap/append.h \
imap/backend.c \
imap/backend.h \
imap/conversations.c \
imap/conversations.h \
imap/convert_code.c \
imap/convert_code.h \
imap/dlist.c \
imap/dlist.h \
imap/duplicate.c \
imap/duplicate.h \
imap/global.c \
imap/global.h \
imap/http_client.c \
imap/http_client.h \
imap/idle.c \
imap/idle.h \
imap/idlemsg.c \
imap/idlemsg.h \
imap/imapparse.c \
imap/imapparse.h \
imap/index.c \
imap/index.h \
imap/json_support.h \
imap/mailbox.c \
imap/mailbox.h \
imap/mbdump.c \
imap/mbdump.h \
imap/mboxkey.c \
imap/mboxkey.h \
imap/mboxlist.c \
imap/mboxlist.h \
imap/mboxevent.c \
imap/mboxevent.h \
imap/mboxname.c \
imap/mboxname.h \
imap/message_guid.c \
imap/message_guid.h \
imap/message.c \
imap/message.h \
imap/message_priv.h \
imap/msgrecord.c \
imap/msgrecord.h \
imap/mupdate-client.c \
imap/mupdate-client.h \
imap/mutex.h \
imap/notify.c \
imap/notify.h \
imap/partlist.c \
imap/partlist.h \
imap/proc.c \
imap/proc.h \
imap/prometheus.c \
imap/prometheus.h \
imap/protocol.h \
imap/quota_db.c \
imap/rfc822_header.c \
imap/rfc822_header.h \
imap/mailbox_header_cache.h \
imap/saslclient.c \
imap/saslclient.h \
imap/saslserver.c \
imap/search_engines.c \
imap/search_engines.h \
imap/search_expr.c \
imap/search_expr.h \
imap/search_query.c \
imap/search_query.h \
imap/search_part.h \
imap/seen.h \
imap/seen_db.c \
imap/sequence.c \
imap/sequence.h \
imap/setproctitle.c \
imap/spool.c \
imap/spool.h \
imap/statuscache.h \
imap/statuscache_db.c \
imap/sync_log.c \
imap/sync_log.h \
imap/telemetry.c \
imap/telemetry.h \
imap/tls.c \
imap/tls.h \
imap/tls_th-lock.c \
imap/tls_th-lock.h \
imap/user.c \
imap/user.h \
imap/userdeny_db.c \
imap/userdeny.h \
imap/version.c \
imap/version.h \
imap/xstats.c \
imap/xstats.h \
imap/xstats_metrics.h
if OBJECTSTORE
imap_libcyrus_imap_la_SOURCES += \
imap/objectstore_db.c \
imap/objectstore_db.h \
imap/objectstore.h
if WITH_OPENIO
imap_libcyrus_imap_la_SOURCES += imap/objectstore_openio.c
else
if WITH_CARINGO
imap_libcyrus_imap_la_SOURCES += imap/objectstore_caringo.c
else
if WITH_OBJSTR_DUMMY
imap_libcyrus_imap_la_SOURCES += imap/objectstore_dummy.c
endif
endif
endif
endif
if USE_SQUAT
imap_libcyrus_imap_la_SOURCES += \
imap/search_squat.c \
imap/squat.c \
imap/squat.h \
imap/squat_build.c \
imap/squat_internal.c \
imap/squat_internal.h
endif
if HTTPD
imap_libcyrus_imap_la_SOURCES += \
imap/caldav_alarm.c \
imap/caldav_alarm.h \
imap/caldav_db.c \
imap/caldav_db.h \
imap/carddav_db.c \
imap/carddav_db.h \
imap/dav_db.c \
imap/dav_db.h \
imap/dav_util.c \
imap/dav_util.h \
imap/ical_support.c \
imap/ical_support.h \
imap/icu_wrap.h \
imap/icu_wrap.cpp \
imap/vcard_support.c \
imap/vcard_support.h \
imap/webdav_db.c \
imap/webdav_db.h
endif # HTTPD
if JMAP
imap_libcyrus_imap_la_SOURCES += \
imap/smtpclient.c \
imap/smtpclient.h
endif
if USE_XAPIAN
imap_libcyrus_imap_la_SOURCES += \
imap/search_xapian.c \
imap/xapian_wrap.h \
imap/xapian_wrap.cpp
imap_libcyrus_imap_la_LIBADD += $(XAPIAN_LIBS)
imap_libcyrus_imap_la_CXXFLAGS += $(XAPIAN_CXXFLAGS)
if HAVE_CLD2
imap_libcyrus_imap_la_LIBADD += $(CLD2_LIBS)
imap_libcyrus_imap_la_CXXFLAGS += $(CLD2_CFLAGS)
endif
endif
imap_lmtpd_SOURCES = \
imap/lmtpd.c \
imap/lmtpd.h \
imap/lmtpengine.c \
imap/lmtpengine.h \
imap/mutex_fake.c \
imap/proxy.c \
imap/spool.c \
master/service.c
nodist_imap_lmtpd_SOURCES = imap/lmtp_err.c imap/lmtpstats.c
if AUTOCREATE
imap_lmtpd_SOURCES += \
imap/autocreate.c \
imap/autocreate.h
endif # AUTOCREATE
if SIEVE
imap_lmtpd_SOURCES += imap/lmtp_sieve.c imap/lmtp_sieve.h imap/smtpclient.c
if JMAP
imap_lmtpd_SOURCES += imap/jmap_util.c imap/jmap_mail_query.c
endif # JMAP
endif # SIEVE
imap_lmtpd_LDADD = $(LD_SIEVE_ADD) $(LD_SERVER_ADD)
SNMPGEN = $(abs_top_srcdir)/snmp/snmpgen
imap/%stats.h imap/%stats.c: imap/%stats.snmp $(SNMPGEN)
$(AM_V_GEN)(cd $(@D) && $(SNMPGEN) $(realpath $<))
imap_mbexamine_SOURCES = imap/cli_fatal.c imap/mbexamine.c imap/mutex_fake.c
imap_mbexamine_LDADD = $(LD_UTILITY_ADD)
imap_mbpath_SOURCES = imap/cli_fatal.c imap/mbpath.c imap/mutex_fake.c
imap_mbpath_LDADD = $(LD_UTILITY_ADD)
imap_mbtool_SOURCES = imap/cli_fatal.c imap/mbtool.c imap/mutex_fake.c
imap_mbtool_LDADD = $(LD_UTILITY_ADD)
imap_message_test_SOURCES = imap/message_test.c imap/mutex_fake.c
imap_message_test_LDADD = $(LD_UTILITY_ADD)
imap_mupdate_SOURCES = \
imap/mupdate.c \
imap/mupdate.h \
imap/mupdate-slave.c \
imap/mutex_pthread.c \
imap/tls_th-lock.c \
master/service-thread.c
imap_mupdate_LDADD = $(LD_SERVER_ADD) -lpthread
imap_mupdate_CFLAGS = $(AM_CFLAGS) -pthread
nodist_imap_nntpd_SOURCES = imap/nntp_err.c
imap_nntpd_SOURCES = \
imap/mutex_fake.c \
imap/nntpd.c \
imap/proxy.c \
imap/smtpclient.c \
imap/smtpclient.h \
imap/spool.c \
imap/spool.h \
master/service.c
imap_nntpd_LDADD = $(LD_SERVER_ADD)
nodist_imap_httpd_SOURCES = \
imap/http_caldav_js.h \
imap/http_carddav_js.h \
imap/tz_err.c \
imap/tz_err.h
imap_httpd_SOURCES = \
imap/css3_color.c \
imap/css3_color.h \
imap/http_admin.c \
imap/http_applepush.c \
imap/http_caldav.c \
imap/http_caldav.h \
imap/http_caldav_js.h \
imap/http_caldav_sched.c \
imap/http_caldav_sched.h \
imap/http_carddav.c \
imap/http_carddav.h \
imap/http_carddav_js.h \
imap/http_cgi.c \
imap/http_dav.c \
imap/http_dav.h \
imap/http_dav_sharing.c \
imap/http_dav_sharing.h \
imap/http_dblookup.c \
imap/http_h2.c \
imap/http_h2.h \
imap/http_ischedule.c \
imap/http_prometheus.c \
imap/http_proxy.c \
imap/http_proxy.h \
imap/http_rss.c \
imap/http_tzdist.c \
imap/http_webdav.c \
imap/http_ws.c \
imap/http_ws.h \
imap/httpd.c \
imap/httpd.h \
imap/jcal.c \
imap/jcal.h \
imap/json_support.h \
imap/mutex_fake.c \
imap/proxy.c \
imap/smtpclient.c \
imap/spool.c \
imap/xcal.c \
imap/xcal.h \
imap/xml_support.c \
imap/xml_support.h \
imap/zoneinfo_db.c \
imap/zoneinfo_db.h \
master/masterconf.c \
master/service.c
imap_httpd_LDADD = $(LD_SERVER_ADD)
if JMAP
BUILT_SOURCES += \
imap/jmap_err.c \
imap/jmap_err.h
imap_httpd_SOURCES += \
imap/http_jmap.c \
imap/http_jmap.h \
imap/jmap_api.c \
imap/jmap_api.h \
imap/jmap_backup.c \
imap/jmap_calendar.c \
imap/jmap_contact.c \
imap/jmap_core.c \
imap/jmap_ical.c \
imap/jmap_ical.h \
imap/jmap_mail.c \
imap/jmap_mail.h \
imap/jmap_mail_query.c \
imap/jmap_mail_query.h \
imap/jmap_mail_submission.c \
imap/jmap_mailbox.c \
+ imap/jmap_notes.c \
imap/jmap_util.c \
imap/jmap_util.h \
imap/jmap_vacation.c \
imap/sync_support.c \
imap/sync_support.h
nodist_imap_httpd_SOURCES += \
imap/jmap_err.c \
imap/jmap_err.h
imap_libcyrus_imap_la_SOURCES += \
imap/jmap_util.c \
imap/jmap_util.h \
imap/json_support.c \
imap/json_support.h
imap_httpd_LDADD += $(LD_SIEVE_ADD)
if CUNIT
cunit_TESTS += cunit/jmap_util.testc
endif # CUNIT
endif # JMAP
imap_pop3d_SOURCES = \
imap/mutex_fake.c \
imap/pop3d.c \
imap/proxy.c \
master/service.c
if AUTOCREATE
imap_pop3d_SOURCES += \
imap/autocreate.c \
imap/autocreate.h
endif # AUTOCREATE
imap_pop3d_LDADD = $(LD_SIEVE_ADD) $(LD_SERVER_ADD)
PROMDATAGEN = $(abs_top_srcdir)/imap/promdatagen
imap/promdata.c: imap/promdata.p $(PROMDATAGEN)
$(AM_V_GEN)$(PROMDATAGEN) -c $@.NEW $< && mv $@.NEW $@
imap/promdata.h: imap/promdata.p $(PROMDATAGEN)
$(AM_V_GEN)$(PROMDATAGEN) -h $@.NEW $< && mv $@.NEW $@
imap_promstatsd_SOURCES = imap/promstatsd.c imap/mutex_fake.c
imap_promstatsd_LDADD = $(LD_UTILITY_ADD)
imap_quota_SOURCES = imap/cli_fatal.c imap/mutex_fake.c imap/quota.c imap/quota.h
imap_quota_LDADD = $(LD_UTILITY_ADD)
imap_reconstruct_SOURCES = imap/cli_fatal.c imap/mutex_fake.c imap/reconstruct.c
imap_reconstruct_LDADD = $(LD_UTILITY_ADD)
imap_dav_reconstruct_SOURCES = imap/cli_fatal.c imap/mutex_fake.c imap/dav_reconstruct.c
imap_dav_reconstruct_LDADD = $(LD_UTILITY_ADD)
imap_ical_apply_patch_SOURCES = imap/cli_fatal.c imap/mutex_fake.c imap/ical_apply_patch.c
imap_ical_apply_patch_LDADD = $(LD_UTILITY_ADD)
imap_search_test_SOURCES = imap/search_test.c imap/mutex_fake.c
imap_search_test_LDADD = $(LD_UTILITY_ADD)
imap_smmapd_SOURCES = imap/mutex_fake.c imap/proxy.c imap/smmapd.c master/service.c
imap_smmapd_LDADD = $(LD_SERVER_ADD)
imap_squatter_SOURCES = imap/cli_fatal.c imap/mutex_fake.c imap/squatter.c
imap_squatter_LDADD = $(LD_UTILITY_ADD)
imap_squat_dump_SOURCES = imap/cli_fatal.c imap/mutex_fake.c imap/squat_dump.c
imap_squat_dump_LDADD = $(LD_UTILITY_ADD)
imap_sync_client_SOURCES = imap/mutex_fake.c imap/sync_client.c imap/sync_support.c imap/sync_support.h
imap_sync_client_LDADD = $(LD_SIEVE_ADD) $(LD_UTILITY_ADD)
imap_sync_reset_SOURCES = imap/mutex_fake.c imap/sync_reset.c imap/sync_support.c imap/sync_support.h
imap_sync_reset_LDADD = $(LD_SIEVE_ADD) $(LD_UTILITY_ADD)
imap_sync_server_SOURCES = imap/mutex_fake.c imap/sync_server.c imap/sync_support.c imap/sync_support.h master/service.c
imap_sync_server_LDADD = $(LD_SIEVE_ADD) $(LD_SERVER_ADD)
imap_tls_prune_SOURCES = imap/cli_fatal.c imap/mutex_fake.c imap/tls_prune.c
imap_tls_prune_LDADD = $(LD_UTILITY_ADD)
imap_unexpunge_SOURCES = imap/cli_fatal.c imap/mutex_fake.c imap/unexpunge.c
imap_unexpunge_LDADD = $(LD_UTILITY_ADD)
# $(COMPILE_ET) creates its output files in the directory it's run from,
# so do a tricky directory change
%_err.h %_err.c: %_err.et $(COMPILE_ET_DEP)
$(AM_V_GEN)(cd $(@D) && $(COMPILE_ET) $(realpath $<))
# xxd cannot have path details in its input filename, otherwise it junks up
# the variable names in the output file. so do a tricky directory change.
# the /dev/null redirection on cd is to prevent shell environments with
# CDPATH echoing the path change on stdout and consequently into the .h file
%_js.h: %.js
$(AM_V_GEN)(cd $(<D) > /dev/null && xxd -i $(<F)) > $@.NEW && mv $@.NEW $@
if MAINTAINER_MODE
imap/rfc822_header.c: imap/rfc822_header.st
$(AM_V_GEN)${top_srcdir}/tools/compile_st.pl -c $< > $@.NEW && mv $@.NEW $@
imap/rfc822_header.h: imap/rfc822_header.st
$(AM_V_GEN)${top_srcdir}/tools/compile_st.pl -h $< > $@.NEW && mv $@.NEW $@
lib/htmlchar.c: lib/htmlchar.st
$(AM_V_GEN)${top_srcdir}/tools/compile_st.pl -c $< > $@.NEW && mv $@.NEW $@
lib/htmlchar.h: lib/htmlchar.st
$(AM_V_GEN)${top_srcdir}/tools/compile_st.pl -h $< > $@.NEW && mv $@.NEW $@
imap/mailbox_header_cache.h: imap/mailbox_header_cache.gperf
$(AM_V_GEN)gperf --ignore-case --initializer-suffix=,0 -p -j1 -i 1 -g -o -t -H mailbox_header_cache_hash -N mailbox_header_cache_lookup -k1,3,$$ $< > $@.NEW && mv $@.NEW $@
endif
imtest_imtest_SOURCES = imtest/imtest.c
imtest_imtest_LDADD = $(LD_BASIC_ADD)
imtest_imtest_CFLAGS = $(AM_CFLAGS) $(CFLAG_VISIBILITY)
nodist_lib_libcyrus_la_SOURCES = lib/chartable.c
lib_libcyrus_la_SOURCES = \
lib/acl.c \
lib/acl_afs.c \
lib/auth.c \
lib/auth_krb.c \
lib/auth_krb5.c \
lib/auth_pts.c \
lib/auth_unix.c \
lib/bitvector.c \
lib/bloom.c \
lib/bsearch.c \
lib/charset.c \
lib/command.c \
lib/cyr_qsort_r.c \
lib/cyrusdb.c \
lib/cyrusdb_flat.c \
lib/cyrusdb_quotalegacy.c \
lib/cyrusdb_skiplist.c \
lib/cyrusdb_twoskip.c \
lib/glob.c \
lib/htmlchar.c \
lib/htmlchar.h \
lib/imapurl.c \
lib/imclient.c \
lib/imparse.c \
lib/iostat.c \
lib/iptostring.c \
lib/libcyr_cfg.c \
lib/lsort.c \
lib/mappedfile.c \
lib/murmurhash.c \
lib/mkgmtime.c \
lib/parseaddr.c \
lib/prot.c \
lib/ptrarray.c \
lib/rfc822tok.c \
lib/signals.c \
lib/stristr.c \
lib/times.c \
lib/tok.c \
lib/wildmat.c
if USE_CYRUSDB_SQL
lib_libcyrus_la_SOURCES += lib/cyrusdb_sql.c
endif
if USE_SQLITE
lib_libcyrus_la_SOURCES += lib/sqldb.c
endif
if GMTOFF_TM
lib_libcyrus_la_SOURCES += lib/gmtoff_tm.c
else
lib_libcyrus_la_SOURCES += lib/gmtoff_gmtime.c
endif
if NONBLOCK_FCNTL
lib_libcyrus_la_SOURCES += lib/nonblock_fcntl.c
else
lib_libcyrus_la_SOURCES += lib/nonblock_ioctl.c
endif
lib_libcyrus_la_LIBADD = libcrc32.la ${LIB_SASL} $(SSL_LIBS) $(GCOV_LIBS)
lib_libcyrus_la_CFLAGS = $(AM_CFLAGS) $(CFLAG_VISIBILITY)
if USE_ZEROSKIP
lib_libcyrus_la_SOURCES += lib/cyrusdb_zeroskip.c
lib_libcyrus_la_LIBADD += $(ZEROSKIP_LIBS)
lib_libcyrus_la_CFLAGS += $(ZEROSKIP_CFLAGS)
AM_CPPFLAGS += $(ZEROSKIP_CFLAGS)
endif
noinst_LTLIBRARIES += libcrc32.la
libcrc32_la_SOURCES = lib/crc32.c lib/crc32c.c
libcrc32_la_CFLAGS = -O3 $(AM_CFLAGS) $(CFLAG_VISIBILITY)
nodist_lib_libcyrus_min_la_SOURCES = \
lib/imapopts.c
lib_libcyrus_min_la_SOURCES = \
lib/arrayu64.c \
lib/assert.c \
lib/bufarray.c \
lib/byteorder64.c \
lib/hash.c \
lib/hashset.c \
lib/hashu64.c \
lib/libconfig.c \
lib/mpool.c \
lib/retry.c \
lib/strarray.c \
lib/strhash.c \
lib/util.c \
lib/vparse.c \
lib/xmalloc.c \
lib/xstrlcat.c \
lib/xstrlcpy.c \
lib/xstrnchr.c
if !HAVE_SSL
lib_libcyrus_min_la_SOURCES += lib/xsha1.c
endif
if IPV6_noGETADDRINFO
lib_libcyrus_min_la_SOURCES += lib/getaddrinfo.c
endif
if IPV6_noGETNAMEINFO
lib_libcyrus_min_la_SOURCES += lib/getnameinfo.c
endif
if LOCK_FCNTL
lib_libcyrus_min_la_SOURCES += lib/lock_fcntl.c
else
lib_libcyrus_min_la_SOURCES += lib/lock_flock.c
endif
if MAP_SHARED
lib_libcyrus_min_la_SOURCES += lib/map_shared.c
else
if MAP_STUPIDSHARED
lib_libcyrus_min_la_SOURCES += lib/map_stupidshared.c
else
lib_libcyrus_min_la_SOURCES += lib/map_nommap.c
endif
endif
lib_libcyrus_min_la_LIBADD = $(LTLIBOBJS) $(LIB_UUID) $(GCOV_LIBS) $(LIBCAP_LIBS)
lib_libcyrus_min_la_CFLAGS = $(AM_CFLAGS) $(CFLAG_VISIBILITY)
# n.b. the order of the -m arguments to mkchartable.pl is important,
# in particular ellie believes unifix.txt must be before UnicodeData.txt
lib/chartable.c: lib/mkchartable.pl lib/charset/unifix.txt \
$(top_srcdir)/lib/charset/*.t \
lib/charset/UnicodeData.txt lib/charset/aliases.txt
@echo "### Building chartables..."
$(AM_V_GEN)perl $(top_srcdir)/lib/mkchartable.pl -m $(top_srcdir)/lib/charset/unifix.txt -m $(top_srcdir)/lib/charset/UnicodeData.txt -a $(top_srcdir)/lib/charset/aliases.txt -o $@ $(top_srcdir)/lib/charset/*.t || (rm -f $@ && exit 1)
@echo "### Done building chartables."
lib/imapopts.h: lib/imapopts.c
lib/imapopts.c: lib/imapoptions tools/config2header
$(AM_V_GEN)$(top_srcdir)/tools/config2header CC="$(CC)" $(top_builddir)/lib/imapopts.c $(top_builddir)/lib/imapopts.h < $(top_srcdir)/lib/imapoptions
imap_cvt_xlist_specialuse_SOURCES = imap/mutex_fake.c imap/cvt_xlist_specialuse.c
imap_cvt_xlist_specialuse_LDADD = $(LD_UTILITY_ADD)
@SET_MAKE@
dist_man1_MANS = \
man/imtest.1 \
man/installsieve.1 \
man/httptest.1 \
man/lmtptest.1 \
man/mupdatetest.1 \
man/nntptest.1 \
man/pop3test.1 \
man/sieveshell.1 \
man/sivtest.1 \
man/smtptest.1
dist_man3_MANS = \
man/imclient.3
dist_man5_MANS = \
man/cyrus.conf.5 \
man/imapd.conf.5 \
man/krb.equiv.5
dist_man8_MANS = \
man/arbitron.8 \
man/backupd.8 \
man/chk_cyrus.8 \
man/ctl_backups.8 \
man/ctl_conversationsdb.8 \
man/ctl_cyrusdb.8 \
man/ctl_deliver.8 \
man/ctl_mboxlist.8 \
man/cvt_cyrusdb.8 \
man/cyr_backup.8 \
man/cyr_buildinfo.8 \
man/cyr_dbtool.8 \
man/cyr_deny.8 \
man/cyr_df.8 \
man/cyr_expire.8 \
man/cyr_info.8 \
man/cyr_synclog.8 \
man/cyr_virusscan.8 \
man/deliver.8 \
man/fud.8 \
man/idled.8 \
man/imapd.8 \
man/ipurge.8 \
man/lmtpd.8 \
man/master.8 \
man/mbexamine.8 \
man/mbpath.8 \
man/mbtool.8 \
man/notifyd.8 \
man/pop3d.8 \
man/quota.8 \
man/reconstruct.8 \
man/restore.8 \
man/smmapd.8 \
man/timsieved.8 \
man/tls_prune.8 \
man/unexpunge.8
if SQUATTER
dist_man8_MANS += \
man/squatter.8
endif # SQUATTER
if NNTPD
dist_man8_MANS += \
man/fetchnews.8 \
man/nntpd.8
endif # NNTPD
if HTTPD
dist_man8_MANS += \
man/ctl_zoneinfo.8 \
man/httpd.8
endif # HTTPD
if BENCH
check_PROGRAMS += bench/cyrdbbench
bench_cyrdbbench_SOURCES = bench/cyrdbbench.c imap/mutex_fake.c
bench_cyrdbbench_LDADD = $(LD_BASIC_ADD)
endif # BENCH
if REPLICATION
dist_man8_MANS += \
man/sync_client.8 \
man/sync_reset.8 \
man/sync_server.8
endif
master_master_SOURCES = \
master/cyrusMasterMIB.c \
master/cyrusMasterMIB.h \
master/master.c \
master/master.h \
master/masterconf.c \
master/masterconf.h \
master/service.h
master_master_LDADD = lib/libcyrus_min.la $(LIB_UCDSNMP) $(LIBS) $(GCOV_LIBS) -lm
netnews_remotepurge_SOURCES = \
netnews/macros.h \
netnews/readconfig.c \
netnews/readconfig.h \
netnews/remotepurge.c
netnews_remotepurge_LDADD = $(LD_BASIC_ADD)
notifyd_notifyd_SOURCES = \
imap/mutex_fake.c \
master/service.c \
notifyd/notify_external.c \
notifyd/notify_external.h \
notifyd/notify_log.c \
notifyd/notify_log.h \
notifyd/notify_mailto.c \
notifyd/notify_mailto.h \
notifyd/notify_null.c \
notifyd/notify_null.h \
notifyd/notifyd.c \
notifyd/notifyd.h
if ZEPHYR
notifyd_notifyd_SOURCES += notifyd/notify_zephyr.c notifyd/notify_zephyr.h
endif
notifyd_notifyd_LDADD = $(LD_SERVER_ADD) $(ZEPHYR_LIBS)
notifyd_notifytest_SOURCES = notifyd/notifytest.c imap/mutex_fake.c
notifyd_notifytest_LDADD = $(LD_BASIC_ADD)
nodist_perl_libcyrus_la_SOURCES = $(nodist_lib_libcyrus_la_SOURCES)
perl_libcyrus_la_SOURCES = $(lib_libcyrus_la_SOURCES)
perl_libcyrus_la_LIBADD = $(lib_libcyrus_la_LIBADD)
nodist_perl_libcyrus_min_la_SOURCES = $(nodist_lib_libcyrus_min_la_SOURCES)
perl_libcyrus_min_la_SOURCES = $(lib_libcyrus_min_la_SOURCES)
perl_libcyrus_min_la_LIBADD = $(lib_libcyrus_min_la_LIBADD)
perl_sieve_lib_libisieve_la_SOURCES = \
perl/sieve/lib/codes.h \
perl/sieve/lib/isieve.c \
perl/sieve/lib/isieve.h \
perl/sieve/lib/lex.c \
perl/sieve/lib/lex.h \
perl/sieve/lib/request.c \
perl/sieve/lib/request.h
ptclient_ptdump_SOURCES = imap/cli_fatal.c imap/mutex_fake.c ptclient/ptdump.c
ptclient_ptdump_LDADD = $(LD_UTILITY_ADD)
ptclient_ptexpire_SOURCES = imap/cli_fatal.c imap/mutex_fake.c ptclient/ptexpire.c
ptclient_ptexpire_LDADD = $(LD_UTILITY_ADD)
ptclient_ptloader_SOURCES = \
imap/mutex_fake.c \
ptclient/ptloader.c \
ptclient/ptloader.h \
master/service-thread.c
ptclient_ptloader_LDFLAGS =
ptclient_ptloader_LDADD = $(LD_SERVER_ADD)
if HAVE_LDAP
ptclient_ptloader_SOURCES += ptclient/ldap.c
ptclient_ptloader_LDADD += $(LDAP_LIBS)
ptclient_ptloader_LDFLAGS += $(LDAP_LDFLAGS)
endif
if USE_AFSKRB
ptclient_ptloader_SOURCES += ptclient/afskrb.c
ptclient_ptloader_LDADD += $(AFS_LIBS)
ptclient_ptloader_LDFLAGS += $(AFS_LDFLAGS)
endif
# Each new version of flex seems to generate new sign-comparison
# warnings, while they switch code around between int/yy_size_t.
#
# This is a pain because most of us Cyrus devs build with -Werror.
#
# Instead of continuing to maintain an ever-growing, increasingly
# precarious set of post-hoc fixes (see: the former lex-fix rule),
# let's just ignore sign-comparison warnings from flex-generated
# sources only.
sieve_libcyrus_sieve_lex_la_SOURCES = \
sieve/addr-lex.l \
sieve/sieve-lex.l
sieve_libcyrus_sieve_lex_la_LIBADD =
sieve_libcyrus_sieve_lex_la_CFLAGS = $(AM_CFLAGS) $(CFLAG_VISIBILITY) $(NOWARN_SIGN_COMPARE)
nodist_sieve_libcyrus_sieve_la_SOURCES = \
sieve/sieve_err.c \
sieve/sieve_err.h
sieve_libcyrus_sieve_la_SOURCES = \
sieve/bytecode.h \
sieve/addr.y \
sieve/bc_emit.c \
sieve/bc_eval.c \
sieve/bc_generate.c \
sieve/bc_parse.c \
sieve/bc_parse.h \
sieve/comparator.c \
sieve/comparator.h \
sieve/flags.c \
sieve/flags.h \
sieve/grammar.c \
sieve/grammar.h \
sieve/interp.c \
sieve/interp.h \
sieve/message.c \
sieve/message.h \
sieve/rebuild.c \
sieve/script.c \
sieve/script.h \
sieve/sieve.y \
sieve/tree.c \
sieve/tree.h \
sieve/variables.c \
sieve/variables.h \
sieve/varlist.c \
sieve/varlist.h
if JMAP
sieve_libcyrus_sieve_la_SOURCES += \
imap/json_support.c \
imap/json_support.h
endif
sieve_libcyrus_sieve_la_LIBADD = \
sieve/libcyrus_sieve_lex.la \
$(COM_ERR_LIBS) \
lib/libcyrus_min.la \
lib/libcyrus.la
sieve_libcyrus_sieve_la_CFLAGS = $(AM_CFLAGS) $(CFLAG_VISIBILITY)
sieve_sievec_LDADD = $(LD_SIEVE_ADD)
sieve_sieved_LDADD = $(LD_SIEVE_ADD)
sieve_test_SOURCES = sieve/test.c imap/mutex_fake.c
if JMAP
sieve_test_SOURCES += imap/jmap_util.c imap/jmap_mail_query.c
endif
sieve_test_LDADD = $(LD_SIEVE_ADD) $(LD_UTILITY_ADD)
sieve_test_mailbox_SOURCES = sieve/test_mailbox.c imap/mutex_fake.c
sieve_test_mailbox_LDADD = $(LD_SIEVE_ADD) $(LD_UTILITY_ADD)
timsieved_timsieved_SOURCES = \
imap/mutex_fake.c \
imap/proxy.c \
master/service.c \
timsieved/actions.c \
timsieved/actions.h \
timsieved/codes.h \
timsieved/lex.c \
timsieved/lex.h \
timsieved/parser.c \
timsieved/parser.h \
timsieved/timsieved.c
timsieved_timsieved_LDADD = $(LD_SIEVE_ADD) $(LD_SERVER_ADD)
# The lex-fix rule is now obsolete, but let's leave a crumb here for a
# while so that custom build scripts that expect it to exist don't choke.
lex-fix:
snapshot::
@echo "Creating snapshot $(PACKAGE_NAME)-$(PACKAGE_VERSION)"
@$(MKDIR_P) snapshot
@git archive --format=tar --prefix=$(PACKAGE_NAME)-$(PACKAGE_VERSION)/ HEAD | tar -C snapshot/ -x -f -
@echo "creating tarfile"
tar -C snapshot -c -f - $(PACKAGE_NAME)-$(PACKAGE_VERSION) | gzip -9 > $(PACKAGE_NAME)-$(PACKAGE_VERSION).tar.gz
@rm -rf snapshot
docsrc/imap/reference/manpages/systemcommands/cyradm.rst: $(top_srcdir)/perl/imap/IMAP/Shell.pm
@$(MKDIR_P) $(@D)
$(AM_V_GEN)$(top_srcdir)/tools/perl2rst cyradm < $< > $@.NEW && mv $@.NEW $@
docsrc/imap/reference/manpages/configs/imapd.conf.rst: $(top_srcdir)/tools/config2rst $(top_srcdir)/lib/imapoptions
@$(MKDIR_P) $(@D)
$(AM_V_GEN)$(top_srcdir)/tools/config2rst $(top_srcdir)/lib/imapoptions > $@
doc/examples/imapd_conf/imapd.conf.sample: $(top_srcdir)/tools/config2sample $(top_srcdir)/lib/imapoptions
@$(MKDIR_P) $(@D)
$(AM_V_GEN)$(top_srcdir)/tools/config2sample $(top_srcdir)/lib/imapoptions > $@
docsrc/imap/reference/manpages/usercommands/sieveshell.rst: $(top_srcdir)/perl/sieve/scripts/sieveshell.pl
@$(MKDIR_P) $(@D)
$(AM_V_GEN)$(top_srcdir)/tools/perl2rst sieveshell < $< > $@.NEW && mv $@.NEW $@
%.1: man/.sphinx-build.stamp
@$(MKDIR_P) $(@D)
$(AM_V_GEN)touch $@
%.3: man/.sphinx-build.stamp
@$(MKDIR_P) $(@D)
$(AM_V_GEN)touch $@
%.5: man/.sphinx-build.stamp
@$(MKDIR_P) $(@D)
$(AM_V_GEN)touch $@
%.8: man/.sphinx-build.stamp
@$(MKDIR_P) $(@D)
$(AM_V_GEN)touch $@
## define this unconditionally because dist-hook references it
SPHINX_CACHE = docsrc/.doctrees
.PHONY: clean-sphinx-cache clean-docsrc
clean-sphinx-cache:
@$(RM) -r $(SPHINX_CACHE)
if HAVE_SPHINX_BUILD
SPHINX_OPTS = -d $(SPHINX_CACHE) -n -q
## detect when source directory is not build directory (i.e. VPATH
## build), and clone the docsrc tree into the build directory, so
## that we have a single "source directory" for sphinx-build to use.
docsrc/.sphinx-build.stamp: docsrc/imap/reference/manpages/configs/imapd.conf.rst docsrc/imap/reference/manpages/systemcommands/cyradm.rst docsrc/imap/reference/manpages/usercommands/sieveshell.rst
$(AM_V_GEN)test x"$(top_srcdir)" = x"$(top_builddir)" || \
(cd $(top_srcdir) && tar cf - --mode=gu+w docsrc doc/examples) | tar xf -
@touch $@
clean-docsrc:
test x"$(top_srcdir)" = x"$(top_builddir)" || \
$(RM) -r $(top_builddir)/docsrc $(top_builddir)/doc/examples
## XXX doesn't detect if other rst sources are updated...
man/.sphinx-build.stamp: docsrc/.sphinx-build.stamp
$(AM_V_GEN)DOCSRC=$(top_builddir)/docsrc $(SPHINX_BUILD) $(SPHINX_OPTS) -b cyrman $(top_builddir)/docsrc $(top_builddir)/man
@touch $@
clean-man:
@$(RM) -r man
## XXX doesn't detect if other rst sources are updated...
doc/html/.sphinx-build.stamp: docsrc/.sphinx-build.stamp
$(AM_V_GEN)DOCSRC=$(top_builddir)/docsrc $(SPHINX_BUILD) $(SPHINX_OPTS) -b html $(top_builddir)/docsrc $(top_builddir)/doc/html
@touch $@
## XXX doesn't detect if other rst sources are updated...
doc/text/.sphinx-build.stamp: docsrc/.sphinx-build.stamp
$(AM_V_GEN)DOCSRC=$(top_builddir)/docsrc $(SPHINX_BUILD) $(SPHINX_OPTS) -b text $(top_builddir)/docsrc $(top_builddir)/doc/text
@touch $@
else
clean-docsrc:
man/.sphinx-build.stamp:
@$(MKDIR_P) $(@D)
$(AM_V_GEN)touch $@
@echo "warning: missing documentation dependencies. man pages will be empty" 1>&2
clean-man:
doc/html/.sphinx-build.stamp:
@echo "error: missing documentation dependencies. cannot build html documentation" 1>&2
@false
doc/text/.sphinx-build.stamp:
@echo "error: missing documentation dependencies. cannot build text documentation" 1>&2
@false
endif
.PHONY: man man-force clean-man
man-force:
## trash the timestamps, so we always re-run sphinx-build.
## sphinx-build tracks source modifications itself, so forced
## rebuilds will still be a lot quicker once it has run once.
@$(RM) docsrc/.sphinx-build.stamp man/.sphinx-build.stamp
man: man-force $(dist_man1_MANS) $(dist_man3_MANS) $(dist_man5_MANS) $(dist_man8_MANS)
# clean-man is conditionally defined above. We do not want to remove
# man pages if the user is unable to rebuild them.
.PHONY: doc-html doc-html-force clean-doc-html
doc-html-force:
@$(RM) docsrc/.sphinx-build.stamp doc/html/.sphinx-build.stamp
doc-html: doc-html-force doc/html/.sphinx-build.stamp
clean-doc-html:
@$(RM) -r doc/html
.PHONY: doc-text doc-text-force clean-doc-text
doc-text-force:
@$(RM) docsrc/.sphinx-build.stamp doc/text/.sphinx-build.stamp
doc-text: doc-text-force doc/text/.sphinx-build.stamp
clean-doc-text:
@$(RM) -r doc/text
GENERATED_EXAMPLES = doc/examples/imapd_conf/imapd.conf.sample
.PHONY: doc-examples doc-examples-force clean-doc-examples
doc-examples-force:
doc-examples: doc-examples-force $(GENERATED_EXAMPLES)
clean-doc-examples:
@$(RM) $(GENERATED_EXAMPLES)
.PHONY: doc clean-doc
doc: man doc-html doc-text doc-examples
clean-doc: clean-man clean-doc-html clean-doc-text clean-doc-examples
.PHONY: clean-local
clean-local: clean-doc clean-sphinx-cache clean-docsrc
distgit:
$(MKDIR_P) dist
@echo "checking out the distribution from tag $(PACKAGE_NAME)-$(PACKAGE_VERSION)"
git archive --format=tar --prefix=$(PACKAGE_NAME)-$(PACKAGE_VERSION)/ $(PACKAGE_NAME)-$(PACKAGE_VERSION) | tar -C dist -x -f -
touch distgit
dist-hook:
find $(top_distdir) -type f -name .sphinx-build.stamp -delete
find $(top_distdir) -type f -name .gitignore -delete
rm -rf $(top_distdir)/$(SPHINX_CACHE)
VERSION: tools/git-version.sh
$(AM_V_GEN)$< > $@.NEW && mv $@.NEW $@
install-data-hook:
if CMULOCAL
$(INSTALL) -m 644 $(top_srcdir)/depot/depot.conf $(DESTDIR)/
endif
install-exec-hook:
if PERL
for s in installsieve sieveshell; \
do \
$(MKDIR_P) $(top_builddir)/perl/sieve/scripts ; \
$(PERL_PREINSTALL) < $(top_srcdir)/perl/sieve/scripts/$$s.pl > $(top_builddir)/perl/sieve/scripts/$$s ;\
$(INSTALL) -m 755 $(top_builddir)/perl/sieve/scripts/$$s $(DESTDIR)$(bindir)/$$s ; \
done
endif
## The @$(MKDIR_P) line is added due to a bug in Automake 1.10 and can be removed if using Automake 1.12.
@$(MKDIR_P) $(DESTDIR)$(libexecdir)
if SERVER
cd $(DESTDIR)$(libexecdir) && \
$(LN_S) -f pop3d pop3proxyd && \
$(LN_S) -f imapd proxyd && \
$(LN_S) -f lmtpd lmtpproxyd
endif
## The @$(MKDIR_P) line is added due to a bug in Automake 1.10 and can be removed if using Automake 1.12.
@$(MKDIR_P) $(DESTDIR)$(bindir)
cd $(DESTDIR)$(bindir) && \
$(LN_S) -f imtest httptest && \
$(LN_S) -f imtest lmtptest && \
$(LN_S) -f imtest mupdatetest && \
$(LN_S) -f imtest nntptest && \
$(LN_S) -f imtest pop3test && \
$(LN_S) -f imtest sivtest && \
$(LN_S) -f imtest smtptest && \
$(LN_S) -f imtest synctest
uninstall-hook: cyrus-makemaker-uninstall-workaround
if PERL
for s in installsieve sieveshell; do \
rm -f $(DESTDIR)$(bindir)/$$s; \
done
endif
.PHONY: cyrus-makemaker-uninstall-workaround
# Workaround for deprecated no-op MakeMaker uninstall.
# Don't remove the packlists themselves, otherwise MakeMaker uninstall will
# crash. Just remove the files MakeMaker uninstall won't remove itself.
#
# We have another layer of workaround in each module's Makefile that
# removes the packlist after MakeMaker uninstall has finished ignoring it.
cyrus-makemaker-uninstall-workaround:
if PERL
( packlists=`find $(DESTDIR)$(libdir) -type f -name .packlist` && \
echo "uninstalling files from packlists: $$packlists" && \
rm -f `cat $$packlists` )
( podfiles=`find $(DESTDIR)$(libdir) -type f -name perllocal.pod` && \
rm -f `echo $$podfiles` )
endif
install-binsymlinks:
## Let's symlink everything else
for prog in $(sbin_PROGRAMS); do $(LN_S) -f $(sbindir)/`basename $$prog` $(DESTDIR)$(bindir)/; done
for prog in $(libexec_PROGRAMS); do $(LN_S) -f $(libexecdir)/`basename $$prog` $(DESTDIR)$(bindir)/; done
SUFFIXES = .fig.png
.fig.png:
mkdir -p $(@D)
fig2dev -L png $< $@
valgrind:
$(MAKE) VG=yes check
libtool: $(LIBTOOL_DEPS)
$(SHELL) ./config.status libtool
# Install for Cassandane
cassandane:
$(MAKE) DESTDIR=`cd ../inst ; /bin/pwd` install
diff --git a/imap/http_jmap.c b/imap/http_jmap.c
index 78e809f03..e7c953363 100644
--- a/imap/http_jmap.c
+++ b/imap/http_jmap.c
@@ -1,1072 +1,1073 @@
/* http_jmap.c -- Routines for handling JMAP requests in httpd
*
* Copyright (c) 1994-2018 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <config.h>
#include <errno.h>
#include "acl.h"
#include "append.h"
#include "httpd.h"
#include "http_jmap.h"
#include "http_proxy.h"
#include "http_ws.h"
#include "mboxname.h"
#include "proxy.h"
#include "times.h"
#include "syslog.h"
#include "user.h"
#include "xstrlcpy.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include "imap/imap_err.h"
#include "imap/jmap_err.h"
#define JMAP_ROOT "/jmap"
#define JMAP_BASE_URL JMAP_ROOT "/"
#define JMAP_WS_COL "ws/"
#define JMAP_UPLOAD_COL "upload/"
#define JMAP_UPLOAD_TPL "{accountId}/"
#define JMAP_DOWNLOAD_COL "download/"
#define JMAP_DOWNLOAD_TPL "{accountId}/{blobId}/{name}?accept={type}"
struct namespace jmap_namespace;
static time_t compile_time;
/* Namespace callbacks */
static void jmap_init(struct buf *serverinfo);
static int jmap_need_auth(struct transaction_t *txn);
static int jmap_auth(const char *userid);
static void jmap_shutdown(void);
/* HTTP method handlers */
static int meth_get(struct transaction_t *txn, void *params);
static int meth_options_jmap(struct transaction_t *txn, void *params);
static int meth_post(struct transaction_t *txn, void *params);
/* JMAP Requests */
static int jmap_get_session(struct transaction_t *txn);
static int jmap_download(struct transaction_t *txn);
static int jmap_upload(struct transaction_t *txn);
/* WebSocket handler */
#define JMAP_WS_PROTOCOL "jmap"
static int jmap_ws(struct buf *inbuf, struct buf *outbuf,
struct buf *logbuf, void **rock);
static struct connect_params ws_params = {
JMAP_BASE_URL JMAP_WS_COL, JMAP_WS_PROTOCOL, &jmap_ws
};
/* Namespace for JMAP */
struct namespace_t namespace_jmap = {
URL_NS_JMAP, 0, "jmap", JMAP_ROOT, "/.well-known/jmap",
jmap_need_auth, /*authschemes*/0,
/*mbtype*/0,
(ALLOW_READ | ALLOW_POST),
&jmap_init, &jmap_auth, NULL, &jmap_shutdown, NULL, /*bearer*/NULL,
{
{ NULL, NULL }, /* ACL */
{ NULL, NULL }, /* BIND */
{ &meth_connect, &ws_params }, /* CONNECT */
{ NULL, NULL }, /* COPY */
{ NULL, NULL }, /* DELETE */
{ &meth_get, NULL }, /* GET */
{ &meth_get, NULL }, /* HEAD */
{ NULL, NULL }, /* LOCK */
{ NULL, NULL }, /* MKCALENDAR */
{ NULL, NULL }, /* MKCOL */
{ NULL, NULL }, /* MOVE */
{ &meth_options_jmap, NULL }, /* OPTIONS */
{ NULL, NULL }, /* PATCH */
{ &meth_post, NULL }, /* POST */
{ NULL, NULL }, /* PROPFIND */
{ NULL, NULL }, /* PROPPATCH */
{ NULL, NULL }, /* PUT */
{ NULL, NULL }, /* REPORT */
{ &meth_trace, NULL }, /* TRACE */
{ NULL, NULL }, /* UNBIND */
{ NULL, NULL } /* UNLOCK */
}
};
/*
* Namespace callbacks
*/
static jmap_settings_t my_jmap_settings = {
HASH_TABLE_INITIALIZER, NULL, { 0 }, PTRARRAY_INITIALIZER
};
static void jmap_init(struct buf *serverinfo)
{
#ifdef USE_XAPIAN
#include "xapian_wrap.h"
buf_printf(serverinfo, " Xapian/%s", xapian_version_string());
#endif
namespace_jmap.enabled =
config_httpmodules & IMAP_ENUM_HTTPMODULES_JMAP;
if (namespace_jmap.enabled && !config_getswitch(IMAPOPT_CONVERSATIONS)) {
syslog(LOG_ERR,
"ERROR: cannot enable %s module with conversations disabled",
namespace_jmap.name);
namespace_jmap.enabled = 0;
}
if (!namespace_jmap.enabled) return;
compile_time = calc_compile_time(__TIME__, __DATE__);
initialize_JMAP_error_table();
construct_hash_table(&my_jmap_settings.methods, 128, 0);
my_jmap_settings.server_capabilities = json_object();
jmap_core_init(&my_jmap_settings);
jmap_mail_init(&my_jmap_settings);
jmap_contact_init(&my_jmap_settings);
jmap_calendar_init(&my_jmap_settings);
jmap_backup_init(&my_jmap_settings);
+ jmap_notes_init(&my_jmap_settings);
if (ws_enabled()) {
json_object_set_new(my_jmap_settings.server_capabilities,
JMAP_URN_WEBSOCKET,
json_pack("{s:s s:b}",
"webSocketUrl", "wss:" JMAP_BASE_URL JMAP_WS_COL,
"supportsWebSocketPush", 0));
}
}
static int jmap_auth(const char *userid __attribute__((unused)))
{
/* Set namespace */
mboxname_init_namespace(&jmap_namespace,
httpd_userisadmin || httpd_userisproxyadmin);
return 0;
}
static int jmap_need_auth(struct transaction_t *txn __attribute__((unused)))
{
/* All endpoints require authentication */
return HTTP_UNAUTHORIZED;
}
static void jmap_shutdown(void)
{
free_hash_table(&my_jmap_settings.methods, NULL);
json_decref(my_jmap_settings.server_capabilities);
}
/*
* HTTP method handlers
*/
enum {
JMAP_ENDPOINT_API,
JMAP_ENDPOINT_WS,
JMAP_ENDPOINT_UPLOAD,
JMAP_ENDPOINT_DOWNLOAD
};
static int jmap_parse_path(struct transaction_t *txn)
{
struct request_target_t *tgt = &txn->req_tgt;
size_t len;
char *p;
if (*tgt->path) return 0; /* Already parsed */
/* Make a working copy of target path */
strlcpy(tgt->path, txn->req_uri->path, sizeof(tgt->path));
p = tgt->path;
/* Sanity check namespace */
len = strlen(namespace_jmap.prefix);
if (strlen(p) < len ||
strncmp(namespace_jmap.prefix, p, len) ||
(tgt->path[len] && tgt->path[len] != '/')) {
txn->error.desc = "Namespace mismatch request target path";
return HTTP_FORBIDDEN;
}
/* Skip namespace */
p += len;
if (!*p) {
/* Canonicalize URL */
txn->location = JMAP_BASE_URL;
return HTTP_MOVED;
}
/* Check for path after prefix */
if (*++p) {
/* Get "collection" */
tgt->collection = p;
if (!strncmp(tgt->collection, JMAP_UPLOAD_COL,
strlen(JMAP_UPLOAD_COL))) {
tgt->flags = JMAP_ENDPOINT_UPLOAD;
tgt->allow = ALLOW_POST;
/* Get "resource" which must be the accountId */
tgt->resource = tgt->collection + strlen(JMAP_UPLOAD_COL);
}
else if (!strncmp(tgt->collection,
JMAP_DOWNLOAD_COL, strlen(JMAP_DOWNLOAD_COL))) {
tgt->flags = JMAP_ENDPOINT_DOWNLOAD;
tgt->allow = ALLOW_READ;
/* Get "resource" */
tgt->resource = tgt->collection + strlen(JMAP_DOWNLOAD_COL);
}
else if (ws_enabled() && !strcmp(tgt->collection, JMAP_WS_COL)) {
tgt->flags = JMAP_ENDPOINT_WS;
tgt->allow = (txn->flags.ver == VER_2) ? ALLOW_CONNECT : ALLOW_READ;
}
else {
return HTTP_NOT_FOUND;
}
}
else {
tgt->flags = JMAP_ENDPOINT_API;
tgt->allow = ALLOW_POST|ALLOW_READ;
}
return 0;
}
/* Perform a GET/HEAD request */
static int meth_get(struct transaction_t *txn,
void *params __attribute__((unused)))
{
int r = jmap_parse_path(txn);
if (!(txn->req_tgt.allow & ALLOW_READ)) {
return HTTP_NOT_FOUND;
}
else if (r) return r;
if (txn->req_tgt.flags == JMAP_ENDPOINT_API) {
return jmap_get_session(txn);
}
else if (txn->req_tgt.flags == JMAP_ENDPOINT_DOWNLOAD) {
return jmap_download(txn);
}
/* Upgrade to WebSockets over HTTP/1.1 on WS endpoint, if requested */
else if ((txn->req_tgt.flags == JMAP_ENDPOINT_WS) &&
(txn->flags.upgrade & UPGRADE_WS)) {
return ws_start_channel(txn, JMAP_WS_PROTOCOL, &jmap_ws);
}
return HTTP_NO_CONTENT;
}
static int json_response(int code, struct transaction_t *txn, json_t *root)
{
size_t flags = JSON_PRESERVE_ORDER;
char *buf;
/* Dump JSON object into a text buffer */
flags |= (config_httpprettytelemetry ? JSON_INDENT(2) : JSON_COMPACT);
buf = json_dumps(root, flags);
json_decref(root);
if (!buf) {
txn->error.desc = "Error dumping JSON object";
return HTTP_SERVER_ERROR;
}
/* Output the JSON object */
switch (code) {
case HTTP_OK:
case HTTP_CREATED:
txn->resp_body.type = "application/json; charset=utf-8";
break;
default:
txn->resp_body.type = "application/problem+json; charset=utf-8";
break;
}
write_body(code, txn, buf, strlen(buf));
free(buf);
return 0;
}
/* Perform a POST request */
static int meth_post(struct transaction_t *txn,
void *params __attribute__((unused)))
{
int ret;
json_t *res = NULL;
ret = jmap_parse_path(txn);
if (ret) return ret;
if (!(txn->req_tgt.allow & ALLOW_POST)) {
return HTTP_NOT_ALLOWED;
}
/* Handle uploads */
if (txn->req_tgt.flags == JMAP_ENDPOINT_UPLOAD) {
return jmap_upload(txn);
}
/* Regular JMAP API request */
ret = jmap_api(txn, &res, &my_jmap_settings);
if (res) {
/* Output the JSON object */
ret = json_response(ret ? ret : HTTP_OK, txn, res);
}
syslog(LOG_DEBUG, ">>>> jmap_post: Exit\n");
return ret;
}
/* Perform an OPTIONS request */
static int meth_options_jmap(struct transaction_t *txn, void *params)
{
/* Parse the path */
int r = jmap_parse_path(txn);
if (r) return r;
return meth_options(txn, params);
}
/*
* JMAP Requests
*/
static char *parse_accept_header(const char **hdr)
{
char *val = NULL;
struct accept *accept = parse_accept(hdr);
if (accept) {
char *type = NULL;
char *subtype = NULL;
struct param *params = NULL;
message_parse_type(accept->token, &type, &subtype, &params);
if (type && subtype && !strchr(type, '*') && !strchr(subtype, '*'))
val = xstrdup(accept->token);
free(type);
free(subtype);
param_free(&params);
struct accept *tmp;
for (tmp = accept; tmp && tmp->token; tmp++) {
free(tmp->token);
}
free(accept);
}
return val;
}
static int jmap_getblob_default_handler(jmap_req_t *req,
const char *blobid,
const char *accept_mime)
{
struct mailbox *mbox = NULL;
msgrecord_t *mr = NULL;
struct body *body = NULL;
const struct body *part = NULL;
struct buf msg_buf = BUF_INITIALIZER;
struct buf blob = BUF_INITIALIZER;
char *decbuf = NULL;
int res = 0;
if (*blobid != 'G') {
req->txn->error.desc = "invalid blobid (doesn't start with G)";
return HTTP_BAD_REQUEST;
}
if (strlen(blobid) != 41) {
/* incomplete or incorrect blobid */
req->txn->error.desc = "invalid blobid (not 41 chars)";
return HTTP_BAD_REQUEST;
}
/* Find part containing blob */
int r = jmap_findblob(req, NULL/*accountid*/, blobid,
&mbox, &mr, &body, &part, &msg_buf);
if (r) {
res = HTTP_NOT_FOUND; // XXX errors?
req->txn->error.desc = "failed to find blob by id";
goto done;
}
// default with no part is the whole message
const char *base = msg_buf.s;
size_t len = msg_buf.len;
if (part) {
// map into just this part
base += part->content_offset;
len = part->content_size;
// binary decode if needed
int encoding = part->charset_enc & 0xff;
base = charset_decode_mimebody(base, len, encoding, &decbuf, &len);
}
// success
buf_setmap(&blob, base, len);
req->txn->resp_body.type = accept_mime ? accept_mime : "application/octet-stream";
req->txn->resp_body.len = buf_len(&blob);
write_body(HTTP_OK, req->txn, buf_base(&blob), buf_len(&blob));
res = HTTP_OK;
done:
free(decbuf);
if (mbox) jmap_closembox(req, &mbox);
if (body) {
message_free_body(body);
free(body);
}
if (mr) {
msgrecord_unref(&mr);
}
buf_free(&msg_buf);
buf_free(&blob);
return res;
}
/* Handle a GET on the download endpoint */
static int jmap_download(struct transaction_t *txn)
{
const char *userid = txn->req_tgt.resource;
const char *slash = strchr(userid, '/');
if (!slash) {
/* XXX - error, needs AccountId */
return HTTP_NOT_FOUND;
}
const char *blobbase = slash + 1;
slash = strchr(blobbase, '/');
if (!slash) {
/* XXX - error, needs blobid */
txn->error.desc = "failed to find blobid";
return HTTP_BAD_REQUEST;
}
size_t bloblen = slash - blobbase;
const char *fname = slash + 1;
/* now we're allocating memory, so don't return from here! */
char *accountid = xstrndup(userid, strchr(userid, '/') - userid);
int res = 0;
struct conversations_state *cstate = NULL;
int r = conversations_open_user(accountid, 1/*shared*/, &cstate);
if (r) {
txn->error.desc = error_message(r);
res = (r == IMAP_MAILBOX_BADNAME) ? HTTP_NOT_FOUND : HTTP_SERVER_ERROR;
free(accountid);
return res;
}
char *blobid = NULL;
char *accept_mime = NULL;
struct buf blob = BUF_INITIALIZER;
/* Initialize request context */
struct jmap_req req;
jmap_initreq(&req);
req.userid = httpd_userid;
req.accountid = accountid;
req.cstate = cstate;
req.authstate = httpd_authstate;
req.txn = txn;
/* Initialize ACL mailbox cache for findblob */
hash_table mboxrights = HASH_TABLE_INITIALIZER;
construct_hash_table(&mboxrights, 64, 0);
req.mboxrights = &mboxrights;
blobid = xstrndup(blobbase, bloblen);
struct strlist *param;
if ((param = hash_lookup("accept", &txn->req_qparams))) {
accept_mime = xstrdup(param->s);
}
const char **hdr;
if (!accept_mime && (hdr = spool_getheader(txn->req_hdrs, "Accept"))) {
accept_mime = parse_accept_header(hdr);
}
/* Set Content-Disposition header */
txn->resp_body.dispo.attach = fname != NULL;
txn->resp_body.dispo.fname = fname;
/* Call blob download handlers */
int i;
for (i = 0; i < ptrarray_size(&my_jmap_settings.getblob_handlers); i++) {
jmap_getblob_handler *h = ptrarray_nth(&my_jmap_settings.getblob_handlers, i);
res = h(&req, blobid, accept_mime);
if (res) break;
}
if (!res) {
/* Try default blob download handler */
res = jmap_getblob_default_handler(&req, blobid, accept_mime);
if (!res) res = HTTP_NOT_FOUND;
}
if (res != HTTP_OK) {
if (!txn->error.desc) txn->error.desc = error_message(res);
txn->resp_body.dispo.attach = 0;
txn->resp_body.dispo.fname = NULL;
}
else if (res == HTTP_OK) res = 0;
buf_free(&blob);
free_hash_table(&mboxrights, free);
conversations_commit(&cstate);
free(accept_mime);
free(accountid);
free(blobid);
jmap_finireq(&req);
return res;
}
static int lookup_upload_collection(const char *accountid, mbentry_t **mbentry)
{
mbname_t *mbname;
const char *uploadname;
int r;
/* Create upload mailbox name from the parsed path */
mbname = mbname_from_userid(accountid);
mbname_push_boxes(mbname, config_getstring(IMAPOPT_JMAPUPLOADFOLDER));
/* XXX - hack to allow @domain parts for non-domain-split users */
if (httpd_extradomain) {
/* not allowed to be cross domain */
if (mbname_localpart(mbname) &&
strcmpsafe(mbname_domain(mbname), httpd_extradomain)) {
r = HTTP_NOT_FOUND;
goto done;
}
mbname_set_domain(mbname, NULL);
}
/* Locate the mailbox */
uploadname = mbname_intname(mbname);
r = http_mlookup(uploadname, mbentry, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* Find location of INBOX */
char *inboxname = mboxname_user_mbox(accountid, NULL);
int r1 = http_mlookup(inboxname, mbentry, NULL);
free(inboxname);
if (r1 == IMAP_MAILBOX_NONEXISTENT) {
r = IMAP_INVALID_USER;
goto done;
}
int rights = httpd_myrights(httpd_authstate, *mbentry);
if (!(rights & ACL_CREATE)) {
r = IMAP_PERMISSION_DENIED;
goto done;
}
if (*mbentry) free((*mbentry)->name);
else *mbentry = mboxlist_entry_create();
(*mbentry)->name = xstrdup(uploadname);
}
else if (!r) {
int rights = httpd_myrights(httpd_authstate, *mbentry);
if (!(rights & ACL_INSERT)) {
r = IMAP_PERMISSION_DENIED;
goto done;
}
}
done:
mbname_free(&mbname);
return r;
}
static int _create_upload_collection(const char *accountid,
struct mailbox **mailbox)
{
/* upload collection */
struct mboxlock *namespacelock = user_namespacelock(accountid);
mbentry_t *mbentry = NULL;
int r = lookup_upload_collection(accountid, &mbentry);
if (r == IMAP_INVALID_USER) {
goto done;
}
else if (r == IMAP_PERMISSION_DENIED) {
goto done;
}
else if (r == IMAP_MAILBOX_NONEXISTENT) {
if (!mbentry) goto done;
else if (mbentry->server) {
proxy_findserver(mbentry->server, &http_protocol, httpd_userid,
&backend_cached, NULL, NULL, httpd_in);
goto done;
}
r = mboxlist_createmailbox(mbentry->name, MBTYPE_COLLECTION,
NULL, 1 /* admin */, accountid,
httpd_authstate, 0, 0, 0, 0, mailbox);
/* we lost the race, that's OK */
if (r == IMAP_MAILBOX_LOCKED) r = 0;
else {
if (r) {
syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
mbentry->name, error_message(r));
}
goto done;
}
}
else if (r) goto done;
if (mailbox) {
/* Open mailbox for writing */
r = mailbox_open_iwl(mbentry->name, mailbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mbentry->name, error_message(r));
}
}
done:
mboxname_release(&namespacelock);
mboxlist_entry_free(&mbentry);
return r;
}
HIDDEN int jmap_open_upload_collection(const char *accountid,
struct mailbox **mailbox)
{
/* upload collection */
mbentry_t *mbentry = NULL;
int r = lookup_upload_collection(accountid, &mbentry);
if (r) {
mboxlist_entry_free(&mbentry);
return _create_upload_collection(accountid, mailbox);
}
if (mailbox) {
/* Open mailbox for writing */
r = mailbox_open_iwl(mbentry->name, mailbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mbentry->name, error_message(r));
}
}
mboxlist_entry_free(&mbentry);
return r;
}
/* Helper function to determine domain of data */
enum {
DOMAIN_7BIT = 0,
DOMAIN_8BIT,
DOMAIN_BINARY
};
static int data_domain(const char *p, size_t n)
{
int r = DOMAIN_7BIT;
while (n--) {
if (!*p) return DOMAIN_BINARY;
if (*p & 0x80) r = DOMAIN_8BIT;
p++;
}
return r;
}
/* Handle a POST on the upload endpoint */
static int jmap_upload(struct transaction_t *txn)
{
strarray_t flags = STRARRAY_INITIALIZER;
struct body *body = NULL;
int ret = HTTP_CREATED;
hdrcache_t hdrcache = txn->req_hdrs;
struct stagemsg *stage = NULL;
FILE *f = NULL;
const char **hdr;
time_t now = time(NULL);
struct appendstate as;
char *normalisedtype = NULL;
int rawmessage = 0;
struct mailbox *mailbox = NULL;
int r = 0;
/* Read body */
txn->req_body.flags |= BODY_DECODE;
r = http_read_req_body(txn);
if (r) {
txn->flags.conn = CONN_CLOSE;
return r;
}
const char *data = buf_base(&txn->req_body.payload);
size_t datalen = buf_len(&txn->req_body.payload);
if (datalen > (size_t) my_jmap_settings.limits[MAX_SIZE_UPLOAD]) {
txn->error.desc = "JSON upload byte size exceeds maxSizeUpload";
return HTTP_PAYLOAD_TOO_LARGE;
}
/* Resource must be {accountId}/ with no trailing path */
char *accountid = xstrdup(txn->req_tgt.resource);
char *slash = strchr(accountid, '/');
if (!slash || *(slash + 1) != '\0') {
ret = HTTP_NOT_FOUND;
goto done;
}
*slash = '\0';
r = jmap_open_upload_collection(accountid, &mailbox);
if (r) {
syslog(LOG_ERR, "jmap_upload: can't open upload collection for %s: %s",
error_message(r), accountid);
ret = HTTP_NOT_FOUND;
goto done;
}
/* Prepare to stage the message */
if (!(f = append_newstage(mailbox->name, now, 0, &stage))) {
syslog(LOG_ERR, "append_newstage(%s) failed", mailbox->name);
txn->error.desc = "append_newstage() failed";
ret = HTTP_SERVER_ERROR;
goto done;
}
const char *type = "application/octet-stream";
if ((hdr = spool_getheader(hdrcache, "Content-Type"))) {
type = hdr[0];
}
/* Remove CFWS and encodings from type */
normalisedtype = charset_decode_mimeheader(type, CHARSET_KEEPCASE);
if (!strcasecmp(normalisedtype, "message/rfc822")) {
struct protstream *stream = prot_readmap(data, datalen);
r = message_copy_strict(stream, f, datalen, 0);
prot_free(stream);
if (!r) {
rawmessage = 1;
goto wrotebody;
}
// otherwise we gotta clean up and make it an attachment
ftruncate(fileno(f), 0L);
fseek(f, 0L, SEEK_SET);
}
/* Create RFC 5322 header for resource */
if ((hdr = spool_getheader(hdrcache, "User-Agent"))) {
fprintf(f, "User-Agent: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "From"))) {
fprintf(f, "From: %s\r\n", hdr[0]);
}
else {
char *mimehdr;
assert(!buf_len(&txn->buf));
if (strchr(httpd_userid, '@')) {
/* XXX This needs to be done via an LDAP/DB lookup */
buf_printf(&txn->buf, "<%s>", httpd_userid);
}
else {
buf_printf(&txn->buf, "<%s@%s>", httpd_userid, config_servername);
}
mimehdr = charset_encode_mimeheader(buf_cstring(&txn->buf),
buf_len(&txn->buf), 0);
fprintf(f, "From: %s\r\n", mimehdr);
free(mimehdr);
buf_reset(&txn->buf);
}
if ((hdr = spool_getheader(hdrcache, "Subject"))) {
fprintf(f, "Subject: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "Date"))) {
fprintf(f, "Date: %s\r\n", hdr[0]);
}
else {
char datestr[80];
time_to_rfc5322(now, datestr, sizeof(datestr));
fprintf(f, "Date: %s\r\n", datestr);
}
if ((hdr = spool_getheader(hdrcache, "Message-ID"))) {
fprintf(f, "Message-ID: %s\r\n", hdr[0]);
}
fprintf(f, "Content-Type: %s\r\n", type);
int domain = data_domain(data, datalen);
switch (domain) {
case DOMAIN_BINARY:
fputs("Content-Transfer-Encoding: BINARY\r\n", f);
break;
case DOMAIN_8BIT:
fputs("Content-Transfer-Encoding: 8BIT\r\n", f);
break;
default:
break; // no CTE == 7bit
}
if ((hdr = spool_getheader(hdrcache, "Content-Disposition"))) {
fprintf(f, "Content-Disposition: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "Content-Description"))) {
fprintf(f, "Content-Description: %s\r\n", hdr[0]);
}
fprintf(f, "Content-Length: %u\r\n", (unsigned) datalen);
fputs("MIME-Version: 1.0\r\n\r\n", f);
/* Write the data to the file */
fwrite(data, datalen, 1, f);
wrotebody:
fclose(f);
/* Prepare to append the message to the mailbox */
r = append_setup_mbox(&as, mailbox, httpd_userid, httpd_authstate,
0, /*quota*/NULL, 0, 0, /*event*/0);
if (r) {
syslog(LOG_ERR, "append_setup(%s) failed: %s",
mailbox->name, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_setup() failed";
goto done;
}
/* Append the message to the mailbox */
strarray_append(&flags, "\\Deleted");
strarray_append(&flags, "\\Expunged"); // custom flag to insta-expunge!
r = append_fromstage(&as, &body, stage, now, 0, &flags, 0, /*annots*/NULL);
if (r) {
append_abort(&as);
syslog(LOG_ERR, "append_fromstage(%s) failed: %s",
mailbox->name, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_fromstage() failed";
goto done;
}
r = append_commit(&as);
if (r) {
syslog(LOG_ERR, "append_commit(%s) failed: %s",
mailbox->name, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_commit() failed";
goto done;
}
char datestr[RFC3339_DATETIME_MAX];
time_to_rfc3339(now + 86400, datestr, RFC3339_DATETIME_MAX);
char blob_id[JMAP_BLOBID_SIZE];
jmap_set_blobid(rawmessage ? &body->guid : &body->content_guid, blob_id);
/* Create response object */
json_t *resp = json_pack("{s:s}", "accountId", accountid);
json_object_set_new(resp, "blobId", json_string(blob_id));
json_object_set_new(resp, "size", json_integer(datalen));
json_object_set_new(resp, "expires", json_string(datestr));
json_object_set_new(resp, "type", json_string(normalisedtype));
/* Output the JSON object */
ret = json_response(HTTP_CREATED, txn, resp);
done:
free(normalisedtype);
free(accountid);
if (body) {
message_free_body(body);
free(body);
}
strarray_fini(&flags);
append_removestage(stage);
if (mailbox) {
if (r) mailbox_abort(mailbox);
else r = mailbox_commit(mailbox);
mailbox_close(&mailbox);
}
return ret;
}
/* Handle a GET on the session endpoint */
static int jmap_get_session(struct transaction_t *txn)
{
json_t *jsession = json_object();
/* URLs */
json_object_set_new(jsession, "username", json_string(httpd_userid));
json_object_set_new(jsession, "apiUrl", json_string(JMAP_BASE_URL));
json_object_set_new(jsession, "downloadUrl",
json_string(JMAP_BASE_URL JMAP_DOWNLOAD_COL JMAP_DOWNLOAD_TPL));
json_object_set_new(jsession, "uploadUrl",
json_string(JMAP_BASE_URL JMAP_UPLOAD_COL JMAP_UPLOAD_TPL));
/* TODO eventSourceUrl */
/* state */
char *inboxname = mboxname_user_mbox(httpd_userid, NULL);
struct buf state = BUF_INITIALIZER;
buf_printf(&state, MODSEQ_FMT, mboxname_readraclmodseq(inboxname));
json_object_set_new(jsession, "state", json_string(buf_cstring(&state)));
free(inboxname);
buf_free(&state);
/* capabilities */
json_object_set(jsession, "capabilities", my_jmap_settings.server_capabilities);
json_t *accounts = json_object();
json_t *primary_accounts = json_object();
jmap_accounts(accounts, primary_accounts);
json_object_set_new(jsession, "accounts", accounts);
json_object_set_new(jsession, "primaryAccounts", primary_accounts);
/* Response should not be cached */
txn->flags.cc |= CC_NOCACHE | CC_NOSTORE | CC_REVALIDATE;
/* Write the JSON response */
return json_response(HTTP_OK, txn, jsession);
}
/*
* WebSockets data callback ('jmap' sub-protocol): Process JMAP API request.
*
* Can be tested with:
* https://github.com/websockets/wscat
* https://chrome.google.com/webstore/detail/web-socket-client/lifhekgaodigcpmnakfhaaaboididbdn
*
* WebSockets over HTTP/2 currently only available in:
* https://www.google.com/chrome/browser/canary.html
*/
static int jmap_ws(struct buf *inbuf, struct buf *outbuf,
struct buf *logbuf, void **rock)
{
struct transaction_t **txnp = (struct transaction_t **) rock;
struct transaction_t *txn = *txnp;
json_t *res = NULL;
int ret;
if (!txn) {
/* Create a transaction rock to use for API requests */
txn = *txnp = xzmalloc(sizeof(struct transaction_t));
txn->meth = METH_UNKNOWN;
txn->req_body.flags = BODY_DONE;
/* Create header cache */
txn->req_hdrs = spool_new_hdrcache();
if (!txn->req_hdrs) {
free(txn);
return HTTP_SERVER_ERROR;
}
/* Set Content-Type of request payload */
spool_cache_header(xstrdup("Content-Type"),
xstrdup("application/json"), txn->req_hdrs);
}
else if (!inbuf) {
/* Free transaction rock */
transaction_free(txn);
free(txn);
return 0;
}
/* Set request payload */
buf_init_ro(&txn->req_body.payload, buf_base(inbuf), buf_len(inbuf));
/* Process the API request */
ret = jmap_api(txn, &res, &my_jmap_settings);
/* Free request payload */
buf_free(&txn->req_body.payload);
if (logbuf) {
/* Log JMAP methods */
const char **hdr = spool_getheader(txn->req_hdrs, ":jmap");
if (hdr) buf_printf(logbuf, "; jmap=%s", hdr[0]);
}
if (!ret) {
/* Return the JSON object */
size_t flags = JSON_PRESERVE_ORDER;
char *buf;
/* Dump JSON object into a text buffer */
flags |= (config_httpprettytelemetry ? JSON_INDENT(2) : JSON_COMPACT);
buf = json_dumps(res, flags);
json_decref(res);
buf_initm(outbuf, buf, strlen(buf));
}
return ret;
}
diff --git a/imap/jmap_api.c b/imap/jmap_api.c
index 626256007..6d3e09ff3 100644
--- a/imap/jmap_api.c
+++ b/imap/jmap_api.c
@@ -1,3117 +1,3118 @@
/* jmap_api.c -- Routines for handling JMAP API requests
*
* Copyright (c) 1994-2018 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <config.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <errno.h>
#include "append.h"
#include "bsearch.h"
#include "cyrusdb.h"
#include "hash.h"
#include "httpd.h"
#include "http_dav.h"
#include "http_dav_sharing.h"
#include "http_jmap.h"
#include "mboxname.h"
#include "msgrecord.h"
#include "proxy.h"
#include "times.h"
#include "strhash.h"
#include "syslog.h"
#include "xstrlcpy.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include "imap/imap_err.h"
#include "imap/jmap_err.h"
static json_t *extract_value(json_t *from, const char *path, ptrarray_t *refs);
static json_t *extract_array_value(json_t *val, const char *idx,
const char *path, ptrarray_t *pool)
{
if (!strcmp(idx, "*")) {
/* Build value from array traversal */
json_t *newval = json_pack("[]");
size_t i;
json_t *v;
json_array_foreach(val, i, v) {
json_t *x = extract_value(v, path, pool);
if (json_is_array(x)) {
/* JMAP spec: "If the result of applying the rest
* of the pointer tokens to a value was itself an
* array, its items should be included individually
* in the output rather than including the array
* itself." */
json_array_extend(newval, x);
} else if (x) {
json_array_append(newval, x);
} else {
json_decref(newval);
newval = NULL;
}
}
if (newval) {
ptrarray_add(pool, newval);
}
return newval;
}
/* Lookup array value by index */
const char *eot = NULL;
bit64 num;
if (parsenum(idx, &eot, 0, &num) || *eot) {
return NULL;
}
val = json_array_get(val, num);
if (!val) {
return NULL;
}
return extract_value(val, path, pool);
}
/* Extract the JSON value at position path from val.
*
* Return NULL, if the the value does not exist or if
* path is erroneous.
*/
static json_t *extract_value(json_t *val, const char *path, ptrarray_t *pool)
{
/* Return value for empty path */
if (*path == '\0') {
return val;
}
/* Be lenient: root path '/' is optional */
if (*path == '/') {
path++;
}
/* Walk over path segments */
while (val && *path) {
const char *top = NULL;
char *p = NULL;
/* Extract next path segment */
if (!(top = strchr(path, '/'))) {
top = strchr(path, '\0');
}
p = jmap_pointer_decode(path, top - path);
if (*p == '\0') {
return NULL;
}
/* Extract array value */
if (json_is_array(val)) {
val = extract_array_value(val, p, top, pool);
free(p);
return val;
}
/* Value MUST be an object now */
if (!json_is_object(val)) {
free(p);
return NULL;
}
/* Step down into object tree */
val = json_object_get(val, p);
free(p);
path = *top ? top + 1 : top;
}
return val;
}
static int process_resultrefs(json_t *args, json_t *resp, json_t **err)
{
json_t *ref;
const char *arg;
int ret = -1;
void *tmp;
json_object_foreach_safe(args, tmp, arg, ref) {
if (*arg != '#' || *(arg+1) == '\0') {
continue;
}
if (json_object_get(args, arg + 1)) {
*err = json_pack("{s:s, s:[s]}",
"type", "invalidArguments", "arguments", arg);
goto fail;
}
const char *of, *path, *name;
json_t *res = NULL;
/* Parse result reference object */
of = json_string_value(json_object_get(ref, "resultOf"));
if (!of || *of == '\0') {
goto fail;
}
path = json_string_value(json_object_get(ref, "path"));
if (!path || *path == '\0') {
goto fail;
}
name = json_string_value(json_object_get(ref, "name"));
if (!name || *name == '\0') {
goto fail;
}
/* Lookup referenced response */
json_t *v;
size_t i;
json_array_foreach(resp, i, v) {
const char *tag = json_string_value(json_array_get(v, 2));
if (!tag || strcmp(tag, of)) {
continue;
}
const char *mname = json_string_value(json_array_get(v, 0));
if (!mname || strcmp(name, mname)) {
goto fail;
}
res = v;
break;
}
if (!res) goto fail;
/* Extract the reference argument value. */
/* We maintain our own pool of newly created JSON objects, since
* tracking reference counts across newly created JSON arrays is
* a pain. Rule: If you incref an existing JSON value or create
* an entirely new one, put it into the pool for cleanup. */
ptrarray_t pool = PTRARRAY_INITIALIZER;
json_t *val = extract_value(json_array_get(res, 1), path, &pool);
if (!val) goto fail;
/* Replace both key and value of the reference entry */
json_object_set(args, arg + 1, val);
json_object_del(args, arg);
/* Clean up reference counts of pooled JSON objects */
json_t *ref;
while ((ref = ptrarray_pop(&pool))) {
json_decref(ref);
}
ptrarray_fini(&pool);
}
return 0;
fail:
return ret;
}
static int parse_json_body(struct transaction_t *txn, json_t **req)
{
const char **hdr;
json_error_t jerr;
int ret;
/* Check Content-Type */
if (!(hdr = spool_getheader(txn->req_hdrs, "Content-Type")) ||
!is_mediatype("application/json", hdr[0])) {
txn->error.desc = "This method requires a JSON request body";
return HTTP_BAD_MEDIATYPE;
}
/* Read body */
txn->req_body.flags |= BODY_DECODE;
ret = http_read_req_body(txn);
if (ret) {
txn->flags.conn = CONN_CLOSE;
return ret;
}
/* Parse the JSON request */
*req = json_loads(buf_cstring(&txn->req_body.payload), 0, &jerr);
if (!*req) {
buf_reset(&txn->buf);
buf_printf(&txn->buf,
"Unable to parse JSON request body: %s", jerr.text);
txn->error.desc = buf_cstring(&txn->buf);
return JMAP_NOT_JSON;
}
return 0;
}
static int validate_request(struct transaction_t *txn, json_t *req,
jmap_settings_t *settings)
{
json_t *using = json_object_get(req, "using");
json_t *calls = json_object_get(req, "methodCalls");
if (!json_is_array(using) || !json_is_array(calls)) {
return JMAP_NOT_REQUEST;
}
/*
* XXX the following maximums are not enforced:
* maxConcurrentUpload
* maxConcurrentRequests
*/
if (buf_len(&txn->req_body.payload) >
(size_t) settings->limits[MAX_SIZE_REQUEST]) {
return JMAP_LIMIT_SIZE;
}
size_t i;
json_t *val;
json_array_foreach(calls, i, val) {
if (json_array_size(val) != 3 ||
!json_is_string(json_array_get(val, 0)) ||
!json_is_object(json_array_get(val, 1)) ||
!json_is_string(json_array_get(val, 2))) {
return JMAP_NOT_REQUEST;
}
if (i >= (size_t) settings->limits[MAX_CALLS_IN_REQUEST]) {
return JMAP_LIMIT_CALLS;
}
const char *mname = json_string_value(json_array_get(val, 0));
mname = strchr(mname, '/');
if (!mname) continue;
if (!strcmp(mname, "get")) {
json_t *ids = json_object_get(json_array_get(val, 1), "ids");
if (json_array_size(ids) >
(size_t) settings->limits[MAX_OBJECTS_IN_GET]) {
return JMAP_LIMIT_OBJS_GET;
}
}
else if (!strcmp(mname, "set")) {
json_t *args = json_array_get(val, 1);
size_t size = json_object_size(json_object_get(args, "create"));
size += json_object_size(json_object_get(args, "update"));
size += json_array_size(json_object_get(args, "destroy"));
if (size > (size_t) settings->limits[MAX_OBJECTS_IN_SET]) {
return JMAP_LIMIT_OBJS_SET;
}
}
}
json_array_foreach(using, i, val) {
const char *s = json_string_value(val);
if (!s) {
return JMAP_NOT_REQUEST;
}
else if (!strcmp(s, "ietf:jmap")) {
syslog(LOG_DEBUG, "old capability %s used", s);
}
else if (!strcmp(s, "ietf:jmapmail")) {
syslog(LOG_DEBUG, "old capability %s used", s);
}
else if (!json_object_get(settings->server_capabilities, s)) {
return JMAP_UNKNOWN_CAPABILITY;
}
}
return 0;
}
HIDDEN int jmap_is_valid_id(const char *id)
{
if (!id || *id == '\0') return 0;
const char *p;
for (p = id; *p; p++) {
if (('0' <= *p && *p <= '9'))
continue;
if (('a' <= *p && *p <= 'z') || ('A' <= *p && *p <= 'Z'))
continue;
if ((*p == '-') || (*p == '_'))
continue;
return 0;
}
return 1;
}
static void _make_created_ids(const char *creation_id, void *val, void *rock)
{
json_t *jcreatedIds = rock;
const char *id = val;
json_object_set_new(jcreatedIds, creation_id, json_string(id));
}
static int jmap_error_response(struct transaction_t *txn,
long code, json_t **res)
{
long http_code = HTTP_BAD_REQUEST;
const char *type, *title, *limit = NULL;
/* Error string is encoded as type NUL title [ NUL limit ] */
type = error_message(code);
title = type + strlen(type) + 1;
switch (code) {
case JMAP_NOT_JSON:
case JMAP_NOT_REQUEST:
case JMAP_UNKNOWN_CAPABILITY:
break;
case JMAP_LIMIT_SIZE:
http_code = HTTP_PAYLOAD_TOO_LARGE;
GCC_FALLTHROUGH
case JMAP_LIMIT_CALLS:
case JMAP_LIMIT_OBJS_GET:
case JMAP_LIMIT_OBJS_SET:
limit = title + strlen(title) + 1;
break;
default:
/* Actually an HTTP code, not a JMAP error code */
return code;
}
if (txn->meth == METH_UNKNOWN) {
/* API request over WebSocket */
*res = json_pack("{s:s s:s s:s s:i}",
"@type", "RequestError", "type", type, "title", title,
"status", atoi(error_message(http_code)));
}
else {
*res = json_pack("{s:s s:s s:i}", "type", type, "title", title,
"status", atoi(error_message(http_code)));
}
if (!*res) {
txn->error.desc = "Unable to create JSON response";
return HTTP_SERVER_ERROR;
}
if (limit) {
json_object_set_new(*res, "limit", json_string(limit));
}
if (txn->error.desc) {
json_object_set_new(*res, "detail", json_string(txn->error.desc));
}
return http_code;
}
HIDDEN int jmap_initreq(jmap_req_t *req)
{
memset(req, 0, sizeof(struct jmap_req));
req->mboxes = ptrarray_new();
return 0;
}
struct _mboxcache_rec {
struct mailbox *mbox;
int refcount;
int rw;
};
HIDDEN void jmap_finireq(jmap_req_t *req)
{
int i;
for (i = 0; i < req->mboxes->count; i++) {
struct _mboxcache_rec *rec = ptrarray_nth(req->mboxes, i);
syslog(LOG_ERR, "jmap: force-closing mailbox %s (refcount=%d)",
rec->mbox->name, rec->refcount);
mailbox_close(&rec->mbox);
free(rec);
}
/* Fail after cleaning up open mailboxes */
assert(!req->mboxes->count);
ptrarray_free(req->mboxes);
req->mboxes = NULL;
jmap_mbentry_cache_free(req);
json_decref(req->perf_details);
req->perf_details = NULL;
}
static jmap_method_t *find_methodproc(const char *name, hash_table *jmap_methods)
{
return hash_lookup(name, jmap_methods);
}
/* Return the ACL for mbentry for the authstate of userid.
* Lookup and store ACL rights in the mboxrights cache. */
static int _rights_for_mbentry(struct auth_state *authstate,
const mbentry_t *mbentry,
hash_table *mboxrights)
{
if (!mbentry) return 0;
/* Lookup cached rights */
int *rightsptr = hash_lookup(mbentry->name, mboxrights);
if (rightsptr) return *rightsptr;
int rights = 0;
/* Lookup ACL */
mbname_t *mbname = mbname_from_intname(mbentry->name);
if (mbentry->mbtype & MBTYPE_INTERMEDIATE) {
// if it's an intermediate mailbox, we get rights from the parent
mbentry_t *parententry = NULL;
if (mboxlist_findparent(mbentry->name, &parententry))
rights = 0;
else
rights = httpd_myrights(authstate, parententry);
mboxlist_entry_free(&parententry);
}
else rights = httpd_myrights(authstate, mbentry);
/* Cache rights */
rightsptr = xmalloc(sizeof(int));
*rightsptr = rights;
hash_insert(mbentry->name, rightsptr, mboxrights);
mbname_free(&mbname);
return rights;
}
struct capabilities_rock {
const char *authuserid;
hash_table *mboxrights;
struct auth_state *authstate;
int is_visible;
int has_mail;
int has_contacts;
int has_calendars;
};
static int capabilities_cb(const mbentry_t *mbentry, void *vrock)
{
struct capabilities_rock *rock = vrock;
if (!mbentry) return 0;
if ((mbentry->mbtype & MBTYPE_DELETED) ||
(mbentry->mbtype & MBTYPE_MOVING) ||
(mbentry->mbtype & MBTYPE_REMOTE) ||
(mbentry->mbtype & MBTYPE_RESERVE)) {
return 0;
}
int rights = _rights_for_mbentry(rock->authstate, mbentry, rock->mboxrights);
if (!(rights & ACL_LOOKUP)) return 0;
rock->is_visible = 1;
mbname_t *mbname = mbname_from_intname(mbentry->name);
const strarray_t *boxes = mbname_boxes(mbname);
if (!rock->has_mail) {
rock->has_mail = mbentry->mbtype == MBTYPE_EMAIL;
}
if (!rock->has_contacts) {
rock->has_contacts = strarray_size(boxes) >= 1 &&
!strcmpsafe(config_getstring(IMAPOPT_ADDRESSBOOKPREFIX),
strarray_nth(boxes, 0));
}
if (!rock->has_calendars) {
rock->has_calendars = strarray_size(boxes) >= 1 &&
!strcmpsafe(config_getstring(IMAPOPT_CALENDARPREFIX),
strarray_nth(boxes, 0));
}
mbname_free(&mbname);
return 0;
}
static json_t *lookup_capabilities(const char *accountid,
const char *authuserid,
struct auth_state *authstate,
hash_table *mboxrights)
{
// we need to know if we can write children of the inbox
mbentry_t *inboxentry = NULL;
char *inboxname = mboxname_user_mbox(accountid, NULL);
if (mboxlist_lookup(inboxname, &inboxentry, NULL)) {
free(inboxname);
return json_null();
}
free(inboxname);
int inboxrights = _rights_for_mbentry(authstate, inboxentry, mboxrights);
mboxlist_entry_free(&inboxentry);
json_t *capas = json_object();
int mayCreateTopLevel = (inboxrights & ACL_CREATE) ? 1 : 0;
if (!strcmp(authuserid, accountid)) {
/* Primary account has all capabilities */
jmap_core_capabilities(capas);
jmap_mail_capabilities(capas, mayCreateTopLevel);
jmap_emailsubmission_capabilities(capas);
jmap_vacation_capabilities(capas);
jmap_contact_capabilities(capas);
jmap_calendar_capabilities(capas);
jmap_backup_capabilities(capas);
+ jmap_notes_capabilities(capas);
}
else {
/* Lookup capabilities for shared account */
struct capabilities_rock rock = {
authuserid, mboxrights, httpd_authstate, 0, 0, 0, 0
};
mboxlist_usermboxtree(accountid, authstate, capabilities_cb,
&rock, MBOXTREE_INTERMEDIATES);
if (rock.is_visible) {
jmap_core_capabilities(capas);
if (rock.has_mail) {
// we don't offer emailsubmission or vacation
// for shared accounts right now
jmap_mail_capabilities(capas, mayCreateTopLevel);
}
if (rock.has_contacts) {
jmap_contact_capabilities(capas);
}
if (rock.has_calendars) {
jmap_calendar_capabilities(capas);
}
// should we offer Backup/restoreXxx for shared accounts?
}
}
if (!json_object_size(capas)) {
json_decref(capas);
capas = json_null();
}
return capas;
}
static void _free_json(void *val)
{
json_decref((json_t *)val);
}
/* Perform an API request */
HIDDEN int jmap_api(struct transaction_t *txn, json_t **res,
jmap_settings_t *settings)
{
json_t *jreq = NULL, *resp = NULL;
size_t i;
int ret, do_perf = 0;
char *account_inboxname = NULL;
int return_created_ids = 0;
hash_table created_ids = HASH_TABLE_INITIALIZER;
hash_table capabilities_by_accountid = HASH_TABLE_INITIALIZER;
hash_table mboxrights = HASH_TABLE_INITIALIZER;
strarray_t methods = STRARRAY_INITIALIZER;
ptrarray_t method_calls = PTRARRAY_INITIALIZER;
ptrarray_t processed_methods = PTRARRAY_INITIALIZER;
strarray_t using_capabilities = STRARRAY_INITIALIZER;
ret = parse_json_body(txn, &jreq);
if (ret) return jmap_error_response(txn, ret, res);
/* Validate Request object */
if ((ret = validate_request(txn, jreq, settings))) {
json_decref(jreq);
return jmap_error_response(txn, ret, res);
}
/* Start JSON response */
resp = json_array();
if (!resp) {
txn->error.desc = "Unable to create JSON response body";
ret = HTTP_SERVER_ERROR;
goto done;
}
/* Set up request-internal state */
construct_hash_table(&capabilities_by_accountid, 8, 0);
construct_hash_table(&mboxrights, 64, 0);
construct_hash_table(&created_ids, 1024, 0);
/* Parse client-supplied creation ids */
json_t *jcreatedIds = json_object_get(jreq, "createdIds");
if (json_is_object(jcreatedIds)) {
return_created_ids = 1;
const char *creation_id;
json_t *jval;
json_object_foreach(jcreatedIds, creation_id, jval) {
if (!json_is_string(jval)) {
txn->error.desc = "Invalid createdIds argument";
ret = HTTP_BAD_REQUEST;
goto done;
}
const char *id = json_string_value(jval);
if (!jmap_is_valid_id(creation_id) || !jmap_is_valid_id(id)) {
txn->error.desc = "Invalid createdIds argument";
ret = HTTP_BAD_REQUEST;
goto done;
}
hash_insert(creation_id, xstrdup(id), &created_ids);
}
}
else if (jcreatedIds && jcreatedIds != json_null()) {
txn->error.desc = "Invalid createdIds argument";
ret = HTTP_BAD_REQUEST;
goto done;
}
json_t *jusing = json_object_get(jreq, "using");
for (i = 0; i < json_array_size(jusing); i++) {
strarray_add(&using_capabilities, json_string_value(json_array_get(jusing, i)));
}
/* Push client method calls on call stack */
json_t *jmethod_calls = json_object_get(jreq, "methodCalls");
for (i = json_array_size(jmethod_calls); i > 0; i--) {
json_t *mc = json_array_get(jmethod_calls, i-1);
ptrarray_push(&method_calls, json_incref(mc));
}
/* Process call stack */
do_perf = strarray_find(&using_capabilities, JMAP_PERFORMANCE_EXTENSION, 0) >= 0;
json_t *mc;
while ((mc = ptrarray_pop(&method_calls))) {
/* Send provisional response, if necessary */
keepalive_response(txn);
/* Mark method as processed */
ptrarray_push(&processed_methods, mc);
/* Process method */
const jmap_method_t *mp;
const char *mname = json_string_value(json_array_get(mc, 0));
json_t *args = json_array_get(mc, 1);
const char *tag = json_string_value(json_array_get(mc, 2));
int r = 0;
strarray_append(&methods, mname);
json_incref(args);
/* Find the message processor */
mp = find_methodproc(mname, &settings->methods);
if (!mp || strarray_find(&using_capabilities, mp->capability, 0) < 0) {
json_array_append_new(resp, json_pack("[s {s:s} s]",
"error", "type", "unknownMethod", tag));
json_decref(args);
continue;
}
/* Validate accountId argument */
const char *accountid = httpd_userid;
json_t *err = NULL;
json_t *arg = json_object_get(args, "accountId");
if (arg && arg != json_null()) {
accountid = json_string_value(arg);
}
if (!accountid) {
err = json_pack("{s:s, s:[s]}",
"type", "invalidArguments", "arguments", "accountId");
json_array_append_new(resp, json_pack("[s,o,s]", "error", err, tag));
json_decref(args);
continue;
}
/* Validate supported capabilities for this account */
json_t *account_capas = hash_lookup(accountid, &capabilities_by_accountid);
if (!account_capas) {
account_capas = lookup_capabilities(accountid, httpd_userid,
httpd_authstate, &mboxrights);
hash_insert(accountid, account_capas, &capabilities_by_accountid);
}
if (json_is_null(account_capas)) {
err = json_pack("{s:s}", "type", "accountNotFound");
}
else if (!json_object_get(account_capas, mp->capability)) {
err = json_pack("{s:s}", "type", "accountNotSupportedByMethod");
}
if (err) {
json_array_append_new(resp, json_pack("[s,o,s]", "error", err, tag));
json_decref(args);
continue;
}
/* Pre-process result references */
if (process_resultrefs(args, resp, &err)) {
if (!err) err = json_pack("{s:s}", "type", "resultReference");
json_array_append_new(resp, json_pack("[s,o,s]", "error", err, tag));
json_decref(args);
continue;
}
struct conversations_state *cstate = NULL;
r = conversations_open_user(accountid, mp->flags & JMAP_SHARED_CSTATE, &cstate);
if (r) {
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
json_decref(args);
goto done;
}
/* Initialize request context */
struct jmap_req req;
jmap_initreq(&req);
req.method = mname;
req.userid = httpd_userid;
req.accountid = accountid;
req.cstate = cstate;
req.authstate = httpd_authstate;
req.args = args;
req.response = resp;
req.tag = tag;
req.created_ids = &created_ids;
req.txn = txn;
req.mboxrights = &mboxrights;
req.method_calls = &method_calls;
req.using_capabilities = &using_capabilities;
if (do_perf) {
struct rusage usage;
getrusage(RUSAGE_SELF, &usage);
req.user_start = timeval_get_double(&usage.ru_utime);
req.sys_start = timeval_get_double(&usage.ru_stime);
req.real_start = now_ms() / 1000.0;
req.perf_details = json_object();
}
/* Read the current state data in */
account_inboxname = mboxname_user_mbox(accountid, NULL);
r = mboxname_read_counters(account_inboxname, &req.counters);
free(account_inboxname);
account_inboxname = NULL;
if (r) {
conversations_abort(&req.cstate);
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
jmap_finireq(&req);
json_decref(args);
goto done;
}
/* Call the message processor. */
r = mp->proc(&req);
/* Finalize request context */
jmap_finireq(&req);
if (r) {
conversations_abort(&req.cstate);
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
json_decref(args);
goto done;
}
conversations_commit(&req.cstate);
json_decref(args);
}
/* tell syslog which methods were called */
spool_replace_header(xstrdup(":jmap"),
strarray_join(&methods, ","), txn->req_hdrs);
/* Build response */
if (txn->meth == METH_UNKNOWN) {
/* API request over WebSocket */
*res = json_pack("{s:s s:O}",
"@type", "Response", "methodResponses", resp);
}
else {
*res = json_pack("{s:O}", "methodResponses", resp);
}
if (return_created_ids) {
json_t *jcreatedIds = json_object();
hash_enumerate(&created_ids, _make_created_ids, jcreatedIds);
json_object_set_new(*res, "createdIds", jcreatedIds);
}
char *user_inboxname = mboxname_user_mbox(httpd_userid, NULL);
struct buf state = BUF_INITIALIZER;
buf_printf(&state, MODSEQ_FMT, mboxname_readraclmodseq(user_inboxname));
free(user_inboxname);
json_object_set_new(*res, "sessionState", json_string(buf_cstring(&state)));
buf_free(&state);
done:
{
/* Clean up call stack */
json_t *jval;
while ((jval = ptrarray_pop(&processed_methods))) {
json_decref(jval);
}
while ((jval = ptrarray_pop(&method_calls))) {
json_decref(jval);
}
ptrarray_fini(&processed_methods);
ptrarray_fini(&method_calls);
}
free_hash_table(&created_ids, free);
free_hash_table(&capabilities_by_accountid, _free_json);
free_hash_table(&mboxrights, free);
free(account_inboxname);
json_decref(jreq);
json_decref(resp);
strarray_fini(&methods);
strarray_fini(&using_capabilities);
return ret;
}
struct findaccounts_rock {
struct buf current_accountid;
int current_rights;
json_t *accounts;
const char *authuserid;
};
static void findaccounts_add(struct findaccounts_rock *rock)
{
if (!buf_len(&rock->current_accountid))
return;
if (!(rock->current_rights & (ACL_LOOKUP|ACL_READ)))
return;
const char *accountid = buf_cstring(&rock->current_accountid);
int is_rw = rock->current_rights & ACL_READ_WRITE;
int is_primary = !strcmp(rock->authuserid, accountid);
json_t *account = json_object();
json_object_set_new(account, "name", json_string(accountid));
json_object_set_new(account, "isPrimary", json_boolean(is_primary));
json_object_set_new(account, "isPersonal", json_boolean(is_primary));
json_object_set_new(account, "isReadOnly", json_boolean(!is_rw));
json_object_set_new(rock->accounts, accountid, account);
}
static int findaccounts_cb(struct findall_data *data, void *vrock)
{
if (!data || !data->mbentry) {
return 0;
}
struct findaccounts_rock *rock = vrock;
const mbentry_t *mbentry = data->mbentry;
mbname_t *mbname = mbname_from_intname(mbentry->name);
if (strcmp(buf_cstring(&rock->current_accountid), mbname_userid(mbname))) {
findaccounts_add(rock);
buf_setcstr(&rock->current_accountid, mbname_userid(mbname));
rock->current_rights = 0;
}
rock->current_rights |= httpd_myrights(httpd_authstate, data->mbentry);
mbname_free(&mbname);
return 0;
}
HIDDEN void jmap_accounts(json_t *accounts, json_t *primary_accounts)
{
/* Find shared accounts */
strarray_t patterns = STRARRAY_INITIALIZER;
char *userpat = xstrdup("user.*");
userpat[4] = jmap_namespace.hier_sep;
strarray_append(&patterns, userpat);
struct findaccounts_rock rock = {
BUF_INITIALIZER, 0, accounts, httpd_userid
};
int r = mboxlist_findallmulti(&jmap_namespace, &patterns, 0, httpd_userid,
httpd_authstate, findaccounts_cb, &rock);
if (r) {
syslog(LOG_ERR, "Can't determine shared JMAP accounts for user %s: %s",
httpd_userid, error_message(r));
}
findaccounts_add(&rock);
/* Add primary accout */
buf_setcstr(&rock.current_accountid, httpd_userid);
rock.current_rights = ACL_FULL;
findaccounts_add(&rock);
/* Determine account capabilities */
hash_table mboxrights = HASH_TABLE_INITIALIZER;
construct_hash_table(&mboxrights, 64, 0);
json_t *jaccount;
const char *accountid;
json_object_foreach(accounts, accountid, jaccount) {
json_t *capas = lookup_capabilities(accountid, httpd_userid,
httpd_authstate, &mboxrights);
json_object_set_new(jaccount, "accountCapabilities", capas);
}
free_hash_table(&mboxrights, free);
json_t *jprimary = json_string(httpd_userid);
json_object_set(primary_accounts, JMAP_URN_MAIL, jprimary);
json_object_set(primary_accounts, JMAP_URN_SUBMISSION, jprimary);
json_object_set(primary_accounts, JMAP_URN_VACATION, jprimary);
json_object_set(primary_accounts, JMAP_CONTACTS_EXTENSION, jprimary);
json_object_set(primary_accounts, JMAP_CALENDARS_EXTENSION, jprimary);
json_object_set(primary_accounts, JMAP_BACKUP_EXTENSION, jprimary);
json_decref(jprimary);
/* Clean up */
buf_free(&rock.current_accountid);
free(userpat);
strarray_fini(&patterns);
}
HIDDEN void jmap_add_subreq(jmap_req_t *req, const char *method,
json_t *args, const char *client_id)
{
if (!client_id) client_id = req->tag;
ptrarray_push(req->method_calls, json_pack("[s,o,s]", method, args, client_id));
}
const char *jmap_lookup_id(jmap_req_t *req, const char *creation_id)
{
return hash_lookup(creation_id, req->created_ids);
}
const char *jmap_id_string_value(jmap_req_t *req, json_t *item)
{
if (!item) return NULL;
if (!json_is_string(item)) return NULL;
const char *id = json_string_value(item);
if (*id == '#')
return jmap_lookup_id(req, id+1);
return id;
}
void jmap_add_id(jmap_req_t *req, const char *creation_id, const char *id)
{
/* It's OK to overwrite existing ids, as per Foo/set:
* "A client SHOULD NOT reuse a creation id anywhere in the same API
* request. If a creation id is reused, the server MUST map the creation
* id to the most recently created item with that id."
*/
hash_insert(creation_id, xstrdup(id), req->created_ids);
}
HIDDEN int jmap_openmbox(jmap_req_t *req, const char *name,
struct mailbox **mboxp, int rw)
{
int i, r;
struct _mboxcache_rec *rec;
for (i = 0; i < req->mboxes->count; i++) {
rec = (struct _mboxcache_rec*) ptrarray_nth(req->mboxes, i);
if (!strcmp(name, rec->mbox->name)) {
if (rw && !rec->rw) {
/* Lock promotions are not supported */
syslog(LOG_ERR, "jmapmbox: failed to grab write-lock"
" on cached read-only mailbox %s", name);
return IMAP_INTERNAL;
}
/* Found a cached mailbox. Increment refcount. */
rec->refcount++;
*mboxp = rec->mbox;
return 0;
}
}
/* Add mailbox to cache */
if (req->force_openmbox_rw)
rw = 1;
r = rw ? mailbox_open_iwl(name, mboxp) : mailbox_open_irl(name, mboxp);
if (r) {
syslog(LOG_ERR, "jmap_openmbox(%s): %s", name, error_message(r));
return r;
}
rec = xzmalloc(sizeof(struct _mboxcache_rec));
rec->mbox = *mboxp;
rec->refcount = 1;
rec->rw = rw;
ptrarray_add(req->mboxes, rec);
return 0;
}
HIDDEN int jmap_isopenmbox(jmap_req_t *req, const char *name)
{
int i;
struct _mboxcache_rec *rec;
for (i = 0; i < req->mboxes->count; i++) {
rec = (struct _mboxcache_rec*) ptrarray_nth(req->mboxes, i);
if (!strcmp(name, rec->mbox->name))
return 1;
}
return 0;
}
HIDDEN void jmap_closembox(jmap_req_t *req, struct mailbox **mboxp)
{
struct _mboxcache_rec *rec = NULL;
int i;
if (mboxp == NULL || *mboxp == NULL) return;
for (i = 0; i < req->mboxes->count; i++) {
rec = (struct _mboxcache_rec*) ptrarray_nth(req->mboxes, i);
if (rec->mbox == *mboxp) {
if (!(--rec->refcount)) {
ptrarray_remove(req->mboxes, i);
mailbox_close(&rec->mbox);
free(rec);
}
*mboxp = NULL;
return;
}
}
syslog(LOG_INFO, "jmap: ignoring non-cached mailbox %s", (*mboxp)->name);
}
HIDDEN void jmap_set_blobid(const struct message_guid *guid, char *buf)
{
buf[0] = 'G';
memcpy(buf+1, message_guid_encode(guid), 40);
buf[41] = '\0';
}
HIDDEN void jmap_set_emailid(const struct message_guid *guid, char *buf)
{
buf[0] = 'M';
// appends NULL for us
bin_to_lchex(&guid->value, 12, buf+1);
}
HIDDEN void jmap_set_threadid(conversation_id_t cid, char *buf)
{
buf[0] = 'T';
memcpy(buf+1, conversation_id_encode(cid), 16);
buf[17] = 0;
}
struct findblob_data {
jmap_req_t *req;
const char *from_accountid;
int is_shared_account;
struct mailbox *mbox;
msgrecord_t *mr;
char *part_id;
};
static int findblob_cb(const conv_guidrec_t *rec, void *rock)
{
struct findblob_data *d = (struct findblob_data*) rock;
jmap_req_t *req = d->req;
int r = 0;
/* Check ACL */
if (d->is_shared_account) {
mbentry_t *mbentry = NULL;
r = mboxlist_lookup(rec->mboxname, &mbentry, NULL);
if (r) {
syslog(LOG_ERR, "jmap_findblob: no mbentry for %s", rec->mboxname);
return r;
}
int rights = jmap_myrights(req, mbentry);
mboxlist_entry_free(&mbentry);
if ((rights & (ACL_LOOKUP|ACL_READ)) != (ACL_LOOKUP|ACL_READ)) {
return 0;
}
}
r = jmap_openmbox(req, rec->mboxname, &d->mbox, 0);
if (r) return r;
r = msgrecord_find(d->mbox, rec->uid, &d->mr);
if (r) {
jmap_closembox(req, &d->mbox);
d->mr = NULL;
return r;
}
d->part_id = rec->part ? xstrdup(rec->part) : NULL;
return IMAP_OK_COMPLETED;
}
HIDDEN int jmap_findblob(jmap_req_t *req, const char *from_accountid,
const char *blobid,
struct mailbox **mbox, msgrecord_t **mr,
struct body **body, const struct body **part,
struct buf *blob)
{
const char *accountid = from_accountid ? from_accountid : req->accountid;
struct findblob_data data = {
req,
/* from_accountid */
accountid,
/* is_shared_account */
strcmp(req->userid, accountid),
/* mbox */
NULL,
/* mr */
NULL,
/* part_id */
NULL
};
struct body *mybody = NULL;
const struct body *mypart = NULL;
int i, r;
struct conversations_state *cstate, *mycstate = NULL;
if (blobid[0] != 'G')
return IMAP_NOTFOUND;
if (strcmp(req->accountid, accountid)) {
cstate = conversations_get_user(accountid);
if (!cstate) {
r = conversations_open_user(accountid, 1/*shared*/, &mycstate);
if (r) goto done;
cstate = mycstate;
}
}
else {
cstate = req->cstate;
}
r = conversations_guid_foreach(cstate, blobid+1, findblob_cb, &data);
if (r != IMAP_OK_COMPLETED) {
if (!r) r = IMAP_NOTFOUND;
goto done;
}
/* Find part containing the data */
if (data.part_id) {
r = msgrecord_extract_bodystructure(data.mr, &mybody);
if (r) goto done;
ptrarray_t parts = PTRARRAY_INITIALIZER;
struct message_guid content_guid;
message_guid_decode(&content_guid, blobid+1);
ptrarray_push(&parts, mybody);
while ((mypart = ptrarray_shift(&parts))) {
if (!message_guid_cmp(&content_guid, &mypart->content_guid)) {
break;
}
if (!mypart->subpart) {
if (data.mbox->mbtype == MBTYPE_ADDRESSBOOK &&
(mypart = jmap_contact_findblob(&content_guid, data.part_id,
data.mbox, data.mr, blob))) {
break;
}
continue;
}
ptrarray_push(&parts, mypart->subpart);
for (i = 1; i < mypart->numparts; i++)
ptrarray_push(&parts, mypart->subpart + i);
}
ptrarray_fini(&parts);
if (!mypart) {
r = IMAP_NOTFOUND;
goto done;
}
}
if (blob && !buf_base(blob)) {
/* Map the message into memory */
r = msgrecord_get_body(data.mr, blob);
if (r) goto done;
}
*mbox = data.mbox;
*mr = data.mr;
*part = mypart;
*body = mybody;
r = 0;
done:
if (mycstate) {
conversations_commit(&mycstate);
}
if (r) {
if (data.mbox) jmap_closembox(req, &data.mbox);
if (mybody) message_free_body(mybody);
}
if (data.part_id) free(data.part_id);
return r;
}
static int findblob_exact_cb(const conv_guidrec_t *rec, void *rock)
{
struct findblob_data *d = (struct findblob_data*) rock;
jmap_req_t *req = d->req;
int r = 0;
// we only want top-level blobs
if (rec->part) return 0;
/* Check ACL */
if (d->is_shared_account) {
mbentry_t *mbentry = NULL;
r = mboxlist_lookup(rec->mboxname, &mbentry, NULL);
if (r) {
syslog(LOG_ERR, "jmap_findblob: no mbentry for %s", rec->mboxname);
return r;
}
int rights = jmap_myrights(req, mbentry);
mboxlist_entry_free(&mbentry);
if ((rights & (ACL_LOOKUP|ACL_READ)) != (ACL_LOOKUP|ACL_READ)) {
return 0;
}
}
r = jmap_openmbox(req, rec->mboxname, &d->mbox, 0);
if (r) return r;
r = msgrecord_find(d->mbox, rec->uid, &d->mr);
if (r) {
jmap_closembox(req, &d->mbox);
d->mr = NULL;
return r;
}
return IMAP_OK_COMPLETED;
}
// we need to pass mbox so we can keep it open until the file has been used
HIDDEN int jmap_findblob_exact(jmap_req_t *req, const char *from_accountid,
const char *blobid,
struct mailbox **mbox, msgrecord_t **mr)
{
const char *accountid = from_accountid ? from_accountid : req->accountid;
struct findblob_data data = {
req,
/* from_accountid */
accountid,
/* is_shared_account */
strcmp(req->userid, accountid),
/* mbox */
NULL,
/* mr */
NULL,
/* part_id */
NULL,
};
int r;
struct conversations_state *cstate, *mycstate = NULL;
if (blobid[0] != 'G')
return IMAP_NOTFOUND;
if (strcmp(req->accountid, accountid)) {
cstate = conversations_get_user(accountid);
if (!cstate) {
r = conversations_open_user(accountid, 1/*shared*/, &mycstate);
if (r) goto done;
cstate = mycstate;
}
}
else {
cstate = req->cstate;
}
r = conversations_guid_foreach(cstate, blobid+1, findblob_exact_cb, &data);
if (r != IMAP_OK_COMPLETED) {
if (!r) r = IMAP_NOTFOUND;
goto done;
}
*mbox = data.mbox;
*mr = data.mr;
r = 0;
done:
if (mycstate) {
conversations_commit(&mycstate);
}
if (r) {
if (data.mbox) jmap_closembox(req, &data.mbox);
}
return r;
}
HIDDEN int jmap_cmpstate(jmap_req_t* req, json_t *state, int mbtype)
{
if (JNOTNULL(state)) {
const char *s = json_string_value(state);
if (!s) {
return -1;
}
modseq_t client_modseq = atomodseq_t(s);
modseq_t server_modseq = 0;
switch (mbtype) {
case MBTYPE_CALENDAR:
server_modseq = req->counters.caldavmodseq;
break;
case MBTYPE_ADDRESSBOOK:
server_modseq = req->counters.carddavmodseq;
break;
case MBTYPE_SUBMISSION:
server_modseq = req->counters.submissionmodseq;
break;
default:
server_modseq = req->counters.mailmodseq;
}
if (client_modseq < server_modseq)
return -1;
else if (client_modseq > server_modseq)
return 1;
else
return 0;
}
return 0;
}
HIDDEN modseq_t jmap_highestmodseq(jmap_req_t *req, int mbtype)
{
modseq_t modseq;
/* Determine current counter by mailbox type. */
switch (mbtype) {
case MBTYPE_CALENDAR:
modseq = req->counters.caldavmodseq;
break;
case MBTYPE_ADDRESSBOOK:
modseq = req->counters.carddavmodseq;
break;
case MBTYPE_SUBMISSION:
modseq = req->counters.submissionmodseq;
break;
case 0:
modseq = req->counters.mailmodseq;
break;
default:
modseq = req->counters.highestmodseq;
}
return modseq;
}
HIDDEN json_t* jmap_getstate(jmap_req_t *req, int mbtype, int refresh)
{
char *inboxname = mboxname_user_mbox(req->accountid, NULL);
if (refresh)
assert (!mboxname_read_counters(inboxname, &req->counters));
struct buf buf = BUF_INITIALIZER;
json_t *state = NULL;
modseq_t modseq = jmap_highestmodseq(req, mbtype);
buf_printf(&buf, MODSEQ_FMT, modseq);
state = json_string(buf_cstring(&buf));
buf_free(&buf);
free(inboxname);
return state;
}
HIDDEN json_t *jmap_fmtstate(modseq_t modseq)
{
struct buf buf = BUF_INITIALIZER;
json_t *state = NULL;
buf_printf(&buf, MODSEQ_FMT, modseq);
state = json_string(buf_cstring(&buf));
buf_free(&buf);
return state;
}
HIDDEN char *jmap_xhref(const char *mboxname, const char *resource)
{
/* XXX - look up root path from namespace? */
struct buf buf = BUF_INITIALIZER;
char *owner = mboxname_to_userid(mboxname);
const char *prefix = NULL;
if (mboxname_isaddressbookmailbox(mboxname, 0)) {
prefix = namespace_addressbook.prefix;
}
else if (mboxname_iscalendarmailbox(mboxname, 0)) {
prefix = namespace_calendar.prefix;
}
/* Path to home-set */
buf_printf(&buf, "%s/%s/%s", prefix, USER_COLLECTION_PREFIX, httpd_userid);
if (!strchr(httpd_userid, '@') && httpd_extradomain) {
buf_printf(&buf, "@%s", httpd_extradomain);
}
buf_putc(&buf, '/');
if (strcmp(owner, httpd_userid)) {
/* Encode shared collection as: <owner> "." <mboxname> */
buf_appendcstr(&buf, owner);
if (!strchr(owner, '@') && httpd_extradomain) {
buf_printf(&buf, "@%s", httpd_extradomain);
}
buf_putc(&buf, SHARED_COLLECTION_DELIM);
}
/* Collection */
buf_printf(&buf, "%s", strrchr(mboxname, '.')+1);
if (resource)
buf_printf(&buf, "/%s", resource);
free(owner);
return buf_release(&buf);
}
HIDDEN int jmap_myrights(jmap_req_t *req, const mbentry_t *mbentry)
{
return _rights_for_mbentry(req->authstate, mbentry, req->mboxrights);
}
// gotta have them all
HIDDEN int jmap_hasrights(jmap_req_t *req, const mbentry_t *mbentry, int rights)
{
int myrights = jmap_myrights(req, mbentry);
if ((myrights & rights) == rights) return 1;
return 0;
}
HIDDEN int jmap_myrights_byname(jmap_req_t *req, const char *mboxname)
{
int *rightsptr = hash_lookup(mboxname, req->mboxrights);
if (rightsptr) return *rightsptr;
// if unable to read, that means no rights
int rights = 0;
mbentry_t *mbentry = NULL;
if (!jmap_mboxlist_lookup(mboxname, &mbentry, NULL)) {
rights = _rights_for_mbentry(req->authstate, mbentry, req->mboxrights);
}
mboxlist_entry_free(&mbentry);
return rights;
}
// gotta have them all
HIDDEN int jmap_hasrights_byname(jmap_req_t *req, const char *mboxname,
int rights)
{
int myrights = jmap_myrights_byname(req, mboxname);
if ((myrights & rights) == rights) return 1;
return 0;
}
HIDDEN void jmap_myrights_delete(jmap_req_t *req, const char *mboxname)
{
int *rightsptr = hash_del(mboxname, req->mboxrights);
free(rightsptr);
}
/* Add performance stats to method response */
static void jmap_add_perf(jmap_req_t *req, json_t *res)
{
struct rusage usage;
getrusage(RUSAGE_SELF, &usage);
json_t *perf = json_pack("{s:f s:f s:f}",
"real", (now_ms() / 1000.0) - req->real_start,
"user", timeval_get_double(&usage.ru_utime) - req->user_start,
"sys", timeval_get_double(&usage.ru_stime) - req->sys_start);
json_object_set(perf, "details", req->perf_details); // incref
json_object_set_new(res, "performance", perf);
}
HIDDEN void jmap_ok(jmap_req_t *req, json_t *res)
{
json_object_set_new(res, "accountId", json_string(req->accountid));
json_t *item = json_pack("[]");
json_array_append_new(item, json_string(req->method));
json_array_append_new(item, res);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
if (jmap_is_using(req, JMAP_PERFORMANCE_EXTENSION))
jmap_add_perf(req, res);
}
HIDDEN void jmap_error(jmap_req_t *req, json_t *err)
{
json_array_append_new(req->response,
json_pack("[s,o,s]", "error", err, req->tag));
}
HIDDEN int jmap_parse_strings(json_t *arg,
struct jmap_parser *parser, const char *prop)
{
if (!json_is_array(arg)) {
jmap_parser_invalid(parser, prop);
return 0;
}
int valid = 1;
size_t i;
json_t *val;
json_array_foreach(arg, i, val) {
if (!json_is_string(val)) {
jmap_parser_push_index(parser, prop, i, NULL);
jmap_parser_invalid(parser, NULL);
jmap_parser_pop(parser);
valid = 0;
}
}
return valid;
}
HIDDEN const jmap_property_t *jmap_property_find(const char *name,
const jmap_property_t props[])
{
const jmap_property_t *prop;
for (prop = props; prop && prop->name; prop++) {
if (!strcmp(name, prop->name)) return prop;
else {
size_t len = strlen(prop->name);
if ((prop->name[len-1] == '*') && !strncmp(name, prop->name, len-1))
return prop;
}
}
return NULL;
}
/* Foo/get */
HIDDEN void jmap_get_parse(jmap_req_t *req,
struct jmap_parser *parser,
const jmap_property_t valid_props[],
int allow_null_ids,
jmap_args_parse_cb args_parse,
void *args_rock,
struct jmap_get *get,
json_t **err)
{
json_t *jargs = req->args;
const char *key;
json_t *arg, *val;
size_t i;
memset(get, 0, sizeof(struct jmap_get));
get->list = json_array();
get->not_found = json_array();
json_object_foreach(jargs, key, arg) {
if (!strcmp(key, "accountId")) {
/* already handled in jmap_api() */
}
else if (!strcmp(key, "ids")) {
if (json_is_array(arg)) {
get->ids = json_array();
/* JMAP spec requires: "If an identical id is included
* more than once in the request, the server MUST only
* include it once in either the list or notFound
* argument of the response."
* So let's weed out duplicate ids here. */
hash_table _dedup = HASH_TABLE_INITIALIZER;
construct_hash_table(&_dedup, json_array_size(arg) + 1, 0);
json_array_foreach(arg, i, val) {
const char *id = json_string_value(val);
if (!id) {
jmap_parser_push_index(parser, "ids", i, NULL);
jmap_parser_invalid(parser, NULL);
jmap_parser_pop(parser);
continue;
}
/* Weed out unknown creation ids and add the ids of known
* creation ids to the requested ids list. THis might
* cause a race if the Foo object pointed to by creation
* id is deleted between parsing the request and answering
* it. But re-checking creation ids for their existence
* later in the control flow just shifts the problem */
if (*id == '#') {
const char *id2 = jmap_lookup_id(req, id + 1);
if (!id2) {
json_array_append_new(get->not_found,
json_string(id));
continue;
}
id = id2;
}
if (hash_lookup(id, &_dedup)) {
continue;
}
json_array_append_new(get->ids, json_string(id));
}
free_hash_table(&_dedup, NULL);
}
else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "ids");
}
}
else if (!strcmp(key, "properties")) {
if (json_is_array(arg)) {
get->props = xzmalloc(sizeof(hash_table));
construct_hash_table(get->props, json_array_size(arg) + 1, 0);
json_array_foreach(arg, i, val) {
const char *name = json_string_value(val);
const jmap_property_t *propdef = NULL;
if (name) {
propdef = jmap_property_find(name, valid_props);
if (propdef && propdef->capability &&
!jmap_is_using(req, propdef->capability)) {
propdef = NULL;
}
}
if (!propdef) {
jmap_parser_push_index(parser, "properties", i, name);
jmap_parser_invalid(parser, NULL);
jmap_parser_pop(parser);
continue;
}
hash_insert(name, (void*)1, get->props);
}
}
else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "properties");
}
}
else if (!args_parse || !args_parse(req, parser, key, arg, args_rock)) {
jmap_parser_invalid(parser, key);
}
}
if (json_array_size(parser->invalid)) {
*err = json_pack("{s:s s:O}", "type", "invalidArguments",
"arguments", parser->invalid);
return;
}
if (!allow_null_ids && !JNOTNULL(get->ids)) {
*err = json_pack("{s:s, s:s}", "type", "requestTooLarge",
"description", "ids must be specified");
return;
}
if (*err) return;
if (get->props == NULL) {
/* Initialize default properties */
int nvalid = 0;
const jmap_property_t *prop;
for (prop = valid_props; prop && prop->name; prop++) {
nvalid++;
}
get->props = xzmalloc(sizeof(hash_table));
construct_hash_table(get->props, nvalid + 1, 0);
for (prop = valid_props; prop && prop->name; prop++) {
if (prop->flags & JMAP_PROP_SKIP_GET) {
continue;
}
if (!prop->capability || jmap_is_using(req, prop->capability)) {
hash_insert(prop->name, (void*)1, get->props);
}
}
}
else {
const jmap_property_t *prop;
for (prop = valid_props; prop && prop->name; prop++) {
if (prop->flags & JMAP_PROP_ALWAYS_GET) {
if (!hash_lookup(prop->name, get->props)) {
hash_insert(prop->name, (void*)1, get->props);
}
}
}
}
/* Number of ids checked in validate_request() */
}
HIDDEN void jmap_get_fini(struct jmap_get *get)
{
free_hash_table(get->props, NULL);
free(get->props);
free(get->state);
json_decref(get->ids);
json_decref(get->list);
json_decref(get->not_found);
}
HIDDEN json_t *jmap_get_reply(struct jmap_get *get)
{
json_t *res = json_object();
json_object_set_new(res, "state", json_string(get->state));
json_object_set(res, "list", get->list);
json_object_set(res, "notFound", get->not_found);
return res;
}
/* Foo/set */
static void jmap_set_validate_props(jmap_req_t *req, json_t *jobj,
const jmap_property_t valid_props[],
json_t **err)
{
json_t *invalid = json_array();
const char *path;
json_t *jval;
json_object_foreach(jobj, path, jval) {
/* Determine property name */
const char *pname = path;
char *tmp = NULL;
const char *slash = strchr(pname, '/');
if (slash) {
tmp = jmap_pointer_decode(pname, slash - path);
if (tmp) pname = tmp;
}
/* Validate against property spec */
const jmap_property_t *prop = jmap_property_find(pname, valid_props);
if (!prop) {
json_array_append_new(invalid, json_string(path));
}
else if (prop->capability && !jmap_is_using(req, prop->capability)) {
json_array_append_new(invalid, json_string(path));
}
/* XXX could check IMMUTABLE and SERVER_SET here, but we can't
* reject such properties if they match the current value */
if (tmp) free(tmp);
}
if (json_array_size(invalid)) {
*err = json_pack("{s:s s:o}",
"type", "invalidProperties",
"properties", invalid);
}
else {
json_decref(invalid);
}
}
HIDDEN void jmap_set_parse(jmap_req_t *req, struct jmap_parser *parser,
const jmap_property_t valid_props[],
jmap_args_parse_cb args_parse, void *args_rock,
struct jmap_set *set, json_t **err)
{
json_t *jargs = req->args;
memset(set, 0, sizeof(struct jmap_set));
set->create = json_object();
set->update = json_object();
set->destroy = json_array();
set->created = json_object();
set->updated = json_object();
set->destroyed = json_array();
set->not_created = json_object();
set->not_updated = json_object();
set->not_destroyed = json_object();
const char *key;
json_t *arg, *val;
json_object_foreach(jargs, key, arg) {
if (!strcmp(key, "accountId")) {
/* already handled in jmap_api() */
}
/* ifInState */
else if (!strcmp(key, "ifInState")) {
if (json_is_string(arg)) {
set->if_in_state = json_string_value(arg);
}
else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "ifInState");
}
}
/* create */
else if (!strcmp(key, "create")) {
if (json_is_object(arg)) {
const char *id;
json_object_foreach(arg, id, val) {
if (!json_is_object(val)) {
jmap_parser_push(parser, "create");
jmap_parser_invalid(parser, id);
jmap_parser_pop(parser);
continue;
}
json_object_set(set->create, id, val);
}
}
else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "create");
}
}
/* update */
else if (!strcmp(key, "update")) {
if (json_is_object(arg)) {
const char *id;
json_object_foreach(arg, id, val) {
if (!json_is_object(val)) {
jmap_parser_push(parser, "update");
jmap_parser_invalid(parser, id);
jmap_parser_pop(parser);
continue;
}
json_object_set(set->update, id, val);
}
}
else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "update");
}
}
/* destroy */
else if (!strcmp(key, "destroy")) {
if (JNOTNULL(arg)) {
jmap_parse_strings(arg, parser, "destroy");
if (!json_array_size(parser->invalid)) {
json_decref(set->destroy);
set->destroy = json_incref(arg);
}
}
}
else if (!args_parse || !args_parse(req, parser, key, arg, args_rock)) {
jmap_parser_invalid(parser, key);
}
}
if (json_array_size(parser->invalid)) {
*err = json_pack("{s:s s:O}", "type", "invalidArguments",
"arguments", parser->invalid);
}
if (valid_props) {
json_t *jval;
/* Make sure no property is set without its capability */
json_object_foreach(json_object_get(jargs, "create"), key, jval) {
json_t *err = NULL;
jmap_set_validate_props(req, jval, valid_props, &err);
if (err) {
json_object_del(set->create, key);
json_object_set_new(set->not_created, key, err);
}
}
json_object_foreach(json_object_get(jargs, "update"), key, jval) {
json_t *err = NULL;
jmap_set_validate_props(req, jval, valid_props, &err);
if (err) {
json_object_del(set->update, key);
json_object_set_new(set->not_updated, key, err);
}
}
// TODO We could report the following set errors here:
// -invalidPatch
// - willDestroy
}
}
HIDDEN void jmap_set_fini(struct jmap_set *set)
{
free(set->old_state);
free(set->new_state);
json_decref(set->create);
json_decref(set->update);
json_decref(set->destroy);
json_decref(set->created);
json_decref(set->updated);
json_decref(set->destroyed);
json_decref(set->not_created);
json_decref(set->not_updated);
json_decref(set->not_destroyed);
}
HIDDEN json_t *jmap_set_reply(struct jmap_set *set)
{
json_t *res = json_object();
json_object_set_new(res, "oldState",
set->old_state ? json_string(set->old_state) : json_null());
json_object_set_new(res, "newState", json_string(set->new_state));
json_object_set(res, "created", json_object_size(set->created) ?
set->created : json_null());
json_object_set(res, "updated", json_object_size(set->updated) ?
set->updated : json_null());
json_object_set(res, "destroyed", json_array_size(set->destroyed) ?
set->destroyed : json_null());
json_object_set(res, "notCreated", json_object_size(set->not_created) ?
set->not_created : json_null());
json_object_set(res, "notUpdated", json_object_size(set->not_updated) ?
set->not_updated : json_null());
json_object_set(res, "notDestroyed", json_object_size(set->not_destroyed) ?
set->not_destroyed : json_null());
return res;
}
/* Foo/changes */
HIDDEN void jmap_changes_parse(jmap_req_t *req,
struct jmap_parser *parser,
jmap_args_parse_cb args_parse,
void *args_rock,
struct jmap_changes *changes,
json_t **err)
{
json_t *jargs = req->args;
const char *key;
json_t *arg;
memset(changes, 0, sizeof(struct jmap_changes));
changes->created = json_array();
changes->updated = json_array();
changes->destroyed = json_array();
json_object_foreach(jargs, key, arg) {
if (!strcmp(key, "accountId")) {
/* already handled in jmap_api() */
}
/* sinceState */
else if (!strcmp(key, "sinceState")) {
if (json_is_string(arg)) {
changes->since_modseq = atomodseq_t(json_string_value(arg));
}
else {
jmap_parser_invalid(parser, "sinceState");
}
}
/* maxChanges */
else if (!strcmp(key, "maxChanges")) {
if (json_is_integer(arg) && json_integer_value(arg) > 0) {
changes->max_changes = json_integer_value(arg);
} else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "maxChanges");
}
}
else if (!args_parse || !args_parse(req, parser, key, arg, args_rock)) {
jmap_parser_invalid(parser, key);
}
}
if (json_array_size(parser->invalid)) {
*err = json_pack("{s:s s:O}", "type", "invalidArguments",
"arguments", parser->invalid);
}
else if (!changes->since_modseq) {
*err = json_pack("{s:s}", "type", "cannotCalculateChanges");
}
}
HIDDEN void jmap_changes_fini(struct jmap_changes *changes)
{
json_decref(changes->created);
json_decref(changes->updated);
json_decref(changes->destroyed);
}
HIDDEN json_t *jmap_changes_reply(struct jmap_changes *changes)
{
json_t *res = json_object();
json_object_set_new(res, "oldState", jmap_fmtstate(changes->since_modseq));
json_object_set_new(res, "newState", jmap_fmtstate(changes->new_modseq));
json_object_set_new(res, "hasMoreChanges",
json_boolean(changes->has_more_changes));
json_object_set(res, "created", changes->created);
json_object_set(res, "updated", changes->updated);
json_object_set(res, "destroyed", changes->destroyed);
return res;
}
/* Foo/copy */
HIDDEN void jmap_copy_parse(jmap_req_t *req, struct jmap_parser *parser,
jmap_args_parse_cb args_parse, void *args_rock,
struct jmap_copy *copy, json_t **err)
{
json_t *jargs = req->args;
memset(copy, 0, sizeof(struct jmap_copy));
copy->blob_copy = !strcmp(req->method, "Blob/copy");
copy->create = copy->blob_copy ? json_array() : json_object();
copy->created = json_object();
copy->not_created = json_object();
const char *key;
json_t *arg;
json_object_foreach(jargs, key, arg) {
/* fromAccountId */
if (!strcmp(key, "fromAccountId")) {
if (json_is_string(arg)) {
copy->from_account_id = json_string_value(arg);
}
else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "fromAccountId");
}
}
/* accountId */
else if (!strcmp(key, "accountId")) {
/* JMAP request parser already set it */
assert(req->accountid);
continue;
}
/* blobIds */
else if (copy->blob_copy &&
!strcmp(key, "blobIds") && json_is_array(arg)) {
struct buf buf = BUF_INITIALIZER;
json_t *id;
size_t i;
json_array_foreach(arg, i, id) {
if (!json_is_string(id)) {
buf_printf(&buf, "blobIds[%zu]", i);
jmap_parser_invalid(parser, buf_cstring(&buf));
buf_reset(&buf);
}
else json_array_append(copy->create, id);
}
}
/* create */
else if (!copy->blob_copy &&
!strcmp(key, "create") && json_is_object(arg)) {
jmap_parser_push(parser, "create");
const char *creation_id;
json_t *obj;
json_object_foreach(arg, creation_id, obj) {
if (!json_is_object(obj)) {
jmap_parser_invalid(parser, creation_id);
}
else if (!json_is_string(json_object_get(obj, "id"))) {
jmap_parser_push(parser, creation_id);
jmap_parser_invalid(parser, "id");
jmap_parser_pop(parser);
}
else json_object_set(copy->create, creation_id, obj);
}
jmap_parser_pop(parser);
}
/* onSuccessDestroyOriginal */
else if (!copy->blob_copy && !strcmp(key, "onSuccessDestroyOriginal") &&
json_is_boolean(arg)) {
copy->on_success_destroy_original = json_boolean_value(arg);
}
else if (!args_parse || !args_parse(req, parser, key, arg, args_rock)) {
jmap_parser_invalid(parser, key);
}
}
if (json_array_size(parser->invalid)) {
*err = json_pack("{s:s s:O}", "type", "invalidArguments",
"arguments", parser->invalid);
}
if (!req->accountid || !copy->from_account_id ||
!strcmp(req->accountid, copy->from_account_id)) {
*err = json_pack("{s:s s:[s,s]}", "type", "invalidArguments",
"arguments", "accountId", "fromAccountId");
}
}
HIDDEN void jmap_copy_fini(struct jmap_copy *copy)
{
json_decref(copy->create);
json_decref(copy->created);
json_decref(copy->not_created);
}
HIDDEN json_t *jmap_copy_reply(struct jmap_copy *copy)
{
json_t *res = json_object();
json_object_set_new(res, "fromAccountId",
json_string(copy->from_account_id));
json_object_set(res, copy->blob_copy ? "copied" : "created",
json_object_size(copy->created) ?
copy->created : json_null());
json_object_set(res, copy->blob_copy ? "notCopied" : "notCreated",
json_object_size(copy->not_created) ?
copy->not_created : json_null());
return res;
}
/* Foo/query */
HIDDEN jmap_filter *jmap_buildfilter(json_t *arg, jmap_buildfilter_cb *parse)
{
jmap_filter *f = (jmap_filter *) xzmalloc(sizeof(struct jmap_filter));
int pe;
const char *val;
int iscond = 1;
/* operator */
pe = jmap_readprop(arg, "operator", 0 /*mandatory*/, NULL, "s", &val);
if (pe > 0) {
if (!strncmp("AND", val, 3)) {
f->op = JMAP_FILTER_OP_AND;
} else if (!strncmp("OR", val, 2)) {
f->op = JMAP_FILTER_OP_OR;
} else if (!strncmp("NOT", val, 3)) {
f->op = JMAP_FILTER_OP_NOT;
}
}
iscond = f->op == JMAP_FILTER_OP_NONE;
/* conditions */
json_t *conds = json_object_get(arg, "conditions");
if (conds && !iscond && json_array_size(conds)) {
size_t i, n_conditions = json_array_size(conds);
for (i = 0; i < n_conditions; i++) {
json_t *cond = json_array_get(conds, i);
ptrarray_push(&f->conditions, jmap_buildfilter(cond, parse));
}
}
if (iscond) {
ptrarray_push(&f->conditions, parse(arg));
}
return f;
}
HIDDEN int jmap_filter_match(jmap_filter *f,
jmap_filtermatch_cb *match, void *rock)
{
if (f->op == JMAP_FILTER_OP_NONE) {
return match(ptrarray_head(&f->conditions), rock);
} else {
int i;
for (i = 0; i < ptrarray_size(&f->conditions); i++) {
int m = jmap_filter_match(ptrarray_nth(&f->conditions, i), match, rock);
if (m && f->op == JMAP_FILTER_OP_OR) {
return 1;
} else if (m && f->op == JMAP_FILTER_OP_NOT) {
return 0;
} else if (!m && f->op == JMAP_FILTER_OP_AND) {
return 0;
}
}
return f->op == JMAP_FILTER_OP_AND || f->op == JMAP_FILTER_OP_NOT;
}
}
HIDDEN void jmap_filter_free(jmap_filter *f, jmap_filterfree_cb *freecond)
{
void *cond;
while ((cond = ptrarray_pop(&f->conditions))) {
if (f->op == JMAP_FILTER_OP_NONE) {
if (freecond) freecond(cond);
}
else {
jmap_filter_free(cond, freecond);
}
}
ptrarray_fini(&f->conditions);
free(f);
}
HIDDEN void jmap_filter_parse(jmap_req_t *req, struct jmap_parser *parser,
json_t *filter, json_t *unsupported,
jmap_filter_parse_cb parse_condition, void *cond_rock,
json_t **err)
{
json_t *arg, *val;
const char *s;
size_t i;
if (err && *err) return;
if (!JNOTNULL(filter) || json_typeof(filter) != JSON_OBJECT) {
jmap_parser_invalid(parser, NULL);
return;
}
arg = json_object_get(filter, "operator");
if ((s = json_string_value(arg))) {
if (strcmp("AND", s) && strcmp("OR", s) && strcmp("NOT", s)) {
jmap_parser_invalid(parser, "operator");
}
arg = json_object_get(filter, "conditions");
if (!json_array_size(arg)) {
jmap_parser_invalid(parser, "conditions");
}
json_array_foreach(arg, i, val) {
jmap_parser_push_index(parser, "conditions", i, NULL);
jmap_filter_parse(req, parser, val, unsupported, parse_condition, cond_rock, err);
jmap_parser_pop(parser);
}
} else if (arg) {
jmap_parser_invalid(parser, "operator");
} else {
parse_condition(req, parser, filter, unsupported, cond_rock, err);
}
}
HIDDEN void jmap_comparator_parse(jmap_req_t *req, struct jmap_parser *parser,
json_t *jsort, json_t *unsupported,
jmap_comparator_parse_cb comp_cb, void *comp_rock,
json_t **err)
{
if (!json_is_object(jsort)) {
jmap_parser_invalid(parser, NULL);
return;
}
struct jmap_comparator comp = { NULL, 0, NULL };
/* property */
json_t *val = json_object_get(jsort, "property");
comp.property = json_string_value(val);
if (!comp.property) {
jmap_parser_invalid(parser, "property");
}
/* isAscending */
comp.is_ascending = 1;
val = json_object_get(jsort, "isAscending");
if (JNOTNULL(val)) {
if (!json_is_boolean(val)) {
jmap_parser_invalid(parser, "isAscending");
}
comp.is_ascending = json_boolean_value(val);
}
/* collation */
val = json_object_get(jsort, "collation");
if (JNOTNULL(val) && !json_is_string(val)) {
jmap_parser_invalid(parser, "collation");
}
comp.collation = json_string_value(val);
if (comp.property && !comp_cb(req, &comp, comp_rock, err)) {
struct buf buf = BUF_INITIALIZER;
json_array_append_new(unsupported,
json_string(jmap_parser_path(parser, &buf)));
buf_free(&buf);
}
}
HIDDEN void jmap_query_parse(jmap_req_t *req, struct jmap_parser *parser,
jmap_args_parse_cb args_parse, void *args_rock,
jmap_filter_parse_cb filter_cb, void *filter_rock,
jmap_comparator_parse_cb comp_cb, void *comp_rock,
struct jmap_query *query, json_t **err)
{
json_t *jargs = req->args;
const char *key;
json_t *arg, *val;
size_t i;
memset(query, 0, sizeof(struct jmap_query));
query->ids = json_array();
json_t *unsupported_filter = json_array();
json_t *unsupported_sort = json_array();
json_object_foreach(jargs, key, arg) {
if (!strcmp(key, "accountId")) {
/* already handled in jmap_api() */
}
/* filter */
else if (!strcmp(key, "filter")) {
if (json_is_object(arg)) {
jmap_parser_push(parser, "filter");
jmap_filter_parse(req, parser, arg, unsupported_filter,
filter_cb, filter_rock, err);
jmap_parser_pop(parser);
query->filter = arg;
if (err && *err) {
goto done;
}
}
else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "filter");
}
}
/* sort */
else if (!strcmp(key, "sort")) {
if (json_is_array(arg)) {
json_array_foreach(arg, i, val) {
jmap_parser_push_index(parser, "sort", i, NULL);
jmap_comparator_parse(req, parser, val, unsupported_sort,
comp_cb, comp_rock, err);
jmap_parser_pop(parser);
if (err && *err) {
goto done;
}
}
if (json_array_size(arg)) {
query->sort = arg;
}
}
else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "sort");
}
}
else if (!strcmp(key, "position")) {
if (json_is_integer(arg)) {
query->position = json_integer_value(arg);
}
else if (arg) {
jmap_parser_invalid(parser, "position");
}
}
else if (!strcmp(key, "anchor")) {
if (json_is_string(arg)) {
query->anchor = json_string_value(arg);
} else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "anchor");
}
}
else if (!strcmp(key, "anchorOffset")) {
if (json_is_integer(arg)) {
query->anchor_offset = json_integer_value(arg);
} else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "anchorOffset");
}
}
else if (!strcmp(key, "limit")) {
if (json_is_integer(arg) && json_integer_value(arg) >= 0) {
query->limit = json_integer_value(arg);
query->have_limit = 1;
} else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "limit");
}
}
else if (!strcmp(key, "calculateTotal")) {
if (json_is_boolean(arg)) {
query->calculate_total = json_boolean_value(arg);
} else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "calculateTotal");
}
}
else if (!args_parse || !args_parse(req, parser, key, arg, args_rock)) {
jmap_parser_invalid(parser, key);
}
}
if (json_array_size(parser->invalid)) {
*err = json_pack("{s:s s:O}", "type", "invalidArguments",
"arguments", parser->invalid);
}
else if (json_array_size(unsupported_filter)) {
*err = json_pack("{s:s s:O}", "type", "unsupportedFilter",
"filters", unsupported_filter);
}
else if (json_array_size(unsupported_sort)) {
*err = json_pack("{s:s s:O}", "type", "unsupportedSort",
"sort", unsupported_sort);
}
done:
json_decref(unsupported_filter);
json_decref(unsupported_sort);
}
HIDDEN void jmap_query_fini(struct jmap_query *query)
{
free(query->query_state);
json_decref(query->ids);
}
HIDDEN json_t *jmap_query_reply(struct jmap_query *query)
{
json_t *res = json_object();
json_object_set(res, "filter", query->filter);
json_object_set(res, "sort", query->sort);
json_object_set_new(res, "queryState", json_string(query->query_state));
json_object_set_new(res, "canCalculateChanges",
json_boolean(query->can_calculate_changes));
json_object_set_new(res, "position", json_integer(query->result_position));
json_object_set_new(res, "total", json_integer(query->total));
/* Special case total */
if (query->position > 0 && query->total < SSIZE_MAX) {
if (query->position > (ssize_t) query->total) {
json_decref(query->ids);
query->ids = json_array();
}
}
/* Special case limit 0 */
if (query->have_limit && query->limit == 0) {
json_array_clear(query->ids);
}
json_object_set(res, "ids", query->ids);
return res;
}
/* Foo/queryChanges */
HIDDEN void jmap_querychanges_parse(jmap_req_t *req,
struct jmap_parser *parser,
jmap_args_parse_cb args_parse, void *args_rock,
jmap_filter_parse_cb filter_cb, void *filter_rock,
jmap_comparator_parse_cb comp_cb, void *comp_rock,
struct jmap_querychanges *query,
json_t **err)
{
json_t *jargs = req->args;
const char *key;
json_t *arg, *val;
size_t i;
memset(query, 0, sizeof(struct jmap_querychanges));
query->removed = json_array();
query->added = json_array();
json_t *unsupported_filter = json_array();
json_t *unsupported_sort = json_array();
json_object_foreach(jargs, key, arg) {
if (!strcmp(key, "accountId")) {
/* already handled in jmap_api() */
}
/* filter */
else if (!strcmp(key, "filter")) {
if (json_is_object(arg)) {
jmap_parser_push(parser, "filter");
jmap_filter_parse(req, parser, arg, unsupported_filter,
filter_cb, filter_rock, err);
jmap_parser_pop(parser);
query->filter = arg;
if (err && *err) {
goto done;
}
}
else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "filter");
}
}
/* sort */
else if (!strcmp(key, "sort")) {
if (json_is_array(arg)) {
json_array_foreach(arg, i, val) {
jmap_parser_push_index(parser, "sort", i, NULL);
jmap_comparator_parse(req, parser, val, unsupported_sort,
comp_cb, comp_rock, err);
jmap_parser_pop(parser);
}
if (json_array_size(arg)) {
query->sort = arg;
}
}
else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "sort");
}
}
/* sinceQueryState */
else if (!strcmp(key, "sinceQueryState")) {
if (json_is_string(arg)) {
query->since_querystate = json_string_value(arg);
} else {
jmap_parser_invalid(parser, "sinceQueryState");
}
}
/* maxChanges */
else if (!strcmp(key, "maxChanges")) {
if (json_is_integer(arg) && json_integer_value(arg) > 0) {
query->max_changes = json_integer_value(arg);
} else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "maxChanges");
}
}
/* upToId */
else if (!strcmp(key, "upToId")) {
if (json_is_string(arg)) {
query->up_to_id = json_string_value(arg);
} else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "upToId");
}
}
/* calculateTotal */
else if (!strcmp(key, "calculateTotal")) {
if (json_is_boolean(arg)) {
query->calculate_total = json_boolean_value(arg);
} else if (JNOTNULL(arg)) {
jmap_parser_invalid(parser, "calculateTotal");
}
}
else if (!args_parse || !args_parse(req, parser, key, arg, args_rock)) {
jmap_parser_invalid(parser, key);
}
}
if (query->since_querystate == NULL) {
jmap_parser_invalid(parser, "sinceQueryState");
}
if (json_array_size(parser->invalid)) {
*err = json_pack("{s:s s:O}", "type", "invalidArguments",
"arguments", parser->invalid);
}
else if (json_array_size(unsupported_filter)) {
*err = json_pack("{s:s s:O}", "type", "unsupportedFilter",
"filters", unsupported_filter);
}
else if (json_array_size(unsupported_sort)) {
*err = json_pack("{s:s s:O}", "type", "unsupportedSort",
"sort", unsupported_sort);
}
done:
json_decref(unsupported_filter);
json_decref(unsupported_sort);
}
HIDDEN void jmap_querychanges_fini(struct jmap_querychanges *query)
{
free(query->new_querystate);
json_decref(query->removed);
json_decref(query->added);
}
HIDDEN json_t *jmap_querychanges_reply(struct jmap_querychanges *query)
{
json_t *res = json_object();
json_object_set(res, "filter", query->filter);
json_object_set(res, "sort", query->sort);
json_object_set_new(res, "oldQueryState",
json_string(query->since_querystate));
json_object_set_new(res, "newQueryState",
json_string(query->new_querystate));
json_object_set_new(res, "upToId", query->up_to_id ?
json_string(query->up_to_id) : json_null());
json_object_set(res, "removed", query->removed);
json_object_set(res, "added", query->added);
json_object_set_new(res, "total", json_integer(query->total));
return res;
}
static json_t *_json_has(int rights, int need)
{
return (((rights & need) == need) ? json_true() : json_false());
}
/* create, update, delete */
#define WRITERIGHTS (ACL_WRITE|ACL_INSERT|ACL_SETSEEN|ACL_DELETEMSG|ACL_EXPUNGE|ACL_ANNOTATEMSG)
HIDDEN json_t *jmap_get_sharewith(const mbentry_t *mbentry)
{
char *aclstr = xstrdupnull(mbentry->acl);
char *owner = mboxname_to_userid(mbentry->name);
int iscalendar = (mbentry->mbtype & MBTYPE_CALENDAR);
json_t *sharewith = json_null();
char *userid;
char *nextid;
for (userid = aclstr; userid; userid = nextid) {
int rights;
char *rightstr;
rightstr = strchr(userid, '\t');
if (!rightstr) break;
*rightstr++ = '\0';
nextid = strchr(rightstr, '\t');
if (!nextid) break;
*nextid++ = '\0';
cyrus_acl_strtomask(rightstr, &rights);
// skip system users and owner
if (is_system_user(userid)) continue;
if (!strcmp(userid, owner)) continue;
// we've got one! Create the object if this is the first
if (!JNOTNULL(sharewith))
sharewith = json_pack("{}");
json_t *obj = json_pack("{}");
json_object_set_new(sharewith, userid, obj);
if (iscalendar)
json_object_set_new(obj, "mayReadFreeBusy",
_json_has(rights, DACL_READFB));
json_object_set_new(obj, "mayRead",
_json_has(rights, ACL_READ|ACL_LOOKUP));
json_object_set_new(obj, "mayWrite",
_json_has(rights, WRITERIGHTS));
json_object_set_new(obj, "mayAdmin",
_json_has(rights, ACL_ADMIN));
}
free(aclstr);
free(owner);
return sharewith;
}
struct acl_item {
unsigned int mayAdmin:1;
unsigned int mayWrite:1;
unsigned int mayPost:1;
unsigned int mayRead:1;
unsigned int mayReadFreeBusy:1;
};
struct acl_change {
struct acl_item old;
struct acl_item new;
};
struct invite_rock {
xmlNodePtr notify;
xmlNsPtr ns[NUM_NAMESPACE];
const char *owner;
const char *mboxname;
struct buf resource;
struct request_target_t tgt;
const struct prop_entry *live_props;
};
static unsigned access_from_acl_item(struct acl_item *item)
{
unsigned access = 0;
if (item->mayReadFreeBusy)
access |= DACL_READFB;
if (item->mayRead)
access |= ACL_READ|ACL_LOOKUP|ACL_SETSEEN;
if (item->mayWrite)
access |= WRITERIGHTS;
if (item->mayPost)
access |= ACL_POST;
if (item->mayAdmin)
access |= ACL_ADMIN|ACL_CREATE|ACL_DELETEMBOX;
return access;
}
/* Create and send a sharing invite */
static void send_dav_invite(const char *userid, void *val, void *rock)
{
struct acl_change *change = (struct acl_change *) val;
struct invite_rock *irock = (struct invite_rock *) rock;
long old = access_from_acl_item(&change->old);
long new = access_from_acl_item(&change->new);
if (old != new) {
int access, r = 0;
if (!new) access = SHARE_NONE;
else if (change->new.mayWrite) access = SHARE_READWRITE;
else access = SHARE_READONLY;
if (!old || !new) {
/* Change subscription */
r = mboxlist_changesub(irock->mboxname, userid, httpd_authstate,
access != SHARE_NONE, 0, /*notify*/1);
}
if (!r) {
static const char *displayname_annot =
DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
struct buf buf = BUF_INITIALIZER;
r = annotatemore_lookupmask(irock->mboxname, displayname_annot,
irock->owner, &buf);
/* Fall back to last part of mailbox name */
if (r || !buf_len(&buf)) {
buf_setcstr(&buf, strrchr(irock->mboxname, '.') + 1);
}
r = dav_create_invite(&irock->notify, irock->ns, &irock->tgt,
irock->live_props, userid, access,
BAD_CAST buf_cstring(&buf));
buf_free(&buf);
}
if (!r) {
/* Create a resource name for the notifications -
We use a consistent naming scheme so that multiple
notifications of the same type for the same resource
are coalesced (overwritten) */
buf_reset(&irock->resource);
buf_printf(&irock->resource, "%x-%x-%x-%x.xml",
strhash(XML_NS_DAV),
strhash(SHARE_INVITE_NOTIFICATION),
strhash(irock->tgt.mbentry->name),
strhash(userid));
r = dav_send_notification(irock->notify->doc,
userid, buf_cstring(&irock->resource));
}
}
}
static void add_useracls(const char *userid, void *val, void *rock)
{
struct acl_change *change = val;
char **aclptr = rock;
unsigned access = access_from_acl_item(&change->new);
if (access)
cyrus_acl_set(aclptr, userid, ACL_MODE_SET, access, NULL, NULL);
}
HIDDEN int jmap_set_sharewith(struct mailbox *mbox,
json_t *shareWith, int overwrite)
{
hash_table user_access = HASH_TABLE_INITIALIZER;
int isdav = (mbox->mbtype & MBTYPES_DAV);
int iscalendar = (mbox->mbtype & MBTYPE_CALENDAR);
char *owner = mboxname_to_userid(mbox->name);
char *acl = xstrdup(mbox->acl);
struct acl_change *change;
const char *userid;
json_t *rights;
int r;
char *newacl = xstrdup(""); /* start with empty ACL */
if (json_is_null(shareWith)) overwrite = 1;
construct_hash_table(&user_access, 64, 0);
/* parse the existing ACL and calculate the types of shares */
char *nextid = NULL;
for (userid = acl; userid; userid = nextid) {
char *rightstr;
int access;
rightstr = strchr(userid, '\t');
if (!rightstr) break;
*rightstr++ = '\0';
nextid = strchr(rightstr, '\t');
if (!nextid) break;
*nextid++ = '\0';
/* Is this a shareable user? (not owner or admin) */
if (strcmp(userid, owner) && !is_system_user(userid)) {
int oldrights;
cyrus_acl_strtomask(rightstr, &oldrights);
/* Add regular user to our table */
change = xzmalloc(sizeof(struct acl_change));
if (oldrights & DACL_READFB)
change->old.mayReadFreeBusy = 1;
if (oldrights & (ACL_READ|ACL_LOOKUP))
change->old.mayRead = 1;
if ((oldrights & WRITERIGHTS) == WRITERIGHTS)
change->old.mayWrite = 1;
if (oldrights & ACL_ADMIN)
change->old.mayAdmin = 1;
if (isdav) change->old.mayPost = change->old.mayWrite;
/* unless we're overwriting, we start with the existing state */
if (!overwrite) change->new = change->old;
hash_insert(userid, (void *) change, &user_access);
}
else {
/* Add owner or system user to new ACL */
cyrus_acl_strtomask(rightstr, &access);
r = cyrus_acl_set(&newacl, userid,
ACL_MODE_SET, access, NULL, NULL);
if (r) {
syslog(LOG_ERR, "cyrus_acl_set(%s, %s) failed: %s",
mbox->name, userid, error_message(r));
goto done;
}
}
}
/* Patch the ACL from shareWith */
json_object_foreach(shareWith, userid, rights) {
const char *right;
json_t *val;
/* Validate user id and rights */
if (!(strlen(userid) && rights &&
(json_is_object(rights) || json_is_null(rights)))) {
continue;
}
/* skip system users and owner */
if (is_system_user(userid)) continue;
if (!strcmp(userid, owner)) continue;
change = hash_lookup(userid, &user_access);
if (!change) {
change = xzmalloc(sizeof(struct acl_change));
hash_insert(userid, (void *) change, &user_access);
}
if (json_is_null(rights)) {
/* remove user from ACL */
struct acl_item zero = {0,0,0,0,0};
if (change) change->new = zero;
}
else {
/* accumulate rights be granted and denied */
json_object_foreach(rights, right, val) {
unsigned set = json_boolean_value(val);
if (!strcmp(right, "mayAdmin"))
change->new.mayAdmin = set;
else if (!strcmp(right, "mayWrite"))
change->new.mayWrite = set;
else if (!strcmp(right, "mayRead"))
change->new.mayRead = set;
else if (iscalendar && !strcmp(right, "mayReadFreeBusy"))
change->new.mayReadFreeBusy = set;
}
if (isdav) change->new.mayPost = change->new.mayWrite;
}
}
/* add all the users back to the share ACL */
hash_enumerate_sorted(&user_access, add_useracls, &newacl, cmpstringp_raw);
/* ok, change the mailboxes database */
r = mboxlist_sync_setacls(mbox->name, newacl);
if (r) {
syslog(LOG_ERR, "mboxlist_sync_setacls(%s) failed: %s",
mbox->name, error_message(r));
}
else {
/* ok, change the backup in cyrus.header */
r = mailbox_set_acl(mbox, newacl, 1 /*dirty_modseq*/);
if (r) {
syslog(LOG_ERR, "mailbox_set_acl(%s) failed: %s",
mbox->name, error_message(r));
}
}
if (!r && isdav) {
/* Send sharing invites */
struct invite_rock irock;
struct meth_params *pparams;
mbname_t *mbname;
const char *errstr = NULL;
memset(&irock, 0, sizeof(struct invite_rock));
irock.owner = owner;
/* Find the DAV namespace for this mailbox */
if (iscalendar)
irock.tgt.namespace = &namespace_calendar;
else if (mbox->mbtype & MBTYPE_ADDRESSBOOK)
irock.tgt.namespace = &namespace_addressbook;
else
irock.tgt.namespace = &namespace_drive;
/* Get "live" properties for the namespace */
pparams = irock.tgt.namespace->methods[METH_PROPFIND].params;
irock.live_props = pparams->propfind.lprops;
/* Create DAV URL for this collection */
mbname = mbname_from_intname(mbox->name);
if (!mbname_domain(mbname)) mbname_set_domain(mbname, httpd_extradomain);
make_collection_url(&irock.resource, irock.tgt.namespace->prefix,
/*haszzzz*/0, mbname, mbname_userid(mbname));
/* Create a request target for this collection */
irock.tgt.flags = TGT_DAV_SHARED; // prevent old-style sharing redirect
r = pparams->parse_path(buf_cstring(&irock.resource), &irock.tgt, &errstr);
if (!r) {
/* Process each user */
irock.mboxname = mbox->name;
hash_enumerate(&user_access, send_dav_invite, &irock);
}
/* Cleanup */
if (irock.notify) xmlFreeDoc(irock.notify->doc);
mboxlist_entry_free(&irock.tgt.mbentry);
free(irock.tgt.userid);
buf_free(&irock.resource);
mbname_free(&mbname);
}
done:
free_hash_table(&user_access, &free);
free(owner);
free(newacl);
free(acl);
return r;
}
HIDDEN void jmap_parse_sharewith_patch(json_t *arg, json_t **shareWith)
{
struct buf buf = BUF_INITIALIZER;
const char *field = NULL;
json_t *jval;
json_object_foreach(arg, field, jval) {
if (!strncmp(field, "shareWith/", 10)) {
const char *userid = field + 10;
const char *right = strchr(userid, '/');
if (!*shareWith) *shareWith = json_object();
if (right) {
/* individual right */
buf_setmap(&buf, userid, right - userid);
userid = buf_cstring(&buf);
json_t *rights = json_object_get(*shareWith, userid);
if (rights) {
/* add to existing ShareRights for this userid */
json_object_set(rights, right+1, jval);
}
else {
/* create new ShareRights for this userid */
json_object_set_new(*shareWith, userid,
json_pack("{s:o}", right+1, jval));
}
}
else {
/* complete ShareRights */
json_object_set(*shareWith, userid, jval);
}
}
}
buf_free(&buf);
}
HIDDEN int jmap_is_using(jmap_req_t *req, const char *capa)
{
return strarray_find(req->using_capabilities, capa, 0) >= 0;
}
/*
* Lookup 'name' in the mailbox list, ignoring reserved/deleted records
*/
HIDDEN int jmap_mboxlist_lookup(const char *name,
mbentry_t **entryptr, struct txn **tid)
{
mbentry_t *entry = NULL;
int r;
r = mboxlist_lookup_allow_all(name, &entry, tid);
if (r) return r;
/* Ignore "reserved" entries, like they aren't there */
if (entry->mbtype & MBTYPE_RESERVE) {
mboxlist_entry_free(&entry);
return IMAP_MAILBOX_RESERVED;
}
/* Ignore "deleted" entries, like they aren't there */
if (entry->mbtype & MBTYPE_DELETED) {
mboxlist_entry_free(&entry);
return IMAP_MAILBOX_NONEXISTENT;
}
if (entryptr) *entryptr = entry;
else mboxlist_entry_free(&entry);
return 0;
}
static int _mbentry_by_uniqueid_cb(const mbentry_t *mbentry, void *rock)
{
struct hash_table *hash = rock;
hash_insert(mbentry->uniqueid, mboxlist_entry_copy(mbentry), hash);
return 0;
}
EXPORTED const mbentry_t *jmap_mbentry_by_uniqueid(jmap_req_t *req, const char *id)
{
if (!req->mbentry_byid) {
req->mbentry_byid = xzmalloc(sizeof(struct hash_table));
construct_hash_table(req->mbentry_byid, 1024, 0);
mboxlist_usermboxtree(req->accountid, req->authstate,
_mbentry_by_uniqueid_cb, req->mbentry_byid,
MBOXTREE_INTERMEDIATES);
}
return (const mbentry_t *)hash_lookup(id, req->mbentry_byid);
}
EXPORTED mbentry_t *jmap_mbentry_by_uniqueid_copy(jmap_req_t *req, const char *id)
{
const mbentry_t *mbentry = jmap_mbentry_by_uniqueid(req, id);
if (!mbentry) return NULL;
return mboxlist_entry_copy(mbentry);
}
static void _free_mbentry(void *rock)
{
mbentry_t *entry = rock;
mboxlist_entry_free(&entry);
}
EXPORTED void jmap_mbentry_cache_free(jmap_req_t *req)
{
if (req->mbentry_byid) {
free_hash_table(req->mbentry_byid, _free_mbentry);
free(req->mbentry_byid);
req->mbentry_byid = NULL;
}
}
diff --git a/imap/jmap_api.h b/imap/jmap_api.h
index 9ff1d7480..0256b163d 100644
--- a/imap/jmap_api.h
+++ b/imap/jmap_api.h
@@ -1,487 +1,490 @@
/* http_api.h -- Routines for handling JMAP API requests
*
* Copyright (c) 1994-2019 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#ifndef JMAP_API_H
#define JMAP_API_H
#include "auth.h"
#include "conversations.h"
#include "hash.h"
#include "jmap_util.h"
#include "json_support.h"
#include "mailbox.h"
#include "mboxname.h"
#include "msgrecord.h"
#include "ptrarray.h"
#include "strarray.h"
#define JMAP_URN_CORE "urn:ietf:params:jmap:core"
#define JMAP_URN_MAIL "urn:ietf:params:jmap:mail"
#define JMAP_URN_SUBMISSION "urn:ietf:params:jmap:submission"
#define JMAP_URN_VACATION "urn:ietf:params:jmap:vacationresponse"
#define JMAP_URN_WEBSOCKET "urn:ietf:params:jmap:websocket"
#define JMAP_CONTACTS_EXTENSION "https://cyrusimap.org/ns/jmap/contacts"
#define JMAP_CALENDARS_EXTENSION "https://cyrusimap.org/ns/jmap/calendars"
#define JMAP_MAIL_EXTENSION "https://cyrusimap.org/ns/jmap/mail"
#define JMAP_PERFORMANCE_EXTENSION "https://cyrusimap.org/ns/jmap/performance"
#define JMAP_DEBUG_EXTENSION "https://cyrusimap.org/ns/jmap/debug"
#define JMAP_QUOTA_EXTENSION "https://cyrusimap.org/ns/jmap/quota"
#define JMAP_SEARCH_EXTENSION "https://cyrusimap.org/ns/jmap/search"
#define JMAP_BACKUP_EXTENSION "https://cyrusimap.org/ns/jmap/backup"
+#define JMAP_NOTES_EXTENSION "https://cyrusimap.org/ns/jmap/notes"
enum {
MAX_SIZE_REQUEST = 0,
MAX_CALLS_IN_REQUEST,
MAX_CONCURRENT_REQUESTS,
MAX_OBJECTS_IN_GET,
MAX_OBJECTS_IN_SET,
MAX_SIZE_UPLOAD,
MAX_CONCURRENT_UPLOAD,
JMAP_NUM_LIMITS /* MUST be last */
};
typedef struct jmap_req {
const char *method;
const char *userid;
const char *accountid;
struct conversations_state *cstate;
struct auth_state *authstate;
json_t *args;
json_t *response;
const char *tag;
struct transaction_t *txn;
struct mboxname_counters counters;
double real_start;
double user_start;
double sys_start;
json_t *perf_details;
/* The JMAP request keeps its own cache of opened mailboxes,
* which can be used by calling jmap_openmbox. If the
* force_openmboxrw is set, this causes all following
* mailboxes to be opened read-writeable, irrespective if
* the caller asked for a read-only lock. This allows to
* prevent lock promotion conflicts, in case a cached mailbox
* was opened read-only by a helper but it now asked to be
* locked exclusively. Since the mailbox lock does not
* support lock promition, this would currently abort with
* an error. */
int force_openmbox_rw;
/* Internal state */
ptrarray_t *mboxes;
hash_table *mboxrights;
hash_table *created_ids;
hash_table *mbentry_byid;
ptrarray_t *method_calls;
const strarray_t *using_capabilities;
} jmap_req_t;
/* Write the contents of the blob identified by blobid on the
* HTTP transaction embedded in JMAP request req.
* If not NULL, accept_mime defines the requested MIME type,
* either defined in the Accept header or {type} URI template
* parameter.
*
* Return HTTP_OK if the blob has been written or any other
* HTTP status on error.
* Return zero if the next blob handler should be called. */
typedef int jmap_getblob_handler(jmap_req_t *req,
const char *blobid,
const char *accept_mime);
typedef struct {
hash_table methods;
json_t *server_capabilities;
long limits[JMAP_NUM_LIMITS];
ptrarray_t getblob_handlers; // array of jmap_getblob_handler
} jmap_settings_t;
typedef struct {
const char *name;
const char *capability;
int (*proc)(struct jmap_req *req);
int flags;
} jmap_method_t;
extern int jmap_api(struct transaction_t *txn, json_t **res,
jmap_settings_t *settings);
extern int jmap_initreq(jmap_req_t *req);
extern void jmap_finireq(jmap_req_t *req);
extern int jmap_is_using(jmap_req_t *req, const char *capa);
#define JMAP_SHARED_CSTATE 1 << 0
/* Protocol implementations */
extern void jmap_core_init(jmap_settings_t *settings);
extern void jmap_mail_init(jmap_settings_t *settings);
extern void jmap_contact_init(jmap_settings_t *settings);
extern void jmap_calendar_init(jmap_settings_t *settings);
extern void jmap_vacation_init(jmap_settings_t *settings);
extern void jmap_backup_init(jmap_settings_t *settings);
+extern void jmap_notes_init(jmap_settings_t *settings);
extern void jmap_core_capabilities(json_t *account_capabilities);
extern void jmap_mail_capabilities(json_t *account_capabilities, int mayCreateTopLevel);
extern void jmap_emailsubmission_capabilities(json_t *account_capabilities);
extern void jmap_vacation_capabilities(json_t *account_capabilities);
extern void jmap_contact_capabilities(json_t *account_capabilities);
extern void jmap_calendar_capabilities(json_t *account_capabilities);
extern void jmap_vacation_capabilities(json_t *account_capabilities);
extern void jmap_backup_capabilities(json_t *account_capabilities);
+extern void jmap_notes_capabilities(json_t *account_capabilities);
extern void jmap_accounts(json_t *accounts, json_t *primary_accounts);
/* Request-scoped mailbox cache */
extern int jmap_openmbox(jmap_req_t *req, const char *name,
struct mailbox **mboxp, int rw);
extern int jmap_isopenmbox(jmap_req_t *req, const char *name);
extern void jmap_closembox(jmap_req_t *req, struct mailbox **mboxp);
extern int jmap_mboxlist_lookup(const char *name,
mbentry_t **entryptr, struct txn **tid);
/* Adds a JMAP sub request to be processed after req has
* finished. Method must be a regular JMAP method name,
* args the JSON-encoded method arguments. If client_id
* is NULL, the subrequest will use the same client id
* as req. The args argument will be unreferenced after
* completion. */
extern void jmap_add_subreq(jmap_req_t *req, const char *method,
json_t *args, const char *client_id);
/* Creation ids */
extern const char *jmap_lookup_id(jmap_req_t *req, const char *creation_id);
extern const char *jmap_id_string_value(jmap_req_t *req, json_t *item);
extern void jmap_add_id(jmap_req_t *req, const char *creation_id, const char *id);
extern int jmap_is_valid_id(const char *id);
/* Request-scoped cache of mailbox rights for authenticated user */
extern int jmap_myrights(jmap_req_t *req, const mbentry_t *mbentry);
extern int jmap_hasrights(jmap_req_t *req, const mbentry_t *mbentry,
int rights);
extern int jmap_myrights_byname(jmap_req_t *req, const char *mboxname);
extern int jmap_hasrights_byname(jmap_req_t *req, const char *mboxname,
int rights);
extern void jmap_myrights_delete(jmap_req_t *req, const char *mboxname);
/* Blob services */
extern int jmap_findblob(jmap_req_t *req, const char *accountid,
const char *blobid,
struct mailbox **mbox, msgrecord_t **mr,
struct body **body, const struct body **part,
struct buf *blob);
extern int jmap_findblob_exact(jmap_req_t *req, const char *accountid,
const char *blobid,
struct mailbox **mbox, msgrecord_t **mr);
extern const struct body *jmap_contact_findblob(struct message_guid *content_guid,
const char *part_id,
struct mailbox *mbox,
msgrecord_t *mr,
struct buf *blob);
#define JMAP_BLOBID_SIZE 42
extern void jmap_set_blobid(const struct message_guid *guid, char *buf);
#define JMAP_EMAILID_SIZE 26
extern void jmap_set_emailid(const struct message_guid *guid, char *buf);
#define JMAP_THREADID_SIZE 18
extern void jmap_set_threadid(conversation_id_t cid, char *buf);
/* JMAP states */
extern json_t* jmap_getstate(jmap_req_t *req, int mbtype, int refresh);
extern json_t *jmap_fmtstate(modseq_t modseq);
extern int jmap_cmpstate(jmap_req_t *req, json_t *state, int mbtype);
extern modseq_t jmap_highestmodseq(jmap_req_t *req, int mbtype);
/* Helpers for DAV-based JMAP types */
extern char *jmap_xhref(const char *mboxname, const char *resource);
/* Patch-object support */
extern void jmap_ok(jmap_req_t *req, json_t *res);
extern void jmap_error(jmap_req_t *req, json_t *err);
extern int jmap_parse_strings(json_t *arg,
struct jmap_parser *parser, const char *prop);
typedef struct jmap_property {
const char *name;
const char *capability;
unsigned flags;
} jmap_property_t;
enum {
JMAP_PROP_SERVER_SET = (1<<0),
JMAP_PROP_IMMUTABLE = (1<<1),
JMAP_PROP_SKIP_GET = (1<<2), // skip in Foo/get if not requested by client
JMAP_PROP_ALWAYS_GET = (1<<3) // always include in Foo/get
};
extern const jmap_property_t *jmap_property_find(const char *name,
const jmap_property_t props[]);
/* Foo/get */
struct jmap_get {
/* Request arguments */
json_t *ids;
json_t *properties;
hash_table *props;
/* Response fields */
char *state;
json_t *list;
json_t *not_found;
};
typedef int jmap_args_parse_cb(jmap_req_t *, struct jmap_parser *,
const char *arg, json_t *val, void *);
extern void jmap_get_parse(jmap_req_t *req, struct jmap_parser *parser,
const jmap_property_t valid_props[],
int allow_null_ids,
jmap_args_parse_cb args_parse, void *args_rock,
struct jmap_get *get,
json_t **err);
extern void jmap_get_fini(struct jmap_get *get);
extern json_t *jmap_get_reply(struct jmap_get *get);
/* Foo/set */
struct jmap_set {
/* Request arguments */
const char *if_in_state;
json_t *create;
json_t *update;
json_t *destroy;
/* Response fields */
char *old_state;
char *new_state;
json_t *created;
json_t *updated;
json_t *destroyed;
json_t *not_created;
json_t *not_updated;
json_t *not_destroyed;
};
extern void jmap_set_parse(jmap_req_t *req, struct jmap_parser *parser,
const jmap_property_t valid_props[],
jmap_args_parse_cb args_parse, void *args_rock,
struct jmap_set *set, json_t **err);
extern void jmap_set_fini(struct jmap_set *set);
extern json_t *jmap_set_reply(struct jmap_set *set);
/* Foo/changes */
struct jmap_changes {
/* Request arguments */
modseq_t since_modseq;
size_t max_changes;
/* Response fields */
modseq_t new_modseq;
short has_more_changes;
json_t *created;
json_t *updated;
json_t *destroyed;
};
extern void jmap_changes_parse(jmap_req_t *req, struct jmap_parser *parser,
jmap_args_parse_cb args_parse, void *args_rock,
struct jmap_changes *changes, json_t **err);
extern void jmap_changes_fini(struct jmap_changes *changes);
extern json_t *jmap_changes_reply(struct jmap_changes *changes);
/* Foo/copy */
struct jmap_copy {
/* Request arguments */
const char *from_account_id;
json_t *create;
int blob_copy;
int on_success_destroy_original;
/* Response fields */
json_t *created;
json_t *not_created;
};
extern void jmap_copy_parse(jmap_req_t *req, struct jmap_parser *parser,
jmap_args_parse_cb args_parse, void *args_rock,
struct jmap_copy *copy, json_t **err);
extern void jmap_copy_fini(struct jmap_copy *copy);
extern json_t *jmap_copy_reply(struct jmap_copy *copy);
/* Foo/query */
struct jmap_query {
/* Request arguments */
json_t *filter;
json_t *sort;
ssize_t position;
const char *anchor;
ssize_t anchor_offset;
size_t limit;
int have_limit;
int calculate_total;
int sort_savedate;
/* Response fields */
char *query_state;
int can_calculate_changes;
size_t result_position;
size_t total;
json_t *ids;
};
enum jmap_filter_op {
JMAP_FILTER_OP_NONE = 0,
JMAP_FILTER_OP_AND,
JMAP_FILTER_OP_OR,
JMAP_FILTER_OP_NOT
};
typedef struct jmap_filter {
enum jmap_filter_op op;
ptrarray_t conditions;
} jmap_filter;
typedef void* jmap_buildfilter_cb(json_t* arg);
typedef int jmap_filtermatch_cb(void* cond, void* rock);
typedef void jmap_filterfree_cb(void* cond);
extern jmap_filter *jmap_buildfilter(json_t *arg, jmap_buildfilter_cb *parse);
extern int jmap_filter_match(jmap_filter *f,
jmap_filtermatch_cb *match, void *rock);
extern void jmap_filter_free(jmap_filter *f, jmap_filterfree_cb *freecond);
typedef void jmap_filter_parse_cb(jmap_req_t *req, struct jmap_parser *parser,
json_t *filter, json_t *unsupported,
void *rock, json_t **err);
extern void jmap_filter_parse(jmap_req_t *req, struct jmap_parser *parser,
json_t *filter, json_t *unsupported,
jmap_filter_parse_cb parse_condition, void *cond_rock,
json_t **err /* fatal, non-parsing error */);
struct jmap_comparator {
const char *property;
short is_ascending;
const char *collation;
};
typedef int jmap_comparator_parse_cb(jmap_req_t *req, struct jmap_comparator *comp,
void *rock, json_t **err);
extern void jmap_comparator_parse(jmap_req_t *req, struct jmap_parser *parser,
json_t *jsort, json_t *unsupported,
jmap_comparator_parse_cb comp_cb, void *comp_rock,
json_t **err);
extern void jmap_query_parse(jmap_req_t *req, struct jmap_parser *parser,
jmap_args_parse_cb args_parse, void *args_rock,
jmap_filter_parse_cb filter_cb, void *filter_rock,
jmap_comparator_parse_cb comp_cb, void *comp_rock,
struct jmap_query *query, json_t **err);
extern void jmap_query_fini(struct jmap_query *query);
extern json_t *jmap_query_reply(struct jmap_query *query);
/* Foo/queryChanges */
struct jmap_querychanges {
/* Request arguments */
json_t *filter;
json_t *sort;
const char *since_querystate;
size_t max_changes;
const char *up_to_id;
int calculate_total;
int sort_savedate;
/* Response fields */
char *new_querystate;
size_t total;
json_t *removed;
json_t *added;
};
extern void jmap_querychanges_parse(jmap_req_t *req,
struct jmap_parser *parser,
jmap_args_parse_cb args_parse, void *args_rock,
jmap_filter_parse_cb filter_cb, void *filter_rock,
jmap_comparator_parse_cb comp_cb, void *sort_rock,
struct jmap_querychanges *query,
json_t **err);
extern void jmap_querychanges_fini(struct jmap_querychanges *query);
extern json_t *jmap_querychanges_reply(struct jmap_querychanges *query);
extern json_t *jmap_get_sharewith(const mbentry_t *mbentry);
extern int jmap_set_sharewith(struct mailbox *mbox,
json_t *shareWith, int overwrite);
extern void jmap_parse_sharewith_patch(json_t *arg, json_t **shareWith);
extern void jmap_mbentry_cache_free(jmap_req_t *req);
extern const mbentry_t *jmap_mbentry_by_uniqueid(jmap_req_t *req, const char *id);
extern mbentry_t *jmap_mbentry_by_uniqueid_copy(jmap_req_t *req, const char *id);
#endif /* JMAP_API_H */
diff --git a/imap/jmap_notes.c b/imap/jmap_notes.c
new file mode 100644
index 000000000..5ebeda4af
--- /dev/null
+++ b/imap/jmap_notes.c
@@ -0,0 +1,1117 @@
+/* jmap_notes.c -- Routines for handling JMAP notes
+ *
+ * Copyright (c) 1994-2019 Carnegie Mellon University. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. The name "Carnegie Mellon University" must not be used to
+ * endorse or promote products derived from this software without
+ * prior written permission. For permission or any legal
+ * details, please contact
+ * Carnegie Mellon University
+ * Center for Technology Transfer and Enterprise Creation
+ * 4615 Forbes Avenue
+ * Suite 302
+ * Pittsburgh, PA 15213
+ * (412) 268-7393, fax: (412) 268-7395
+ * innovation@andrew.cmu.edu
+ *
+ * 4. Redistributions of any form whatsoever must retain the following
+ * acknowledgment:
+ * "This product includes software developed by Computing Services
+ * at Carnegie Mellon University (http://www.cmu.edu/computing/)."
+ *
+ * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
+ * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
+ * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ */
+
+#include <config.h>
+
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#include <ctype.h>
+#include <string.h>
+#include <syslog.h>
+#include <assert.h>
+#include <errno.h>
+
+#include "acl.h"
+#include "append.h"
+#include "http_jmap.h"
+#include "http_proxy.h"
+#include "jmap_mail.h"
+#include "jmap_util.h"
+#include "json_support.h"
+#include "proxy.h"
+#include "sync_support.h"
+#include "times.h"
+#include "user.h"
+#include "util.h"
+
+/* generated headers are not necessarily in current directory */
+#include "imap/http_err.h"
+#include "imap/imap_err.h"
+
+#define APPLE_NOTES_ID "com.apple.mail-note"
+
+static int jmap_notes_get(jmap_req_t *req);
+static int jmap_notes_set(jmap_req_t *req);
+static int jmap_notes_changes(jmap_req_t *req);
+
+static jmap_method_t jmap_notes_methods_standard[] = {
+ { NULL, NULL, NULL, 0}
+};
+
+static jmap_method_t jmap_notes_methods_nonstandard[] = {
+ {
+ "Notes/get",
+ JMAP_NOTES_EXTENSION,
+ &jmap_notes_get,
+ JMAP_SHARED_CSTATE
+ },
+ {
+ "Notes/set",
+ JMAP_NOTES_EXTENSION,
+ &jmap_notes_set,
+ /*flags*/0
+ },
+ {
+ "Notes/changes",
+ JMAP_NOTES_EXTENSION,
+ &jmap_notes_changes,
+ JMAP_SHARED_CSTATE
+ },
+ { NULL, NULL, NULL, 0}
+};
+
+HIDDEN void jmap_notes_init(jmap_settings_t *settings)
+{
+ if (!config_getstring(IMAPOPT_NOTESMAILBOX)) return;
+
+ jmap_method_t *mp;
+ for (mp = jmap_notes_methods_standard; mp->name; mp++) {
+ hash_insert(mp->name, mp, &settings->methods);
+ }
+
+ json_object_set_new(settings->server_capabilities,
+ JMAP_NOTES_EXTENSION, json_object());
+
+ if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
+ for (mp = jmap_notes_methods_nonstandard; mp->name; mp++) {
+ hash_insert(mp->name, mp, &settings->methods);
+ }
+ }
+}
+
+HIDDEN void jmap_notes_capabilities(json_t *account_capabilities)
+{
+ if (!config_getstring(IMAPOPT_NOTESMAILBOX)) return;
+
+ if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
+ json_object_set_new(account_capabilities,
+ JMAP_NOTES_EXTENSION, json_object());
+ }
+}
+
+static int lookup_notes_collection(const char *accountid, mbentry_t **mbentry)
+{
+ mbname_t *mbname;
+ const char *notesname;
+ int r;
+
+ /* Create notes mailbox name from the parsed path */
+ mbname = mbname_from_userid(accountid);
+ mbname_push_boxes(mbname, config_getstring(IMAPOPT_NOTESMAILBOX));
+
+ /* XXX - hack to allow @domain parts for non-domain-split users */
+ if (httpd_extradomain) {
+ /* not allowed to be cross domain */
+ if (mbname_localpart(mbname) &&
+ strcmpsafe(mbname_domain(mbname), httpd_extradomain)) {
+ r = HTTP_NOT_FOUND;
+ goto done;
+ }
+ mbname_set_domain(mbname, NULL);
+ }
+
+ /* Locate the mailbox */
+ notesname = mbname_intname(mbname);
+ r = http_mlookup(notesname, mbentry, NULL);
+ if (r == IMAP_MAILBOX_NONEXISTENT) {
+ /* Find location of INBOX */
+ char *inboxname = mboxname_user_mbox(accountid, NULL);
+
+ int r1 = http_mlookup(inboxname, mbentry, NULL);
+ free(inboxname);
+ if (r1 == IMAP_MAILBOX_NONEXISTENT) {
+ r = IMAP_INVALID_USER;
+ goto done;
+ }
+
+ int rights = httpd_myrights(httpd_authstate, *mbentry);
+ if (!(rights & ACL_CREATE)) {
+ r = IMAP_PERMISSION_DENIED;
+ goto done;
+ }
+
+ if (*mbentry) free((*mbentry)->name);
+ else *mbentry = mboxlist_entry_create();
+ (*mbentry)->name = xstrdup(notesname);
+ }
+ else if (!r) {
+ int rights = httpd_myrights(httpd_authstate, *mbentry);
+ if (!(rights & ACL_INSERT)) {
+ r = IMAP_PERMISSION_DENIED;
+ goto done;
+ }
+ }
+
+ done:
+ mbname_free(&mbname);
+ return r;
+}
+
+
+static int ensure_notes_collection(const char *accountid, mbentry_t **mbentryp)
+{
+ mbentry_t *mbentry = NULL;
+
+ /* notes collection */
+ int r = lookup_notes_collection(accountid, &mbentry);
+ if (!r) { // happy path
+ if (mbentryp) *mbentryp = mbentry;
+ else mboxlist_entry_free(&mbentry);
+ return 0;
+ }
+
+ // otherwise, clean up ready for next attempt
+ mboxlist_entry_free(&mbentry);
+
+ struct mboxlock *namespacelock = user_namespacelock(accountid);
+
+ // did we lose the race?
+ r = lookup_notes_collection(accountid, &mbentry);
+
+ if (r == IMAP_MAILBOX_NONEXISTENT) {
+ if (!mbentry) goto done;
+ else if (mbentry->server) {
+ proxy_findserver(mbentry->server, &http_protocol, httpd_userid,
+ &backend_cached, NULL, NULL, httpd_in);
+ goto done;
+ }
+
+ r = mboxlist_createmailbox(mbentry->name, MBTYPE_EMAIL,
+ NULL, 1 /* admin */, accountid,
+ httpd_authstate,
+ 0, 0, 0, 0, NULL);
+ if (r) {
+ syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
+ mbentry->name, error_message(r));
+ }
+ else {
+ char *userid = mboxname_to_userid(mbentry->name);
+ static const char *annot = "/specialuse";
+ struct buf buf = BUF_INITIALIZER;
+
+ buf_init_ro_cstr(&buf, "\\XNotes");
+ r = annotatemore_write(mbentry->name, annot, userid, &buf);
+ free(userid);
+ buf_reset(&buf);
+ if (r) {
+ syslog(LOG_ERR, "failed to write annotation %s: %s",
+ annot, error_message(r));
+ goto done;
+ }
+ }
+ }
+
+ done:
+ mboxname_release(&namespacelock);
+ if (mbentryp && !r) *mbentryp = mbentry;
+ else mboxlist_entry_free(&mbentry);
+ return r;
+}
+
+struct get_rock {
+ struct jmap_get *get;
+ struct buf *buf;
+};
+
+static int _note_get(message_t *msg, json_t *note, hash_table *props,
+ int want_created, struct buf *buf)
+{
+ int r;
+
+ /* created */
+ if (want_created) {
+ r = message_get_field(msg, "X-Mail-Created-Date",
+ MESSAGE_DECODED|MESSAGE_TRIM, buf);
+ if (r) return r;
+
+ json_object_set_new(note, "created", json_string(buf_cstring(buf)));
+ }
+
+ /* isFlagged */
+ if (jmap_wantprop(props, "isFlagged")) {
+ uint32_t system_flags;
+
+ r = message_get_systemflags(msg, &system_flags);
+ if (r) return r;
+
+ json_object_set_new(note, "isFlagged",
+ json_boolean(system_flags & FLAG_FLAGGED));
+ }
+
+ /* lastSaved */
+ if (jmap_wantprop(props, "lastSaved")) {
+ char datestr[RFC3339_DATETIME_MAX];
+ time_t t;
+
+ r = message_get_savedate(msg, &t);
+ if (r) return r;
+
+ time_to_rfc3339(t, datestr, RFC3339_DATETIME_MAX);
+ json_object_set_new(note, "lastSaved", json_string(datestr));
+ }
+
+ /* title */
+ if (jmap_wantprop(props, "title")) {
+ buf_reset(buf);
+ r = message_get_subject(msg, buf);
+ if (r) return r;
+
+ json_object_set_new(note, "title", json_string(buf_cstring(buf)));
+ }
+
+ /* body */
+ if (jmap_wantprop(props, "body")) {
+ int encoding = 0;
+ const char *charset_id = NULL;
+ charset_t charset = CHARSET_UNKNOWN_CHARSET;
+// const struct body *body = NULL;
+
+ buf_reset(buf);
+ r = message_get_body(msg, buf);
+// if (!r) r = message_get_cachebody(msg, &body);
+ if (!r) r = message_get_encoding(msg, &encoding);
+ if (!r) r = message_get_charset_id(msg, &charset_id);
+ if (r) return r;
+
+ charset = charset_lookupname(charset_id);
+ if (encoding || strcasecmp(charset_name(charset), "utf-8")) {
+ char *dec = charset_to_utf8(buf_cstring(buf), buf_len(buf),
+ charset, encoding);
+ buf_initm(buf, dec, strlen(dec));
+ }
+ charset_free(&charset);
+
+ json_object_set_new(note, "body", json_string(buf_cstring(buf)));
+ }
+
+ /* isHTML */
+ if (jmap_wantprop(props, "isHTML")) {
+ const char *type = NULL, *subtype = NULL;
+
+ r = message_get_type(msg, &type);
+ if (!r) r = message_get_subtype(msg, &subtype);
+ if (r) return r;
+
+ int isHTML =
+ !strcasecmpsafe("text", type) && !strcasecmpsafe("html", subtype);
+ json_object_set_new(note, "isHTML", json_boolean(isHTML));
+ }
+
+ return 0;
+}
+
+static void _notes_get_cb(const char *id, message_t *msg,
+ void *data __attribute__((unused)), void *rock)
+{
+ struct get_rock *grock = (struct get_rock *) rock;
+ struct jmap_get *get = grock->get;
+ json_t *note = json_pack("{s:s}", "id", id);
+
+ int r = _note_get(msg, note, get->props, 0/*want_created*/, grock->buf);
+
+ if (!r) {
+ json_array_append_new(get->list, note);
+ }
+ else {
+ syslog(LOG_ERR, "jmap: Notes/get(%s): %s", id, error_message(r));
+ json_array_append_new(get->not_found, json_string(id));
+ json_decref(note);
+ }
+}
+
+static void foreach_note(struct mailbox *mbox, hash_table *ids,
+ void (*proc)(const char *, message_t *, void *, void *),
+ void *rock)
+{
+ struct mailbox_iter *iter = mailbox_iter_init(mbox, 0, ITER_SKIP_EXPUNGED);
+ struct buf buf = BUF_INITIALIZER;
+ message_t *msg;
+
+ while ((msg = (message_t *) mailbox_iter_step(iter))) {
+ int r = message_get_field(msg, "X-Uniform-Type-Identifier",
+ MESSAGE_DECODED|MESSAGE_TRIM, &buf);
+ if (r || strcmp(APPLE_NOTES_ID, buf_cstring(&buf))) continue;
+
+ r = message_get_field(msg, "X-Universally-Unique-Identifier",
+ MESSAGE_DECODED|MESSAGE_TRIM, &buf);
+ if (r) continue;
+
+ void *data = NULL;
+ const char *id = buf_cstring(&buf);
+ if (ids->size) {
+ /* Do we have this id? */
+ data = hash_lookup(id, ids);
+ if (!data) {
+ /* Not in our list to act on */
+ continue;
+ }
+
+ hash_del(id, ids);
+ }
+
+ proc(id, msg, data, rock);
+ }
+
+ mailbox_iter_done(&iter);
+ buf_free(&buf);
+}
+
+static void not_found_cb(const char *id,
+ void *data __attribute__((unused)), void *rock)
+{
+ json_t *json = (json_t *) rock;
+
+ if (json_is_object(json)) {
+ json_t *err = json_pack("{s:s}", "type", "notFound");
+ json_object_set_new(json, id, err);
+ }
+ else {
+ json_array_append_new(json, json_string(id));
+ }
+}
+
+static const jmap_property_t notes_props[] = {
+ {
+ "id",
+ NULL,
+ JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE | JMAP_PROP_ALWAYS_GET
+ },
+ {
+ "isFlagged",
+ NULL,
+ 0
+ },
+ {
+ "lastSaved",
+ NULL,
+ JMAP_PROP_SERVER_SET
+ },
+ {
+ "title",
+ NULL,
+ 0
+ },
+ {
+ "body",
+ NULL,
+ 0
+ },
+ {
+ "isHTML",
+ NULL,
+ 0
+ },
+ { NULL, NULL, 0 }
+};
+
+static int jmap_notes_get(jmap_req_t *req)
+{
+ struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
+ struct jmap_get get;
+ json_t *err = NULL;
+ mbentry_t *mbentry = NULL;
+ struct mailbox *mbox = NULL;
+ hash_table ids = HASH_TABLE_INITIALIZER;
+ struct buf buf = BUF_INITIALIZER;
+ int rights;
+
+ jmap_get_parse(req, &parser, notes_props, /*allow_null_ids*/1,
+ NULL, NULL, &get, &err);
+ if (err) {
+ jmap_error(req, err);
+ goto done;
+ }
+
+ int r = ensure_notes_collection(req->accountid, &mbentry);
+ if (r) {
+ syslog(LOG_ERR,
+ "jmap_notes_get: ensure_notes_collection(%s): %s",
+ req->accountid, error_message(r));
+ goto done;
+ }
+
+ rights = jmap_myrights(req, mbentry);
+
+ r = jmap_openmbox(req, mbentry->name, &mbox, 0);
+ mboxlist_entry_free(&mbentry);
+ if (r) goto done;
+
+ /* Does the client request specific notes? */
+ if (JNOTNULL(get.ids)) {
+ size_t i;
+ json_t *val;
+
+ construct_hash_table(&ids, 32, 0);
+
+ json_array_foreach(get.ids, i, val) {
+ hash_insert(json_string_value(val), (void *) 1, &ids);
+ }
+ }
+
+ if (rights & ACL_READ) {
+ struct get_rock grock = { &get, &buf };
+ foreach_note(mbox, &ids, &_notes_get_cb, &grock);
+ }
+
+ /* Any remaining ids are not found */
+ hash_enumerate(&ids, &not_found_cb, get.not_found);
+ free_hash_table(&ids, NULL);
+
+ /* Build response */
+ buf_reset(&buf);
+ buf_printf(&buf, MODSEQ_FMT, mbox->i.highestmodseq);
+ get.state = buf_release(&buf);
+ jmap_ok(req, jmap_get_reply(&get));
+
+ jmap_closembox(req, &mbox);
+
+done:
+ jmap_parser_fini(&parser);
+ jmap_get_fini(&get);
+
+ return 0;
+}
+
+static int _notes_setargs_check(const char *id, json_t *args, json_t **err)
+{
+ struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
+ int is_create = !id;
+ json_t *arg;
+ int r = 0;
+
+ /* Reject read-only properties */
+ if (json_object_get(args, "lastSaved")) {
+ jmap_parser_invalid(&parser, "lastSaved");
+ }
+
+ arg = json_object_get(args, "id");
+ if (arg && (is_create || strcmpnull(id, json_string_value(arg)))) {
+ jmap_parser_invalid(&parser, "id");
+ }
+
+ /* Type-check other properties */
+ arg = json_object_get(args, "isFlagged");
+ if (arg && !json_is_boolean(arg)) {
+ jmap_parser_invalid(&parser, "isFlagged");
+ }
+
+ arg = json_object_get(args, "title");
+ if (arg && !json_is_string(arg)) {
+ jmap_parser_invalid(&parser, "title");
+ }
+
+ arg = json_object_get(args, "body");
+ if (arg && (!json_is_string(arg) || !json_object_get(args, "isHTML"))) {
+ jmap_parser_invalid(&parser, "body");
+ }
+
+ arg = json_object_get(args, "isHTML");
+ if (arg && (!json_is_boolean(arg) || !json_object_get(args, "body"))) {
+ jmap_parser_invalid(&parser, "isHTML");
+ }
+
+ if (json_array_size(parser.invalid)) {
+ *err = json_pack("{s:s}", "type", "invalidProperties");
+ json_object_set(*err, "properties", parser.invalid);
+ r = -1;
+ }
+
+ jmap_parser_fini(&parser);
+
+ return r;
+}
+
+static int _note_create(struct mailbox *mailbox, json_t *note, json_t **new_note)
+{
+ struct stagemsg *stage = NULL;
+ struct appendstate as;
+ strarray_t flags = STRARRAY_INITIALIZER;
+ struct buf buf = BUF_INITIALIZER;
+ struct body *bodypart = NULL;
+ char datestr[80], *from, *title, *body;
+ FILE *f = NULL;
+ int r = 0, isFlagged = 0, isHTML = 0, qpencode = 0;
+ time_t now = time(0);
+ json_t *prop;
+ const char *uid = NULL, *created = NULL;
+
+ /* Prepare to stage the message */
+ if (!(f = append_newstage(mailbox->name, now, 0, &stage))) {
+ syslog(LOG_ERR, "append_newstage(%s) failed", mailbox->name);
+ r = IMAP_IOERROR;
+ goto done;
+ }
+
+ *new_note = json_object();
+
+ prop = json_object_get(note, "id");
+ if (prop) uid = json_string_value(prop);
+ else {
+ uid = makeuuid();
+ json_object_set_new(*new_note, "id", json_string(uid));
+ }
+
+ time_to_rfc3339(now, datestr, sizeof(datestr));
+ json_object_set_new(*new_note, "lastSaved", json_string(datestr));
+
+ time_to_rfc5322(now, datestr, sizeof(datestr));
+
+ prop = json_object_get(note, "created");
+ if (prop) created = json_string_value(prop);
+ else created = datestr;
+
+ prop = json_object_get(note, "title");
+ if (prop) {
+ buf_init_ro_cstr(&buf, json_string_value(prop));
+ title = charset_encode_mimeheader(buf_base(&buf), buf_len(&buf), 0);
+ }
+ else {
+ title = xstrdup("");
+ json_object_set_new(*new_note, "title", json_string(title));
+ }
+
+ prop = json_object_get(note, "body");
+ if (prop) {
+ buf_init_ro_cstr(&buf, json_string_value(prop));
+
+ const char *cp;
+ for (cp = buf_base(&buf); *cp; cp++) {
+ if (*cp & 0x80) {
+ qpencode = 1;
+ break;
+ }
+ }
+ if (qpencode) {
+ body = charset_qpencode_mimebody(buf_base(&buf),
+ buf_len(&buf), 0, NULL);
+ }
+ else body = buf_release(&buf);
+ }
+ else {
+ body = xstrdup("");
+ json_object_set_new(*new_note, "body", json_string(body));
+ }
+
+ prop = json_object_get(note, "isHTML");
+ if (prop) isHTML = json_boolean_value(prop);
+ else json_object_set_new(*new_note, "isHTML", json_false());
+
+ prop = json_object_get(note, "isFlagged");
+ if (prop) isFlagged = json_boolean_value(prop);
+ else json_object_set_new(*new_note, "isFlagged", json_false());
+
+ buf_reset(&buf);
+ if (strchr(httpd_userid, '@')) {
+ /* XXX This needs to be done via an LDAP/DB lookup */
+ buf_printf(&buf, "<%s>", httpd_userid);
+ }
+ else {
+ buf_printf(&buf, "<%s@%s>", httpd_userid, config_servername);
+ }
+ from = charset_encode_mimeheader(buf_cstring(&buf), buf_len(&buf), 0);
+
+ fprintf(f, "MIME-Version: 1.0 (Cyrus-JMAP/%s)\r\n"
+ "X-Uniform-Type-Identifier: %s\r\n"
+ "X-Universally-Unique-Identifier: %s\r\n"
+ "X-Mail-Created-Date: %s\r\n"
+ "Date: %s\r\n"
+ "From: %s\r\n"
+ "Subject: %s\r\n"
+ "Content-Type: text/%s; charset=utf-8\r\n"
+ "Content-Transfer-Encoding: %s\r\n\r\n"
+ "%s",
+ CYRUS_VERSION, APPLE_NOTES_ID, uid, created, datestr, from, title,
+ isHTML ? "html" : "plain",
+ qpencode ? "quoted-printable" : "7-bit",
+ body);
+ free(title);
+ free(from);
+ free(body);
+ if (ferror(f) || fflush(f)) {
+ r = IMAP_IOERROR;
+ }
+ fclose(f);
+ if (r) goto done;
+
+ /* Prepare to append the message to the mailbox */
+ r = append_setup_mbox(&as, mailbox, httpd_userid, httpd_authstate,
+ 0, /*quota*/NULL, 0, 0, /*event*/0);
+ if (r) {
+ syslog(LOG_ERR, "append_setup(%s) failed: %s",
+ mailbox->name, error_message(r));
+ goto done;
+ }
+
+ /* Append the message to the mailbox */
+ if (isFlagged) strarray_append(&flags, "\\Flagged");
+ r = append_fromstage(&as, &bodypart, stage, now, 0,
+ &flags, 0, /*annots*/NULL);
+
+ if (r) {
+ append_abort(&as);
+ syslog(LOG_ERR, "append_fromstage(%s) failed: %s",
+ mailbox->name, error_message(r));
+ goto done;
+ }
+
+ r = append_commit(&as);
+ if (r) {
+ syslog(LOG_ERR, "append_commit(%s) failed: %s",
+ mailbox->name, error_message(r));
+ goto done;
+ }
+
+ done:
+ buf_free(&buf);
+ if (bodypart) {
+ message_free_body(bodypart);
+ free(bodypart);
+ }
+ strarray_fini(&flags);
+ append_removestage(stage);
+ if (mailbox) {
+ if (r) mailbox_abort(mailbox);
+ else r = mailbox_commit(mailbox);
+ }
+ if (r) {
+ json_decref(*new_note);
+ *new_note = NULL;
+ }
+
+ return r;
+}
+
+struct set_rock {
+ struct jmap_set *set;
+ struct buf *buf;
+ int rights;
+};
+
+static void _notes_update_cb(const char *id, message_t *msg,
+ void *data, void *rock)
+{
+ json_t *patch = (json_t *) data;
+ struct set_rock *srock = (struct set_rock *) rock;
+ struct jmap_set *set = srock->set;
+ json_t *err = NULL, *updated_note = NULL;
+ int r;
+
+ if ((srock->rights & (ACL_INSERT|ACL_EXPUNGE)) != (ACL_INSERT|ACL_EXPUNGE)) {
+ int read_only = !(srock->rights & ACL_READ);
+
+ err = json_pack("{s:s}", "type",
+ read_only ? "notFound" : "forbidden");
+ }
+ else if (!_notes_setargs_check(id, patch, &err)) {
+ json_t *note = json_pack("{s:s}", "id", id);
+
+ r = _note_get(msg, note, NULL, 1/*want_created*/, srock->buf);
+
+ if (r) {
+ syslog(LOG_ERR, "jmap: Notes/update(%s) fetch: %s",
+ id, error_message(r));
+ }
+ else {
+ json_t *new_note = jmap_patchobject_apply(note, patch, NULL);
+
+ if (new_note) {
+ r = _note_create(msg_mailbox(msg), new_note, &updated_note);
+ json_decref(new_note);
+ }
+ else {
+ r = IMAP_INTERNAL;
+ syslog(LOG_ERR, "jmap: Notes/update(%s) patch: %s",
+ id, error_message(r));
+ }
+ }
+ json_decref(note);
+
+ if (!r) {
+ const struct index_record *record = msg_record(msg);
+ struct mailbox *mailbox = msg_mailbox(msg);
+ struct index_record newrecord;
+ int userflag;
+
+ memcpy(&newrecord, record, sizeof(struct index_record));
+ r = mailbox_user_flag(mailbox, DFLAG_UNBIND, &userflag, 1);
+ if (!r) {
+ newrecord.user_flags[userflag/32] |= 1 << (userflag & 31);
+ newrecord.internal_flags |= FLAG_INTERNAL_EXPUNGED;
+
+ r = mailbox_rewrite_index_record(mailbox, &newrecord);
+ }
+ }
+
+ if (r) {
+ err = json_pack("{s:s, s:s}", "type", "serverFail",
+ "description", error_message(r));
+ }
+ }
+
+ if (err) {
+ json_object_set_new(set->not_updated, id, err);
+ }
+ else {
+ json_object_set_new(set->updated, id, updated_note);
+ }
+}
+
+static void _notes_destroy_cb(const char *id, message_t *msg,
+ void *data __attribute__((unused)), void *rock)
+{
+ struct set_rock *srock = (struct set_rock *) rock;
+ struct jmap_set *set = srock->set;
+ json_t *err = NULL;
+
+ if (!(srock->rights & ACL_EXPUNGE)) {
+ int read_only = !(srock->rights & ACL_READ);
+
+ err = json_pack("{s:s}", "type",
+ read_only ? "notFound" : "forbidden");
+ }
+ else {
+ const struct index_record *record = msg_record(msg);
+ struct index_record newrecord;
+
+ memcpy(&newrecord, record, sizeof(struct index_record));
+ newrecord.internal_flags |= FLAG_INTERNAL_EXPUNGED;
+
+ int r = mailbox_rewrite_index_record(msg_mailbox(msg), &newrecord);
+ if (r) {
+ err = json_pack("{s:s, s:s}", "type", "serverFail",
+ "description", error_message(r));
+ }
+ }
+
+ if (err) {
+ json_object_set_new(set->not_destroyed, id, err);
+ }
+ else {
+ json_array_append_new(set->destroyed, json_string(id));
+ }
+}
+
+static int jmap_notes_set(jmap_req_t *req)
+{
+ struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
+ struct jmap_set set;
+ struct mailbox *mbox = NULL;
+ mbentry_t *mbentry = NULL;
+ struct buf buf = BUF_INITIALIZER;
+ json_t *err = NULL;
+ int rights, r;
+
+ /* Parse request */
+ jmap_set_parse(req, &parser, notes_props, NULL, NULL, &set, &err);
+ if (err) {
+ jmap_error(req, err);
+ goto done;
+ }
+
+ if (json_array_size(parser.invalid)) {
+ err = json_pack("{s:s}", "type", "invalidProperties");
+ json_object_set(err, "properties", parser.invalid);
+ jmap_error(req, err);
+ goto done;
+ }
+
+ /* Process request */
+
+ r = ensure_notes_collection(req->accountid, &mbentry);
+ if (r) {
+ syslog(LOG_ERR,
+ "jmap_notes_set: ensure_notes_collection(%s): %s",
+ req->accountid, error_message(r));
+ goto done;
+ }
+
+ rights = jmap_myrights(req, mbentry);
+
+ r = jmap_openmbox(req, mbentry->name, &mbox, 1);
+ assert(mbox);
+ mboxlist_entry_free(&mbentry);
+ if (r) goto done;
+
+ buf_printf(&buf, MODSEQ_FMT, mbox->i.highestmodseq);
+ set.old_state = buf_release(&buf);
+
+ if (set.if_in_state && strcmp(set.if_in_state, set.old_state)) {
+ jmap_error(req, json_pack("{s:s}", "type", "stateMismatch"));
+ goto done;
+ }
+
+
+ /* create */
+ const char *creation_id, *id;
+ json_t *val;
+
+ json_object_foreach(set.create, creation_id, val) {
+ json_t *new_note = NULL;
+
+ if (!(rights & ACL_INSERT)) {
+ err = json_pack("{s:s}", "type", "forbidden");
+ }
+ else if (!_notes_setargs_check(NULL/*id*/, val, &err)) {
+ r = _note_create(mbox, val, &new_note);
+ if (r) err = jmap_server_error(r);
+ }
+ if (err) {
+ json_object_set_new(set.not_created, creation_id, err);
+ }
+ else {
+ /* Report note as created */
+ json_object_set_new(set.created, creation_id, new_note);
+
+ /* Register creation id */
+ id = json_string_value(json_object_get(new_note, "id"));
+ jmap_add_id(req, creation_id, id);
+ }
+ }
+
+
+ /* update */
+ hash_table ids = HASH_TABLE_INITIALIZER;
+ struct set_rock srock = { &set, &buf, rights };
+
+ construct_hash_table(&ids, 32, 0);
+
+ /* Build hash of ids */
+ json_object_foreach(set.update, id, val) {
+ hash_insert(id, val, &ids);
+ }
+
+ /* Iterate through each message and update matching ids */
+ foreach_note(mbox, &ids, &_notes_update_cb, &srock);
+
+ /* Any remaining ids are not updated */
+ hash_enumerate(&ids, &not_found_cb, set.not_updated);
+ free_hash_table(&ids, NULL);
+
+
+ /* destroy */
+ construct_hash_table(&ids, 32, 0);
+
+ /* Build hash of ids */
+ size_t i;
+ json_array_foreach(set.destroy, i, val) {
+ hash_insert(json_string_value(val), (void *) 1, &ids);
+ }
+
+ /* Iterate through each message and destroy matching ids */
+ foreach_note(mbox, &ids, &_notes_destroy_cb, &srock);
+
+ /* Any remaining ids are not destroyed */
+ hash_enumerate(&ids, &not_found_cb, set.not_destroyed);
+ free_hash_table(&ids, NULL);
+
+
+ /* force modseq to stable */
+ mailbox_unlock_index(mbox, NULL);
+
+ /* Build response */
+ buf_reset(&buf);
+ buf_printf(&buf, MODSEQ_FMT, mbox->i.highestmodseq);
+ set.new_state = buf_release(&buf);
+ jmap_ok(req, jmap_set_reply(&set));
+
+done:
+ jmap_closembox(req, &mbox);
+ jmap_parser_fini(&parser);
+ jmap_set_fini(&set);
+ return 0;
+}
+
+struct change {
+ modseq_t modseq;
+ json_t *id;
+ json_t *list;
+};
+
+static int change_cmp(const void **a, const void **b)
+{
+ const struct change *chg_a = (const struct change *) *a;
+ const struct change *chg_b = (const struct change *) *b;
+
+ return (chg_a - chg_b);
+}
+
+static int jmap_notes_changes(jmap_req_t *req)
+{
+ struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
+ struct jmap_changes changes;
+ struct mailbox *mbox = NULL;
+ mbentry_t *mbentry = NULL;
+ int userflag;
+
+ json_t *err = NULL;
+ jmap_changes_parse(req, &parser, NULL, NULL, &changes, &err);
+ if (err) {
+ jmap_error(req, err);
+ return 0;
+ }
+
+ int r = ensure_notes_collection(req->accountid, &mbentry);
+ if (r) {
+ syslog(LOG_ERR,
+ "jmap_notes_changes: ensure_submission_collection(%s): %s",
+ req->accountid, error_message(r));
+ goto done;
+ }
+
+ r = jmap_openmbox(req, mbentry->name, &mbox, 0);
+ mboxlist_entry_free(&mbentry);
+
+ r = mailbox_user_flag(mbox, DFLAG_UNBIND, &userflag, 0);
+ if (r) userflag = -1;
+
+ if (r) goto done;
+
+ struct mailbox_iter *iter = mailbox_iter_init(mbox, changes.since_modseq, 0);
+ modseq_t highest_modseq = mbox->i.highestmodseq;
+ ptrarray_t changed_msgs = PTRARRAY_INITIALIZER;
+ struct buf buf = BUF_INITIALIZER;
+ const message_t *msg;
+
+ while ((msg = mailbox_iter_step(iter))) {
+ const struct index_record *record = msg_record(msg);
+ json_t *change_list;
+ const char *id;
+
+ /* Determine the type of change */
+ if (record->internal_flags & FLAG_INTERNAL_EXPUNGED) {
+ /* Skip any notes that have been replaced by an update */
+ if (userflag >= 0 &&
+ record->user_flags[userflag/32] & (1<<(userflag & 31))) continue;
+
+ /* Skip any notes created AND deleted since modseq */
+ if (record->createdmodseq > changes.since_modseq) continue;
+
+ change_list = changes.destroyed;
+ }
+ else if (record->createdmodseq > changes.since_modseq) {
+ change_list = changes.created;
+ }
+ else {
+ change_list = changes.updated;
+ }
+
+ /* Fetch note uid */
+ if (message_get_field((message_t *) msg,
+ "X-Universally-Unique-Identifier",
+ MESSAGE_DECODED|MESSAGE_TRIM, &buf)) continue;
+ id = buf_cstring(&buf);
+
+ if (changes.max_changes) {
+ /* Add change to a list to be sorted nby modseq later */
+ struct change *change = xmalloc(sizeof(struct change));
+
+ ptrarray_append(&changed_msgs, change);
+ change->modseq = record->modseq;
+ change->id = json_string(id);
+ change->list = change_list;
+ }
+ else {
+ /* Add change to the proper array in the response */
+ json_array_append_new(change_list, json_string(id));
+ }
+ }
+ mailbox_iter_done(&iter);
+ jmap_closembox(req, &mbox);
+ buf_free(&buf);
+
+ if (changes.max_changes) {
+ int i;
+
+ if ((size_t) ptrarray_size(&changed_msgs) > changes.max_changes) {
+ /* Sort changes by modseq */
+ changes.has_more_changes = 1;
+ ptrarray_sort(&changed_msgs, &change_cmp);
+
+ /* Determine where to cutoff our list of changes
+ (MUST NOT split changes having the same modseq) */
+ struct change *change =
+ ptrarray_nth(&changed_msgs, changes.max_changes);
+ highest_modseq = change->modseq;
+
+ for (i = changes.max_changes - 1; i >= 0; i--) {
+ change = ptrarray_nth(&changed_msgs, i);
+ if (change->modseq < highest_modseq) {
+ highest_modseq = change->modseq;
+ changes.max_changes = i+1;
+ break;
+ }
+ }
+ if (i < 0) {
+ /* too many changes */
+ }
+ }
+
+ /* Output and/or free the changes */
+ for (i = 0; i < ptrarray_size(&changed_msgs); i++) {
+ struct change *change = ptrarray_nth(&changed_msgs, i);
+
+ if ((size_t) i < changes.max_changes) {
+ /* Add change to the proper array in the response */
+ json_array_append_new(change->list, change->id);
+ }
+ else {
+ /* Throw this change away */
+ json_decref(change->id);
+ }
+
+ free(change);
+ }
+ }
+ ptrarray_fini(&changed_msgs);
+
+ /* Set new state */
+ changes.new_modseq = highest_modseq;
+
+ jmap_ok(req, jmap_changes_reply(&changes));
+
+done:
+ jmap_changes_fini(&changes);
+ jmap_parser_fini(&parser);
+ return 0;
+}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Apr 4, 6:02 AM (1 w, 1 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820539
Default Alt Text
(240 KB)

Event Timeline