Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117753740
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
240 KB
Referenced Files
None
Subscribers
None
View Options
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, ¶ms);
if (type && subtype && !strchr(type, '*') && !strchr(subtype, '*'))
val = xstrdup(accept->token);
free(type);
free(subtype);
param_free(¶ms);
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, ¬_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, ¬_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, ¬_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
Details
Attached
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)
Attached To
Mode
R111 cyrus-imapd
Attached
Detach File
Event Timeline