Projects
Kolab:3.4
cyrus-imapd
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 105
View file
cyrus-imapd.spec
Changed
@@ -38,13 +38,13 @@ Name: cyrus-imapd Summary: A high-performance mail server with IMAP, POP3, NNTP and SIEVE support Version: 2.5 -Release: 0.1.dev20141017.git9704e5d4%{?dist} +Release: 0.1.dev20141030.git273403fc%{?dist} License: BSD Group: System Environment/Daemons URL: http://www.cyrusimap.org # Upstream sources -# From 9704e5d41981a18f46a63af78cf02c63fe3824a6 +# From 273403fcb26e095462cecd1b0dbf9b4f8e3f71d7 Source0: ftp://ftp.andrew.cmu.edu/pub/cyrus/%{_name}-%{real_version}%{?dot_snapshot_version}.tar.gz Source1: cyrus-imapd.imap-2.3.x-conf Source2: cyrus-imapd.cvt_cyrusdb_all
View file
cyrus-imapd-2.5.tar.gz/.gitignore
Changed
@@ -39,6 +39,7 @@ imap/ctl_cyrusdb imap/ctl_deliver imap/ctl_mboxlist +imap/ctl_zoneinfo imap/cvt_cyrusdb imap/cyr_charset imap/cyr_dbtool @@ -50,9 +51,13 @@ imap/cyr_synclog imap/cyr_userseen imap/cyrdump +imap/dav_reconstruct imap/deliver imap/fetchnews imap/fud +imap/http_err.c +imap/http_err.h +imap/httpd imap/idled imap/imap_err.c imap/imap_err.h @@ -81,6 +86,8 @@ imap/sync_reset imap/sync_server imap/tls_prune +imap/tz_err.c +imap/tz_err.h imap/unexpunge imtest/imtest install-sh @@ -141,5 +148,6 @@ sieve/tests/ stamp-h1 timsieved/timsieved +tools/vzic/vzic xversion.h ylwrap
View file
cyrus-imapd-2.5.tar.gz/Makefile.am
Changed
@@ -60,6 +60,10 @@ imap/mupdate_err.h \ imap/nntp_err.c \ imap/nntp_err.h \ + imap/http_err.c \ + imap/http_err.h \ + imap/tz_err.c \ + imap/tz_err.h \ com_err/et/compile_et \ perl/annotator/Makefile \ perl/annotator/Makefile.PL \ @@ -154,6 +158,13 @@ if NNTPD service_PROGRAMS += imap/nntpd endif +if HTTPD +AM_CPPFLAGS += $(HTTP_CPPFLAGS) +AM_LDFLAGS += $(HTTP_LIBS) +BUILT_SOURCES += imap/http_err.c imap/http_err.h imap/tz_err.c imap/tz_err.h +service_PROGRAMS += imap/httpd +user_PROGRAMS += imap/dav_reconstruct +endif if REPLICATION user_PROGRAMS += imap/sync_client imap/sync_reset service_PROGRAMS += imap/sync_server @@ -238,10 +249,12 @@ cunit/key.pem \ cunit/vg.supp \ doc \ + imap/http_err.et \ imap/imap_err.et \ imap/mupdate_err.et \ imap/nntp_err.et \ imap/rfc822_header.st \ + imap/tz_err.et \ lib/charset/aliases.txt \ lib/charset/big5.t \ lib/charset/gb2312.t \ @@ -804,8 +817,6 @@ imap/tls.h \ imap/tls_th-lock.c \ imap/tls_th-lock.h \ - imap/upgrade_index.c \ - imap/upgrade_index.h \ imap/user.c \ imap/user.h \ imap/userdeny_db.c \ @@ -818,6 +829,32 @@ imap_libcyrus_imap_la_LIBADD += $(JANSSON_LIBS) imap_libcyrus_imap_la_CFLAGS += $(JANSSON_CFLAGS) endif +if HTTPD +imap_libcyrus_imap_la_SOURCES += \ + imap/caldav_db.c \ + imap/carddav_db.c \ + imap/dav_db.c \ + imap/dav_util.c \ + imap/http_caldav.c \ + imap/http_carddav.c \ + imap/http_client.c \ + imap/http_dav.c \ + imap/http_dblookup.c \ + imap/http_ischedule.c \ + imap/http_proxy.c \ + imap/http_rss.c \ + imap/http_timezone.c \ + imap/httpd.c \ + imap/jcal.c \ + imap/proxy.c \ + imap/smtpclient.c \ + imap/spool.c \ + imap/xcal.c \ + imap/zoneinfo_db.c +nodist_imap_libcyrus_imap_la_SOURCES += \ + imap/http_err.c \ + imap/tz_err.c +endif imap_lmtpd_SOURCES = \ imap/lmtpd.c \ @@ -882,6 +919,36 @@ master/service.c imap_nntpd_LDADD = $(LD_SERVER_ADD) +nodist_imap_httpd_SOURCES = \ + imap/http_err.c \ + imap/http_err.h \ + imap/tz_err.c \ + imap/tz_err.h +imap_httpd_SOURCES = \ + imap/carddav_db.c \ + imap/caldav_db.c \ + imap/dav_db.c \ + imap/dav_util.c \ + imap/http_caldav.c \ + imap/http_carddav.c \ + imap/http_client.c \ + imap/http_dav.c \ + imap/http_dblookup.c \ + imap/http_ischedule.c \ + imap/http_proxy.c \ + imap/http_rss.c \ + imap/http_timezone.c \ + imap/httpd.c \ + imap/jcal.c \ + imap/mutex_fake.c \ + imap/proxy.c \ + imap/smtpclient.c \ + imap/spool.c \ + imap/xcal.c \ + imap/zoneinfo_db.c \ + master/service.c +imap_httpd_LDADD = $(LD_SERVER_ADD) + imap_pop3d_SOURCES = \ imap/mutex_fake.c \ imap/pop3d.c \ @@ -906,6 +973,9 @@ 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_smmapd_SOURCES = imap/mutex_fake.c imap/proxy.c imap/smmapd.c master/service.c imap_smmapd_LDADD = $(LD_SERVER_ADD) @@ -937,6 +1007,12 @@ imap/nntp_err.h imap/nntp_err.c: imap/nntp_err.et $(COMPILE_ET_DEP) cd imap && $(COMPILE_ET) ../$(top_srcdir)/imap/nntp_err.et +imap/http_err.h imap/http_err.c: imap/http_err.et $(COMPILE_ET_DEP) + cd imap && $(COMPILE_ET) ../$(top_srcdir)/imap/http_err.et + +imap/tz_err.h imap/tz_err.c: imap/tz_err.et $(COMPILE_ET_DEP) + cd imap && $(COMPILE_ET) ../$(top_srcdir)/imap/tz_err.et + if MAINTAINER_MODE imap/rfc822_header.c: imap/rfc822_header.st ${top_srcdir}/tools/compile_st.pl -c $< > $@.NEW && mv $@.NEW $@ @@ -1091,6 +1167,7 @@ man/deliver.8 \ man/fetchnews.8 \ man/fud.8 \ + man/httpd.8 \ man/idled.8 \ man/imapd.8 \ man/ipurge.8 \ @@ -1208,6 +1285,8 @@ sieve/bc_generate.c \ sieve/comparator.c \ sieve/comparator.h \ + sieve/flags.c \ + sieve/flags.h \ sieve/interp.c \ sieve/interp.h \ sieve/message.c \ @@ -1217,7 +1296,9 @@ sieve/sieve-lex.l \ sieve/sieve.y \ sieve/tree.c \ - sieve/tree.h + sieve/tree.h \ + sieve/varlist.c \ + sieve/varlist.h sieve_libcyrus_sieve_la_LIBADD = $(COM_ERR_LIBS) lib/libcyrus_min.la lib/libcyrus.la sieve_libcyrus_sieve_la_CFLAGS = $(AM_CFLAGS) $(CFLAG_VISIBILITY)
View file
cyrus-imapd-2.5.tar.gz/configure.ac
Changed
@@ -1093,6 +1093,7 @@ AC_ARG_ENABLE(nntp, [AS_HELP_STRING([--enable-nntp], [enable NNTP support])],,[enable_nntp="no";]) AM_CONDITIONAL([NNTPD], [test "$enable_nntp" != "no"]) + dnl dnl see if we're compiling the Murder support programs dnl @@ -1105,6 +1106,76 @@ fi dnl +dnl see if we're compiling with HTTP support +dnl +ENABLE_HTTP=no +AC_ARG_ENABLE(http, + [AS_HELP_STRING([--enable-http], [enable HTTP support])],, [enable_http="no";]) +AM_CONDITIONAL([HTTPD], [test "$enable_http" != "no"]) + +HTTP_CPPFLAGS= +HTTP_LIBS= +if test "$enable_http" != no; then +dnl +dnl make sure all the modules we need are present +dnl + ENABLE_RSS=yes + ENABLE_DAV=yes + PKG_CHECK_MODULES([XML2], [libxml-2.0],,AC_MSG_ERROR([Need libxml-2.0 for http])) + PKG_CHECK_MODULES([ICAL], [libical],,AC_MSG_ERROR([Need libical for http])) + PKG_CHECK_MODULES([SQLITE3], [sqlite3],,AC_MSG_ERROR([Need sqlite3 for http])) + PKG_CHECK_MODULES([JSON], [jansson],,AC_MSG_ERROR([Need jansson for http])) + + HTTP_LIBS="${XML2_LIBS} ${ICAL_LIBS} ${SQLITE3_LIBS} ${JSON_LIBS}" + HTTP_CPPFLAGS="${JSON_CFLAGS} ${SQLITE3_CFLAGS} ${ICAL_CFLAGS} ${XML2_CFLAGS}" + + AC_DEFINE(HAVE_XML2,[],[Build in libxml support?]) + AC_DEFINE(HAVE_ICAL,[],[Build in ical support?]) + AC_DEFINE(HAVE_SQLITE3,[],[Build in SQLite support?]) + + AC_DEFINE(WITH_DAV,[],[Build DAV support into httpd?]) + AC_DEFINE(WITH_JSON,[],[Build jCal/jCard/TZdist support into httpd?]) + + AC_CHECK_LIB(ical, icaltimezone_set_builtin_tzdata, + AC_DEFINE(HAVE_TZ_BY_REF,[], + [Build TZ by ref support into httpd?])) + + AC_EGREP_HEADER(ICAL_TZUNTIL_PROPERTY, ical.h, + AC_DEFINE(HAVE_TZDIST_PROPS,[], + [Do we have built-in support for TZdist props?])) + + CYRUS_OPENDKIM_CHK() + if test "$opendkimlib" = "yes"; then + AC_SUBST(DKIM_CFLAGS) + AC_SUBST(DKIM_LIBS) + AC_DEFINE(WITH_DKIM,[],[Build DKIM support into iSchedule?]) + fi + + AC_EGREP_HEADER(ICAL_SCHEDULESTATUS_PARAMETER, ical.h, + AC_DEFINE(HAVE_SCHEDULING_PARAMS,[], + [Do we have built-in support for scheduling params?])) + + AC_EGREP_HEADER(ICAL_VAVAILABILITY_COMPONENT, ical.h, + AC_DEFINE(HAVE_VAVAILABILITY,[], + [Build VAVAILABILITY support into httpd?])) + + AC_EGREP_HEADER(ICAL_VPOLL_COMPONENT, ical.h, + AC_DEFINE(HAVE_VPOLL,[], + [Build VPOLL support into httpd?])) + + ENABLE_RSCALE=yes + PKG_CHECK_MODULES([ICU], [icu-i18n],, ENABLE_RSCALE=no) + if test "$ENABLE_RSCALE" != no; then + AC_SUBST(ICU_CFLAGS) + AC_SUBST(ICU_LIBS) + AC_CHECK_LIB(ical, icalrecur_rscale_token_handling_is_supported, + AC_DEFINE(HAVE_RSCALE,[], [Build RSCALE support into httpd?])) + fi +fi +AC_SUBST(HTTP_CPPFLAGS) +AC_SUBST(HTTP_LIBS) + +dnl dnl see if we're compiling replication support programs dnl AC_ARG_ENABLE(replication, @@ -1368,6 +1439,10 @@ AC_MSG_NOTICE([Disabling unit tests because the required CUnit library is not installed]) enable_unit_tests=no fi + AC_CHECK_HEADER([CUnit/Basic.h], + AC_CHECK_TYPE([CU_SetUpFunc],AC_DEFINE(HAVE_CU_SETUPFUNC,[],[Do we have CU_SetUpFunc?]),, + [#include <CUnit/Basic.h>]) + ,) fi if test "$enable_unit_tests" = "yes" ; then dnl Valgrind is an amazingly useful tool for running tests. It doesn't @@ -1505,10 +1580,6 @@ # define HAVE_SS_FAMILY #endif /* !HAVE_STRUCT_SOCKADDR_STORAGE */ -#ifndef HAVE_SS_FAMILY -#define ss_family __ss_family -#endif - #ifndef AF_INET6 /* Define it to something that should never appear */ #define AF_INET6 AF_MAX @@ -1631,6 +1702,7 @@ gssapi: $gssapi autocreate: $enable_autocreate idled: $enable_idled + http: $enable_http kerberos V4: $krb4 murder: $enable_murder nntpd: $enable_nntp
View file
cyrus-imapd-2.5.tar.gz/contrib/dkim_canon_ischedule.patch
Added
@@ -0,0 +1,80 @@ +diff --git a/libopendkim/dkim-canon.c b/libopendkim/dkim-canon.c +index 125bf12..3531273 100644 +--- a/libopendkim/dkim-canon.c ++++ b/libopendkim/dkim-canon.c +@@ -342,6 +342,7 @@ dkim_canon_header_string(struct dkim_dstring *dstr, dkim_canon_t canon, + break; + + case DKIM_CANON_RELAXED: ++ case DKIM_CANON_ISCHEDULE: + /* process header field name (before colon) first */ + for (p = hdr; p < hdr + hdrlen; p++) + { +@@ -847,7 +848,8 @@ dkim_add_canon(DKIM *dkim, _Bool hdr, dkim_canon_t canon, int hashtype, + DKIM_CANON *new; + + assert(dkim != NULL); +- assert(canon == DKIM_CANON_SIMPLE || canon == DKIM_CANON_RELAXED); ++ assert(canon == DKIM_CANON_SIMPLE || canon == DKIM_CANON_RELAXED || ++ canon == DKIM_CANON_ISCHEDULE); + if (dkim_libfeature(dkim->dkim_libhandle, DKIM_FEATURE_SHA256)) + { + assert(hashtype == DKIM_HASHTYPE_SHA1 || +@@ -1381,7 +1383,8 @@ dkim_canon_runheaders(DKIM *dkim) + tmphdr.hdr_flags = 0; + tmphdr.hdr_next = NULL; + +- if (cur->canon_canon == DKIM_CANON_RELAXED) ++ if (cur->canon_canon == DKIM_CANON_RELAXED || ++ cur->canon_canon == DKIM_CANON_ISCHEDULE) + dkim_lowerhdr(tmphdr.hdr_text); + (void) dkim_canon_header(dkim, cur, &tmphdr, FALSE); + dkim_canon_buffer(cur, NULL, 0); +@@ -1510,7 +1513,8 @@ dkim_canon_signature(DKIM *dkim, struct dkim_header *hdr) + tmphdr.hdr_textlen = dkim_dstring_len(dkim->dkim_hdrbuf); + tmphdr.hdr_flags = 0; + tmphdr.hdr_next = NULL; +- if (cur->canon_canon == DKIM_CANON_RELAXED) ++ if (cur->canon_canon == DKIM_CANON_RELAXED || ++ cur->canon_canon == DKIM_CANON_ISCHEDULE) + dkim_lowerhdr(tmphdr.hdr_text); + + /* canonicalize the signature */ +diff --git a/libopendkim/dkim-tables.c b/libopendkim/dkim-tables.c +index 3b1aaea..d22c8bb 100644 +--- a/libopendkim/dkim-tables.c ++++ b/libopendkim/dkim-tables.c +@@ -94,6 +94,7 @@ static struct nametable prv_canonicalizations[] = /* canonicalizations */ + { + { "simple", DKIM_CANON_SIMPLE }, + { "relaxed", DKIM_CANON_RELAXED }, ++ { "ischedule-relaxed", DKIM_CANON_ISCHEDULE }, + { NULL, -1 }, + }; + struct nametable *canonicalizations = prv_canonicalizations; +diff --git a/libopendkim/dkim.c b/libopendkim/dkim.c +index 4dd9a95..4a9c02f 100644 +--- a/libopendkim/dkim.c ++++ b/libopendkim/dkim.c +@@ -5200,7 +5200,8 @@ dkim_sign(DKIM_LIB *libhandle, const unsigned char *id, void *memclosure, + assert(selector != NULL); + assert(domain != NULL); + assert(hdrcanonalg == DKIM_CANON_SIMPLE || +- hdrcanonalg == DKIM_CANON_RELAXED); ++ hdrcanonalg == DKIM_CANON_RELAXED || ++ hdrcanonalg == DKIM_CANON_ISCHEDULE); + assert(bodycanonalg == DKIM_CANON_SIMPLE || + bodycanonalg == DKIM_CANON_RELAXED); + assert(signalg == DKIM_SIGN_DEFAULT || +diff --git a/libopendkim/dkim.h b/libopendkim/dkim.h +index 09b70f4..fc8f762 100644 +--- a/libopendkim/dkim.h ++++ b/libopendkim/dkim.h +@@ -173,6 +173,7 @@ typedef int dkim_canon_t; + #define DKIM_CANON_UNKNOWN (-1) /* unknown method */ + #define DKIM_CANON_SIMPLE 0 /* as specified in DKIM spec */ + #define DKIM_CANON_RELAXED 1 /* as specified in DKIM spec */ ++#define DKIM_CANON_ISCHEDULE 2 /* as specified in iSchedule spec */ + + #define DKIM_CANON_DEFAULT DKIM_CANON_SIMPLE +
View file
cyrus-imapd-2.5.tar.gz/cunit/annotate.testc
Changed
@@ -5,6 +5,7 @@ #include "assert.h" #include "xmalloc.h" #include "retry.h" +#include "util.h" #include "imap/global.h" #include "libcyr_cfg.h" #include "imap/annotate.h"
View file
cyrus-imapd-2.5.tar.gz/cunit/buf.testc
Changed
@@ -5,6 +5,7 @@ #include "util.h" #include "retry.h" #include "map.h" +#include "util.h" #include <sys/mman.h> static void test_simple(void) @@ -1244,113 +1245,6 @@ buf_free(&b); } -static int is_valid_mapping(const char *p, size_t len) -{ - /* To test the validity of an address mapping in a portable - * way, we're using a system call which will fail gracefully - * if the address is invalid. On Linux, the mremap() call - * would be the best way to do this, but we're only assuming - * POSIX systems so we use msync() */ - int r; - unsigned long page_size = sysconf(_SC_PAGESIZE); - - /* round up to a page size */ - len = ((len + page_size-1) / page_size) * page_size; - - r = msync((void *)p, len, 0); - - if (r == 0) - return 1; /* valid mapping */ - - if (r < 0 && - (errno == EFAULT || errno == ENOMEM)) - return 0; /* not a valid mapping */ - - fprintf(stderr, "is_valid_mapping: unexpected error %d (%s)\n", - errno, error_message(errno)); - return -1; -} - -static void test_mmap(void) -{ - struct buf b = BUF_INITIALIZER; - char *fname = xstrdup("/tmp/cyrus-cunit-mmapXXXXXX"); - int fd = mkstemp(fname); - const char *base = NULL; - size_t len = 0; - static const char DATA0[] = "Brooklyn keffiyeh tattooed, " - "letterpress cosby sweater ennui " - "Austin."; - - /* write out a trailing NUL too so we don't walk past - * the end of the mmap'ed region when comparing - just - * in case Valgrind is smart enough to detect that */ - retry_write(fd, DATA0, sizeof(DATA0)); - - map_refresh(fd, /*onceonly*/1, &base, &len, sizeof(DATA0), fname, NULL); - - CU_ASSERT(is_valid_mapping(base, len)); - - buf_init_mmap(&b, base, len); - CU_ASSERT_EQUAL(b.len, sizeof(DATA0)); - CU_ASSERT_EQUAL(b.alloc, 0); - CU_ASSERT_EQUAL(buf_len(&b), b.len); - CU_ASSERT_PTR_NOT_NULL(b.s); - CU_ASSERT_STRING_EQUAL(b.s, DATA0); - - CU_ASSERT(is_valid_mapping(base, len)); - buf_free(&b); - CU_ASSERT(!is_valid_mapping(base, len)); - - unlink(fname); - free(fname); - close(fd); -} - -static void test_mmap_cow(void) -{ - struct buf b = BUF_INITIALIZER; - char *fname = xstrdup("/tmp/cyrus-cunit-mmapXXXXXX"); - int fd = mkstemp(fname); - const char *base = NULL; - size_t len = 0; - static const char DATA0[] = "Seitan mcsweeney's letterpress " - "bespoke, bushwick iphone banh mi"; - - /* write out a trailing NUL too so we don't walk past - * the end of the mmap'ed region when comparing - just - * in case Valgrind is smart enough to detect that */ - retry_write(fd, DATA0, sizeof(DATA0)); - - map_refresh(fd, /*onceonly*/1, &base, &len, sizeof(DATA0), fname, NULL); - - CU_ASSERT(is_valid_mapping(base, len)); - - buf_init_mmap(&b, base, len); - CU_ASSERT_EQUAL(b.len, sizeof(DATA0)); - CU_ASSERT_EQUAL(b.alloc, 0); - CU_ASSERT_EQUAL(buf_len(&b), b.len); - CU_ASSERT_PTR_NOT_NULL(b.s); - CU_ASSERT_STRING_EQUAL(b.s, DATA0); - CU_ASSERT(is_valid_mapping(base, len)); - - /* writing to the buf does a CoW, which munmap()s the data */ - buf_putc(&b, 'X'); - CU_ASSERT_EQUAL(b.len, sizeof(DATA0)+1); - CU_ASSERT(b.alloc >= b.len); - CU_ASSERT_EQUAL(buf_len(&b), b.len); - CU_ASSERT_PTR_NOT_NULL(b.s); - CU_ASSERT_PTR_NOT_EQUAL(b.s, base); - CU_ASSERT_STRING_EQUAL(b.s, DATA0); - CU_ASSERT(b.s[sizeof(DATA0)] == 'X'); - CU_ASSERT(!is_valid_mapping(base, len)); - - buf_free(&b); - unlink(fname); - free(fname); - close(fd); -} - static void test_findchar(void) { struct buf b = BUF_INITIALIZER;
View file
cyrus-imapd-2.5.tar.gz/cunit/cunit.pl
Changed
@@ -700,6 +700,9 @@ or die "Cannot open $file for writing: $!"; print WRAP "/* Automatically generated by cunit.pl, do not edit */\n"; print WRAP "#include \"$suite->{relpath}\"\n"; + print WRAP "#ifdef HAVE_CONFIG_H\n"; + print WRAP "#include <config.h>\n"; + print WRAP "#endif\n"; if (scalar @{$suite->{params}}) { @@ -745,8 +748,18 @@ } print WRAP " CU_TEST_INFO_NULL\n};\n"; + print WRAP "#ifdef HAVE_CU_SETUPFUNC\n"; + + print WRAP "const CU_SuiteInfo $suite->{suitevar} = {" . + "\"$suite->{name}\", NULL, NULL, NULL, NULL, _tests};\n"; + + print WRAP "#else\n"; + print WRAP "const CU_SuiteInfo $suite->{suitevar} = {" . "\"$suite->{name}\", NULL, NULL, _tests};\n"; + + print WRAP "#endif\n"; + close WRAP; atomic_rewrite_end($suite->{wrap});
View file
cyrus-imapd-2.5.tar.gz/cunit/dlist.testc
Changed
@@ -380,4 +380,31 @@ buf_free(&b2); } +static void test_deepstructure(void) +{ + struct dlist *dl = NULL; + struct dlist *di = NULL; + struct buf b = BUF_INITIALIZER; + struct buf b2 = BUF_INITIALIZER; + int r; + + buf_setcstr(&b, "%(toplevel %(sub thing) ANOTHER (value is %(list with interesting things) (in it)) ExTrA ExTrA READ \"all about it\" INCLUDING 123456)"); + + r = dlist_parsemap(&dl, 0, b.s, b.len); + + CU_ASSERT_EQUAL(r, 0); + CU_ASSERT_PTR_NOT_NULL(dl); + + di = dlist_getchild(dl, "INCLUDING"); + CU_ASSERT_EQUAL(dlist_num(di), 123456); + + dlist_printbuf(dl, 0, &b2); + + CU_ASSERT_STRING_EQUAL(buf_cstring(&b2), buf_cstring(&b)); + + dlist_free(&dl); + buf_free(&b); + buf_free(&b2); +} + /* vim: set ft=c: */
View file
cyrus-imapd-2.5.tar.gz/cunit/imapurl.testc
Changed
@@ -228,22 +228,6 @@ CU_ASSERT_STRING_EQUAL(buf, URL); } -static void test_tourl_shared(void) -{ - static const char URL[] = "imap://wooster@jeeves/shared/test"; - struct imapurl iurl; - char buf[300]; - - memset(&iurl, 0, sizeof(iurl)); - iurl.user = "wooster"; - iurl.server = "jeeves"; - iurl.mailbox = "shared/test"; - memset(buf, 0x45, sizeof(buf)); - imapurl_toURL(buf, &iurl); - CU_ASSERT_STRING_EQUAL(buf, URL); -} - - static void test_tourl_options(void) { static const char URL[] = "imap://jeeves/deverill"
View file
cyrus-imapd-2.5.tar.gz/cunit/mboxname.testc
Changed
@@ -49,278 +49,41 @@ CU_ASSERT_EQUAL(c, 'J'); } -static void test_mboxname_to_parts_user(void) +static void test_to_parts(void) { - static const char USER[] = "user.john"; - static const char USER_SUB[] = "user.john.sub"; - - struct mboxname_parts parts; - int r; - - r = mboxname_to_parts(USER, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_PTR_NULL(parts.domain); - CU_ASSERT_EQUAL(parts.is_deleted, 0); - CU_ASSERT_STRING_EQUAL(parts.userid, "john"); - CU_ASSERT_PTR_NULL(parts.box); - mboxname_free_parts(&parts); - - r = mboxname_to_parts(USER_SUB, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_PTR_NULL(parts.domain); - CU_ASSERT_EQUAL(parts.is_deleted, 0); - CU_ASSERT_STRING_EQUAL(parts.userid, "john"); - CU_ASSERT_STRING_EQUAL(parts.box, "sub"); - mboxname_free_parts(&parts); -} - -static void test_mboxname_to_parts_duser(void) -{ - static const char DUSER[] = "example.org!user.john"; - static const char DUSER_SUB[] = "example.org!user.john.sub"; - - struct mboxname_parts parts; - int r; - - r = mboxname_to_parts(DUSER, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 0); - CU_ASSERT_STRING_EQUAL(parts.userid, "john"); - CU_ASSERT_PTR_NULL(parts.box); - mboxname_free_parts(&parts); - - r = mboxname_to_parts(DUSER_SUB, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 0); - CU_ASSERT_STRING_EQUAL(parts.userid, "john"); - CU_ASSERT_STRING_EQUAL(parts.box, "sub"); - mboxname_free_parts(&parts); -} - -static void test_mboxname_to_parts_suser(void) -{ - static const char SUSER[] = "user.john^doe"; - static const char SUSER_SUB[] = "user.john^doe.sub"; - - struct mboxname_parts parts; - int r; - - r = mboxname_to_parts(SUSER, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_PTR_NULL(parts.domain); - CU_ASSERT_EQUAL(parts.is_deleted, 0); - CU_ASSERT_STRING_EQUAL(parts.userid, "john^doe"); - CU_ASSERT_PTR_NULL(parts.box); - mboxname_free_parts(&parts); - - r = mboxname_to_parts(SUSER_SUB, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_PTR_NULL(parts.domain); - CU_ASSERT_EQUAL(parts.is_deleted, 0); - CU_ASSERT_STRING_EQUAL(parts.userid, "john^doe"); - CU_ASSERT_STRING_EQUAL(parts.box, "sub"); - mboxname_free_parts(&parts); -} - -static void test_mboxname_to_parts_sduser(void) -{ - static const char SDUSER[] = "example.org!user.john^doe"; - static const char SDUSER_SUB[] = "example.org!user.john^doe.sub"; - - struct mboxname_parts parts; - int r; - - r = mboxname_to_parts(SDUSER, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 0); - CU_ASSERT_STRING_EQUAL(parts.userid, "john^doe"); - CU_ASSERT_PTR_NULL(parts.box); - mboxname_free_parts(&parts); - - r = mboxname_to_parts(SDUSER_SUB, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 0); - CU_ASSERT_STRING_EQUAL(parts.userid, "john^doe"); - CU_ASSERT_STRING_EQUAL(parts.box, "sub"); - mboxname_free_parts(&parts); -} - -static void test_mboxname_to_parts_xuser(void) -{ - static const char XUSER[] = "DELETED.user.john"; - static const char XUSER_SUB[] = "DELETED.user.john.sub"; - - struct mboxname_parts parts; - int r; - - r = mboxname_to_parts(XUSER, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_PTR_NULL(parts.domain); - CU_ASSERT_EQUAL(parts.is_deleted, 1); - CU_ASSERT_STRING_EQUAL(parts.userid, "john"); - CU_ASSERT_PTR_NULL(parts.box); - mboxname_free_parts(&parts); - - r = mboxname_to_parts(XUSER_SUB, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_PTR_NULL(parts.domain); - CU_ASSERT_EQUAL(parts.is_deleted, 1); - CU_ASSERT_STRING_EQUAL(parts.userid, "john"); - CU_ASSERT_STRING_EQUAL(parts.box, "sub"); - mboxname_free_parts(&parts); -} - -static void test_mboxname_to_parts_sxuser(void) -{ - static const char SXUSER[] = "DELETED.user.john^doe"; - static const char SXUSER_SUB[] = "DELETED.user.john^doe.sub"; - + static const char FRED_DRAFTS[] = "user.fred.Drafts"; + static const char JANEAT_SENT[] = "bloggs.com!user.jane.Sent"; + static const char SHARED[] = "shared.Gossip"; + static const char SHAREDAT[] = "foonly.com!shared.Tattle"; struct mboxname_parts parts; int r; - r = mboxname_to_parts(SXUSER, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_PTR_NULL(parts.domain); - CU_ASSERT_EQUAL(parts.is_deleted, 1); - CU_ASSERT_STRING_EQUAL(parts.userid, "john^doe"); - CU_ASSERT_PTR_NULL(parts.box); - mboxname_free_parts(&parts); - - r = mboxname_to_parts(SXUSER_SUB, &parts); + r = mboxname_to_parts(FRED_DRAFTS, &parts); CU_ASSERT_EQUAL(r, 0); CU_ASSERT_PTR_NULL(parts.domain); - CU_ASSERT_EQUAL(parts.is_deleted, 1); - CU_ASSERT_STRING_EQUAL(parts.userid, "john^doe"); - CU_ASSERT_STRING_EQUAL(parts.box, "sub"); - mboxname_free_parts(&parts); -} - -static void test_mboxname_to_parts_dxuser(void) -{ - static const char DXUSER[] = "example.org!DELETED.user.john"; - static const char DXUSER_SUB[] = "example.org!DELETED.user.john.sub"; - - struct mboxname_parts parts; - int r; - - r = mboxname_to_parts(DXUSER, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 1); - CU_ASSERT_STRING_EQUAL(parts.userid, "john"); - CU_ASSERT_PTR_NULL(parts.box); + CU_ASSERT_STRING_EQUAL(parts.userid, "fred"); + CU_ASSERT_STRING_EQUAL(parts.box, "Drafts"); mboxname_free_parts(&parts); - r = mboxname_to_parts(DXUSER_SUB, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 1); - CU_ASSERT_STRING_EQUAL(parts.userid, "john"); - CU_ASSERT_STRING_EQUAL(parts.box, "sub"); - mboxname_free_parts(&parts); -} - -static void test_mboxname_to_parts_dsxuser(void) -{ - static const char DSXUSER[] = "example.org!DELETED.user.john^doe"; - static const char DSXUSER_SUB[] = "example.org!DELETED.user.john^doe.sub"; - - struct mboxname_parts parts; - int r; - - r = mboxname_to_parts(DSXUSER, &parts); + r = mboxname_to_parts(JANEAT_SENT, &parts); CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 1); - CU_ASSERT_STRING_EQUAL(parts.userid, "john^doe"); - CU_ASSERT_PTR_NULL(parts.box); + CU_ASSERT_STRING_EQUAL(parts.domain, "bloggs.com"); + CU_ASSERT_STRING_EQUAL(parts.userid, "jane"); + CU_ASSERT_STRING_EQUAL(parts.box, "Sent"); mboxname_free_parts(&parts); - r = mboxname_to_parts(DSXUSER_SUB, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 1); - CU_ASSERT_STRING_EQUAL(parts.userid, "john^doe"); - CU_ASSERT_STRING_EQUAL(parts.box, "sub"); - mboxname_free_parts(&parts); -} - -static void test_mboxname_to_parts_shared(void) -{ - static const char SHARED[] = "shared.foo"; - static const char SHARED_SUB[] = "shared.foo.bar"; - - struct mboxname_parts parts; - int r; - r = mboxname_to_parts(SHARED, &parts); CU_ASSERT_EQUAL(r, 0); CU_ASSERT_PTR_NULL(parts.domain); - CU_ASSERT_EQUAL(parts.is_deleted, 0); - CU_ASSERT_PTR_NULL(parts.userid); - CU_ASSERT_STRING_EQUAL(parts.box, "shared.foo"); - mboxname_free_parts(&parts); - - r = mboxname_to_parts(SHARED_SUB, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_PTR_NULL(parts.domain); - CU_ASSERT_EQUAL(parts.is_deleted, 0); - CU_ASSERT_PTR_NULL(parts.userid); - CU_ASSERT_STRING_EQUAL(parts.box, "shared.foo.bar"); - mboxname_free_parts(&parts); -} - -static void test_mboxname_to_parts_dshared(void) -{ - static const char DSHARED[] = "example.org!shared.foo"; - static const char DSHARED_SUB[] = "example.org!shared.foo.bar"; - - struct mboxname_parts parts; - int r; - - r = mboxname_to_parts(DSHARED, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 0); CU_ASSERT_PTR_NULL(parts.userid); - CU_ASSERT_STRING_EQUAL(parts.box, "shared.foo"); + CU_ASSERT_STRING_EQUAL(parts.box, "shared.Gossip"); mboxname_free_parts(&parts); - r = mboxname_to_parts(DSHARED_SUB, &parts); + r = mboxname_to_parts(SHAREDAT, &parts); CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 0); + CU_ASSERT_STRING_EQUAL(parts.domain, "foonly.com"); CU_ASSERT_PTR_NULL(parts.userid); - CU_ASSERT_STRING_EQUAL(parts.box, "shared.foo.bar"); - mboxname_free_parts(&parts); -} - -static void test_mboxname_to_parts_dxshared(void) -{ - static const char DXSHARED[] = "example.org!DELETED.shared.foo"; - static const char DXSHARED_SUB[] = "example.org!DELETED.shared.foo.bar"; - - struct mboxname_parts parts; - int r; - - r = mboxname_to_parts(DXSHARED, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 1); - CU_ASSERT_PTR_NULL(parts.userid); - CU_ASSERT_STRING_EQUAL(parts.box, "shared.foo"); - mboxname_free_parts(&parts); - - r = mboxname_to_parts(DXSHARED_SUB, &parts); - CU_ASSERT_EQUAL(r, 0); - CU_ASSERT_STRING_EQUAL(parts.domain, "example.org"); - CU_ASSERT_EQUAL(parts.is_deleted, 1); - CU_ASSERT_PTR_NULL(parts.userid); - CU_ASSERT_STRING_EQUAL(parts.box, "shared.foo.bar"); + CU_ASSERT_STRING_EQUAL(parts.box, "shared.Tattle"); mboxname_free_parts(&parts); } @@ -585,49 +348,8 @@ toexternal_helper("user.jane", "Uvvers.jane"); toexternal_helper("user.jane.baz", "Uvvers.jane.baz"); toexternal_helper("shared.quux", "Chaired.shared.quux"); - - conf.virtdomains = IMAP_ENUM_VIRTDOMAINS_ON; - toexternal_helper("example.org!shared.quux", "Chaired.shared.quux@example.org"); - conf.userid = "john@example.org"; - toexternal_helper("example.org!shared.quux", "Chaired.shared.quux"); - conf.isadmin = 1; - toexternal_helper("example.org!shared.quux", "shared.quux"); - -} - -static void test_altnamespace_unixhierarchysep(void) -{ - memset(&conf, 0, sizeof(conf)); - conf.altnamespace = 1; - conf.unixhierarchysep = 1; - conf.userprefix = "Other Users"; - conf.sharedprefix = "Shared Folders"; - conf.isadmin = 1; - - toexternal_helper("user.fred", "user/fred"); - toexternal_helper("user.fred.foo", "user/fred/foo"); - toexternal_helper("user.fred.foo.barracuda", "user/fred/foo/barracuda"); - - conf.isadmin = 0; - conf.userid = "fred"; - - toexternal_helper("user.jane", "Other Users/jane"); - toexternal_helper("user.jane.baz", "Other Users/jane/baz"); - toexternal_helper("shared.quux", "Shared Folders/shared/quux"); - - conf.virtdomains = IMAP_ENUM_VIRTDOMAINS_ON; - conf.userid = "fred@example.org"; - toexternal_helper("example.org!shared.quux", "Shared Folders/shared/quux"); - - conf.isadmin = 1; - toexternal_helper("example.org!shared.quux", "shared/quux"); - - conf.userid = "cyrus-admin"; - toexternal_helper("example.org!shared.quux", "shared/quux@example.org"); - } - static enum enum_value old_config_virtdomains; static int set_up(void)
View file
cyrus-imapd-2.5.tar.gz/cunit/message.testc
Changed
@@ -3,6 +3,7 @@ #endif #include "cunit/cunit.h" #include "parseaddr.h" +#include "util.h" #include "imap/message.h" static void test_parse_trivial(void)
View file
cyrus-imapd-2.5.tar.gz/cunit/sieve.testc
Changed
@@ -16,6 +16,7 @@ #include "retry.h" #include "imap/spool.h" #include "map.h" +#include "util.h" #include "cyrusdb.h" #include "libcyr_cfg.h" #include "libconfig.h"
View file
cyrus-imapd-2.5.tar.gz/doc/changes.html
Changed
@@ -8,6 +8,145 @@ </head> <body> +<h1>Changes to the Cyrus IMAP Server since 2.4.17-caldav-beta10</h1> +<ul> +<li>Properly do time range filtering on a recurring event where the + recurring component isn't the first component in the iCalendar + stream.</li> +<li>Properly do time range filtering on an uncompleted task with no + due time.</li> +<li>Added short-circuits to calculation of recurring event time span.</li> +<li>Faster free/busy calculation for non-recurring events.</li> +<li>Added <tt>caldav_mindatetime</tt> and <tt>caldav_maxdatetime</tt> + options.</li> +</ul> + +<h1>Changes to the Cyrus IMAP Server since 2.4.17-caldav-beta9</h1> +<ul> +<li>DAV DB changes now occur in the mailbox API which means + that replication works for calendars and addressbooks.</li> +<li>IMAP XFER is now based on replication.</li> +<li>Added support for free/busy query URL to CalDAV.</li> +<li>Authentication for GET/HEAD requests is now done on-demand so that + free/busy queries and/or calendar subscriptions can be done + anonymously (subject to ACL).</li> +<li>Added support for VAVAILABILITY, VPOLL, RSCALE to CalDAV based on + current drafts (requires libical from git).</li> +<li>Updated http_timezone.c to be compliant with current draft.</li> +<li>Numerous other CalDAV fixes/enhancements.</li> +</ul> + +<h1>Changes to the Cyrus IMAP Server since 2.4.17-caldav-beta8</h1> +<ul> +<li>Fixed bug in parsing of Accept header (now accepts <tt>*/*</tt> + and <tt>type/*</tt>).</li> +<li>Fixed telemetry logging bug (old garbage appearing in log).</li> +<li>Added a workaround for the DELETE bug in MacOS X 10.9.0 Calendar + client.</li> +</ul> + +<h1>Changes to the Cyrus IMAP Server since 2.4.17-caldav-beta7</h1> +<ul> +<li>Added Timzone Service module along with associated admin tools + (ctl_zoneinfo, vzic).</li> +<li>Added support for accepting/returning jCal (requires Jansson) and + xCal data wherever iCalendar data is allowed.</li> +<li>Proxied responses (including chunked) are now piped to client + rather than being buffered and forwarded.</li> +<li>Better handling of COPY/MOVE between backends (including + LOCKs).</li> +<li>Fixed "annotation truncation" bug where the largest allowed + annotation value was slightly less than 4k (solves problem reading + CalDavZAP settings).</li> +<li>GET on ./well-known now returns a list of /.well-known/ URLs on + the server.</li> +<li>Added support for X-HTTP-Method-Override header in POST requests.</li> +<li>Added replacement functions for those not present in libxml2 < + v2.8.0</li> +<li>Plugged a few more memory leaks found by Valgrind.</li> +</ul> + +<h1>Changes to the Cyrus IMAP Server since 2.4.17-caldav-beta6</h1> +<ul> +<li>Plugged several memory leaks found by Valgrind</li> +<li>Less verbose reconnect communication between frontend and + backend</li> +<li>GET on calendar-home-set now returns a list of subscribe-able + calendars</li> +<li>Auto-provisioning of calendars/addressbooks now works via a + frontend proxy</li> +<li>Fixed several conformance bugs detected by CalDAVTester</li> +<li>Added support for optionally adding Content-MD5 header to + responses (see <tt>httpcontentmd5</tt> option)</li> +<li>Fixed time-based queries for components other than VEVENT</li> +</ul> + +<h1>Changes to the Cyrus IMAP Server since 2.4.17-caldav-beta5</h1> +<ul> +<li>RSS module now produces Atom 1.0 output rather than RSS 2.0 (we + prefer IETF standards)</li> +<li>PROPFIND/REPORT allprop/propname requests are now supported</li> +<li><tt>unixhierarchysep</tt> is now supported by all HTTP + modules</li> +<li>Completely rewrote write_body() - Range requests are now supported + on non-chunked Content-Encoded data</li> +<li>Added cache control response headers where appropriate to make + Redbot happy</li> +<li>Fixed handling of telemetry log file descriptors and + truncation</li> +<li>Allow GET on calendar collections for "exporting" entire + calendar</li> +<li>Fixed POST on calendar collection (add-member)</li> +<li>Fixed parsing of calendar-query filter</li> +<li>Added several CalDAV/iCalendar validation checks based on + CalDAVTester results</li> +</ul> + +<h1>Changes to the Cyrus IMAP Server since 2.4.17-caldav-beta4</h1> +<ul> +<li>Always verify authorization credentials supplied by the client (a + proxy may be reusing an existing connection as a different user)</li> +<li>Don't bother supporting Digest qop=auth-int since no clients seem + to use it</li> +<li>Don't show addressbook mailboxes in IMAP LIST output</li> +<li>Plugged leaked memory found by Valgrind</li> +<li>Better handling of request/response bodies</li> +<li>Added <tt>httpprettytelemetry</tt> option</li> +<li>Added <tt>httpallowcors</tt> option (Cross-Origin Resource Sharing)</li> +</ul> + +<h1>Changes to the Cyrus IMAP Server since 2.4.17-caldav-beta3</h1> +<ul> +<li>Plugged leaked memory found by Valgrind</li> +<li>Rewrote list_feeds() to not use memmem()</li> +<li>OPTIONS method can be used without authentication</li> +<li>Better handling of Connection:keep-alive</li> +<li>Added <tt>httpallowedurls</tt> option</li> +</ul> + +<h1>Changes to the Cyrus IMAP Server since 2.4.17-caldav-beta2</h1> +<ul> +<li>Fixed security bug where a user-agent could access files + outside of <tt>httpdocroot</tt>. +<li>Changed annotation entry name scheme to be + "/vendor/cmu/cyrus-httpd/<" ns-href ">" prop</li> +</ul> + +<h1>Changes to the Cyrus IMAP Server since 2.4.17-caldav-beta1</h1> +<ul> +<li>Fixed Bug #3792: Build failures on Solaris 10</li> +<li>Path to user's DAV DB will be created, if necessary</li> +<li>Fixed bug where calendars and addressbooks were using the same + mailbox type flag</li> +<li>Fixed [un]signed int / size_t mismatches in formatted output</li> +</ul> + +<h1>Changes to the Cyrus IMAP Server since 2.4.17</h1> +<ul> +<li>Added httpd service with CalDAV, CardDAV, RSS, and iSchedule + modules</li> +</ul> + <h1>Changes to the Cyrus IMAP Server since 2.4.16</h1> <ul> <li>A bunch of cleanups and fixes to compiling</li>
View file
cyrus-imapd-2.5.tar.gz/doc/install-http.html
Added
@@ -0,0 +1,1163 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> +<title>Cyrus HTTP</title> +</head> +<body> + +<h1>Cyrus HTTP (<a href="#RSS">RSS</a>, <a href="#CalDAV">CalDAV</a>, + <a href="#CardDAV">CardDAV</a>, <a href="#iSchedule">iSchedule</a>, + <a href="#TimeZone">TimeZone</a>)</h1> + +<b><i>Note that the HTTP service and associated modules in Cyrus are + still under development. This release should be considered beta + quality.</i></b> + +<h2 id="Intro">Introduction</h2> + +<p>Cyrus <tt>http</tt> service has the ability to:</p> + +<ul> +<li>Serve IMAP mailboxes as RSS feeds.</li> +<li>Act as a calendar and scheduling (CalDAV) server by using IMAP + mailboxes as calendar collections and RFC 5322 messages to store + iCalendar data.</li> +<li>Act as a contacts (CardDAV) server by using IMAP mailboxes as + addressbook collections and RFC 5322 messages to store vCard + data.</li> +<li>Allow scheduling transactions between separate calendaring and + scheduling systems via the iSchedule protocol <i>(currently only used + within a Cyrus Murder)</i>.</li> +<li>Act as a Time Zone Distribution Service by serving iCalendar (VTIMEZONE) + data to client systems.</li> +<li>Serve static content (such as the RSS feed list template and/or + the CalDAV/CardDAV JavaScript clients mentioned below).</li> +</ul> + +<i>Unlike the <a href="http://httpd.apache.org/">Apache HTTP + Server</a>, Cyrus HTTP is NOT a general purpose HTTP server. Its + feature set is limited to what is required to support the + facilities listed above.</i> + +<p>This document assumes that you are familiar with building and + configuring a Cyrus server. If you have not already done so, please + read and understand the rest of the <a href="install.html">installation</a> + documentation before continuing. Note: The + "<a href="#Install">Installation</a>" section below augments the + "<a href="install-compile.html">Compiling the IMAP Server</a>" + document. The remaining sections assume that your Cyrus server has + already been + successfully <a href="install-configure.html">configured</a>.</p> + +<p>This document also assumes that you are familiar with RSS, WebDAV, + calendaring, and contacts.</p> + + +<h2 id="Install">Installation</h2> + +<p>You will need to build Cyrus with + the <tt>--enable-http</tt> configure option. This builds httpd + and the associated modules and utilities based on the availability + of the prerequisites listed below.</p> + +<h3>General Requirements</h3> + +<ul> +<li>Must have <a href="http://xmlsoft.org/">LibXML2</a> + installed.</li> +<li>Must have a recent <a href="http://www.cyrusimap.org/">SASL</a> + release (v2.1.26 or later) in order to support HTTP Digest, + Negotiate, and NTLM authentication. Otherwise, only HTTP Basic + authentication will be available.</li> +<li>Optionally install <a href="http://www.openssl.org/">OpenSSL</a> + for HTTPS support.</li> +<li>Optionally install <a href="http://www.zlib.net/">Zlib</a> for + compression support.</li> +</ul> + +<h3>Additional CalDAV / CardDAV Requirements</h3> +<ul> +<li>Must + have <a href="http://libical.github.io/libical/">Libical</a> + installed.</li> +<li>Must have <a href="http://www.sqlite.org/">SQLite</a> v3.x (or + later) installed.</li> +<li>Optionally + install <a href="http://www.digip.org/jansson/">Jansson</a> for + jCal/jCard support.</li> +<li>Optionally + install <a href="http://site.icu-project.org/">ICU4C</a> for + non-Gregorian calendar support, + if <a href="http://libical.github.io/libical/">Libical</a> has + support for the RSCALE extension.</li> +</ul> + +<h3>Additional iSchedule Requirements</h3> +<ul> +<li>Must meet CalDAV requirements above.</li> +<li>Must + have <a href="http://www.opendkim.org/">OpenDKIM</a> with support + for iSchedule canonicalization installed (currently requires a + <a href="http://git.cyrusimap.org/cyrus-imapd/plain/contrib/dkim_canon_ischedule.patch?h=caldav-2.4"/> + CMU patch</a>).</li> +</ul> + +<h3>Additional Time Zone Distribution Service Requirements</h3> +<ul> +<li>Must + have <a href="http://libical.github.io/libical/">Libical</a> + installed.</li> +<li>Must have <a href="http://www.digip.org/jansson/">Jansson</a> + installed.</li> +</ul> + +<h2 id="config">General Configuration</h2> + +<p>The Cyrus <tt>httpd</tt> service is configurable via several + options in <tt>imapd.conf</tt>. Several of those options are + discussed in the sections below. Admins should consult + the <tt>imapd.conf(5)</tt> manpage for the full list of options used + by the <tt>httpd</tt> service and its various modules.<p> + +<p>The support for RSS, CalDAV, and CardDAV is divided into separate + modules which run as part of the Cyrus <tt>httpd</tt> + service. Selection of which module(s) are enabled is + done by setting the <tt>httpmodules</tt> option accordingly. By + default, no modules are enabled.</p> + +<p>Cyrus <tt>httpd</tt> also can serve <i>static</i> content, the + location of which is set by the <tt>httpdocroot</tt> option. Any + content contained in the specified directory (including + sub-directories) will be served as static content only. + Cyrus <tt>httpd</tt> does NOT have the ability to execute any + server-side scripts.</p> + +<h3>HTTP Authentication</h3> + +<p>As with other Cyrus services, the Cyrus <tt>httpd</tt> service uses + Cyrus SASL to perform its authentication. Cyrus supports the + following HTTP authentication schemes: Basic, Digest, Negotiate + (Kerberos only), and NTLM. While Basic is available in all versions + of SASL, the remaining schemes are only available in Cyrus SASL + 2.1.16 (and higher).</p> + +<p>Similar to plaintext login commands supported by the other Cyrus + services (IMAP LOGIN, POP3 USER/PASS), the Cyrus <tt>httpd</tt> + service determines whether to advertise the HTTP Basic + authentication scheme based on the <tt>allowplaintext</tt> option + and whether the client has connected over a TLS protected connection + (HTTPS).</p> + +<p>The availability of the other HTTP authentication schemes is + controlled by the <tt>sasl_mech_list</tt> option. For + Cyrus <tt>httpd</tt> the <tt>DIGEST-MD5</tt>, <tt>GSS-SPNEGO</tt>, + and <tt>NTLM</tt> SASL plugins support the Digest, Negotiate, + and NTLM authentication schemes respectively, provided that these + plugins are installed on the server.</p> + +<h2 id="RSS">RSS Module</h2> + +<h3>Configuration</h3> + +<p>When enabled, the RSS module will default to serving ALL mailboxes + to which the authenticated user has access as RSS feeds. + The <tt>rss_feeds</tt> option can be used to limit the set of + mailboxes that can be served as RSS feeds. For example, + setting <tt>rss_feeds</tt> to <tt>*,!user</tt> will serve all shared + mailboxes, but no personal mailboxes.</p> + +<p>The list of available RSS feeds can be obtained by clients by + accessing the <tt>/rss/</tt> URL on the Cyrus server. By default, + the server will present the list as a simple unordered list in an + HTML document. To customize the look and feel of the feed list, + the <tt>rss_feedlist_template</tt> option can be used to point to a + HTML template file. This file can utilize Cascading Style Sheets, + JavaScript, etc. Any and all content that the template file + references MUST reside under the <tt>httpdocroot</tt> as set above. + Consult the <tt>imapd.conf(5)</tt> manpage for specifics on the + required contents of this custom file. Note that for sites + running Cyrus Murder, <tt>rss_feedlist_template</tt> only needs to + be set on frontend servers, since only those servers have the + complete mailbox list.</p> + +<h2 id="CalDAV">CalDAV Module</h2> + +<h3>Configuration</h3> + +<p>When enabled, the CalDAV module allows Cyrus to function as a + calendar and scheduling server. This module uses a subset of the + mailbox hierarchy as calendar collections, the toplevel of which is + specified by the <tt>calendarprefix</tt> option. The public + calendar hierarchy will reside at the toplevel of the shared mailbox + namespace. A user's personal calendar hierarchy will be a child of + their Inbox. For example, using the default value + for <tt>calendarprefix</tt>, a calendar named <tt>Default</tt> for + user <tt>murch</tt> would reside in the mailbox + named <tt>user.murch.#calendars.Default</tt>.<p> + +<p><i>Note that mailboxes in the calendar hierarchies (those + under <tt>calendarprefix</tt>) <b>SHOULD NOT</b> be accessed with an IMAP + client as doing so will leave a mailbox in a state unsuitable + for CalDAV. To this end, calendar mailboxes will not be returned by + Cyrus <tt>imapd</tt> in response to an IMAP client's request for the + available mailbox list, but Cyrus <tt>imapd</tt> will not otherwise + prevent an IMAP client from accessing them.</i></p> + +<p>By default, the CalDAV module will automatically perform scheduling + operations when a scheduling object (invite or reply) is stored + on or deleted from the server. Support for the calendar-auto-schedule + feature can be disabled with the <tt>caldav_allowscheduling</tt> + option.</p> + + +<h3>Administration</h3> + +<h4>Calendar provisioning</h4> + +<p>The CalDAV module will automatically create the required calendars + for a user the first time that the user authenticates to the CalDAV + server. Note that the user MUST have an + existing <a href="install-admin-mb.html">IMAP Inbox</a> in order for + the calendars to be created.</p> + +<h4 id="ACLs">Calendar access controls</h4> + +<p>The CalDAV module uses the same access controls as the other Cyrus + services. The <tt>cyradm(1)</tt> tool can be used to adjust ACLs on + calendars as needed. The tables below show how the access controls + are used by the CalDAV module.</p> + +<br> +<table border> + <caption>Mapping of IMAP Rights to WebDAV Privileges & HTTP Methods</caption> + <tr> + <th>IMAP right</th> + <th>WebDAV privilege</th> + <th>HTTP methods</th> + </tr> + <tr> + <td>l - lookup</td> + <td rowspan=2>DAV:read + <br><i>(aggregates DAV:read-current-user-privilege-set, + <br>CALDAV:read-free-busy)</i></td> + <td rowspan=2>GET/HEAD, PROPFIND, REPORT, + <br>COPY/MOVE <i>(on target)</i></td> + </tr> + <tr> + <td>r - read</td> + </tr> + <tr> + <td>s - seen</td> + <td colspan=2/> + </tr> + <tr> + <td>w - write</td> + <td>DAV:write-properties</i></td> + <td>PROPPATCH, COPY/MOVE <i>(on target)</i></td> + </tr> + <tr> + <td>i - insert</td> + <td>DAV:write-content</td> + <td>PUT, LOCK, COPY/MOVE <i>(on target)</i></td> + </tr> + <tr> + <td>p - post</td> + <td>CYRUS:add-resource <i>(aggregated under DAV:bind)</i></td> + <td>POST</td> + </tr> + <tr> + <td>k - create mailbox</td> + <td>CYRUS:make-collection <i>(aggregated under DAV:bind)</i></td> + <td>MKCOL, MKCALENDAR</td> + </tr> + <tr> + <td>x - delete mailbox</td> + <td>CYRUS:remove-collection <i>(aggregated under DAV:unbind)</i></td> + <td>DELETE</td> + </tr> + <tr> + <td>t - delete message</td> + <td>CYRUS:remove-resource <i>(aggregated under DAV:unbind)</i></td> + <td>DELETE, MOVE <i>(on source)</i></td> + </tr> + <tr> + <td>e - expunge</td> + <td colspan=2/> + </tr> + <tr> + <td>a - admin</td> + <td>CYRUS:admin + <br><i>(aggregates DAV:read-acl, DAV:write-acl, DAV:unlock)</i></td> + <td>ACL, UNLOCK, PROPFIND <i>(DAV:acl only)</i></td> + </tr> + <tr> + <td rowspan=3>9 - free/busy</td> + <td>CALDAV:read-free-busy <i>(regular calendar collection only)</i></td> + <td>REPORT <i>(CALDAV:free-busy-query only)</i><td/> + </tr> + <tr> + <td>CALDAV:schedule-query-freebusy <i>(Scheduling Inbox only)</i></td> + <td rowspan=6/> + </tr> + <tr> + <td>CALDAV:schedule-send-freebusy <i>(Scheduling Outbox only)</i></td> + <tr> + <td rowspan=2>8 - invite</td> + <td>CALDAV:schedule-deliver-invite <i>(Scheduling Inbox only)</i></td> + </tr> + <tr> + <td>CALDAV:schedule-send-invite <i>(Scheduling Outbox only)</i></td> + </tr> + <tr> + <td rowspan=2>7 - reply</td> + <td>CALDAV:schedule-deliver-reply <i>(Scheduling Inbox only)</i></td> + </tr> + <tr> + <td>CALDAV:schedule-send-reply <i>(Scheduling Outbox only)</i></td> + </tr> +</table> +<br> + +<br> +<table border> + <caption>Default WebDAV Privileges by Collection</caption> + <tr> + <th>Collection</th> + <th>ACL</th> + </tr> + <tr> + <td>Regular Calendar</td> + <td>owner - DAV:all + CALDAV:read-free-busy (lrwipkxta9) + <br>anyone - CALDAV:read-free-busy (9)</td> + </tr> + <tr> + <td>Scheduling Inbox</td> + <td>owner - DAV:all + CALDAV:schedule-deliver (lrwipkxta789) + <br>anyone - CALDAV:schedule-deliver (789)</td> + </tr> + <tr> + <td>Scheduling Outbox</td> + <td>owner - DAV:all + CALDAV:schedule-send (lrwipkxta789)</td> + </tr> +</table> +<br> + + +<h3>Client Setup</h3> + +<h4>Mozilla Lightning</h4> + +<p>For each calendar that you would like to add to this client, + perform the following steps: + +<ol> +<li>Select the "File -> New -> Calendar..." menu option.</li> +<li>Select the "On the Network" option and click Continue.</li> +<li>Select "CalDAV" as the Format.</li> +<li>Enter a URL of the following form as + the Location: <tt>https://<servername>/dav/calendars/user/<userid>/<calendar>/</tt></li> +</ol> +</p> +<p>Cyrus will auto-provision a calendar with name "Default" which can + be used in the URL above.</p> + +<h4>Apple iCal</h4> + +<p>This client will autodetect all available calendars on a server. + To add a Cyrus server to this client, perform the following steps: + +<ol> +<li>Select the "Calendar -> Preferences" menu option.</li> +<li>Select the "Accounts" tab.</li> +<li>Click the "+" button.</li> +<li>Select "CalDAV" as the Account Type. +<li>Fill in User Name, Password, and Server Address accordingly.</li> +<li>Click Create.</li> +</ol> +</p> + +<h4>Apple iOS Calendar</h4> + +<p>This client will autodetect all available calendars on a server. + To add a Cyrus server to this client, perform the following steps: + +<ol> +<li>Run the "Settings" app.</li> +<li>Select the "Mail, Contacts, Calendars" menu.</li> +<li>Select the "Add Account..." menu.</li> +<li>Select the "Other" menu.</li> +<li>Select the "Add CalDAV Account" menu.</li> +<li>Fill in Server, User Name, Password, and Description accordingly.</li> +<li>Click Next.</li> +</ol> +</p> + +<h4>Evolution</h4> + +<p>This client will autodetect all available calendars on a server. + For each calendar that you would like to add to this client, + perform the following steps: + +<ol> +<li>Select the "New -> Calendar" menu option.</li> +<li>Select "CalDAV" as the Type.</li> +<li>Fill in Server and User accordingly.</li> +<li>Click "Find Calendars".</li> +<li>Select the desired calendar from the list.</li> +<li>Click "Apply".</li> +<li>Click "OK".</li> +</ol> +</p> + +<h4><a href="http://www.acal.me">aCal</a></h4> + +<p>This client will autodetect all available calendars on a server. + To add a Cyrus server to this client, perform the following steps: + +<ol> +<li>Press the Andoid "Menu" button.</li> +<li>Select "Settings".</li> +<li>Select "Servers".</li> +<li>Select "Add Server".</li> +<li>Select "Manual Configuration".</li> +<li>Fill in Username, Password, and User URL (servername) accordingly.</li> +<li>Press "Apply".</li> +</ol> +</p> + +<h4><a href="http://www.inf-it.com/open-source/clients/caldavzap/"> + CalDavZAP</a></h4> + +<p>This client will autodetect all available calendars on a server. + To configure this client for a Cyrus server, edit <tt>config.js</tt> as + follows: + +<ol> +<li>Set the <tt>href</tt> value in + the <tt>globalNetworkCheckSettings</tt> array to a URL of the following + form: <tt>https://<servername>/dav/principals/user/</tt> + <br>Note that the trailing "/" is REQUIRED.</li> +<li>Set the <tt>globalSettingsType</tt> option + to <tt>calendar-home-set</tt></li> +<li>Set any other options as desired + (e.g. <tt>globalDatepickerFirstDayOfWeek</tt>, <tt>globalTimeZone</tt>).</li> +</ol> +</p> + + +<h2 id="CardDAV">CardDAV Module</h2> + +<h3>Configuration</h3> + +<p>When enabled, the CardDAV module allows Cyrus to function as a + contacts server. This module uses a subset of the + mailbox hierarchy as addressbook collections, the toplevel of which is + specified by the <tt>addressbookprefix</tt> option. The public + addressbook hierarchy will reside at the toplevel of the shared mailbox + namespace. A user's personal addressbook hierarchy will be a child of + their Inbox. For example, using the default value + for <tt>addressbookprefix</tt>, an addressbook named <tt>Default</tt> for + user <tt>murch</tt> would reside in the mailbox + named <tt>user.murch.#addressbooks.Default</tt>.<p> + +<p><i>Note that mailboxes in the addressbook hierarchies (those + under <tt>addressbookprefix</tt>) <b>SHOULD NOT</b> be accessed with an IMAP + client as doing so will leave a mailbox in a state unsuitable + for CardDAV. To this end, addressbook mailboxes will not returned by + Cyrus <tt>imapd</tt> in response to an IMAP client's request for the + available mailbox list, but Cyrus <tt>imapd</tt> will not otherwise + prevent an IMAP client from accessing them.</i></p> + +<h3>Administration</h3> + +<h4>Addressbook provisioning</h4> + +<p>The CardDAV module will automatically create a default addressbook + for a user the first time that the user authenticates to the CardDAV + server. Note that the user MUST have an + existing <a href="install-admin-mb.html">IMAP Inbox</a> in order for + the addressbook to be created.</p> + +<h4>Addressbook access controls</h4> + +<p>Cyrus uses the same access controls for addressbooks as it does for + <a href="#ACLs">calendars</a>, except that the scheduling rights (7, + 8, 9) have no use with addressbooks and are ignored.</p> + +<h3>Client Setup</h3> + +<h4>Apple Contacts</h4> + +<p>This client will autodetect all available addressbooks on a server. + To add a Cyrus server to this client, perform the following steps: + +<ol> +<li>Select the "Contacts -> Preferences" menu option.</li> +<li>Select the "Accounts" tab.</li> +<li>Click the "+" button.</li> +<li>Select "CardDAV" as the Account Type. +<li>Fill in User Name, Password, and Server Address accordingly.</li> +<li>Click Create.</li> +</ol> +</p> + +<h4>Apple iOS Contacts</h4> + +<p>This client will autodetect all available addressbooks on a server. + To add a Cyrus server to this client, perform the following steps: + +<ol> +<li>Run the "Settings" app.</li> +<li>Select the "Mail, Contacts, Calendars" menu.</li> +<li>Select the "Add Account..." menu.</li> +<li>Select the "Other" menu.</li> +<li>Select the "Add CardDAV Account" menu.</li> +<li>Fill in Server, User Name, Password, and Description accordingly.</li> +<li>Click Next.</li> +</ol> +</p> + +<h4><a href="http://www.inf-it.com/open-source/clients/carddavmate/"> + CardDavMATE</a></h4> + +<p>This client will autodetect all available addressbooks on a server. + To configure this client for a Cyrus server, edit <tt>config.js</tt> as + follows: + +<ol> +<li>Set the <tt>href</tt> value in + the <tt>globalNetworkCheckSettings</tt> array to a URL of the following + form: <tt>https://<servername>/dav/principals/user/</tt> + <br>Note that the trailing "/" is REQUIRED.</li> +<li>Set the <tt>globalSettingsType</tt> option + to <tt>addressbook-home-set</tt></li> +<li>Set any other options as desired.</li> +</ol> +</p> + + +<h2 id="TimeZone">Time Zone Distribution Service Module</h2> + +<h3>Configuration</h3> + +<p>When enabled, the Time Zone module allows Cyrus to function as a + Time Zone Distribution Service, providing time zone data to client systems. This + module stores time zone data in the <tt>zoneinfo/</tt> subdirectory of + the Cyrus configuration directory (as specified by + the <tt>configdir</tt> option). The data is indexed by a database + whose location is specified by the <tt>zoneinfo_db_path</tt> option, + using the format specified by the <tt>zoneinfo_db</tt> option.</p> + +<h3>Administration</h3> + +<p>This module is designed to use the <i>IANA Time Zone Database</i> data + (a.k.a. <i>Olson Database</i>) converted to the iCalendar format. The + steps to populate the Cyrus <tt>zoneinfo/</tt> directory are as follows: + +<ol start=0> +<li>Build the <tt>vzic</tt> utility located in + the <tt>tools/vzic/</tt> subdirectory of the Cyrus source code. + Simply running <tt>make</tt> in the <tt>tools/vzic/</tt> + subdirectory should suffice.</li> +<li>Download the latest version of the Time Zone Database data + from <a href="http://www.iana.org/time-zones">IANA</a>. <i>Only the + data is required, NOT the code</i>.</li> +<li>Expand the downloaded time zone data into the temporary directory + of your choice.</li> +<li>Populate <tt>configdir/zoneinfo/</tt> with iCalendar data: + <p><i>Initial Install Only</i></p> + <ol type=a> + <li>Convert the raw data into iCalendar format by + running <tt>vzic</tt> as follows: + <p><tt>vzic --pure --olson-dir <location-of-raw-data> + --output-dir <configdir>/zoneinfo</tt></p> + <p>This will create and install iCalendar data directly into + the <tt>configdir/zoneinfo/</tt> directory.</p> + </li> + </ol> + <p><i>Updating Data Only</i></p> + <ol type=a> + <li>Convert the raw data into iCalendar format by + running <tt>vzic</tt> as follows: + <p><tt>vzic --pure --olson-dir <location-of-raw-data></tt></p> + <p>This will create a <tt>zoneinfo/</tt> subdirectory in your + current location (preferably <tt>tools/vzic/</tt>).</p> + </li> + <li>Merge new/updated iCalendar data into + the <tt>configdir/zoneinfo/</tt> directory by + running <tt>vzic-merge.pl</tt> in your current location: + <p><tt>vzic-merge.pl</tt></p> + </li> + </ol> +</li> +<li>Rebuild the Cyrus zoneinfo index by + running <tt>ctl_zoneinfo</tt> as follows: + <p><tt>ctl_zoneinfo -r <version-string></tt></p> + <p>where <tt><version-string></tt> describes the recently + downloaded time zone data (e.g. "IANA Time Zone Database + v.2013h").</p></li> +<li>Verify that the zoneinfo index database and all iCalendar data + files/links are readable by the <tt>cyrus</tt> user.</li> +</ol> +</p> + + +<h2 id="iSchedule">iSchedule Module</h2> + +<p>This module will be automatically enabled if and only if both the + CalDAV module and the <tt>caldav_allowscheduling</tt> options are + enabled in a Cyrus Murder.</p> + +<p><i>Support for scheduling with external servers is currently under + development and will require a future release of OpenDKIM.</i></p> + +<!-- +<h3>Configuration</h3> + +<h3>Administration</h3> +--> + + +<h2>DomainKey Module</h2> + +<p><i>Currently unavailable. Will be available once iSchedule support to + external servers is available.</i></p> + +</body></html> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> +<title>Cyrus HTTP</title> +</head> +<body> + +<h1>Cyrus HTTP (RSS, CalDAV, CardDAV, iSchedule, DomainKey)</h1> + +<b><i>Note that the HTTP service and associated modules in Cyrus are + still under development. This release should be considered beta + quality.</i></b> + +<h2>Introduction</h2> + +<p>Cyrus <tt>http</tt> service has the ability to:</p> + +<ul> +<li>Serve IMAP mailboxes as RSS feeds.</li> +<li>Act as a calendar and scheduling (CalDAV) server by using IMAP + mailboxes as calendar collections and RFC 5322 messages to store + iCalendar data.</li> +<li>Act as a contacts (CardDAV) server by using IMAP mailboxes as + addressbook collections and RFC 5322 messages to store vCard + data.</li> +<li>Allow scheduling transactions between separate calendaring and + scheduling systems via the iSchedule protocol <i>(currently only used + within a Cyrus Murder)</i>.</li> +</ul> + +<i>Unlike the <a href="http://httpd.apache.org/">Apache HTTP + Server</a>, Cyrus HTTP is NOT a general purpose HTTP server. Its + feature set is limited to what is required to support the + facilities listed above.</i> + +<p>This document assumes that you are familiar with building and + configuring a Cyrus server. If you have not already done so, please + read and understand the rest of the <a href="install.html">installation</a> + documentation before continuing. Note: The + "<a href="#install">Installation</a>" section below augments the + "<a href="install-compile.html">Compiling the IMAP Server</a>" + document. The remaining sections assume that your Cyrus server has + already been + successfully <a href="install-configure.html">configured</a>.</p> + +<p>This document also assumes that you are familiar with RSS, WebDAV, + calendaring, and contacts.</p> + + +<h2 id="install">Installation</h2> + +<p>You will need to build Cyrus with + the <tt>--enable-http</tt> configure option. This builds httpd + and the associated modules and utilities based on the availability + of the prerequisites listed below.</p> + +<h3>General Requirements</h3> + +<ul> +<li>Must have <a href="http://xmlsoft.org/">libxml2</a> + installed.</li> +<li>Must have a recent SASL build (v2.1.26 or later) in order to + support HTTP Digest, Negotiate, and NTLM authentication. + Otherwise, only HTTP Basic authentication will be available</li> +</ul> + +<h3>CalDAV / CardDAV Requirements</h3> +<ul> +<li>Must + have <a href="http://freeassociation.sourceforge.net/">libical</a> + installed.</li> +<li>Must have <a href="http://www.sqlite.org/">SQLite</a> v3.x (or + later) installed.</li> +<li>Optionally + install <a href="https://github.com/jehiah/json-c">json-c</a> for + jCal/jCard support.</li> +</ul> + +<!-- +<h3>iSchedule Requirements</h3> +<ul> +<li>Must + have <a href="http://www.opendkim.org/">OpenDKIM v2.9.x (or higher)</a> + installed.</li> +</ul> +--> + +<h2 id="config">General Configuration</h2> + +<p>The Cyrus <tt>httpd</tt> service is configurable via several + options in <tt>imapd.conf</tt>. Several of those options are + discussed in the sections below. Admins should consult + the <tt>imapd.conf(5)</tt> manpage for the full list of options used + by the <tt>httpd</tt> service and its various modules.<p> + +<p>The support for RSS, CalDAV, and CardDAV is divided into separate + modules which run as part of the Cyrus <tt>httpd</tt> + service. Selection of which module(s) are enabled is + done by setting the <tt>httpmodules</tt> option accordingly. By + default, no modules are enabled.</p> + +<p>Cyrus <tt>httpd</tt> also can serve <i>static</i> content, the + location of which is set by the <tt>httpdocroot</tt> option. Any + content contained in the specified directory (including + sub-directories) will be served as static content only. + Cyrus <tt>httpd</tt> does NOT have the ability to execute any + server-side scripts.</p> + +<h3>HTTP Authentication</h3> + +<p>As with other Cyrus services, the Cyrus <tt>httpd</tt> service uses + Cyrus SASL to perform its authentication. Cyrus supports the + following HTTP authentication schemes: Basic, Digest, Negotiate + (Kerberos only), and NTLM. While Basic is available in all versions + of SASL, the remaining schemes are only available in Cyrus SASL + 2.1.16 (and higher).</p> + +<p>Similar to plaintext login commands supported by the other Cyrus + services (IMAP LOGIN, POP3 USER/PASS), the Cyrus <tt>httpd</tt> + service determines whether to advertise the HTTP Basic + authentication scheme based on the <tt>allowplaintext</tt> option + and whether the client has connected over a TLS protected connection + (HTTPS).</p> + +<p>The availability of the other HTTP authentication schemes is + controlled by the <tt>sasl_mech_list</tt> option. For + Cyrus <tt>httpd</tt> the <tt>DIGEST-MD5</tt>, <tt>GSS-SPNEGO</tt>, + and <tt>NTLM</tt> SASL plugins support the Digest, Negotiate, + and NTLM authentication schemes respectively, provided that these + plugins are installed on the server.</p> + +<h2>RSS Module</h2> + +<h3>Configuration</h3> + +<p>When enabled, the RSS module will default to serving ALL mailboxes + to which the authenticated user has access as RSS feeds. + The <tt>rss_feeds</tt> option can be used to limit the set of + mailboxes that can be served as RSS feeds. For example, + setting <tt>rss_feeds</tt> to <tt>*,!user</tt> will serve all shared + mailboxes, but no personal mailboxes.</p> + +<p>The list of available RSS feeds can be obtained by clients by + accessing the <tt>/rss/</tt> URL on the Cyrus server. By default, + the server will present the list as a simple unordered list in an + HTML document. To customize the look and feel of the feed list, + the <tt>rss_feedlist_template</tt> option can be used to point to a + HTML template file. This file can utilize Cascading Style Sheets, + JavaScript, etc. Any and all content that the template file + references MUST reside under the <tt>httpdocroot</tt> as set above. + Consult the <tt>imapd.conf(5)</tt> manpage for specifics on the + required contents of this custom file. Note that for sites + running Cyrus Murder, <tt>rss_feedlist_template</tt> only needs to + be set on frontend servers, since only those servers have the + complete mailbox list.</p> + +<h2>CalDAV Module</h2> + +<h3>Configuration</h3> + +<p>When enabled, the CalDAV module allows Cyrus to function as a + calendar and scheduling server. This module uses a subset of the + mailbox hierarchy as calendar collections, the toplevel of which is + specified by the <tt>calendarprefix</tt> option. The public + calendar hierarchy will reside at the toplevel of the shared mailbox + namespace. A user's personal calendar hierarchy will be a child of + their Inbox. For example, using the default value + for <tt>calendarprefix</tt>, a calendar named <tt>Default</tt> for + user <tt>murch</tt> would reside in the mailbox + named <tt>user.murch.#calendars.Default</tt>.<p> + +<p><i>Note that mailboxes in the calendar hierarchies (those + under <tt>calendarprefix</tt>) <b>SHOULD NOT</b> be accessed with an IMAP + client as doing so will leave a mailbox in a state unsuitable + for CalDAV. To this end, calendar mailboxes will not returned by + Cyrus <tt>imapd</tt> in response to an IMAP client's request for the + available mailbox list, but Cyrus <tt>imapd</tt> will not otherwise + prevent an IMAP client from accessing them.</i></p> + +<p>By default, the CalDAV module will automatically perform scheduling + operations when a scheduling object (invite or reply) is stored + on or deleted from the server. Support for the calendar-auto-schedule + feature can be disabled with the <tt>caldav_allowscheduling</tt> + option.</p> + + +<h3>Administration</h3> + +<h4>Calendar provisioning</h4> + +<p>The CalDAV module will automatically create the required calendars + for a user the first time that the user authenticates to the CalDAV + server. Note that the user MUST have an + existing <a href="install-admin-mb.html">IMAP Inbox</a> in order for + the calendars to be created.</p> + +<h4 id="ACLs">Calendar access controls</h4> + +<p>The CalDAV module uses the same access controls as the other Cyrus + services. The <tt>cyradm(1)</tt> tool can be used to adjust ACLs on + calendars as needed. The tables below show how the access controls + are used by the CalDAV module.</p> + +<br> +<table border> + <caption>Mapping of IMAP Rights to WebDAV Privileges & HTTP Methods</caption> + <tr> + <th>IMAP right</th> + <th>WebDAV privilege</th> + <th>HTTP methods</th> + </tr> + <tr> + <td>l - lookup</td> + <td rowspan=2>DAV:read + <br><i>(aggregates DAV:read-current-user-privilege-set, + <br>CALDAV:read-free-busy)</i></td> + <td rowspan=2>GET/HEAD, PROPFIND, REPORT, + <br>COPY/MOVE <i>(on target)</i></td> + </tr> + <tr> + <td>r - read</td> + </tr> + <tr> + <td>s - seen</td> + <td colspan=2/> + </tr> + <tr> + <td>w - write</td> + <td>DAV:write-properties</i></td> + <td>PROPPATCH, COPY/MOVE <i>(on target)</i></td> + </tr> + <tr> + <td>i - insert</td> + <td>DAV:write-content</td> + <td>PUT, LOCK, COPY/MOVE <i>(on target)</i></td> + </tr> + <tr> + <td>p - post</td> + <td>CYRUS:add-resource <i>(aggregated under DAV:bind)</i></td> + <td>POST</td> + </tr> + <tr> + <td>k - create mailbox</td> + <td>CYRUS:make-collection <i>(aggregated under DAV:bind)</i></td> + <td>MKCOL, MKCALENDAR</td> + </tr> + <tr> + <td>x - delete mailbox</td> + <td>CYRUS:remove-collection <i>(aggregated under DAV:unbind)</i></td> + <td>DELETE</td> + </tr> + <tr> + <td>t - delete message</td> + <td>CYRUS:remove-resource <i>(aggregated under DAV:unbind)</i></td> + <td>DELETE, MOVE <i>(on source)</i></td> + </tr> + <tr> + <td>e - expunge</td> + <td colspan=2/> + </tr> + <tr> + <td>a - admin</td> + <td>CYRUS:admin + <br><i>(aggregates DAV:read-acl, DAV:write-acl, DAV:unlock)</i></td> + <td>ACL, UNLOCK, PROPFIND <i>(DAV:acl only)</i></td> + </tr> + <tr> + <td rowspan=3>9 - free/busy</td> + <td>CALDAV:read-free-busy <i>(regular calendar collection only)</i></td> + <td>REPORT <i>(CALDAV:free-busy-query only)</i><td/> + </tr> + <tr> + <td>CALDAV:schedule-query-freebusy <i>(Scheduling Inbox only)</i></td> + <td rowspan=6/> + </tr> + <tr> + <td>CALDAV:schedule-send-freebusy <i>(Scheduling Outbox only)</i></td> + <tr> + <td rowspan=2>8 - invite</td> + <td>CALDAV:schedule-deliver-invite <i>(Scheduling Inbox only)</i></td> + </tr> + <tr> + <td>CALDAV:schedule-send-invite <i>(Scheduling Outbox only)</i></td> + </tr> + <tr> + <td rowspan=2>7 - reply</td> + <td>CALDAV:schedule-deliver-reply <i>(Scheduling Inbox only)</i></td> + </tr> + <tr> + <td>CALDAV:schedule-send-reply <i>(Scheduling Outbox only)</i></td> + </tr> +</table> +<br> + +<br> +<table border> + <caption>Default WebDAV Privileges by Collection</caption> + <tr> + <th>Collection</th> + <th>ACL</th> + </tr> + <tr> + <td>Regular Calendar</td> + <td>owner - DAV:all + CALDAV:read-free-busy (lrwipkxta9) + <br>anyone - CALDAV:read-free-busy (9)</td> + </tr> + <tr> + <td>Scheduling Inbox</td> + <td>owner - DAV:all + CALDAV:schedule-deliver (lrwipkxta789) + <br>anyone - CALDAV:schedule-deliver (789)</td> + </tr> + <tr> + <td>Scheduling Outbox</td> + <td>owner - DAV:all + CALDAV:schedule-send (lrwipkxta789)</td> + </tr> +</table> +<br> + + +<h3>Client Setup</h3> + +<h4>Mozilla Lightning</h4> + +<p>For each calendar that you would like to add to this client, + perform the following steps: + +<ol> +<li>Select the "File -> New -> Calendar..." menu option.</li> +<li>Select the "On the Network" option and click Continue.</li> +<li>Select "CalDAV" as the Format.</li> +<li>Enter a URL of the following form as + the Location: <tt>https://<servername>/dav/calendars/user/<userid>/<calendar>/</tt></li> +</ol> +</p> +<p>Cyrus will auto-provision a calendar with name "Default" which can + be used in the URL above.</p> + +<h4>Apple iCal</h4> + +<p>This client will autodetect all available calendars on a server. + To add a Cyrus server to this client, perform the following steps: + +<ol> +<li>Select the "Calendar -> Preferences" menu option.</li> +<li>Select the "Accounts" tab.</li> +<li>Click the "+" button.</li> +<li>Select "CalDAV" as the Account Type. +<li>Fill in User Name, Password, and Server Address accordingly.</li> +<li>Click Create.</li> +</ol> +</p> + +<h4>Apple iOS Calendar</h4> + +<p>This client will autodetect all available calendars on a server. + To add a Cyrus server to this client, perform the following steps: + +<ol> +<li>Run the "Settings" app.</li> +<li>Select the "Mail, Contacts, Calendars" menu.</li> +<li>Select the "Add Account..." menu.</li> +<li>Select the "Other" menu.</li> +<li>Select the "Add CalDAV Account" menu.</li> +<li>Fill in Server, User Name, Password, and Description accordingly.</li> +<li>Click Next.</li> +</ol> +</p> + +<h4>Evolution</h4> + +<p>This client will autodetect all available calendars on a server. + For each calendar that you would like to add to this client, + perform the following steps: + +<ol> +<li>Select the "New -> Calendar" menu option.</li> +<li>Select "CalDAV" as the Type.</li> +<li>Fill in Server and User accordingly.</li> +<li>Click "Find Calendars".</li> +<li>Select the desired calendar from the list.</li> +<li>Click "Apply".</li> +<li>Click "OK".</li> +</ol> +</p> + +<h4><a href="http://www.acal.me">aCal</a></h4> + +<p>This client will autodetect all available calendars on a server. + To add a Cyrus server to this client, perform the following steps: + +<ol> +<li>Press the Andoid "Menu" button.</li> +<li>Select "Settings".</li> +<li>Select "Servers".</li> +<li>Select "Add Server".</li> +<li>Select "Manual Configuration".</li> +<li>Fill in Username, Password, and User URL (servername) accordingly.</li> +<li>Press "Apply".</li> +</ol> +</p> + +<h4><a href="http://www.inf-it.com/open-source/clients/caldavzap/"> + CalDavZAP</a></h4> + +<p>This client will autodetect all available calendars on a server. + To configure this client for a Cyrus server, edit <tt>config.js</tt> as + follows: + +<ol> +<li>Set the <tt>href</tt> value in + the <tt>globalNetworkCheckSettings</tt> array to a URL of the following + form: <tt>https://<servername>/dav/principals/user/</tt> + <br>Note that the trailing "/" is REQUIRED.</li> +<li>Set the <tt>globalSettingsType</tt> option + to <tt>calendar-home-set</tt></li> +<li>Set any other options as desired + (e.g. <tt>globalDatepickerFirstDayOfWeek</tt>, <tt>globalTimeZone</tt>).</li> +</ol> +</p> + + +<h2>CardDAV Module</h2> + +<h3>Configuration</h3> + +<p>When enabled, the CardDAV module allows Cyrus to function as a + contacts server. This module uses a subset of the + mailbox hierarchy as addressbook collections, the toplevel of which is + specified by the <tt>addressbookprefix</tt> option. The public + addressbook hierarchy will reside at the toplevel of the shared mailbox + namespace. A user's personal addressbook hierarchy will be a child of + their Inbox. For example, using the default value + for <tt>addressbookprefix</tt>, an addressbook named <tt>Default</tt> for + user <tt>murch</tt> would reside in the mailbox + named <tt>user.murch.#addressbooks.Default</tt>.<p> + +<p><i>Note that mailboxes in the addressbook hierarchies (those + under <tt>addressbookprefix</tt>) <b>SHOULD NOT</b> be accessed with an IMAP + client as doing so will leave a mailbox in a state unsuitable + for CardDAV. To this end, addressbook mailboxes will not returned by + Cyrus <tt>imapd</tt> in response to an IMAP client's request for the + available mailbox list, but Cyrus <tt>imapd</tt> will not otherwise + prevent an IMAP client from accessing them.</i></p> + +<h3>Administration</h3> + +<h4>Addressbook provisioning</h4> + +<p>The CardDAV module will automatically create a default addressbook + for a user the first time that the user authenticates to the CardDAV + server. Note that the user MUST have an + existing <a href="install-admin-mb.html">IMAP Inbox</a> in order for + the addressbook to be created.</p> + +<h4>Addressbook access controls</h4> + +<p>Cyrus uses the same access controls for addressbooks as it does for + <a href="#ACLs">calendars</a>, except that the scheduling rights (7, + 8, 9) have no use with addressbooks and are ignored.</p> + +<h3>Client Setup</h3> + +<h4>Apple Contacts</h4> + +<p>This client will autodetect all available addressbooks on a server. + To add a Cyrus server to this client, perform the following steps: + +<ol> +<li>Select the "Contacts -> Preferences" menu option.</li> +<li>Select the "Accounts" tab.</li> +<li>Click the "+" button.</li> +<li>Select "CardDAV" as the Account Type. +<li>Fill in User Name, Password, and Server Address accordingly.</li> +<li>Click Create.</li> +</ol> +</p> + +<h4>Apple iOS Contacts</h4> + +<p>This client will autodetect all available addressbooks on a server. + To add a Cyrus server to this client, perform the following steps: + +<ol> +<li>Run the "Settings" app.</li> +<li>Select the "Mail, Contacts, Calendars" menu.</li> +<li>Select the "Add Account..." menu.</li> +<li>Select the "Other" menu.</li> +<li>Select the "Add CardDAV Account" menu.</li> +<li>Fill in Server, User Name, Password, and Description accordingly.</li> +<li>Click Next.</li> +</ol> +</p> + +<h4><a href="http://www.inf-it.com/open-source/clients/carddavmate/"> + CardDavMATE</a></h4> + +<p>This client will autodetect all available addressbooks on a server. + To configure this client for a Cyrus server, edit <tt>config.js</tt> as + follows: + +<ol> +<li>Set the <tt>href</tt> value in + the <tt>globalNetworkCheckSettings</tt> array to a URL of the following + form: <tt>https://<servername>/dav/principals/user/</tt> + <br>Note that the trailing "/" is REQUIRED.</li> +<li>Set the <tt>globalSettingsType</tt> option + to <tt>addressbook-home-set</tt></li> +<li>Set any other options as desired.</li> +</ol> +</p> + + +<h2>iSchedule Module</h2> + +<p>This module will be automatically enabled if and only if both the + CalDAV module and the <tt>caldav_allowscheduling</tt> options are + enabled in a Cyrus Murder.</p> + +<p><i>Support for scheduling with external servers is currently under + development and will require a future release of OpenDKIM.</i></p> + +<!-- +<h3>Configuration</h3> + +<h3>Administration</h3> +--> + +<h2>DomainKey Module</h2> + +<p><i>Currently unavailable. Will be available once iSchedule support to + external servers is available.</i></p> + +</body></html>
View file
cyrus-imapd-2.5.tar.gz/doc/install-upgrade.html
Changed
@@ -10,9 +10,28 @@ <h1>Upgrading From Previous Versions</h1> -<h2>Upgrading from 2.4 to 2.5</h2> +<h2>Upgrading from 2.4.17-caldav-beta10 or earlier</h2> <ul> -<li>The default port for Sieve was changed from 2000 to 4190. +<li>The schema used for the CalDAV database has changed. ALL sites + MUST run the <tt>dav_reconstruct</tt> utility for each of their + CalDAV users.</li> +</ul> + +<h2>Upgrading from 2.4.17-caldav-beta9 or earlier</h2> +<ul> +<li>The time span calculations for recurring events in which the + component containing the RRULE is not the first component have + been fixed. We recommend ALL sites run + the <tt>dav_reconstruct</tt> utility for each of their CalDAV + users.</li> +</ul> + +<h2>Upgrading from 2.4.17-caldav-beta6 or earlier</h2> +<ul> +<li>The time span calculations for various VCALENDAR components, + especially VTODO (tasks), have been fixed. We recommend ALL sites + run the <tt>dav_reconstruct</tt> utility for each of their CalDAV + users.</li> </ul> <h2>Upgrading from 2.4.12</h2>
View file
cyrus-imapd-2.5.tar.gz/doc/install.html
Changed
@@ -37,6 +37,8 @@ <li><a href="install-sieve.html">Installing SIEVE</a> <li><a href="install-snmpmon.html">SNMP Monitoring</a> <li><a href="install-netnews.html">Cyrus and Netnews</a> +<li><a href="install-http.html">Cyrus HTTP (RSS, CalDAV, CardDAV, + Timezone Service)</a> <li><a href="install-virtdomains.html">Virtual Domains</a> <li><a href="install-replication.html">Cyrus Replication</a> <li><a href="install-murder.html">Cyrus Murder: The IMAP @@ -60,7 +62,5 @@ </ul> <P><HR> -last modified: $Date: 2006/11/30 17:11:16 $ -<br> <A HREF="index.html">Return</A> to the Cyrus IMAP Server Home Page </BODY></HTML>
View file
cyrus-imapd-2.5.tar.gz/doc/internal/caldav_scheduling_flowchart.html
Added
@@ -0,0 +1,399 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" + "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> + <title>Cyrus CalDAV Scheduling Flowchart</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> +</head> +<body> + <h1>Cyrus CalDAV Scheduling Flowchart</h1> + + <h3 id="caldav_put">caldav_put() - create/modify via HTTP PUT on a + resource or POST (add-member) on a calendar</h3> + <ol> + <li>Check if the new resource is a scheduling resource (contains + ORGANIZER property). If not, skip to step 4.</li> + <li>Check for (and load) any existing resource.</li> + <li>Check if the authenticated user matches ORGANIZER: + <ul> + <li>If yes: + <ul> + <li>If only voter (VPOLL) responses changed, + goto <a href="#sched_pollstatus">sched_pollstatus()</a>.</li> + <li>Otherwise, + goto <a href="#sched_request">sched_request()</a>.</li> + </ul> + <li>Otherwise, goto <a href="#sched_reply">sched_reply()</a>.</li> + </ul> + <li>Store the new/modified resource.</li> + </ol> + + <h3 id="caldav_delete_sched">caldav_delete_sched() - remove via HTTP + DELETE on a resource</h3> + <ol> + <li>Check if the existing resource is a scheduling resource (has + Schedule-Tag). If not, we are done.</li> + <li>Load the existing resource.</li> + <li>Check if the authenticated user matches ORGANIZER. If yes, + goto <a href="#sched_request">sched_request()</a>, otherwise + goto <a href="#sched_reply">sched_reply()</a>.</li> + </ol> + + <h3 id="caldav_post">caldav_post() - busytime query via HTTP POST on + Scheduling Outbox</h3> + <ol> + <li>Check the ACL on the owner's Scheduling Outbox. If the + authenticated user doesn't have the DACL_SCHEDFB right, fail.</li> + <li><a href="#sched_busytime_query">sched_busytime_query()</a>.</li> + </ol> + + <hr> + + <h3 id="sched_pollstatus">sched_pollstatus - perform a voter + response update</h3> + <ol> + <li></li> + </ol> + + <hr> + + <h3 id="sched_request">sched_request() - perform an organizer + request / attendee status update</h3> + <ol> + <li>Check the ACL on the owner's Scheduling Outbox. If the + authenticated user doesn't have the DACL_INVITE right, fail.</li> + <li>If the request includes a resource, then set METHOD:REQUEST, + otherwise set METHOD:CANCEL.</li> + <li>Create an iTIP message template, copying over any + CALSCALE property and VTIMEZONE components.</li> + <li>If not an attendee status update and the existing resource is a + scheduling resource: + Foreach component in the existing resource, add it and + its SEQUENCE to our hash table keyed by RECURRENCE-ID (for + comparison against new/modified resource).</li> + <li>Create a hash table of attendees. This will hold + attendee-specific iTIP messages.</li> + <li>Foreach component in the new/modified resource:</li> + <ol type=a> + <li>Lookup (by RECURRENCE-ID) and remove the component from the + hash table of existing components.</li> + <li>If the component exists compare all of DTSTART, DTEND, + DURATION, RRULE, RDATE, EXDATE to those of the new + component.</li> + <li>If the component is new or changed, + then <a href="#process_attendees">process_attendees()</a>.</li> + </ol> + <li>Foreach remaining component in the hash table of existing + components do <a href="#sched_cancel">sched_cancel()</a>.</li> + <li>Foreach iTIP message in our hash table of + ATTENDEES, <a href="#sched_deliver">sched_deliver()</a> the iTIP + message.</li> + <li>Foreach component in the new/modified resource update the + SCHEDULE-STATUS of each ATTENDEE.</li> + </ol> + + <h3 id="process_attendees">process_attendees() - create a suitable + iTIP request message for each attendee</h3> + <ol> + <li>Foreach ATTENDEE in the component, remove the SCHEDULE-STATUS + parameter, and set PROPSTAT=NEEDS-ACTION if required.</li> + <li>Make a copy of the component and + <a href="#clean_component">clean_component()</a>.</li> + <li>Foreach ATTENDEE in the cleaned component:</li> + <ol type=a> + <li>Check the CalDAV Scheduling parameters. If SCHEDULE-AGENT + != SERVER, skip to the next attendee.</li> + <li>Lookup attendee in our hash table.</li> + <li>If it doesn't exist, create a clone of our iTIP template and + insert it into our hash table of attendees.</li> + <li>Add the component to the attendee's iTIP message.</li> + <li>Add the component “number” to our mask of new components + appearing in the attendee's iTIP message.</li> + </ol> + <li>If the component is not the "master", foreach attendee do + <a href="#sched_exclude">sched_exclude()</a>.</li> + </ol> + + <h3 id="sched_exclude">sched_exclude() - exclude an attendee from a + recurrence instance</h3> + <ol> + <li>If the component did not appear in the attendee's iTIP + message, add an EXDATE property (based on the RECURRENCE-ID of + the component) to the master component of the attendee's iTIP + message.</li> + </ol> + + <h3 id="sched_cancel">sched_cancel() - cancel an organizer event/task</h3> + <ol> + <li>Set STATUS:CANCELLED on the component.</li> + <li><a href="#process_attendees">process_attendees()</a>.</li> + </ol> + + <hr> + + <h3 id="sched_reply">sched_reply() - perform an attendee reply</h3> + <ol> + <li>Check the CalDAV Scheduling parameters on ORGANIZER. If + SCHEDULE-AGENT != SERVER, we are done.</li> + <li>Check the ACL on the owner's Scheduling Outbox. If the + authenticated user doesn't have the DACL_REPLY right, fail.</li> + <li>Create a new iTIP (METHOD:REPLY) message, copying over any + CALSCALE property and VTIMEZONE components.</li> + <li>Foreach component in the existing resource:</li> + <ol type=a> + <li><a href="#trim_attendees">trim_attendees()</a>.</li> + <li>Add the trimmed component and the attendee's PARTSTAT to our + hash table keyed by RECURRENCE-ID (for comparison against + new/modified resource).</li> + </ol> + <li>Foreach component in the new/modified resource:</li> + <ol type=a> + <li><a href="#trim_attendees">trim_attendees()</a>.</li> + <li>Lookup (by RECURRENCE-ID) and remove the component from the + hash table of existing components.</li> + <li>If the component exists:</li> + <ol type=i> + <li>If component is VPOLL, add voter responses to REPLY + via <a href="#sched_vpoll_reply">sched_vpoll_reply().</a></li> + <li>Otherwise, compare the PARTSTAT of the ATTENDEE to that of + the new component.</li> + </ol> + <li>If the component is new or the PARTSTAT has changed:</li> + <ol type=i> + <li><a href="#clean_component">clean_component()</a>.</li> + <li>Add the component to our iTIP message.</li> + <li>Add the component “number” to our mask of new components + appearing in our iTIP message.</li> + </ol> + </ol> + <li>Foreach remaining component in the hash table of existing + components do <a href="#sched_decline">sched_decline()</a>.</li> + <li><a href="#sched_deliver">sched_deliver()</a> our iTIP + message.</li> + <li>Foreach component in the new/modified resource that appeared + in our iTIP message, update the SCHEDULE-STATUS of the ORGANIZER.</li> + </ol> + + <h3 id="trim_attendees">trim_attendees() - remove all attendees + other than the one replying</h3> + <ol> + <li>Clone the component and remove all ATTENDEE properties other + than the one corresponding to the owner of the calendar.</li> + <li>Return the ATTENDEE property of owner, his/her PARTSTAT + parameter, and the RECURRENCE-ID of the component.</li> + </ol> + + <h3 id="sched_vpoll_reply">sched_vpoll_reply() - add voter + responses to VPOLL reply</h3> + <ol> + <li></li> + </ol> + + <h3 id="sched_decline">sched_decline() - decline a recurrence + instance for an attendee</h3> + <ol> + <li>Set PARTSTAT of ATTENDEE to DECLINED.</li> + <li><a href="#clean_component">clean_component()</a>.</li> + <li>Add the component to our iTIP message.</li> + </ol> + + <hr> + + <h3 id="clean_component">clean_component() - sanitize a component + for use in an iTIP message</h3> + <ol> + <li>Update DTSTAMP.</li> + <li>Remove any VALARM components.</li> + <li>For a reply/decline only, remove scheduling parameters from + ORGANIZER.</li> + </ol> + + <h3 id="sched_deliver">sched_deliver() - deliver an iTIP message to + a recipient</h3> + <ol> + <li>Lookup the recipient.</li> + <li>If local to our server goto + <a href="#sched_deliver_local">sched_deliver_local()</a>, + otherwise goto + <a href="#sched_deliver_remote">sched_deliver_remote()</a>.</li> + </ol> + + <hr> + + <h3 id="sched_deliver_local">sched_deliver_local() - deliver an + iTIP message to a local user</h3> + <ol> + <li>Check the ACL on the owner's Scheduling Inbox. If the + sender doesn't have the proper right (DACL_INVITE for + request/cancel, DACL_REPLY for reply), fail.</li> + <li>Search the recipient's calendars for a resource having the + specified UID.</li> + <li>If the resource doesn't exist:</li> + <ol type=a> + <li>If the iTIP method is REPLY, fail (we are done).</li> + <li>If the iTIP method is CANCEL, ignore it (we are done).</li> + <li>Otherwise, create a new (empty) attendee object and target + the recipient's Default calendar.</li> + </ol> + <li>Otherwise, load the existing resource.</li> + <li>Update the new/existing resource:</li> + <ol type=a> + <li>If the iTIP method is CANCEL, set STATUS:CANCELLED on all + existing components.</li> + <li>If the iTIP method is REPLY, do + <a href="#deliver_merge_reply">deliver_merge_reply()</a>.</li> + <li>If the iTIP method is REQUEST, do + <a href="#deliver_merge_request">deliver_merge_request()</a>.</li> + <li>If the iTIP method is POLLSTATUS, do + <a href="#deliver_merge_pollstatus">deliver_merge_pollstatus()</a>.</li> + </ol> + <li>Store the new/updated resource in the recipient's target + calendar.</li> + <li>Record the delivery status (SCHEDULE-STATUS).</li> + <li>If the iTIP message is something other than just a PARTSTAT + update from an attendee, store the iTIP message as a new + resource in the recipient's Inbox.</li> + <li>If the iTIP method is REPLY, send an update other attendees + via <a href="#sched_pollstatus">sched_pollstatus()</a> (VPOLL only) + or <a href="#sched_request">sched_request()</a>.</li> + </ol> + + <h3 id="deliver_merge_reply">deliver_merge_reply() - update + an organizer resource with an attendee reply</h3> + <ol> + <li>Foreach component in the existing resource, add it to our + hash table keyed by RECURRENCE-ID (for comparison against + iTIP message).</li> + <li>Foreach component in the iTIP message:</li> + <ol type=a> + <li>Lookup (by RECURRENCE-ID) the component from the + hash table of existing components.</li> + <li>If the component doesn't exist (new recurrence overridden by + ATTENDEE) create a new recurring component:</li> + <ol type=i> + <li>Clone the existing master component.</li> + <li>Remove the RRULE property.</li> + <li>Add the RECURRENCE-ID from the iTIP message.</li> + <li>Replace the DTSTART, DTEND, SEQUENCE properties with those + from the iTIP message.</li> + <li>Add the new component to our existing resource.</li> + </ol> + <li>Get the sending ATTENDEE from the iTIP message.</li> + <li>Find the matching ATTENDEE in the existing component.</li> + <li>If not found (ATTENDEE added themselves to this recurrence), + add new ATTENDEE to the component.</li> + <li>Set the ATTENDEE PARTSTAT, RSVP, and SCHEDULE-STATUS + parameters in the existing component.</li> + <li>If the component is VPOLL, update the voter responses in the + existing component via + <a href="#deliver_merge_vpoll_reply">deliver_merge_vpoll_reply()</a>.</li> + </ol> + <li>Return the sending ATTENDEE.</li> + </ol> + + <h3 id="deliver_merge_vpoll_reply">deliver_merge_vpoll_reply() - update + an organizer resource with voter responses</h3> + <ol> + <li>Foreach sub-component in the existing resource, replace any + voter response(s) with those from the reply.</li> + </ol> + + <h3 id="deliver_merge_request">deliver_merge_request() - + create/update an attendee resource with an organizer request</h3> + <ol> + <li>Foreach VTIMEZONE component in the existing resource, add it + to our hash table keyed by TZID (for comparison against iTIP + message).</li> + <li>Foreach VTIMEZONE component in the iTIP message:</li> + <ol type=a> + <li>Lookup (by TZID) the VTIMEZONE component from the hash table of + existing components.</li> + <li>If the component exists, remove it from the existing + object.</li> + <li>Add the VTIMEZONE from the iTIP message to our existing + object.</li> + </ol> + <li>Foreach component in the existing resource, add it to our + hash table keyed by RECURRENCE-ID (for comparison against + iTIP message).</li> + <li>Foreach component in the iTIP message:</li> + <ol type=a> + <li>Clone a new component from the iTIP component.</li> + <li>Lookup (by RECURRENCE-ID) the component from the + hash table of existing components.</li> + <li>If the component exists:</li> + <ol type=i> + <li>Compare the SEQUENCE of the new component to the existing + component to see if it has changed.</li> + <li>Copy any COMPLETED, PERCENT-COMPLETE, or TRANSP properties + from the existing component to the new component.</li> + <li>Copy any ORGANIZER SCHEDULE-STATUS parameter + from the existing component to the new component.</li> + <li>Remove the existing component from the existing object.</li> + </ol> + <li>Add the new component to the existing object.</li> + </ol> + </ol> + + <h3 id="deliver_merge_pollstatus">deliver_merge_pollstatus() - + update voter responses on a voter resource</h3> + <ol> + <li>Foreach sub-component in the existing resource, add it to our + hash table keyed by POLL-ITEM-ID (for comparison against + iTIP message). The sub-component entry includes a hash table + of VOTERs.</li> + <li>Foreach sub-component in the iTIP message:</li> + <ol type=a> + <li>Lookup (by POLL-ITEM-ID) the sub-component from the + hash table of existing sub-components.</li> + <li>If the component exists, foreach VOTER in the sub-component + in the iTIP message: + <ol type=i> + <li>Lookup VOTER in the hash table of existing + sub-component.</li> + <li>Add/update VOTER response.</li> + </ol> + </li> + </ol> + </ol> + + <hr> + + <h3 id="sched_deliver_remote">sched_deliver_remote() - deliver an + iTIP message to a remote user</h3> + <ol> + <li>If the recipient is local to our Murder, goto + <a href="#isched_send">isched_send()</a>, otherwise + goto <a href="#imip_send">imip_send()</a>.</li> + <li>Retrieve status of iTIP message delivery.</li> + </ol> + + <h3 id="isched_send">isched_send() - deliver an iTIP message to a + remote user via iSchedule (HTTP)</h3> + <ol> + </ol> + + <h3 id="imip_send">imip_send() - deliver an iTIP message to a + remote user via iMIP (SMTP)</h3> + <ol> + </ol> + + <hr> + + <h3 id="sched_busytime_query">sched_busytime_query() - perform a + busytime query</h3> + <ol> + </ol> + + <h3 id="busytime_query_local">busytime_query_local() - perform a + busytime query on a local user</h3> + <ol> + </ol> + + <h3 id="busytime_query_remote">busytime_query_remote() - perform a + busytime query on a remote user</h3> + <ol> + </ol> + +</body> +</html>
View file
cyrus-imapd-2.5.tar.gz/doc/internal/database-formats.html
Changed
@@ -158,6 +158,21 @@ Data: <Version>TAB<Deny List (comma-separated wildmat patterns)>TAB<Deny Message> </pre> +<h2>Timezone Info (zoneinfo.db)</h2> + +<p>This database is used for the timezone service and contains records + relating to timezones and their aliases. The database is indexed by + timezone ID and each data record contains the database version + number, a record type, a timestamp, and an optional list of strings + (either aliases for a timezone or the reference timezone for an + alias). The format of each record is as follows:</p> + +<pre> +Key: <TZID> + +Data: <Version>SP<Record Type>SP<Timestamp>SP<Data Strings (TAB-separated)> +</pre> + <h2>Seen State (<userid>.seen)</h2> <p>This database is a per-user database and maintains the list of @@ -176,7 +191,7 @@ <h2>Subscriptions (<userid>.sub)</h2> -<p>This database is a per-user database and contains the list of +<p>This database is per-user and contains the list of mailboxes to which the user has subscribed. The database is indexed by mailbox name and each data record contains no data. The format of each record is follows:</p> @@ -190,7 +205,7 @@ <h2>Mailbox Keys (<userid>.mboxkey)</h2> -<p>This database is a per-user database and contains the list of +<p>This database is per-user and contains the list of mailbox access keys which are used for generating URLAUTH-authorized URLs. The database is indexed by mailbox name and each data record contains the database version number and the associated access key. @@ -202,5 +217,75 @@ Data: <Version (2 bytes)><Access Key (multi-byte)> </pre> + +<h2>DAV Index (<userid>.dav)</h2> + +<p>This SQLite database is per-user and primarily maintains a + mapping from DAV resource names (URLs) to the corresponding Cyrus + mailboxes and IMAP message UIDs. The database is designed to have + one table per resource type (iCalendar, vCard, etc) with each table + containing metadata specific to that resource type.</p> + +<h3>CalDAV</h3> + +<p>The format of the iCalendar table used by CalDAV is as follows:</p> + +<pre> +CREATE TABLE ical_objs ( + rowid INTEGER PRIMARY KEY, + creationdate INTEGER, + mailbox TEXT NOT NULL, + resource TEXT NOT NULL, + imap_uid INTEGER, + lock_token TEXT, + lock_owner TEXT, + lock_ownerid TEXT, + lock_expire INTEGER, + comp_type INTEGER, + ical_uid TEXT, + organizer TEXT, + dtstart TEXT, + dtend TEXT, + comp_flags INTEGER, + sched_tag TEXT, + UNIQUE( mailbox, resource ) +); +</pre> + +<p>Because CalDAV Scheduling requires the server to locate a resource + by iCalendar UID regardless of which calendar collection (mailbox) + it resides in, the iCalendar table has an additional index as follows:</p> + +<pre> +CREATE INDEX idx_ical_uid ON ical_objs ( ical_uid ); +</pre> + +<h3>CardDAV</h3> + +<p>The format of the vCard table used by CardDAV is as follows (work + in progress):</p> + +<pre> +CREATE TABLE vcard_objs ( + rowid INTEGER PRIMARY KEY, + creationdate INTEGER, + mailbox TEXT NOT NULL, + resource TEXT NOT NULL, + imap_uid INTEGER, + lock_token TEXT, + lock_owner TEXT, + lock_ownerid TEXT, + lock_expire INTEGER, + version INTEGER, + vcard_uid TEXT, + kind INTEGER, + fullname TEXT, + name TEXT, + nickname TEXT, + email TEXT, + UNIQUE( mailbox, resource ) +); +</pre> + </body> </html>
View file
cyrus-imapd-2.5.tar.gz/doc/internal/index-format-history.txt
Added
@@ -0,0 +1,165 @@ +cyrus.index format history. + +We only track Cyrus version 2 index formats here, so starting with version +3 as shipped with 2.0.0. + +NOTE: offsets in the code were sometimes defined in terms of the previous +field rather than exact offsets, which is bogus since the format on disk +will always be exact offsets, so all offsets here are specified exactly. + +Version 3: 2.0.0 2000-04-25 H:56 R:52 +============================================= + +/* Offsets of index header fields */ +#define OFFSET_GENERATION_NO 0 +#define OFFSET_FORMAT 4 +#define OFFSET_MINOR_VERSION 8 +#define OFFSET_START_OFFSET 12 +#define OFFSET_RECORD_SIZE 16 +#define OFFSET_EXISTS 20 +#define OFFSET_LAST_APPENDDATE 24 +#define OFFSET_LAST_UID 28 +#define OFFSET_QUOTA_MAILBOX_USED 32 +#define OFFSET_POP3_LAST_LOGIN 36 +#define OFFSET_UIDVALIDITY 40 +#define OFFSET_DELETED 44>------/* added for ACAP */ +#define OFFSET_ANSWERED 48 +#define OFFSET_FLAGGED 52 + +/* Offsets of index_record fields in index file */ +#define OFFSET_UID 0 +#define OFFSET_INTERNALDATE 4 +#define OFFSET_SENTDATE 8 +#define OFFSET_SIZE 12 +#define OFFSET_HEADER_SIZE 16 +#define OFFSET_CONTENT_OFFSET 20 +#define OFFSET_CACHE_OFFSET 24 +#define OFFSET_LAST_UPDATED 28 +#define OFFSET_SYSTEM_FLAGS 32 +#define OFFSET_USER_FLAGS 36 + + +Version 4: 2.1.4 2002-04-30 H:76 R:52 +============================================= + +The UIDL format was changed to include the mailbox UIDVALIDITY, but this +breaks existing caching when upgrading, so a header field was added to +track when an existing mailbox ever got empty, so that the UIDL format +could be changed from then. + +Also defined some spares so new fields could be added to the header +without a format change. + +#define OFFSET_POP3_NEW_UIDL 56>/* added for Outlook stupidity */ +#define OFFSET_SPARE0 60 +#define OFFSET_SPARE1 64 +#define OFFSET_SPARE2 68 +#define OFFSET_SPARE3 72 + + +Version 5: 2.2.1 2003-07-16 H:76 R:52 +============================================= + +To save repacking the cache file every single EXPUNGE, a new header field +was added to track when the cache had enough dirty data to justify a repack. +Strictly this didn't need a version bump. + +#define OFFSET_LEAKED_CACHE 60 /* Number of leaked records in cache file */ + +Version 6: 2.2.2 2003-10-28 H:76 R:60 +============================================= +#define OFFSET_POP3_NEW_UIDL 56>/* added for Outlook stupidity */ + + +Version 7: 2.3.0 2005-12-05 H:76 R:72 +============================================= + +Added replication support, and the UUID field to allow efficient replication +of copies. The UUID is 12 bytes long. + +#define OFFSET_MESSAGE_UUID 60 + + +Version 8: 2.3.4 2006-05-23 H:92 R:80 +============================================= + +Added support for CONDSTORE (RFC4551) in the header, including renaming +the POP3_NEW_UIDL to a more general bitmap of options (to which +CONDSTORE_ENABLED was added) + +#define OFFSET_MAILBOX_OPTIONS 60 + +#define OFFSET_HIGHESTMODSEQ_64 68 /* CONDSTORE (64-bit modseq) */ +#define OFFSET_HIGHESTMODSEQ 72 /* CONDSTORE (32-bit modseq) */ +#define OFFSET_SPARE0 76 /* Spares - only use these if the index */ +#define OFFSET_SPARE1 80 /* record size remains the same */ +#define OFFSET_SPARE2 84 /* (see note above about spares) */ +#define OFFSET_SPARE3 88 + +And the record: + +#define OFFSET_MODSEQ_64 72 /* CONDSTORE (64-bit modseq) */ +#define OFFSET_MODSEQ 76 /* CONDSTORE (32-bit modseq) */ + + +Version 9: 2.3.7 2006-07-10 H:96 R:80 +============================================= + +Due to the header not being divisible by 8 bytes, the 64 bit reads for +modseq values were inefficient - so this change just added extra padding +for alignment. + +#define OFFSET_SPARE4 92 + + +Version 10: 2.3.10 2007-10-24 H:96 R:88 +============================================= + +Switch UUID to GUID, which is a 20 byte sha1 instead of a 12 byte +calculated value. + +#define OFFSET_MESSAGE_GUID 60 +#define OFFSET_MODSEQ_64 80 /* CONDSTORE (64-bit modseq) */ +#define OFFSET_MODSEQ 84 /* CONDSTORE (32-bit modseq) */ + + +Version 11 never existed, it was used internally at Fastmail but never +released in a public Cyrus release. + +Version 12: 2.4.0 2010-11-10 H:128 R:96 +============================================= + +Major rewrite. Renamed exists to NUM_RECORDS and added a separate +new record for EXISTS (number of unexpunged records) + +#define OFFSET_NUM_RECORDS 20 + +Also DELETEDMODSEQ for QRESYNC support, CRCs throughout, fields for +supporting per-user SEEN storage in the cyrus.index file (RECENTUID +and RECENTTIME) and fields to support expunge cleanup. + +#define OFFSET_DELETEDMODSEQ_64 76 /* CONDSTORE (64-bit modseq) */ +#define OFFSET_DELETEDMODSEQ 80 /* CONDSTORE (32-bit modseq) */ +#define OFFSET_EXISTS 84 /* Non-expunged records */ +#define OFFSET_FIRST_EXPUNGED 88 /* last_updated of oldest expunged message */ +#define OFFSET_LAST_REPACK_TIME 92 /* time of last expunged cleanup */ +#define OFFSET_HEADER_FILE_CRC 96 /* CRC32 of the index header file */ +#define OFFSET_SYNC_CRC 100 /* XOR of SYNC CRCs of unexpunged records */ +#define OFFSET_RECENTUID 104 /* last UID the owner was told about */ +#define OFFSET_RECENTTIME 108 /* last timestamp for seen data */ +#define OFFSET_SPARE0 112 /* Spares - only use these if the index */ +#define OFFSET_SPARE1 116 /* record size remains the same */ +#define OFFSET_SPARE2 120 /* (see note above about spares) */ +#define OFFSET_HEADER_CRC 124 /* includes all zero for the spares! */ + +And the record had some changes too. Header size was always identical to +content offset, so replace that with a GMTIME field for efficient sort by +REVERSE ARRIVAL. + +#define OFFSET_GMTIME 20 + +Also, two new CRC fields for integrity checks on both the cache record and +the index record itself + +#define OFFSET_CACHE_CRC 88 /* CRC32 of cache record */ +#define OFFSET_RECORD_CRC 92
View file
cyrus-imapd-2.5.tar.gz/doc/internal/mailbox-api.html
Changed
@@ -313,12 +313,6 @@ <h3>then: close</h3> -<p>First close looks to see if the first_expunged date is old -(based on the configuration variable <tt>expunge_days</tt>), -if so - and there's an exclusive index lock - then it will -attempt an expunge cleanup. This allows for "automatic" -cleanup rather than running cyr_expire if wanted.</p> - <p>next the index is unlocked (see above)</p> <p>third, any "unlink" commands scheduled for email files are
View file
cyrus-imapd-2.5.tar.gz/doc/internal/replication_protocol.html
Changed
@@ -2,537 +2,79 @@ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> -<title>Cyrus IMAP Server: Replication Protocol</title> +<title>Cyrus IMAP Server: Replication Protocol v2.4+</title> </head> <body> -<h1>Cyrus IMAP Server: Replication Protocol</h1> +<h1>Cyrus IMAP Server: Replication Protocol v2.4+</h1> -<h2>Replication Wire protocol : types</h2> -<pre> -ulong:: - Unsigned long integer 0 <= x <= (2^32)-1 - -quota_t:: - EITHER : Unsigned long integer :: 0 <= x <= (2^32)-1 - OR : Unsigned long long integer :: 0 <= x <= (2^64)-1 - -time_t:: - Timestamp (currently 32 bit unsigned integer, offset into Unix epoch) - -literal+:: - {count+}\r\n -Arbitrary string of count bytes\r\n - -astring:: - Atom or string - -qstring:: - Quoted string - -flag_list_t - List of system and user flags of the form: (\Deleted \Answered Hello World) -</pre> - -<h3>Return values</h3> - -<p>Each command sent to the server is a single logical line of arbitrary length -(literal encoding may make the single logical line span multiple lines).</p> - -<p>Responses follow the IMAP three state module:</p> - -<pre> - OK English text\r\n -- Operation completed okay - NO English text\r\n -- Operation failed: reason attached -BAD English text\r\n -- replication protocol error. -</pre> - -<h3>Unsolicited/Multiline responses</h3> - -<p>Some commands generate responses which span multiple lines. The final line -in the response will start "OK", "NO", or "BAD". Intermediate lines will -all start "*". Some commands may generate structured responses (e.g: a list -of mailboxes plus all of their contents) where the number of "*" at the -start of the line defines the structure of the response.</p> - -<h2>Replication Wire Protocol : Brief text descriptions</h2> - -<p>The replication protocol defines 32 verbs at the time of writing.</p> - -<hr/> -<h4>ADDSUB</h4> -<pre> subscription :: astring</pre> - -<p>Add string to list of subscription on server.</p> - -<hr/> -<h4>ACTIVATE_SIEVE</h4> -<pre> sieve_name :: astring</pre> - -<p>Activate sieve file of given name for this user.</p> - -<hr/> -<h4>CREATE</h4> -<pre> - mailboxname :: astring -- e.g: user.dpc22.zzz - uniqueid :: astring -- Cyrus UniqueID (currently 64bit hex number) - acl :: astring -- Initial ACL for this mailbox - mbtype :: int - uidvalidity :: ulong -</pre> - -<p>Create mailbox with given name. UniqueID currently derived from -UIDvalidity and mailbox name, but no guarantee that this will always -be the case.</p> - -<p>Special value "NIL" for ACL means that mailbox has come from UW world or -some other location without ACL. We construct appropriate default ACL:</p> - -<pre> - userid --> "lrswipcda" - anonymous --> "0" (for FUD daemon) -</pre> - -<hr/> -<h4>CONTENTS</h4> - -<p>Dump complete contents of current folder to output screen. Used for -downloading messages from Cyrus into UW world.</p> - -<p>Response is series of lines of the form:</p> - -<pre> - * [uid] [internaldate] - [flags :: flag_list_t] - [message body :: literal] -</pre> - -<p>followed by final "OK" line. flag_list includes \Seen state for current -users, unlike STATUS, USER_ALL and USER_SOME.</p> - -<hr/> -<h4>DEACTIVATE_SIEVE</h4> - -<p>Deactivate the active sieve file for this user.</p> - -<hr/> -<h4>DELETE</h4> -<pre> mailbox :: astring.</pre> - -<p>Delete mailbox with given name.</p> - -<hr/> -<h4>DELSUB</h4> -<pre> mailbox :: astring</pre> - -<p>Delete subscription with given name</p> - -<hr/> -<h4>DELETE_SIEVE</h4> -<pre> sieve_name :: astring</pre> - -<p>Delete sieve file with given name.</p> - -<hr/> -<h4>EXIT</h4> - -<p>Shut down sync_server</p> - -<hr/> -<h4>EXPUNGE</h4> -<pre> - uid0 :: ulong - . . . - uidn :: ulong -</pre> - -<p>Remove messages matching given list of UIDS from currently selected -folder. Will return single line "OK Expunge Complete" on success.</p> - -<hr/> -<h4>ENDUSER</h4> - -<p>Unlock current user and call auth_freestate() to discard current -authentication.</p> - -<hr/> -<h4>GET_SIEVE sieve_name :: astring</h4> - -<p>Get sieve script with given name. Successful response is of form:</p> - -<pre> -OK {%lu+}\r\n -Sieve script -\r\n -</pre> - -<hr/> -<h4>INFO</h4> - -<p>Get meta information for currently selected folder. Response is single line -of the form:</p> - -<pre> -OK - UIDvalidity :: ulong - UIDlast :: ulong - Flags :: flag_list_t -</pre> - -<p>"INFO" command is almost certainly historical baggage and should be -removed.</p> - -<hr/> -<h4>LIST</h4> - -<p>List mail folders for currently selected list. Multiline response of -the form:</p> - -<pre> - * [Uniqueid :: astring] [Name :: astring] [Acl :: astring] - . . . - * [Uniqueid :: astring] [Name :: astring] [Acl :: astring] - OK List complete -</pre> - -<p>List command probably obsolete with advent of USER_ALL and USER_SOME -commands.</p> - -<hr/> -<h4>LSUB</h4> - -<p>List subscriptions for currently selected user. Multiline response of the -form:</p> - -<pre> - * [Subscription :: astring] - . . . - * [Subscription :: astring] - OK Lsub complete -</pre> - -<hr/> -<h4>LIST_SIEVE</h4> - -<p>List available sieve files for currently selected users. Multiline -response of the form:</p> - -<pre> - * [Sieve_name :: astring] [*] <-- "*" for active sieve file - . . . - * [Sieve_name :: astring] - OK List_sieve complete -</pre> - -<hr/> -<h4>QUOTA</h4> -<pre> quota_root :: astring</pre> - -<p>Get quota root information. Response is single line of the form:</p> +<h2>DList 1.0</h2> -<pre> -OK - Limit :: quota_t (Kbytes) - Expunged_timeout :: ulong (seconds) - Expunged_vol_min :: quota_t (KBytes) - Expunged_vol_max :: quota_t (KBytes) - Expunged_vol_overflow :: quota_t (KBytes) - Mailbox_limit :: ulong (KBytes) -</pre> - -<hr/> -<h4>RENAME</h4> -<pre> - old_name :: astring - new_name :: astring -</pre> - -<p>Rename mailbox old_name --> new_name</p> +<p>The DList protocol is based closely on the IMAP wire protocol, +using atoms and literals as the basis, but with two extended types +of data:</p> -<hr/> -<h4>RESET</h4> -<pre> account :: astring</pre> - -<p>Reset named account back to initial state. Wipes all of the -following:</p> <ul> -<li>mailboxes</li> -<li>subscriptions</li> -<li>seen data</li> -<li>Sieve files.</li> +<li>kvlist</li> +<li>rfc822-object</li> </ul> -<hr/> -<h4>RESERVE</h4> -<pre> - mailbox :: astring - guid0 :: ulong - . . . - guidn :: ulong -</pre> - -<p>Reserves collection of messages matching list of GUIDs in given mailbox. -Message data is linked into /var/spool/imap/sync. staging directory -so that we can later link the messages into other folders even if the -original has been expunged. Multiline response of the form:</p> - -<pre> - * [GUID :: astring] - . . . - * [GUID :: astring] - OK Reserve complete -</pre> - -<p>so that client knows which reserve operations have been complete and which -have failed.</p> - -<hr/> -<h4>RESTART</h4> - -<p>sync_client wants to negotiate a restart to clear up. Clears out all staged -messages causes by upload and reserve sync_server child process exits -cleanly, parent forks off a new child to resolve any memory leaks.</p> - -Responds: -<pre> -OK restarting. -</pre> - -<hr/> -<h4>SELECT</h4> -<pre> mailbox :: astring</pre> - -<p>Select named mailbox as current folder (calls mailbox_open()).</p> - -<hr/> -<h4>STATUS</h4> - -<p>Return complete status of currently selected mailbox. Multiline response:</p> - -<pre> - * [uid :: ulong] [guid :: astring] [flags :: flag_list] - . . . - * [uid :: ulong] [guid :: astring] [flags :: flag_list] - OK [last_uid :: ulong] -</pre> - -<p>Believe that this command has been obsoleted by USER_ALL and USER_SOME. -Occasionally useful for debugging purposes.</p> - -<hr/> -<h4>SETFLAGS</h4> -<pre> - [ - uid :: ulong - flags :: flag_list_t e.g: (\Deleted \Flagged Hello World) - ] + -</pre> - -<p>Set flags for messages with given series of UIDs. SETFLAGS can take -an arbitrary number of "uid (flags) " pairs on a single line.</p> - -<hr/> -<h4>SETSEEN</h4> -<pre> - user :: astring - lastread :: time_t - lastuid :: time_t - lastchange :: time_t - seenuid :: astring -</pre> - -<p>Update seen database for current selected mailbox + nominated userid. -seenuid is opaque string used by seendb code. The replication engine -doesn't attempt to decompose seenuid strings to work out if anything -has changed, it just uses the lastchange timestamp to work out if -the seenuid string should be updated.</p> - -Successful Response: -<pre> - OK Setseen Succeeded -</pre> - -<hr/> -<h4>SETACL</h4> -<pre> - mailbox :: astring - acl :: astring -</pre> - -<p>Set ACL on given mailbox. ACL will typically contain tab characters, -so the astring will be the quoted form e.g:</p> +A wart of the protocol is the rfc822-object, which contains an +explicit cyrus backend partition. This will be removed in future +versions of Cyrus. -<pre> - "dpc22\ttlrswipcda\tanonymous\t0\t" -</pre> +<h3>types</h3> -<hr/> -<h4>SETQUOTA</h4> -<pre> - quotaroot :: astring - limit :: quota_t - expunged_timeout :: ulong - expunged_vol_min :: ulong - expunged_vol_max :: ulong - expunged_vol_overflow :: ulong - mailbox_limit :: ulong (KBytes) -</pre> +<h4>atom</h4> -<p>Set limits and overrides for given quota root.</p> +<p>An atom is actually a sequence of any character other than '\0', +the NULL byte. Character encoding is not specified, but it can +contain 8 bit characters, and is probably utf8</p> -<hr/> -<h4>UPLOAD</h4> -<pre> - newlastuid :: ulong - last_appenddate :: time_t -</pre> +<h4>flag</h4> -<p>Arbitrary list of messages on single logical line , with three forms:</p> +<p>Flag is a horrible special case of atom to allow \word to be +represented as an IMAP atom on the wire. This is one of many +special cases in the IMAP protocol, and is duplicated into dlist +just to make it easier to read</p> -<pre> - SIMPLE # Unparsed message that we need to parse - GUID :: astring - UID :: ulong # "NIL" => Use GUID 0. - internaldate :: time_t - sentdate :: time_t - last_updated :: time_t - flags :: flag_list_t - message_text :: literal+ +<h4>num32/num</h4> - PARSED # Message plus pre-parsed cyrus.cache entry - GUID :: astring - UID :: ulong - internaldate :: time_t - sentdate :: time_t - last_updated :: time_t - flags :: flag_list_t - message_cache :: literal+ - message_text :: literal+ +<p>Both stored as 64 bit integers internally, and sent as decimal +numbers over the wire, this type exists only in the API, it just +looks like a string on the wire.</p> - COPY # Copy of message which has already - GUID :: astring # been UPLOADED or RESERVED - UID :: ulong # Message body and cache available - internaldate :: time_t # from staging directory - sentdate :: time_t -</pre> +<h4>hex32/hex</h4> -<p>Upload an arbitrary list of messages to the currently selected folder.</p> +<p>Stored like num internally, but sent as an 8 or 16 character +hexadecimal string on the wire</p> -<p>Will normally only add messages to the end of the mailfolder in current use -(and the code has special optimised path for that case). However if -message list contains UIDs which belong in the middle of a mailbox, or a -given UID exists on both client and server but the GUID values don't match -(which normally indicates the two ends have lost sync with each other), -the the UPLOAD command will merge in new and replacement messages.</p> +<h4>map</h4> -Response: -<pre> - OK Upload %lu messages okay -</pre> +<p>Like atom, but can contain NULL. All values are parsed off +the wire as 'map' type and then converted on demand into the +requested type</p> -<hr/> -<h4>UIDLAST</h4> -<pre> - last_uid :: ulong - last_appenddate :: ulong -</pre> - -<p>Update last_uid and last_appenddate timestamps on mailfolder. Typically -used when new messages have been arrived and then been expunged between -replication runs. Essentially UPLOAD operation with an empty message list.</p> - -<hr/> -<h4>USER</h4> -<pre> - user ::astring -</pre> +<h4>list</h4> -<p>Select given user: locks out concurrent access from other replication -runners and calls auth_newstate to authenticate us as the user in question.</p> +<p>Encoded in parentheses like so (item item item) a list type can +be nested inside other lists: (item (sub sub) item)</p> -Response: -<pre> - OK Locked [username] -</pre> +<h4>kvlist</h4> -<hr/> -<h4>USER_ALL</h4> -<pre> - user ::astring -</pre> +<p>A kvlist allows named parameters, and is indicated with a leading +% character. % is invalid in atoms, so parsing is unambiguous, e.g:</p> -<p>Select given user: locks out concurrent access from other replication -runners and calls auth_newstate to authenticate us as the user in question.</p> +<p>%(key1 value1 key2 (list of values) key3 value3)</p> -<p>Response is a complete list of _everything_ in the target account on -the replica system in a single round trip. Multi line response, -with various kinds of "*" response indicating different objects. -Format should be familiar from other commands which examine only a -portion of the target account.</p> +<h4>rfc822-object/file</h4> -<pre> -**** Sieve script. Single line response of the form: - - **** [name :: astring] - [last_update :: time_t] - [* - if active Sieve] - -*** Subscription. Single line response of the form: - - *** [name :: astring] - -** Mailbox. Single line response of the form: - - ** [uniqueid :: astring] - [name :: astring] - [acl :: astring] - [lastuid :: ulong] - [lastchange :: time_t] - -* Message within last mailbox listed as a "**" item. Format is the - same as that generated by STATUS command: - - * [uid :: ulong] - [guid :: astring] - [flags :: flag_list_t] -</pre> - -<p>Final line of successful response is used to report QUOTA information -in the same format:</p> - -<pre> -OK - Limit :: quota_t (Kbytes) - expunged_timeout :: ulong (seconds) - expunged_vol_min :: quota_t (KBytes) - expunged_vol_max :: quota_t (KBytes) - expunged_vol_overflow :: quota_t (KBytes) - mailbox_limit :: ulong (KBytes) -</pre> - -<hr/> -<h4>USER_SOME</h4> -<pre> - user :: astring - mailbox1 :: astring - . . . - mailboxn :: astring -</pre> - -<p>Similar to USER_ALL command, but only returns mailbox and message lists -for given set of mailboxes. Doesn't return sieve or subscription -info.</p> - -<hr/> -<h4>UPLOAD_SIEVE</h4> -<pre> - sieve_name :: astring - sieve_file :: literal+ -</pre> +<p>Finally the ugly one. These look like a literal, but with a leading % +and two more fields:</p> -<p>Response on success:</p> <pre> - Upload_sieve completed. +%{partition sha1 size}\r\n +data </pre> </body>
View file
cyrus-imapd-2.5.tar.gz/doc/man.html
Changed
@@ -47,6 +47,7 @@ <LI><A HREF="man/deliver.8.html"><TT>deliver</TT>(8)</A> <LI><A HREF="man/fetchnews.8.html"><TT>fetchnews</TT>(8)</A> <LI><A HREF="man/fud.8.html"><TT>fud</TT>(8)</A> +<LI><A HREF="man/httpd.8.html"><TT>httpd</TT>(8)</A> <LI><A HREF="man/idled.8.html"><TT>idled</TT>(8)</A> <LI><A HREF="man/imapd.8.html"><TT>imapd</TT>(8)</A> <LI><A HREF="man/ipurge.8.html"><TT>ipurge</TT>(8)</A>
View file
cyrus-imapd-2.5.tar.gz/doc/specs.html
Changed
@@ -182,8 +182,12 @@ <TD>Sieve Email Filtering: Vacation Extension</TD></TR> <TR><TD><A HREF="http://www.ietf.org/rfc/rfc5231.txt">RFC 5231</A></TD> <TD>Sieve Email Filtering: Relational Extension</TD></TR> +<TR><TD><A HREF="http://www.ietf.org/rfc/rfc5232.txt">RFC 5232</A></TD> +<TD>Sieve Email Filtering: Imap4flags Extension</TD></TR> <TR><TD><A HREF="http://www.ietf.org/rfc/rfc5233.txt">RFC 5233</A></TD> <TD>Sieve Email Filtering: Subaddress Extension</TD></TR> +<TR><TD><A HREF="http://www.ietf.org/rfc/rfc5260.txt">RFC 5260</A></TD> +<TD>Sieve Email Filtering: Date and Index Extensions</TD></TR> <TR><TD><A HREF="http://www.ietf.org/rfc/rfc5173.txt">RFC 5173</A></TD> <TD>Sieve Email Filtering: Body Extension</TD></TR> <TR><TD><A HREF="http://www.ietf.org/rfc/rfc3894.txt">RFC 3894</A></TD> @@ -211,7 +215,87 @@ draft-ietf-sieve-managesieve</A></TD> <TD>A Protocol for Remotely Managing Sieve Scripts</TD></TR> +<TR><TD COLSPAN=2><br><h2>HTTP</h2></TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc7230">RFC 7230</A></TD> +<TD>Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc7231">RFC 7231</A></TD> +<TD>Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc7232">RFC 7232</A></TD> +<TD>Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc7233">RFC 7233</A></TD> +<TD>Hypertext Transfer Protocol (HTTP/1.1): Range Requests</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc7234">RFC 7234</A></TD> +<TD>Hypertext Transfer Protocol (HTTP/1.1): Caching</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc7235">RFC 7235</A></TD> +<TD>Hypertext Transfer Protocol (HTTP/1.1): Authentication</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc2617">RFC 2617</A> +<BR><I>being updated by</I> +<BR><A HREF="http://tools.ietf.org/html/draft-ietf-httpauth-basicauth-update"> +draft-ietf-httpauth-basicauth-update</A> +<BR><A HREF="http://tools.ietf.org/html/draft-ietf-httpauth-digest"> +draft-ietf-httpauth-digest</A></TD> +<TD>HTTP Authentication: Basic and Digest Access Authentication</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc4559">RFC 4559</A></TD> +<TD>SPNEGO-based Kerberos and NTLM HTTP Authentication in Microsoft Windows</TD></TR> +<TR><TD><A HREF="http://download.microsoft.com/download/a/e/6/ae6e4142-aa58-45c6-8dcf-a657e5900cd3/[MS-NTHT].pdf">[MS-NTHT]</A></TD> +<TD>NTLM Over HTTP Protocol Specification</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc2817">RFC 2817</A></TD> +<TD>HTTP Upgrading to TLS Within HTTP/1.1</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc2818">RFC 2818</A></TD> +<TD>HTTP Over TLS</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc6797">RFC 6797</A></TD> +<TD>HTTP Strict Transport Security (HSTS)</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc7239">RFC 7239</A></TD> +<TD>Forwarded HTTP Extension</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc7240">RFC 7240</A></TD> +<TD>Prefer Header for HTTP</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc4918">RFC 4918</A></TD> +<TD>HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc3253">RFC 3253</A></TD> +<TD>Versioning Extensions to WebDAV (Web Distributed Authoring and Versioning)</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc3744">RFC 3744</A></TD> +<TD>Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc4331">RFC 4331</A></TD> +<TD>Quota and Size Properties for Distributed Authoring and Versioning (DAV) Collections</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc5397">RFC 5397</A></TD> +<TD>WebDAV Current Principal Extension</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc5689">RFC 5689</A></TD> +<TD>Extended MKCOL for Web Distributed Authoring and Versioning (WebDAV)</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc5995">RFC 5995</A></TD> +<TD>Using POST to Add Members to Web Distributed Authoring and Versioning (WebDAV) Collections</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc6578">RFC 6578</A></TD> +<TD>Collection Synchronization for Web Distributed Authoring and Versioning (WebDAV)</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc4791">RFC 4791</A></TD> +<TD>Calendaring Extensions to WebDAV (CalDAV)</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc6638">RFC 6638</a></TD> +<TD>Scheduling Extensions to CalDAV</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc6352">RFC 6352</a></TD> +<TD>CardDAV: vCard Extensions to Web Distributed Authoring and Versioning (WebDAV)</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc6764">RFC 6764</A></TD> +<TD>Locating Services for Calendaring Extensions to WebDAV (CalDAV) and vCard Extensions to WebDAV (CardDAV)</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/draft-ietf-tzdist-service">draft-ietf-tzdist-service</A></TD> +<TD>Time Zone Data Distribution Service</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/draft-ietf-tzdist-caldav-timezone-ref">draft-ietf-tzdist-caldav-timezone-ref</A></TD> +<TD>CalDAV: Time Zones by Reference</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/draft-ietf-calext-rscale">draft-ietf-calext-rscale</A></TD> +<TD>Non-Gregorian Recurrence Rules in iCalendar</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/draft-daboo-calendar-availability">draft-daboo-calendar-availability</A></TD> +<TD>Calendar Availability</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/draft-york-vpoll">draft-york-vpoll</A></TD> +<TD>VPOLL: Consensus Scheduling Component for iCalendar</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/draft-desruisseaux-ischedule">draft-desruisseaux-ischedule</A></TD> +<TD>Internet Calendar Scheduling Protocol (iSchedule)</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/draft-thomson-hybi-http-timeout">draft-thomson-hybi-http-timeout</A></TD> +<TD>Hypertext Transfer Protocol (HTTP) Keep-Alive Header</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/draft-murchison-webdav-prefer">draft-murchison-webdav-prefer</A></TD> +<TD>Use of the Prefer Header Field in Web Distributed Authoring and Versioning (WebDAV)</TD></TR> +<TR><TD><A HREF="http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-ctag.txt">caldav-ctag</A></TD> +<TD>Calendar Collection Entity Tag (CTag) in CalDAV</TD></TR> +<TR><TD><A HREF="http://msdn.microsoft.com/en-us/library/aa142743%28v=exchg.65%29.aspx">Brief Header</A></TD><TD>Microsoft 'Brief' header extension</TD></TR> + <TR><TD COLSPAN=2><br><h2>Other</h2></TD></TR> +<TR><TD><A HREF="http://www.ietf.org/rfc/rfc3656.txt">RFC 3656</a></TD> +<TD>MUPDATE Protocol (For Cyrus Murder)</TD></TR> <TR><TD><A HREF="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</A></TD> <TD>Internet Message Format</TD></TR> <TR><TD><A HREF="http://www.ietf.org/rfc/rfc5536.txt">RFC 5536</A></TD> @@ -222,4 +306,22 @@ <TD>MUPDATE Protocol (For Cyrus Murder)</TD></TR> <TR><TD><A HREF="http://www.ietf.org/rfc/rfc5423.txt">RFC 5423</A></TD> <TD>Internet Message Store Events</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc4287">RFC 4287</a></TD> +<TD>The Atom Syndication Format</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc5545">RFC 5545</a></TD> +<TD>Internet Calendaring and Scheduling Core Object Specification (iCalendar) +</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc5546">RFC 5546</a></TD> +<TD>iCalendar Transport-Independent Interoperability Protocol (iTIP) +</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc6047">RFC 6047</a></TD> +<TD>iCalendar Message-Based Interoperability Protocol (iMIP)</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc6231">RFC 6231</a></TD> +<TD>xCal: The XML Format for iCalendar</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc7265">RFC 7265</a></TD> +<TD>jCal: The JSON Format for iCalendar</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc6350">RFC 6350</a></TD> +<TD>vCard Format Specification</TD></TR> +<TR><TD><A HREF="http://tools.ietf.org/html/rfc6376">RFC 6376</a></TD> +<TD>DomainKeys Identified Mail (DKIM) Signatures</TD></TR> </TABLE>
View file
cyrus-imapd-2.5.tar.gz/imap/annotate.c
Changed
@@ -817,56 +817,45 @@ const char **entryp, const char **useridp) { -#define NFIELDS 3 - const char *fields[NFIELDS]; - int nfields = 0; + static struct buf keybuf; const char *p; - unsigned int uid = 0; - const char *mboxname = ""; - - /* paranoia: ensure the last character in the key is - * a NUL, which it should be because of the way we - * always build keys */ - if (key[keysize-1]) - return IMAP_ANNOTATION_BADENTRY; - keysize--; + const char *end; + + buf_setmap(&keybuf, key, keysize); + buf_putc(&keybuf, '\0'); /* safety tricks due to broken FM code */ + p = buf_cstring(&keybuf); + end = p + keysize; + /* * paranoia: split the key into fields on NUL characters. * We would use strarray_nsplit() for this, except that * by design that function cannot split on NULs and does * not handle embedded NULs. */ - fields[nfields++] = key; - for (p = key ; (p-key) < keysize ; p++) { - if (!*p) { - if (nfields == NFIELDS) - return IMAP_ANNOTATION_BADENTRY; - fields[nfields++] = p+1; - } - } - if (nfields != NFIELDS) - return IMAP_ANNOTATION_BADENTRY; if (d->mboxname) { - /* per-folder db for message scope annotations */ - char *end = NULL; - uid = strtoul(fields[0], &end, 10); - if (uid == 0 || end == NULL || *end) - return IMAP_ANNOTATION_BADENTRY; - mboxname = d->mboxname; + *mboxnamep = d->mboxname; + *uidp = 0; + while (*p && p < end) *uidp = (10 * (*uidp)) + (*p++ - '0'); + if (p < end) p++; + else return IMAP_ANNOTATION_BADENTRY; } else { /* global db for mailnbox & server scope annotations */ - uid = 0; - mboxname = fields[0]; + *uidp = 0; + *mboxnamep = p; + while (*p && p < end) p++; + if (p < end) p++; + else return IMAP_ANNOTATION_BADENTRY; } - if (mboxnamep) *mboxnamep = mboxname; - if (uidp) *uidp = uid; - if (entryp) *entryp = fields[1]; - if (useridp) *useridp = fields[2]; + *entryp = p; /* XXX: trailing NULLs on non-userid keys? Bogus just at FM */ + while (*p && p < end) p++; + if (p < end && !*p) + *useridp = p+1; + else + *useridp = NULL; return 0; -#undef NFIELDS } #if DEBUG @@ -955,26 +944,28 @@ struct find_rock *frock = (struct find_rock *) rock; const char *mboxname, *entry, *userid; unsigned int uid; + char newkey[MAX_MAILBOX_NAME+1]; + size_t newkeylen; struct buf value = BUF_INITIALIZER; - char keycopy[MAX_MAILBOX_PATH+1]; int r; assert(keylen < MAX_MAILBOX_PATH); - /* take a copy, we may be deleting this record, so the key - * pointer will no longer be valid */ - memcpy(keycopy, key, keylen); - #if DEBUG syslog(LOG_ERR, "find_cb: found key %s in %s", - key_as_string(frock->d, keycopy, keylen), frock->d->filename); + key_as_string(frock->d, key, keylen), frock->d->filename); #endif - r = split_key(frock->d, keycopy, keylen, &mboxname, + r = split_key(frock->d, key, keylen, &mboxname, &uid, &entry, &userid); if (r) return r; + newkeylen = make_key(mboxname, uid, entry, userid, newkey, sizeof(newkey)); + if (keylen != newkeylen || strncmp(newkey, key, keylen)) { + syslog(LOG_ERR, "find_cb: bogus key %s %d %s %s (%d %d)", mboxname, uid, entry, userid, (int)keylen, (int)newkeylen); + } + r = split_attribs(data, datalen, &value); if (!r) r = frock->proc(mboxname, uid, entry, userid, &value, frock->rock); @@ -2264,13 +2255,19 @@ /************************** Annotation Storing *****************************/ EXPORTED int annotatemore_lookup(const char *mboxname, const char *entry, - const char *userid, struct buf *value) + const char *userid, struct buf *value) { return annotatemore_msg_lookup(mboxname, /*uid*/0, entry, userid, value); } +EXPORTED int annotatemore_lookupmask(const char *mboxname, const char *entry, + const char *userid, struct buf *value) +{ + return annotatemore_msg_lookupmask(mboxname, /*uid*/0, entry, userid, value); +} + EXPORTED int annotatemore_msg_lookup(const char *mboxname, uint32_t uid, const char *entry, - const char *userid, struct buf *value) + const char *userid, struct buf *value) { char key[MAX_MAILBOX_PATH+1]; size_t keylen, datalen; @@ -2302,6 +2299,16 @@ return r; } +EXPORTED int annotatemore_msg_lookupmask(const char *mboxname, uint32_t uid, const char *entry, + const char *userid, struct buf *value) +{ + int r = annotatemore_msg_lookup(mboxname, uid, entry, userid, value); + if (value->len == 0) { + r = annotatemore_msg_lookup(mboxname, uid, entry, NULL, value); + } + return r; +} + static int read_old_value(annotate_db_t *d, const char *key, int keylen, struct buf *valp) @@ -2423,6 +2430,66 @@ return r; } +EXPORTED int annotatemore_rawwrite(const char *mboxname, const char *entry, + const char *userid, const struct buf *value) +{ + char key[MAX_MAILBOX_PATH+1]; + int keylen, r; + annotate_db_t *d = NULL; + uint32_t uid = 0; + + r = _annotate_getdb(mboxname, uid, CYRUSDB_CREATE, &d); + if (r) goto done; + + /* must be in a transaction to modify the db */ + annotate_begin(d); + + keylen = make_key(mboxname, uid, entry, userid, key, sizeof(key)); + + if (value->s == NULL) { + do { + r = cyrusdb_delete(d->db, key, keylen, tid(d), /*force*/1); + } while (r == CYRUSDB_AGAIN); + } + else { + struct buf data = BUF_INITIALIZER; + unsigned long l; + static const char contenttype[] = "text/plain"; /* fake */ + + l = htonl(value->len); + buf_appendmap(&data, (const char *)&l, sizeof(l)); + + buf_appendmap(&data, value->s, value->len); + buf_putc(&data, '\0'); + + /* + * Older versions of Cyrus expected content-type and + * modifiedsince fields after the value. We don't support those + * but we write out default values just in case the database + * needs to be read by older versions of Cyrus + */ + buf_appendcstr(&data, contenttype); + buf_putc(&data, '\0'); + + l = 0; /* fake modifiedsince */ + buf_appendmap(&data, (const char *)&l, sizeof(l)); + + do { + r = cyrusdb_store(d->db, key, keylen, data.s, data.len, tid(d)); + } while (r == CYRUSDB_AGAIN); + + buf_free(&data); + } + + if (r) goto done; + r = annotate_commit(d); + +done: + annotate_putdb(&d); + + return r; +} + EXPORTED int annotatemore_write(const char *mboxname, const char *entry, const char *userid, const struct buf *value) { @@ -2434,15 +2501,24 @@ { struct mailbox *mailbox = NULL; int r = 0; + annotate_db_t *d = NULL; - if (mboxname) + r = _annotate_getdb(mboxname, uid, CYRUSDB_CREATE, &d); + if (r) goto done; + + if (mboxname) { r = mailbox_open_iwl(mboxname, &mailbox); + if (r) goto done; + } + + r = write_entry(mailbox, uid, entry, userid, value, /*ignorequota*/1); + if (r) goto done; - if (!r) - r = write_entry(mailbox, uid, entry, userid, value, /*ignorequota*/1); + r = annotate_commit(d); - if (mailbox) - mailbox_close(&mailbox); +done: + annotate_putdb(&d); + mailbox_close(&mailbox); return r; } @@ -3001,7 +3077,7 @@ const char *newuserid = userid; if (rrock->olduserid && rrock->newuserid && - !strcmp(rrock->olduserid, userid)) { + !strcmpsafe(rrock->olduserid, userid)) { /* renaming a user, so change the userid for priv annots */ newuserid = rrock->newuserid; }
View file
cyrus-imapd-2.5.tar.gz/imap/annotate.h
Changed
@@ -157,14 +157,23 @@ const char *userid, const struct buf *value); int annotatemore_msg_write(const char *mboxname, uint32_t uid, const char *entry, const char *userid, const struct buf *value); +/* flat out ignore modseq and quota and everything */ +int annotatemore_rawwrite(const char *mboxname, const char *entry, + const char *userid, const struct buf *value); /* lookup a single annotation and return result */ int annotatemore_lookup(const char *mboxname, const char *entry, const char *userid, struct buf *value); +/* same but check shared if per-user doesn't exist */ +int annotatemore_lookupmask(const char *mboxname, const char *entry, + const char *userid, struct buf *value); /* lookup a single per-message annotation and return result */ int annotatemore_msg_lookup(const char *mboxname, uint32_t uid, const char *entry, const char *userid, struct buf *value); +/* same but check shared if per-user doesn't exist */ +int annotatemore_msg_lookupmask(const char *mboxname, uint32_t uid, const char *entry, + const char *userid, struct buf *value); /* store annotations. Requires an open transaction */ int annotate_state_store(annotate_state_t *state, struct entryattlist *l);
View file
cyrus-imapd-2.5.tar.gz/imap/arbitron.c
Changed
@@ -135,7 +135,7 @@ break; case 'D': { - unsigned month, day, year; + unsigned month = 0, day = 0, year = 0; struct tm date; if (strlen(optarg) < 8 ||
View file
cyrus-imapd-2.5.tar.gz/imap/caldav_db.c
Added
@@ -0,0 +1,930 @@ +/* caldav_db.c -- implementation of per-user CalDAV database + * + * Copyright (c) 1994-2012 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 <syslog.h> +#include <string.h> + +#include <libical/ical.h> + +#include "caldav_db.h" +#include "cyrusdb.h" +#include "exitcodes.h" +#include "httpd.h" +#include "http_dav.h" +#include "libconfig.h" +#include "mboxname.h" +#include "util.h" +#include "xstrlcat.h" +#include "xmalloc.h" + + +enum { + STMT_SELRSRC, + STMT_SELUID, + STMT_SELMBOX, + STMT_INSERT, + STMT_UPDATE, + STMT_DELETE, + STMT_DELMBOX, + STMT_BEGIN, + STMT_COMMIT, + STMT_ROLLBACK +}; + +#define NUM_STMT 10 + +struct caldav_db { + sqlite3 *db; /* DB handle */ + char sched_inbox[MAX_MAILBOX_BUFFER];/* DB owner's scheduling Inbox */ + sqlite3_stmt *stmt[NUM_STMT]; /* prepared statements */ + struct buf mailbox; /* buffers for copies of column text */ + struct buf resource; + struct buf lock_token; + struct buf lock_owner; + struct buf lock_ownerid; + struct buf ical_uid; + struct buf organizer; + struct buf dtstart; + struct buf dtend; + struct buf sched_tag; +}; + + +static struct namespace caldav_namespace; +time_t caldav_epoch = -1; +time_t caldav_eternity = -1; + +EXPORTED int caldav_init(void) +{ + int r; + struct icaltimetype date; + + /* Set namespace -- force standard (internal) */ + if ((r = mboxname_init_namespace(&caldav_namespace, 1))) { + syslog(LOG_ERR, "%s", error_message(r)); + fatal(error_message(r), EC_CONFIG); + } + + /* Get min date-time */ + date = icaltime_from_string(config_getstring(IMAPOPT_CALDAV_MINDATETIME)); + if (!icaltime_is_null_time(date)) { + caldav_epoch = icaltime_as_timet_with_zone(date, NULL); + } + if (caldav_epoch == -1) caldav_epoch = INT_MIN; + + /* Get max date-time */ + date = icaltime_from_string(config_getstring(IMAPOPT_CALDAV_MAXDATETIME)); + if (!icaltime_is_null_time(date)) { + caldav_eternity = icaltime_as_timet_with_zone(date, NULL); + } + if (caldav_eternity == -1) caldav_eternity = INT_MAX; + + return dav_init(); +} + + +EXPORTED int caldav_done(void) +{ + return dav_done(); +} + + +#define CMD_DROP "DROP TABLE IF EXISTS ical_objs;" + +#define CMD_CREATE \ + "CREATE TABLE IF NOT EXISTS ical_objs (" \ + " rowid INTEGER PRIMARY KEY," \ + " creationdate INTEGER," \ + " mailbox TEXT NOT NULL," \ + " resource TEXT NOT NULL," \ + " imap_uid INTEGER," \ + " lock_token TEXT," \ + " lock_owner TEXT," \ + " lock_ownerid TEXT," \ + " lock_expire INTEGER," \ + " comp_type INTEGER," \ + " ical_uid TEXT," \ + " organizer TEXT," \ + " dtstart TEXT," \ + " dtend TEXT," \ + " comp_flags INTEGER," \ + " sched_tag TEXT," \ + " UNIQUE( mailbox, resource ) );" \ + "CREATE INDEX IF NOT EXISTS idx_ical_uid ON ical_objs ( ical_uid );" + +static struct caldav_db *caldav_open_fname(const char *fname, int flags) +{ + sqlite3 *db; + struct caldav_db *caldavdb = NULL; + const char *cmds = CMD_CREATE; + + if (flags & CALDAV_TRUNC) cmds = CMD_DROP CMD_CREATE; + + db = dav_open(fname, cmds); + + if (db) { + caldavdb = xzmalloc(sizeof(struct caldav_db)); + caldavdb->db = db; + } + + return caldavdb; +} + +EXPORTED struct caldav_db *caldav_open_userid(const char *userid, int flags) +{ + struct caldav_db *caldavdb = NULL; + struct buf fname = BUF_INITIALIZER; + + dav_getpath_byuserid(&fname, userid); + caldavdb = caldav_open_fname(buf_cstring(&fname), flags); + buf_free(&fname); + + /* Construct mbox name corresponding to userid's scheduling Inbox */ + strncpy(caldavdb->sched_inbox, caldav_mboxname(userid, SCHED_INBOX), sizeof(caldavdb->sched_inbox)); + + return caldavdb; +} + +/* Open DAV DB corresponding to userid */ +EXPORTED struct caldav_db *caldav_open_mailbox(struct mailbox *mailbox, int flags) +{ + struct caldav_db *caldavdb = NULL; + const char *userid = mboxname_to_userid(mailbox->name); + + if (userid) { + caldavdb = caldav_open_userid(userid, flags); + } + else { + struct buf fname = BUF_INITIALIZER; + dav_getpath(&fname, mailbox); + caldavdb = caldav_open_fname(buf_cstring(&fname), flags); + buf_free(&fname); + } + + return caldavdb; +} + + +/* Close DAV DB */ +EXPORTED int caldav_close(struct caldav_db *caldavdb) +{ + int i, r = 0; + + if (!caldavdb) return 0; + + buf_free(&caldavdb->mailbox); + buf_free(&caldavdb->resource); + buf_free(&caldavdb->lock_token); + buf_free(&caldavdb->lock_owner); + buf_free(&caldavdb->lock_ownerid); + buf_free(&caldavdb->ical_uid); + buf_free(&caldavdb->organizer); + buf_free(&caldavdb->dtstart); + buf_free(&caldavdb->dtend); + buf_free(&caldavdb->sched_tag); + + for (i = 0; i < NUM_STMT; i++) { + sqlite3_stmt *stmt = caldavdb->stmt[i]; + if (stmt) sqlite3_finalize(stmt); + } + + r = dav_close(caldavdb->db); + + free(caldavdb); + + return r; +} + + +#define CMD_BEGIN "BEGIN TRANSACTION;" + +EXPORTED int caldav_begin(struct caldav_db *caldavdb) +{ + return dav_exec(caldavdb->db, CMD_BEGIN, NULL, NULL, NULL, + &caldavdb->stmt[STMT_BEGIN]); +} + + +#define CMD_COMMIT "COMMIT TRANSACTION;" + +EXPORTED int caldav_commit(struct caldav_db *caldavdb) +{ + return dav_exec(caldavdb->db, CMD_COMMIT, NULL, NULL, NULL, + &caldavdb->stmt[STMT_COMMIT]); +} + + +#define CMD_ROLLBACK "ROLLBACK TRANSACTION;" + +EXPORTED int caldav_abort(struct caldav_db *caldavdb) +{ + return dav_exec(caldavdb->db, CMD_ROLLBACK, NULL, NULL, NULL, + &caldavdb->stmt[STMT_ROLLBACK]); +} + + +struct read_rock { + struct caldav_db *db; + struct caldav_data *cdata; + int (*cb)(void *rock, void *data); + void *rock; +}; + +static const char *column_text_to_buf(const char *text, struct buf *buf) +{ + if (text) { + buf_setcstr(buf, text); + text = buf_cstring(buf); + } + + return text; +} + +static int read_cb(sqlite3_stmt *stmt, void *rock) +{ + struct read_rock *rrock = (struct read_rock *) rock; + struct caldav_db *db = rrock->db; + struct caldav_data *cdata = rrock->cdata; + unsigned flags; + int r = 0; + + memset(cdata, 0, sizeof(struct caldav_data)); + + cdata->dav.rowid = sqlite3_column_int(stmt, 0); + cdata->dav.creationdate = sqlite3_column_int(stmt, 1); + cdata->dav.imap_uid = sqlite3_column_int(stmt, 4); + cdata->dav.lock_expire = sqlite3_column_int(stmt, 8); + cdata->comp_type = sqlite3_column_int(stmt, 9); + flags = sqlite3_column_int(stmt, 14); + memcpy(&cdata->comp_flags, &flags, sizeof(struct comp_flags)); + + if (rrock->cb) { + /* We can use the column data directly for the callback */ + cdata->dav.mailbox = (const char *) sqlite3_column_text(stmt, 2); + cdata->dav.resource = (const char *) sqlite3_column_text(stmt, 3); + cdata->dav.lock_token = (const char *) sqlite3_column_text(stmt, 5); + cdata->dav.lock_owner = (const char *) sqlite3_column_text(stmt, 6); + cdata->dav.lock_ownerid = (const char *) sqlite3_column_text(stmt, 7); + cdata->ical_uid = (const char *) sqlite3_column_text(stmt, 10); + cdata->organizer = (const char *) sqlite3_column_text(stmt, 11); + cdata->dtstart = (const char *) sqlite3_column_text(stmt, 12); + cdata->dtend = (const char *) sqlite3_column_text(stmt, 13); + cdata->sched_tag = (const char *) sqlite3_column_text(stmt, 15); + r = rrock->cb(rrock->rock, cdata); + } + else { + /* For single row SELECTs like caldav_read(), + * we need to make a copy of the column data before + * it gets flushed by sqlite3_step() or sqlite3_reset() */ + cdata->dav.mailbox = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 2), + &db->mailbox); + cdata->dav.resource = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 3), + &db->resource); + cdata->dav.lock_token = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 5), + &db->lock_token); + cdata->dav.lock_owner = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 6), + &db->lock_owner); + cdata->dav.lock_ownerid = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 7), + &db->lock_ownerid); + cdata->ical_uid = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 10), + &db->ical_uid); + cdata->organizer = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 11), + &db->organizer); + cdata->dtstart = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 12), + &db->dtstart); + cdata->dtend = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 13), + &db->dtend); + cdata->sched_tag = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 15), + &db->sched_tag); + } + + return r; +} + + +#define CMD_SELRSRC \ + "SELECT rowid, creationdate, mailbox, resource, imap_uid," \ + " lock_token, lock_owner, lock_ownerid, lock_expire," \ + " comp_type, ical_uid, organizer, dtstart, dtend," \ + " comp_flags, sched_tag" \ + " FROM ical_objs" \ + " WHERE ( mailbox = :mailbox AND resource = :resource );" + +EXPORTED int caldav_lookup_resource(struct caldav_db *caldavdb, + const char *mailbox, const char *resource, + int lock, struct caldav_data **result) +{ + struct bind_val bval[] = { + { ":mailbox", SQLITE_TEXT, { .s = mailbox } }, + { ":resource", SQLITE_TEXT, { .s = resource } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + static struct caldav_data cdata; + struct read_rock rrock = { caldavdb, &cdata, NULL, NULL }; + int r; + + *result = memset(&cdata, 0, sizeof(struct caldav_data)); + + if (lock) { + /* begin a transaction */ + r = dav_exec(caldavdb->db, CMD_BEGIN, NULL, NULL, NULL, + &caldavdb->stmt[STMT_BEGIN]); + if (r) return r; + } + + r = dav_exec(caldavdb->db, CMD_SELRSRC, bval, &read_cb, &rrock, + &caldavdb->stmt[STMT_SELRSRC]); + if (!r && !cdata.dav.rowid) r = CYRUSDB_NOTFOUND; + + /* always add the mailbox and resource, so error responses don't + * crash out */ + cdata.dav.mailbox = mailbox; + cdata.dav.resource = resource; + + return r; +} + + +#define CMD_SELUID \ + "SELECT rowid, creationdate, mailbox, resource, imap_uid," \ + " lock_token, lock_owner, lock_ownerid, lock_expire," \ + " comp_type, ical_uid, organizer, dtstart, dtend," \ + " comp_flags, sched_tag" \ + " FROM ical_objs" \ + " WHERE ( ical_uid = :ical_uid AND mailbox != :inbox);" + +EXPORTED int caldav_lookup_uid(struct caldav_db *caldavdb, const char *ical_uid, + int lock, struct caldav_data **result) +{ + struct bind_val bval[] = { + { ":ical_uid", SQLITE_TEXT, { .s = ical_uid } }, + { ":inbox", SQLITE_TEXT, { .s = caldavdb->sched_inbox } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + static struct caldav_data cdata; + struct read_rock rrock = { caldavdb, &cdata, NULL, NULL }; + int r; + + *result = memset(&cdata, 0, sizeof(struct caldav_data)); + + if (lock) { + /* begin a transaction */ + r = dav_exec(caldavdb->db, CMD_BEGIN, NULL, NULL, NULL, + &caldavdb->stmt[STMT_BEGIN]); + if (r) return r; + } + + r = dav_exec(caldavdb->db, CMD_SELUID, bval, &read_cb, &rrock, + &caldavdb->stmt[STMT_SELUID]); + if (!r && !cdata.dav.rowid) r = CYRUSDB_NOTFOUND; + + return r; +} + + +#define CMD_SELMBOX \ + "SELECT rowid, creationdate, mailbox, resource, imap_uid," \ + " lock_token, lock_owner, lock_ownerid, lock_expire," \ + " comp_type, ical_uid, organizer, dtstart, dtend," \ + " comp_flags, sched_tag" \ + " FROM ical_objs WHERE mailbox = :mailbox;" + +EXPORTED int caldav_foreach(struct caldav_db *caldavdb, const char *mailbox, + int (*cb)(void *rock, void *data), + void *rock) +{ + struct bind_val bval[] = { + { ":mailbox", SQLITE_TEXT, { .s = mailbox } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + struct caldav_data cdata; + struct read_rock rrock = { caldavdb, &cdata, cb, rock }; + + return dav_exec(caldavdb->db, CMD_SELMBOX, bval, &read_cb, &rrock, + &caldavdb->stmt[STMT_SELMBOX]); +} + + +#define CMD_INSERT \ + "INSERT INTO ical_objs (" \ + " creationdate, mailbox, resource, imap_uid," \ + " lock_token, lock_owner, lock_ownerid, lock_expire," \ + " comp_type, ical_uid, organizer, dtstart, dtend," \ + " comp_flags, sched_tag )" \ + " VALUES (" \ + " :creationdate, :mailbox, :resource, :imap_uid," \ + " :lock_token, :lock_owner, :lock_ownerid, :lock_expire," \ + " :comp_type, :ical_uid, :organizer, :dtstart, :dtend," \ + " :comp_flags, :sched_tag );" + +#define CMD_UPDATE \ + "UPDATE ical_objs SET" \ + " imap_uid = :imap_uid," \ + " lock_token = :lock_token," \ + " lock_owner = :lock_owner," \ + " lock_ownerid = :lock_ownerid," \ + " lock_expire = :lock_expire," \ + " comp_type = :comp_type," \ + " ical_uid = :ical_uid," \ + " organizer = :organizer," \ + " dtstart = :dtstart," \ + " dtend = :dtend," \ + " comp_flags = :comp_flags," \ + " sched_tag = :sched_tag" \ + " WHERE rowid = :rowid;" + +EXPORTED int caldav_write(struct caldav_db *caldavdb, struct caldav_data *cdata, + int commit) +{ + struct bind_val bval[] = { + { ":imap_uid", SQLITE_INTEGER, { .i = cdata->dav.imap_uid } }, + { ":lock_token", SQLITE_TEXT, { .s = cdata->dav.lock_token } }, + { ":lock_owner", SQLITE_TEXT, { .s = cdata->dav.lock_owner } }, + { ":lock_ownerid", SQLITE_TEXT, { .s = cdata->dav.lock_ownerid } }, + { ":lock_expire", SQLITE_INTEGER, { .i = cdata->dav.lock_expire } }, + { ":comp_type", SQLITE_INTEGER, { .i = cdata->comp_type } }, + { ":ical_uid", SQLITE_TEXT, { .s = cdata->ical_uid } }, + { ":organizer", SQLITE_TEXT, { .s = cdata->organizer } }, + { ":dtstart", SQLITE_TEXT, { .s = cdata->dtstart } }, + { ":dtend", SQLITE_TEXT, { .s = cdata->dtend } }, + { ":sched_tag", SQLITE_TEXT, { .s = cdata->sched_tag } }, + { NULL, SQLITE_NULL, { .s = NULL } }, + { NULL, SQLITE_NULL, { .s = NULL } }, + { NULL, SQLITE_NULL, { .s = NULL } }, + { NULL, SQLITE_NULL, { .s = NULL } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + const char *cmd; + sqlite3_stmt **stmt; + unsigned flags; + int r; + + memcpy(&flags, &cdata->comp_flags, sizeof(struct comp_flags)); + bval[11].name = ":comp_flags"; + bval[11].type = SQLITE_INTEGER; + bval[11].val.i = flags; + + if (cdata->dav.rowid) { + cmd = CMD_UPDATE; + stmt = &caldavdb->stmt[STMT_UPDATE]; + + bval[12].name = ":rowid"; + bval[12].type = SQLITE_INTEGER; + bval[12].val.i = cdata->dav.rowid; + } + else { + cmd = CMD_INSERT; + stmt = &caldavdb->stmt[STMT_INSERT]; + + bval[12].name = ":creationdate"; + bval[12].type = SQLITE_INTEGER; + bval[12].val.i = cdata->dav.creationdate; + bval[13].name = ":mailbox"; + bval[13].type = SQLITE_TEXT; + bval[13].val.s = cdata->dav.mailbox; + bval[14].name = ":resource"; + bval[14].type = SQLITE_TEXT; + bval[14].val.s = cdata->dav.resource; + } + + r = dav_exec(caldavdb->db, cmd, bval, NULL, NULL, stmt); + + if (!r && commit) { + /* commit transaction */ + return dav_exec(caldavdb->db, CMD_COMMIT, NULL, NULL, NULL, + &caldavdb->stmt[STMT_COMMIT]); + } + + return r; +} + + +#define CMD_DELETE "DELETE FROM ical_objs WHERE rowid = :rowid;" + +EXPORTED int caldav_delete(struct caldav_db *caldavdb, unsigned rowid, int commit) +{ + struct bind_val bval[] = { + { ":rowid", SQLITE_INTEGER, { .i = rowid } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + int r; + + r = dav_exec(caldavdb->db, CMD_DELETE, bval, NULL, NULL, + &caldavdb->stmt[STMT_DELETE]); + + if (!r && commit) { + /* commit transaction */ + return dav_exec(caldavdb->db, CMD_COMMIT, NULL, NULL, NULL, + &caldavdb->stmt[STMT_COMMIT]); + } + + return r; +} + + +#define CMD_DELMBOX "DELETE FROM ical_objs WHERE mailbox = :mailbox;" + +EXPORTED int caldav_delmbox(struct caldav_db *caldavdb, const char *mailbox, int commit) +{ + struct bind_val bval[] = { + { ":mailbox", SQLITE_TEXT, { .s = mailbox } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + int r; + + r = dav_exec(caldavdb->db, CMD_DELMBOX, bval, NULL, NULL, + &caldavdb->stmt[STMT_DELMBOX]); + + if (!r && commit) { + /* commit transaction */ + return dav_exec(caldavdb->db, CMD_COMMIT, NULL, NULL, NULL, + &caldavdb->stmt[STMT_COMMIT]); + } + + return r; +} + + +/* Get time period (start/end) of a component based in RFC 4791 Sec 9.9 */ +static void get_period(icalcomponent *comp, icalcomponent_kind kind, + struct icalperiodtype *period) +{ + icaltimezone *utc = icaltimezone_get_utc_timezone(); + + period->start = + icaltime_convert_to_zone(icalcomponent_get_dtstart(comp), utc); + period->end = + icaltime_convert_to_zone(icalcomponent_get_dtend(comp), utc); + period->duration = icaldurationtype_null_duration(); + + switch (kind) { + case ICAL_VEVENT_COMPONENT: + if (icaltime_is_null_time(period->end)) { + /* No DTEND or DURATION */ + if (icaltime_is_date(period->start)) { + /* DTSTART is not DATE-TIME */ + struct icaldurationtype dur = icaldurationtype_null_duration(); + + dur.days = 1; + period->end = icaltime_add(period->start, dur); + } + else + memcpy(&period->end, &period->start, sizeof(struct icaltimetype)); + } + break; + +#ifdef HAVE_VPOLL + case ICAL_VPOLL_COMPONENT: +#endif + case ICAL_VTODO_COMPONENT: { + struct icaltimetype due = (kind == ICAL_VPOLL_COMPONENT) ? + icalcomponent_get_dtend(comp) : icalcomponent_get_due(comp); + + if (!icaltime_is_null_time(period->start)) { + /* Has DTSTART */ + if (icaltime_is_null_time(period->end)) { + /* No DURATION */ + memcpy(&period->end, &period->start, + sizeof(struct icaltimetype)); + + if (!icaltime_is_null_time(due)) { + /* Has DUE (DTEND for VPOLL) */ + if (icaltime_compare(due, period->start) < 0) + memcpy(&period->start, &due, sizeof(struct icaltimetype)); + if (icaltime_compare(due, period->end) > 0) + memcpy(&period->end, &due, sizeof(struct icaltimetype)); + } + } + } + else { + icalproperty *prop; + + /* No DTSTART */ + if (!icaltime_is_null_time(due)) { + /* Has DUE (DTEND for VPOLL) */ + memcpy(&period->start, &due, sizeof(struct icaltimetype)); + memcpy(&period->end, &due, sizeof(struct icaltimetype)); + } + else if ((prop = + icalcomponent_get_first_property(comp, + ICAL_COMPLETED_PROPERTY))) { + /* Has COMPLETED */ + period->start = + icaltime_convert_to_zone(icalproperty_get_completed(prop), + utc); + memcpy(&period->end, &period->start, sizeof(struct icaltimetype)); + + if ((prop = + icalcomponent_get_first_property(comp, + ICAL_CREATED_PROPERTY))) { + /* Has CREATED */ + struct icaltimetype created = + icaltime_convert_to_zone(icalproperty_get_created(prop), + utc); + if (icaltime_compare(created, period->start) < 0) + memcpy(&period->start, &created, sizeof(struct icaltimetype)); + if (icaltime_compare(created, period->end) > 0) + memcpy(&period->end, &created, sizeof(struct icaltimetype)); + } + } + else if ((prop = + icalcomponent_get_first_property(comp, + ICAL_CREATED_PROPERTY))) { + /* Has CREATED */ + period->start = + icaltime_convert_to_zone(icalproperty_get_created(prop), + utc); + period->end = + icaltime_from_timet_with_zone(caldav_eternity, 0, NULL); + } + else { + /* Always */ + period->start = + icaltime_from_timet_with_zone(caldav_epoch, 0, NULL); + period->end = + icaltime_from_timet_with_zone(caldav_eternity, 0, NULL); + } + } + break; + } + + case ICAL_VJOURNAL_COMPONENT: + if (!icaltime_is_null_time(period->start)) { + /* Has DTSTART */ + memcpy(&period->end, &period->start, + sizeof(struct icaltimetype)); + + if (icaltime_is_date(period->start)) { + /* DTSTART is not DATE-TIME */ + struct icaldurationtype dur; + + dur = icaldurationtype_from_int(60*60*24 - 1); /* P1D */ + icaltime_add(period->end, dur); + } + } + else { + /* Never */ + period->start = + icaltime_from_timet_with_zone(caldav_eternity, 0, NULL); + period->end = + icaltime_from_timet_with_zone(caldav_epoch, 0, NULL); + } + break; + + case ICAL_VFREEBUSY_COMPONENT: + if (icaltime_is_null_time(period->start) || + icaltime_is_null_time(period->end)) { + /* No DTSTART or DTEND */ + icalproperty *fb = + icalcomponent_get_first_property(comp, + ICAL_FREEBUSY_PROPERTY); + + + if (fb) { + /* Has FREEBUSY */ + /* XXX Convert FB period into our period */ + } + else { + /* Never */ + period->start = + icaltime_from_timet_with_zone(caldav_eternity, 0, NULL); + period->end = + icaltime_from_timet_with_zone(caldav_epoch, 0, NULL); + } + } + break; + + case ICAL_VAVAILABILITY_COMPONENT: + if (icaltime_is_null_time(period->start)) { + /* No DTSTART */ + period->start = + icaltime_from_timet_with_zone(caldav_epoch, 0, NULL); + } + if (icaltime_is_null_time(period->end)) { + /* No DTEND */ + period->end = + icaltime_from_timet_with_zone(caldav_eternity, 0, NULL); + } + break; + + default: + break; + } +} + + +/* icalcomponent_foreach_recurrence() callback to find earliest/latest time */ +static void recur_cb(icalcomponent *comp, struct icaltime_span *span, + void *rock) +{ + struct icalperiodtype *period = (struct icalperiodtype *) rock; + int is_date = icaltime_is_date(icalcomponent_get_dtstart(comp)); + icaltimezone *utc = icaltimezone_get_utc_timezone(); + struct icaltimetype start = + icaltime_from_timet_with_zone(span->start, is_date, utc); + struct icaltimetype end = + icaltime_from_timet_with_zone(span->end, is_date, utc); + + if (icaltime_compare(start, period->start) < 0) + memcpy(&period->start, &start, sizeof(struct icaltimetype)); + + if (icaltime_compare(end, period->end) > 0) + memcpy(&period->end, &end, sizeof(struct icaltimetype)); +} + + +EXPORTED void caldav_make_entry(icalcomponent *ical, struct caldav_data *cdata) +{ + icalcomponent *comp = icalcomponent_get_first_real_component(ical); + icalcomponent_kind kind; + icalproperty *prop; + unsigned mykind = 0, recurring = 0, transp = 0, status = 0; + struct icalperiodtype span; + + /* Get iCalendar UID */ + cdata->ical_uid = icalcomponent_get_uid(comp); + + /* Get component type and optional status */ + kind = icalcomponent_isa(comp); + switch (kind) { + case ICAL_VEVENT_COMPONENT: + mykind = CAL_COMP_VEVENT; + switch (icalcomponent_get_status(comp)) { + case ICAL_STATUS_CANCELLED: status = CAL_STATUS_CANCELED; break; + case ICAL_STATUS_TENTATIVE: status = CAL_STATUS_TENTATIVE; break; + default: status = CAL_STATUS_BUSY; break; + } + break; + case ICAL_VTODO_COMPONENT: mykind = CAL_COMP_VTODO; break; + case ICAL_VJOURNAL_COMPONENT: mykind = CAL_COMP_VJOURNAL; break; + case ICAL_VFREEBUSY_COMPONENT: mykind = CAL_COMP_VFREEBUSY; break; + case ICAL_VAVAILABILITY_COMPONENT: mykind = CAL_COMP_VAVAILABILITY; break; +#ifdef HAVE_VPOLL + case ICAL_VPOLL_COMPONENT: mykind = CAL_COMP_VPOLL; break; +#endif + default: break; + } + cdata->comp_type = mykind; + cdata->comp_flags.status = status; + + /* Get organizer */ + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + if (prop) cdata->organizer = icalproperty_get_organizer(prop)+7; + else cdata->organizer = NULL; + + /* Get transparency */ + prop = icalcomponent_get_first_property(comp, ICAL_TRANSP_PROPERTY); + if (prop) { + icalvalue *transp_val = icalproperty_get_value(prop); + + switch (icalvalue_get_transp(transp_val)) { + case ICAL_TRANSP_TRANSPARENT: + case ICAL_TRANSP_TRANSPARENTNOCONFLICT: + transp = 1; + break; + + default: + transp = 0; + break; + } + } + cdata->comp_flags.transp = transp; + + /* Initialize span to be nothing */ + span.start = icaltime_from_timet_with_zone(caldav_eternity, 0, NULL); + span.end = icaltime_from_timet_with_zone(caldav_epoch, 0, NULL); + span.duration = icaldurationtype_null_duration(); + + do { + struct icalperiodtype period; + icalproperty *rrule; + + /* Get base dtstart and dtend */ + get_period(comp, kind, &period); + + /* See if its a recurring event */ + rrule = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY); + if (rrule || + icalcomponent_get_first_property(comp, ICAL_RDATE_PROPERTY) || + icalcomponent_get_first_property(comp, ICAL_EXDATE_PROPERTY)) { + /* Recurring - find widest time range that includes events */ + int expand = recurring = 1; + + if (rrule) { + struct icalrecurrencetype recur = icalproperty_get_rrule(rrule); + + if (!icaltime_is_null_time(recur.until)) { + /* Recurrence ends - calculate dtend of last recurrence */ + struct icaldurationtype duration; + icaltimezone *utc = icaltimezone_get_utc_timezone(); + + duration = icaltime_subtract(period.end, period.start); + period.end = + icaltime_add(icaltime_convert_to_zone(recur.until, utc), + duration); + + /* Do RDATE expansion only */ + /* XXX This is destructive but currently doesn't matter */ + icalcomponent_remove_property(comp, rrule); + free(rrule); + } + else if (!recur.count) { + /* Recurrence never ends - set end of span to eternity */ + span.end = + icaltime_from_timet_with_zone(caldav_eternity, 0, NULL); + + /* Skip RRULE & RDATE expansion */ + expand = 0; + } + } + + /* Expand (remaining) recurrences */ + if (expand) { + icalcomponent_foreach_recurrence( + comp, + icaltime_from_timet_with_zone(caldav_epoch, 0, NULL), + icaltime_from_timet_with_zone(caldav_eternity, 0, NULL), + recur_cb, &span); + } + } + + /* Check our dtstart and dtend against span */ + if (icaltime_compare(period.start, span.start) < 0) + memcpy(&span.start, &period.start, sizeof(struct icaltimetype)); + + if (icaltime_compare(period.end, span.end) > 0) + memcpy(&span.end, &period.end, sizeof(struct icaltimetype)); + + } while ((comp = icalcomponent_get_next_component(ical, kind))); + + cdata->dtstart = icaltime_as_ical_string(span.start); + cdata->dtend = icaltime_as_ical_string(span.end); + cdata->comp_flags.recurring = recurring; +} + + +EXPORTED const char *caldav_mboxname(const char *userid, const char *name) +{ + struct buf boxbuf = BUF_INITIALIZER; + const char *res = NULL; + + buf_setcstr(&boxbuf, config_getstring(IMAPOPT_CALENDARPREFIX)); + + if (name) { + size_t len = strcspn(name, "/"); + buf_putc(&boxbuf, '.'); + buf_appendmap(&boxbuf, name, len); + } + + res = mboxname_user_mbox(userid, buf_cstring(&boxbuf)); + + buf_free(&boxbuf); + + return res; +}
View file
cyrus-imapd-2.5.tar.gz/imap/caldav_db.h
Added
@@ -0,0 +1,170 @@ +/* caldav_db.h -- abstract interface for per-user CalDAV database + * + * Copyright (c) 1994-2012 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 CALDAV_DB_H +#define CALDAV_DB_H + +#include <config.h> + +extern time_t caldav_epoch; +extern time_t caldav_eternity; + +#include <libical/ical.h> + +#include "dav_db.h" + +#ifndef HAVE_VAVAILABILITY +/* Allow us to compile without #ifdef HAVE_VAVAILABILITY everywhere */ +#define ICAL_VAVAILABILITY_COMPONENT ICAL_X_COMPONENT +#define ICAL_XAVAILABLE_COMPONENT ICAL_X_COMPONENT +#endif + +#ifndef HAVE_VPOLL +/* Allow us to compile without #ifdef HAVE_VPOLL everywhere */ +#define ICAL_VPOLL_COMPONENT ICAL_NO_COMPONENT +#define ICAL_METHOD_POLLSTATUS ICAL_METHOD_NONE +#define ICAL_VOTER_PROPERTY ICAL_NO_PROPERTY +#define icalproperty_get_voter icalproperty_get_attendee +#endif + +/* Bitmask of calendar components */ +enum { + /* "Real" components - MUST remain in this order (values used in DAV DB) */ + CAL_COMP_VEVENT = (1<<0), + CAL_COMP_VTODO = (1<<1), + CAL_COMP_VJOURNAL = (1<<2), + CAL_COMP_VFREEBUSY = (1<<3), + CAL_COMP_VAVAILABILITY = (1<<4), + CAL_COMP_VPOLL = (1<<5), + /* Append additional "real" components here */ + + /* Other components - values don't matter - prepend here */ + CAL_COMP_VALARM = (1<<13), + CAL_COMP_VTIMEZONE = (1<<14), + CAL_COMP_VCALENDAR = (1<<15) +}; + +struct caldav_db; + +#define CALDAV_CREATE 0x01 +#define CALDAV_TRUNC 0x02 + +struct comp_flags { + unsigned recurring : 1; + unsigned transp : 1; + unsigned status : 2; +}; + +/* Status values */ +enum { + CAL_STATUS_BUSY = 0, + CAL_STATUS_CANCELED, + CAL_STATUS_TENTATIVE, + CAL_STATUS_UNAVAILABLE +}; + +struct caldav_data { + struct dav_data dav; /* MUST be first so we can typecast */ + unsigned comp_type; + const char *ical_uid; + const char *organizer; + const char *dtstart; + const char *dtend; + struct comp_flags comp_flags; + const char *sched_tag; +}; + +/* prepare for caldav operations in this process */ +int caldav_init(void); + +/* done with all caldav operations for this process */ +int caldav_done(void); + +/* get a database handle corresponding to mailbox */ +struct caldav_db *caldav_open_mailbox(struct mailbox *mailbox, int flags); +struct caldav_db *caldav_open_userid(const char *userid, int flags); + +/* close this handle */ +int caldav_close(struct caldav_db *caldavdb); + +/* lookup an entry from 'caldavdb' by resource + (optionally inside a transaction for updates) */ +int caldav_lookup_resource(struct caldav_db *caldavdb, + const char *mailbox, const char *resource, + int lock, struct caldav_data **result); + +/* lookup an entry from 'caldavdb' by iCal UID + (optionally inside a transaction for updates) */ +int caldav_lookup_uid(struct caldav_db *caldavdb, const char *ical_uid, + int lock, struct caldav_data **result); + +/* process each entry for 'mailbox' in 'caldavdb' with cb() */ +int caldav_foreach(struct caldav_db *caldavdb, const char *mailbox, + int (*cb)(void *rock, void *data), + void *rock); + +/* write an entry to 'caldavdb' */ +int caldav_write(struct caldav_db *caldavdb, struct caldav_data *cdata, + int commit); + +/* delete an entry from 'caldavdb' */ +int caldav_delete(struct caldav_db *caldavdb, unsigned rowid, int commit); + +/* delete all entries for 'mailbox' from 'caldavdb' */ +int caldav_delmbox(struct caldav_db *caldavdb, const char *mailbox, int commit); + +/* begin transaction */ +int caldav_begin(struct caldav_db *caldavdb); + +/* commit transaction */ +int caldav_commit(struct caldav_db *caldavdb); + +/* abort transaction */ +int caldav_abort(struct caldav_db *caldavdb); + +/* create caldav_data from icalcomponent */ +void caldav_make_entry(icalcomponent *ical, struct caldav_data *cdata); + +const char *caldav_mboxname(const char *userid, const char *name); + +#endif /* CALDAV_DB_H */
View file
cyrus-imapd-2.5.tar.gz/imap/carddav_db.c
Added
@@ -0,0 +1,637 @@ +/* carddav_db.c -- implementation of per-user CardDAV database + * + * Copyright (c) 1994-2013 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 <syslog.h> +#include <string.h> + +#include "carddav_db.h" +#include "cyrusdb.h" +#include "httpd.h" +#include "http_dav.h" +#include "libconfig.h" +#include "util.h" +#include "xstrlcat.h" +#include "xmalloc.h" + + +enum { + STMT_SELRSRC, + STMT_SELUID, + STMT_SELMBOX, + STMT_INSERT, + STMT_DELETE, + STMT_DELMBOX, + STMT_BEGIN, + STMT_COMMIT, + STMT_ROLLBACK, + STMT_INSERT_EMAIL, + STMT_INSERT_GROUP, + STMT_GETEMAIL, + STMT_GETGROUP, + NUM_STMT +}; + +#define NUM_BUFS 10 + +struct carddav_db { + sqlite3 *db; /* DB handle */ + sqlite3_stmt *stmt[NUM_STMT]; /* prepared statements */ + struct buf bufs[NUM_BUFS]; /* buffers for copies of column text */ +}; + + +EXPORTED int carddav_init(void) +{ + return dav_init(); +} + + +EXPORTED int carddav_done(void) +{ + return dav_done(); +} + + +#define CMD_DROP_OBJ "DROP TABLE IF EXISTS vcard_objs;" + +#define CMD_CREATE_OBJ \ + "CREATE TABLE IF NOT EXISTS vcard_objs (" \ + " rowid INTEGER PRIMARY KEY," \ + " creationdate INTEGER," \ + " mailbox TEXT NOT NULL," \ + " resource TEXT NOT NULL," \ + " imap_uid INTEGER," \ + " lock_token TEXT," \ + " lock_owner TEXT," \ + " lock_ownerid TEXT," \ + " lock_expire INTEGER," \ + " version INTEGER," \ + " vcard_uid TEXT," \ + " kind INTEGER," \ + " fullname TEXT," \ + " name TEXT," \ + " nickname TEXT," \ + " UNIQUE( mailbox, resource ) );" \ + "CREATE INDEX IF NOT EXISTS idx_vcard_fn ON vcard_objs ( fullname );" \ + "CREATE INDEX IF NOT EXISTS idx_vcard_uid ON vcard_objs ( vcard_uid );" + +#define CMD_DROP_EM "DROP TABLE IF EXISTS vcard_emails;" + +#define CMD_CREATE_EM \ + "CREATE TABLE IF NOT EXISTS vcard_emails (" \ + " rowid INTEGER PRIMARY KEY," \ + " objid INTEGER," \ + " pos INTEGER NOT NULL," /* for sorting */ \ + " email TEXT NOT NULL," \ + " FOREIGN KEY (objid) REFERENCES vcard_objs (rowid) ON DELETE CASCADE );" \ + "CREATE INDEX IF NOT EXISTS idx_vcard_email ON vcard_emails ( email );" + +#define CMD_DROP_GR "DROP TABLE IF EXISTS vcard_groups;" + +#define CMD_CREATE_GR \ + "CREATE TABLE IF NOT EXISTS vcard_groups (" \ + " rowid INTEGER PRIMARY KEY," \ + " objid INTEGER," \ + " pos INTEGER NOT NULL," /* for sorting */ \ + " member_uid TEXT NOT NULL," \ + " FOREIGN KEY (objid) REFERENCES vcard_objs (rowid) ON DELETE CASCADE );" + +#define CMD_DROP CMD_DROP_OBJ CMD_DROP_EM CMD_DROP_GR +#define CMD_CREATE CMD_CREATE_OBJ CMD_CREATE_EM CMD_CREATE_GR + +/* Open DAV DB corresponding to mailbox */ +static struct carddav_db *carddav_open_fname(const char *fname, int flags) +{ + sqlite3 *db; + struct carddav_db *carddavdb = NULL; + const char *cmds = CMD_CREATE; + + if (flags & CARDDAV_TRUNC) cmds = CMD_DROP CMD_CREATE; + + db = dav_open(fname, cmds); + + if (db) { + carddavdb = xzmalloc(sizeof(struct carddav_db)); + carddavdb->db = db; + } + + return carddavdb; +} + +EXPORTED struct carddav_db *carddav_open_userid(const char *userid, int flags) +{ + struct buf fname = BUF_INITIALIZER; + struct carddav_db *carddavdb = NULL; + + dav_getpath_byuserid(&fname, userid); + carddavdb = carddav_open_fname(buf_cstring(&fname), flags); + buf_free(&fname); + + return carddavdb; +} + +EXPORTED struct carddav_db *carddav_open_mailbox(struct mailbox *mailbox, int flags) +{ + struct buf fname = BUF_INITIALIZER; + struct carddav_db *carddavdb = NULL; + + dav_getpath(&fname, mailbox); + carddavdb = carddav_open_fname(buf_cstring(&fname), flags); + buf_free(&fname); + + return carddavdb; +} + + +/* Close DAV DB */ +EXPORTED int carddav_close(struct carddav_db *carddavdb) +{ + int i, r = 0; + + if (!carddavdb) return 0; + + for (i = 0; i < NUM_BUFS; i++) { + buf_free(&carddavdb->bufs[i]); + } + + for (i = 0; i < NUM_STMT; i++) { + sqlite3_stmt *stmt = carddavdb->stmt[i]; + if (stmt) sqlite3_finalize(stmt); + } + + r = dav_close(carddavdb->db); + + free(carddavdb); + + return r; +} + + +#define CMD_BEGIN "BEGIN TRANSACTION;" + +EXPORTED int carddav_begin(struct carddav_db *carddavdb) +{ + return dav_exec(carddavdb->db, CMD_BEGIN, NULL, NULL, NULL, + &carddavdb->stmt[STMT_BEGIN]); +} + + +#define CMD_COMMIT "COMMIT TRANSACTION;" + +EXPORTED int carddav_commit(struct carddav_db *carddavdb) +{ + return dav_exec(carddavdb->db, CMD_COMMIT, NULL, NULL, NULL, + &carddavdb->stmt[STMT_COMMIT]); +} + + +#define CMD_ROLLBACK "ROLLBACK TRANSACTION;" + +EXPORTED int carddav_abort(struct carddav_db *carddavdb) +{ + return dav_exec(carddavdb->db, CMD_ROLLBACK, NULL, NULL, NULL, + &carddavdb->stmt[STMT_ROLLBACK]); +} + + +struct read_rock { + struct carddav_db *db; + struct carddav_data *cdata; + int (*cb)(void *rock, void *data); + void *rock; +}; + +static const char *column_text_to_buf(const char *text, struct buf *buf) +{ + if (text) { + buf_setcstr(buf, text); + text = buf_cstring(buf); + } + + return text; +} + +static int read_cb(sqlite3_stmt *stmt, void *rock) +{ + struct read_rock *rrock = (struct read_rock *) rock; + struct carddav_db *db = rrock->db; + struct carddav_data *cdata = rrock->cdata; + int r = 0; + + memset(cdata, 0, sizeof(struct carddav_data)); + + cdata->dav.rowid = sqlite3_column_int(stmt, 0); + cdata->dav.creationdate = sqlite3_column_int(stmt, 1); + cdata->dav.imap_uid = sqlite3_column_int(stmt, 4); + cdata->dav.lock_expire = sqlite3_column_int(stmt, 8); + cdata->version = sqlite3_column_int(stmt, 9); + cdata->kind = sqlite3_column_int(stmt, 11); + + if (rrock->cb) { + /* We can use the column data directly for the callback */ + cdata->dav.mailbox = (const char *) sqlite3_column_text(stmt, 2); + cdata->dav.resource = (const char *) sqlite3_column_text(stmt, 3); + cdata->dav.lock_token = (const char *) sqlite3_column_text(stmt, 5); + cdata->dav.lock_owner = (const char *) sqlite3_column_text(stmt, 6); + cdata->dav.lock_ownerid = (const char *) sqlite3_column_text(stmt, 7); + cdata->vcard_uid = (const char *) sqlite3_column_text(stmt, 10); + cdata->fullname = (const char *) sqlite3_column_text(stmt, 12); + cdata->name = (const char *) sqlite3_column_text(stmt, 13); + cdata->nickname = (const char *) sqlite3_column_text(stmt, 14); + r = rrock->cb(rrock->rock, cdata); + } + else { + /* For single row SELECTs like carddav_read(), + * we need to make a copy of the column data before + * it gets flushed by sqlite3_step() or sqlite3_reset() */ + cdata->dav.mailbox = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 2), + &db->bufs[0]); + cdata->dav.resource = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 3), + &db->bufs[1]); + cdata->dav.lock_token = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 5), + &db->bufs[2]); + cdata->dav.lock_owner = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 6), + &db->bufs[3]); + cdata->dav.lock_ownerid = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 7), + &db->bufs[4]); + cdata->vcard_uid = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 10), + &db->bufs[5]); + cdata->fullname = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 12), + &db->bufs[6]); + cdata->name = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 13), + &db->bufs[7]); + cdata->nickname = + column_text_to_buf((const char *) sqlite3_column_text(stmt, 14), + &db->bufs[8]); + } + + return r; +} + + +#define CMD_SELRSRC \ + "SELECT rowid, creationdate, mailbox, resource, imap_uid," \ + " lock_token, lock_owner, lock_ownerid, lock_expire," \ + " version, vcard_uid, kind, fullname, name, nickname" \ + " FROM vcard_objs" \ + " WHERE ( mailbox = :mailbox AND resource = :resource );" + +EXPORTED int carddav_lookup_resource(struct carddav_db *carddavdb, + const char *mailbox, const char *resource, + int lock, struct carddav_data **result) +{ + struct bind_val bval[] = { + { ":mailbox", SQLITE_TEXT, { .s = mailbox } }, + { ":resource", SQLITE_TEXT, { .s = resource } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + static struct carddav_data cdata; + struct read_rock rrock = { carddavdb, &cdata, NULL, NULL }; + int r; + + *result = memset(&cdata, 0, sizeof(struct carddav_data)); + + if (lock) { + /* begin a transaction */ + r = dav_exec(carddavdb->db, CMD_BEGIN, NULL, NULL, NULL, + &carddavdb->stmt[STMT_BEGIN]); + if (r) return r; + } + + r = dav_exec(carddavdb->db, CMD_SELRSRC, bval, &read_cb, &rrock, + &carddavdb->stmt[STMT_SELRSRC]); + if (!r && !cdata.dav.rowid) r = CYRUSDB_NOTFOUND; + + /* always mailbox and resource so error paths don't fail */ + cdata.dav.mailbox = mailbox; + cdata.dav.resource = resource; + + return r; +} + + +#define CMD_SELUID \ + "SELECT rowid, creationdate, mailbox, resource, imap_uid," \ + " lock_token, lock_owner, lock_ownerid, lock_expire," \ + " version, vcard_uid, kind, fullname, name, nickname" \ + " FROM vcard_objs" \ + " WHERE ( vcard_uid = :vcard_uid );" + +EXPORTED int carddav_lookup_uid(struct carddav_db *carddavdb, const char *vcard_uid, + int lock, struct carddav_data **result) +{ + struct bind_val bval[] = { + { ":vcard_uid", SQLITE_TEXT, { .s = vcard_uid } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + static struct carddav_data cdata; + struct read_rock rrock = { carddavdb, &cdata, NULL, NULL }; + int r; + + *result = memset(&cdata, 0, sizeof(struct carddav_data)); + + if (lock) { + /* begin a transaction */ + r = dav_exec(carddavdb->db, CMD_BEGIN, NULL, NULL, NULL, + &carddavdb->stmt[STMT_BEGIN]); + if (r) return r; + } + + r = dav_exec(carddavdb->db, CMD_SELUID, bval, &read_cb, &rrock, + &carddavdb->stmt[STMT_SELUID]); + if (!r && !cdata.dav.rowid) r = CYRUSDB_NOTFOUND; + + return r; +} + + +#define CMD_SELMBOX \ + "SELECT rowid, creationdate, mailbox, resource, imap_uid," \ + " lock_token, lock_owner, lock_ownerid, lock_expire," \ + " version, vcard_uid, kind, fullname, name, nickname" \ + " FROM vcard_objs WHERE mailbox = :mailbox;" + +EXPORTED int carddav_foreach(struct carddav_db *carddavdb, const char *mailbox, + int (*cb)(void *rock, void *data), + void *rock) +{ + struct bind_val bval[] = { + { ":mailbox", SQLITE_TEXT, { .s = mailbox } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + struct carddav_data cdata; + struct read_rock rrock = { carddavdb, &cdata, cb, rock }; + + return dav_exec(carddavdb->db, CMD_SELMBOX, bval, &read_cb, &rrock, + &carddavdb->stmt[STMT_SELMBOX]); +} + +#define CMD_GETEMAIL \ + "SELECT rowid" \ + " FROM vcard_emails" \ + " WHERE ( email = :email );" + +static int foundemail_cb(sqlite3_stmt *stmt, void *rock) +{ + int *foundp = (int *)rock; + if (sqlite3_column_int(stmt, 0)) + *foundp = 1; + return 0; +} + +EXPORTED int carddav_getemail(struct carddav_db *carddavdb, const char *email) +{ + struct bind_val bval[] = { + { ":email", SQLITE_TEXT, { .s = email } }, + { NULL, SQLITE_NULL, { .s = NULL } } + }; + int found = 0; + int r; + + r = dav_exec(carddavdb->db, CMD_GETEMAIL, bval, &foundemail_cb, &found, + &carddavdb->stmt[STMT_GETEMAIL]); + if (r) { + /* XXX syslog */ + } + + return found; +} + +#define CMD_GETGROUP \ + "SELECT E.email FROM vcard_emails E" \ + " JOIN vcard_objs CO JOIN vcard_groups G JOIN vcard_objs GO" \ + " WHERE E.objid = CO.rowid AND CO.vcard_uid = G.member_uid AND G.objid = GO.rowid" \ + " AND E.pos = 0 AND GO.fullname = :group" + +static int foundgroup_cb(sqlite3_stmt *stmt, void *rock) +{ + strarray_t *array = (strarray_t *)rock; + strarray_add(array, (const char *)sqlite3_column_text(stmt, 0)); + return 0; +} + +EXPORTED strarray_t *carddav_getgroup(struct carddav_db *carddavdb, const char *group) +{ + struct bind_val bval[] = { + { ":group", SQLITE_TEXT, { .s = group } }, + { NULL, SQLITE_NULL, { .s = NULL } } + }; + strarray_t *found = strarray_new(); + int r; + + r = dav_exec(carddavdb->db, CMD_GETGROUP, bval, &foundgroup_cb, found, + &carddavdb->stmt[STMT_GETGROUP]); + if (r) { + /* XXX syslog */ + } + + if (!strarray_size(found)) { + strarray_free(found); + return NULL; + } + + return found; +} + + +#define CMD_INSERT_EMAIL \ + "INSERT INTO vcard_emails ( objid, pos, email )" \ + " VALUES ( :objid, :pos, :email );" + +/* no commit */ +static int carddav_write_emails(struct carddav_db *carddavdb, struct carddav_data *cdata) +{ + struct bind_val bval[] = { + { ":objid", SQLITE_INTEGER, { .i = cdata->dav.rowid } }, + { ":pos", SQLITE_INTEGER, { .i = 0 } }, + { ":email", SQLITE_TEXT, { .s = NULL } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + int r; + int i; + + for (i = 0; i < strarray_size(&cdata->emails); i++) { + bval[1].val.i = i; + bval[2].val.s = strarray_nth(&cdata->emails, i); + r = dav_exec(carddavdb->db, CMD_INSERT_EMAIL, bval, NULL, NULL, + &carddavdb->stmt[STMT_INSERT_EMAIL]); + if (r) return r; + } + + return 0; +} + +#define CMD_INSERT_GROUP \ + "INSERT INTO vcard_groups ( objid, pos, member_uid )" \ + " VALUES ( :objid, :pos, :member_uid );" + +/* no commit */ +static int carddav_write_groups(struct carddav_db *carddavdb, struct carddav_data *cdata) +{ + struct bind_val bval[] = { + { ":objid", SQLITE_INTEGER, { .i = cdata->dav.rowid } }, + { ":pos", SQLITE_INTEGER, { .i = 0 } }, + { ":member_uid", SQLITE_TEXT, { .s = NULL } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + int r; + int i; + + for (i = 0; i < strarray_size(&cdata->member_uids); i++) { + bval[1].val.i = i; + bval[2].val.s = strarray_nth(&cdata->member_uids, i); + r = dav_exec(carddavdb->db, CMD_INSERT_GROUP, bval, NULL, NULL, + &carddavdb->stmt[STMT_INSERT_GROUP]); + if (r) return r; + } + + return 0; +} + +#define CMD_INSERT \ + "INSERT INTO vcard_objs (" \ + " creationdate, mailbox, resource, imap_uid," \ + " lock_token, lock_owner, lock_ownerid, lock_expire," \ + " version, vcard_uid, kind, fullname, name, nickname)" \ + " VALUES (" \ + " :creationdate, :mailbox, :resource, :imap_uid," \ + " :lock_token, :lock_owner, :lock_ownerid, :lock_expire," \ + " :version, :vcard_uid, :kind, :fullname, :name, :nickname );" + +EXPORTED int carddav_write(struct carddav_db *carddavdb, struct carddav_data *cdata, + int commit) +{ + struct bind_val bval[] = { + { ":creationdate", SQLITE_INTEGER, { .i = cdata->dav.creationdate } }, + { ":mailbox", SQLITE_TEXT, { .s = cdata->dav.mailbox } }, + { ":resource", SQLITE_TEXT, { .s = cdata->dav.resource } }, + { ":imap_uid", SQLITE_INTEGER, { .i = cdata->dav.imap_uid } }, + { ":lock_token", SQLITE_TEXT, { .s = cdata->dav.lock_token } }, + { ":lock_owner", SQLITE_TEXT, { .s = cdata->dav.lock_owner } }, + { ":lock_ownerid", SQLITE_TEXT, { .s = cdata->dav.lock_ownerid } }, + { ":lock_expire", SQLITE_INTEGER, { .i = cdata->dav.lock_expire } }, + { ":version", SQLITE_INTEGER, { .i = cdata->version } }, + { ":vcard_uid", SQLITE_TEXT, { .s = cdata->vcard_uid } }, + { ":kind", SQLITE_INTEGER, { .i = cdata->kind } }, + { ":fullname", SQLITE_TEXT, { .s = cdata->fullname } }, + { ":name", SQLITE_TEXT, { .s = cdata->name } }, + { ":nickname", SQLITE_TEXT, { .s = cdata->nickname } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + int r; + + if (cdata->dav.rowid) { + r = carddav_delete(carddavdb, cdata->dav.rowid, /*commit*/0); + if (r) return r; + } + + r = dav_exec(carddavdb->db, CMD_INSERT, bval, NULL, NULL, + &carddavdb->stmt[STMT_INSERT]); + if (r) return r; + + cdata->dav.rowid = sqlite3_last_insert_rowid(carddavdb->db); + + r = carddav_write_emails(carddavdb, cdata); + if (r) return r; + + r = carddav_write_groups(carddavdb, cdata); + if (r) return r; + + /* commit transaction */ + if (commit) { + r = carddav_commit(carddavdb); + if (r) return r; + } + + return 0; +} + + +#define CMD_DELETE "DELETE FROM vcard_objs WHERE rowid = :rowid;" + +EXPORTED int carddav_delete(struct carddav_db *carddavdb, unsigned rowid, int commit) +{ + struct bind_val bval[] = { + { ":rowid", SQLITE_INTEGER, { .i = rowid } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + int r; + + r = dav_exec(carddavdb->db, CMD_DELETE, bval, NULL, NULL, + &carddavdb->stmt[STMT_DELETE]); + if (r) return r; + + /* commit transaction */ + if (commit) { + r = carddav_commit(carddavdb); + if (r) return r; + } + + return 0; +} + + +#define CMD_DELMBOX "DELETE FROM vcard_objs WHERE mailbox = :mailbox;" + +EXPORTED int carddav_delmbox(struct carddav_db *carddavdb, const char *mailbox, int commit) +{ + struct bind_val bval[] = { + { ":mailbox", SQLITE_TEXT, { .s = mailbox } }, + { NULL, SQLITE_NULL, { .s = NULL } } }; + int r; + + r = dav_exec(carddavdb->db, CMD_DELMBOX, bval, NULL, NULL, + &carddavdb->stmt[STMT_DELMBOX]); + if (r) return r; + + /* commit transaction */ + if (commit) { + r = carddav_commit(carddavdb); + if (r) return r; + } + + return 0; +}
View file
cyrus-imapd-2.5.tar.gz/imap/carddav_db.h
Added
@@ -0,0 +1,121 @@ +/* carddav_db.h -- abstract interface for per-user CardDAV database + * + * Copyright (c) 1994-2013 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 CARDDAV_DB_H +#define CARDDAV_DB_H + +#include <config.h> + +#include "dav_db.h" +#include "strarray.h" + +struct carddav_db; + +#define CARDDAV_CREATE 0x01 +#define CARDDAV_TRUNC 0x02 + +struct carddav_data { + struct dav_data dav; /* MUST be first so we can typecast */ + unsigned version; + const char *vcard_uid; + unsigned kind; + const char *fullname; + const char *name; + const char *nickname; + strarray_t emails; + strarray_t member_uids; +}; + +/* prepare for carddav operations in this process */ +int carddav_init(void); + +/* done with all carddav operations for this process */ +int carddav_done(void); + +/* get a database handle corresponding to mailbox */ +struct carddav_db *carddav_open_mailbox(struct mailbox *mailbox, int flags); +struct carddav_db *carddav_open_userid(const char *userid, int flags); + +/* close this handle */ +int carddav_close(struct carddav_db *carddavdb); + +/* lookup an entry from 'carddavdb' by resource + (optionally inside a transaction for updates) */ +int carddav_lookup_resource(struct carddav_db *carddavdb, + const char *mailbox, const char *resource, + int lock, struct carddav_data **result); + +/* lookup an entry from 'carddavdb' by iCal UID + (optionally inside a transaction for updates) */ +int carddav_lookup_uid(struct carddav_db *carddavdb, const char *ical_uid, + int lock, struct carddav_data **result); + +/* check if an email address exists on any card */ +int carddav_getemail(struct carddav_db *carddavdb, const char *key); +strarray_t *carddav_getgroup(struct carddav_db *carddavdb, const char *key); + +/* process each entry for 'mailbox' in 'carddavdb' with cb() */ +int carddav_foreach(struct carddav_db *carddavdb, const char *mailbox, + int (*cb)(void *rock, void *data), + void *rock); + +/* write an entry to 'carddavdb' */ +int carddav_write(struct carddav_db *carddavdb, struct carddav_data *cdata, + int commit); + +/* delete an entry from 'carddavdb' */ +int carddav_delete(struct carddav_db *carddavdb, unsigned rowid, int commit); + +/* delete all entries for 'mailbox' from 'carddavdb' */ +int carddav_delmbox(struct carddav_db *carddavdb, const char *mailbox, int commit); + +/* begin transaction */ +int carddav_begin(struct carddav_db *carddavdb); + +/* commit transaction */ +int carddav_commit(struct carddav_db *carddavdb); + +/* abort transaction */ +int carddav_abort(struct carddav_db *carddavdb); + +#endif /* CARDDAV_DB_H */
View file
cyrus-imapd-2.5.tar.gz/imap/ctl_cyrusdb.c
Changed
@@ -134,7 +134,7 @@ if (userid) { struct buf buf = BUF_INITIALIZER; buf_setcstr(&buf, mbentry->legacy_specialuse); - annotatemore_write(name, "/specialuse", userid, &buf); + annotatemore_rawwrite(name, "/specialuse", userid, &buf); buf_free(&buf); } free(mbentry->legacy_specialuse);
View file
cyrus-imapd-2.5.tar.gz/imap/ctl_mboxlist.c
Changed
@@ -903,7 +903,8 @@ static int verify_cb(void *rockp, const char *key, size_t keylen, - const char *data, size_t datalen) + const char *data __attribute__((unused)), + size_t dataleni __attribute__((unused))) { // This function is called for every entry in the database, // and supplied an inventory in &found. *data however does
View file
cyrus-imapd-2.5.tar.gz/imap/ctl_zoneinfo.c
Added
@@ -0,0 +1,312 @@ +/* ctl_zoneinfo.c -- Program to perform operations on zoneinfo db + * + * Copyright (c) 1994-2013 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 <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <dirent.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <libical/ical.h> + +#include "annotate.h" /* for strlist functionality */ +#include "global.h" +#include "hash.h" +#include "map.h" +#include "util.h" +#include "xmalloc.h" +#include "zoneinfo_db.h" + +extern int optind; +extern char *optarg; + +/* config.c stuff */ +const int config_need_data = 0; + +int verbose = 0; + +/* forward declarations */ +void usage(void); +void free_zoneinfo(void *data); +void store_zoneinfo(const char *tzid, void *data, void *rock); +void do_zonedir(const char *prefix, struct hash_table *tzentries, + struct zoneinfo *info); +void shut_down(int code); + + +int main(int argc, char **argv) +{ + int opt, r = 0; + char *alt_config = NULL, *version = NULL; + enum { REBUILD, NONE } op = NONE; + + if ((geteuid()) == 0 && (become_cyrus() != 0)) { + fatal("must run as the Cyrus user", EC_USAGE); + } + + while ((opt = getopt(argc, argv, "C:r:v")) != EOF) { + switch (opt) { + case 'C': /* alt config file */ + alt_config = optarg; + break; + + case 'r': + if (op == NONE) { + op = REBUILD; + version = optarg; + } + else usage(); + break; + + case 'v': + verbose = 1; + break; + + default: + usage(); + } + } + + cyrus_init(alt_config, "ctl_zoneinfo", 0); + + signals_set_shutdown(&shut_down); + signals_add_handlers(0); + + switch (op) { + case REBUILD: { + struct hash_table tzentries; + struct zoneinfo *zi; + struct txn *tid = NULL; + char prefix[2048]; + + construct_hash_table(&tzentries, 500, 1); + + /* Add INFO record (overall lastmod and TZ DB source version) */ + zi = xzmalloc(sizeof(struct zoneinfo)); + zi->type = ZI_INFO; + appendstrlist(&zi->data, version); + hash_insert(INFO_TZID, zi, &tzentries); + + snprintf(prefix, sizeof(prefix), "%s%s", config_dir, FNAME_ZONEINFODIR); + + do_zonedir(prefix, &tzentries, zi); + + zoneinfo_open(NULL); + + hash_enumerate(&tzentries, &store_zoneinfo, &tid); + + zoneinfo_close(tid); + + free_hash_table(&tzentries, &free_zoneinfo); + break; + } + + case NONE: + r = 2; + usage(); + break; + } + + cyrus_done(); + + return r; +} + + +void usage(void) +{ + fprintf(stderr, + "usage: zoneinfo_reconstruct [-C <alt_config>] [-v]" + " -r <version-string>\n"); + exit(EC_USAGE); +} + + +/* Add all ZONEs and LINKs in the given directory to the hash table */ +void do_zonedir(const char *dir, struct hash_table *tzentries, + struct zoneinfo *info) +{ + DIR *dirp; + struct dirent *dirent; + + signals_poll(); + + if (verbose) printf("Rebuilding %s\n", dir); + + dirp = opendir(dir); + if (!dirp) { + fprintf(stderr, "can't open zoneinfo directory %s\n", dir); + } + + while ((dirent = readdir(dirp))) { + char path[2048], *tzid; + int plen; + struct stat sbuf; + struct zoneinfo *zi; + + if (*dirent->d_name == '.') continue; + + plen = snprintf(path, sizeof(path), "%s/%s", dir, dirent->d_name); + lstat(path, &sbuf); + + if (S_ISDIR(sbuf.st_mode)) { + /* Path is a directory (region) */ + do_zonedir(path, tzentries, info); + } + else if (S_ISLNK(sbuf.st_mode)) { + /* Path is a symlink (alias) */ + char link[1024], *alias; + ssize_t llen; + + /* Isolate tzid in path */ + if ((llen = readlink(path, link, sizeof(link))) < 0) continue; + link[llen-4] = '\0'; /* Trim ".ics" */ + for (tzid = link; !strncmp(tzid, "../", 3); tzid += 3); + + /* Isolate alias in path */ + path[plen-4] = '\0'; /* Trim ".ics" */ + alias = path + strlen(config_dir) + strlen("zoneinfo") + 2; + + if (verbose) printf("\tLINK: %s -> %s\n", alias, tzid); + + /* Create hash entry for alias */ + if (!(zi = hash_lookup(alias, tzentries))) { + zi = xzmalloc(sizeof(struct zoneinfo)); + hash_insert(alias, zi, tzentries); + } + zi->type = ZI_LINK; + appendstrlist(&zi->data, tzid); + + /* Create/update hash entry for tzid */ + if (!(zi = hash_lookup(tzid, tzentries))) { + zi = xzmalloc(sizeof(struct zoneinfo)); + hash_insert(tzid, zi, tzentries); + } + zi->type = ZI_ZONE; + appendstrlist(&zi->data, alias); + } + else if (S_ISREG(sbuf.st_mode)) { + /* Path is a regular file (zone) */ + int fd; + const char *base = NULL; + unsigned long len = 0; + icalcomponent *ical, *comp; + icalproperty *prop; + + /* Parse the iCalendar file for important properties */ + if ((fd = open(path, O_RDONLY)) == -1) continue; + map_refresh(fd, 1, &base, &len, MAP_UNKNOWN_LEN, path, NULL); + close(fd); + + ical = icalparser_parse_string(base); + map_free(&base, &len); + + if (!ical) continue; /* skip non-iCalendar files */ + + comp = icalcomponent_get_first_component(ical, + ICAL_VTIMEZONE_COMPONENT); + prop = icalcomponent_get_first_property(comp, ICAL_TZID_PROPERTY); + tzid = (char *) icalproperty_get_value_as_string(prop); + + if (verbose) printf("\tZONE: %s\n", tzid); + + /* Create/update hash entry for tzid */ + if (!(zi = hash_lookup(tzid, tzentries))) { + zi = xzmalloc(sizeof(struct zoneinfo)); + hash_insert(tzid, zi, tzentries); + } + zi->type = ZI_ZONE; + prop = icalcomponent_get_first_property(comp, + ICAL_LASTMODIFIED_PROPERTY); + zi->dtstamp = icaltime_as_timet(icalproperty_get_lastmodified(prop)); + + icalcomponent_free(ical); + + /* Check overall lastmod */ + if (zi->dtstamp > info->dtstamp) info->dtstamp = zi->dtstamp; + } + else { + fprintf(stderr, "unknown path type %s\n", path); + } + } + + closedir(dirp); +} + + +/* Free a malloc'd struct zoneinfo */ +void free_zoneinfo(void *data) +{ + struct zoneinfo *zi = (struct zoneinfo *) data; + + freestrlist(zi->data); + free(zi); +} + + +/* Store a struct zoneinfo into zoneinfo.db using the given txn */ +void store_zoneinfo(const char *tzid, void *data, void *rock) +{ + struct zoneinfo *zi = (struct zoneinfo *) data; + struct txn **tid = (struct txn **) rock; + + zoneinfo_store(tzid, zi, tid); +} + + +/* + * Cleanly shut down and exit + */ +void shut_down(int code) __attribute__((noreturn)); +void shut_down(int code) +{ + in_shutdown = 1; + + exit(code); +}
View file
cyrus-imapd-2.5.tar.gz/imap/cyr_dbtool.c
Changed
@@ -63,11 +63,14 @@ #include "global.h" #include "mailbox.h" #include "util.h" +#include "retry.h" #include "xmalloc.h" #define STACKSIZE 64000 static char stack[STACKSIZE+1]; +int outfd; + static struct db *db = NULL; static int read_key_value(char **keyptr, size_t *keylen, char **valptr, size_t *vallen) { @@ -108,7 +111,16 @@ const char *key, size_t keylen, const char *data, size_t datalen) { - printf("%.*s\t%.*s\n", (int)keylen, key, (int)datalen, data); + struct iovec io[4]; + io[0].iov_base = (char *)key; + io[0].iov_len = keylen; + io[1].iov_base = "\t"; + io[1].iov_len = 1; + io[2].iov_base = (char *)data; + io[2].iov_len = datalen; + io[3].iov_base = "\n"; + io[3].iov_len = 1; + retry_writev(outfd, io, 4); return 0; } @@ -234,9 +246,9 @@ int use_stdin = 0; int db_flags = 0; struct txn *tid = NULL; - struct txn **tidp = &tid; + struct txn **tidp = NULL; - while ((opt = getopt(argc, argv, "C:nt")) != EOF) { + while ((opt = getopt(argc, argv, "C:ntT")) != EOF) { switch (opt) { case 'C': /* alt config file */ alt_config = optarg; @@ -244,8 +256,12 @@ case 'n': /* create new */ db_flags |= CYRUSDB_CREATE; break; - case 't': + case 't': /* legacy - now the default, but don't break existing users */ tidp = NULL; + break; + case 'T': + tidp = &tid; + break; } } @@ -290,6 +306,8 @@ exit(EC_OSERR); } + outfd = fileno(stdout); + cyrus_init(alt_config, "cyr_dbtool", 0, 0); r = cyrusdb_open(argv[optind+1], fname, db_flags, &db);
View file
cyrus-imapd-2.5.tar.gz/imap/cyr_userseen.c
Changed
@@ -70,17 +70,19 @@ } /* Callback for use by delete_seen */ -static int deluserseen(char *name, - int matchlen __attribute__((unused)), - int maycreate __attribute__((unused)), - void *rock __attribute__((unused))) +static int deluserseen(void *rock __attribute__((unused)), + const char *key, + size_t keylen, + const char *val __attribute__((unused)), + size_t vallen __attribute__((unused))) { + char *name = xstrndup(key, keylen); struct mailbox *mailbox = NULL; const char *userid; - int r; + int r = 0; r = mailbox_open_irl(name, &mailbox); - if (r) return r; + if (r) goto done; userid = mboxname_to_userid(name); if (userid) { @@ -90,12 +92,13 @@ mailbox_close(&mailbox); - return 0; +done: + free(name); + return r; } int main(int argc, char *argv[]) { - char pattern[2] = { '*', '\0' }; extern char *optarg; int opt; char *alt_config = NULL; @@ -126,8 +129,7 @@ mboxlist_open(NULL); /* build a list of mailboxes - we're using internal names here */ - mboxlist_findall(NULL, pattern, 1, NULL, - NULL, deluserseen, NULL); + mboxlist_allmbox("", deluserseen, NULL, /*incdel*/0); mboxlist_close(); mboxlist_done();
View file
cyrus-imapd-2.5.tar.gz/imap/dav_db.c
Added
@@ -0,0 +1,280 @@ +/* dav_db.c -- implementation of per-user DAV database + * + * Copyright (c) 1994-2012 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 <stdlib.h> +#include <syslog.h> +#include <string.h> +#include <errno.h> +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif +#include <sys/stat.h> +#include <sys/types.h> + +#include "assert.h" +#include "cyrusdb.h" +#include "dav_db.h" +#include "global.h" +#include "util.h" +#include "xmalloc.h" + +#define FNAME_DAVSUFFIX ".dav" /* per-user DAV DB extension */ + +struct open_davdb { + sqlite3 *db; + char *path; + unsigned refcount; + struct open_davdb *next; +}; + +static struct open_davdb *open_davdbs; + +static int dbinit = 0; + +EXPORTED int dav_init(void) +{ + if (!dbinit++) { +#if SQLITE_VERSION_NUMBER >= 3006000 + sqlite3_initialize(); +#endif + } + + assert(!open_davdbs); + + return 0; +} + + +EXPORTED int dav_done(void) +{ + if (--dbinit) { +#if SQLITE_VERSION_NUMBER >= 3006000 + sqlite3_shutdown(); +#endif + } + + /* XXX - report the problems? */ + assert(!open_davdbs); + + return 0; +} + + +static void dav_debug(void *fname, const char *sql) +{ + syslog(LOG_DEBUG, "dav_exec(%s): %s", (const char *) fname, sql); +} + +static void free_dav_open(struct open_davdb *open) +{ + free(open->path); + free(open); +} + +/* Open DAV DB corresponding in file */ +EXPORTED sqlite3 *dav_open(const char *fname, const char *cmds) +{ + int rc = SQLITE_OK; + struct stat sbuf; + struct open_davdb *open; + + for (open = open_davdbs; open; open = open->next) { + if (!strcmp(open->path, fname)) { + /* already open! */ + open->refcount++; + goto docmds; + } + } + + open = xzmalloc(sizeof(struct open_davdb)); + open->path = xstrdup(fname); + + rc = stat(open->path, &sbuf); + if (rc == -1 && errno == ENOENT) { + rc = cyrus_mkdir(open->path, 0755); + } + +#if SQLITE_VERSION_NUMBER >= 3006000 + rc = sqlite3_open_v2(open->path, &open->db, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); +#else + rc = sqlite3_open(open->path, &open->db); +#endif + if (rc != SQLITE_OK) { + syslog(LOG_ERR, "dav_open(%s) open: %s", + open->path, open->db ? sqlite3_errmsg(open->db) : "failed"); + sqlite3_close(open->db); + free_dav_open(open); + return NULL; + } + else { +#if SQLITE_VERSION_NUMBER >= 3006000 + sqlite3_extended_result_codes(open->db, 1); +#endif + sqlite3_trace(open->db, dav_debug, open->path); + } + + /* stitch on up */ + open->refcount = 1; + open->next = open_davdbs; + open_davdbs = open; + + docmds: + if (cmds) { + rc = sqlite3_exec(open->db, cmds, NULL, NULL, NULL); + if (rc != SQLITE_OK) { + /* XXX - fatal? */ + syslog(LOG_ERR, "dav_open(%s) cmds: %s", + open->path, sqlite3_errmsg(open->db)); + } + } + + return open->db; +} + + +/* Close DAV DB */ +EXPORTED int dav_close(sqlite3 *davdb) +{ + int rc, r = 0; + struct open_davdb *open, *prev = NULL; + + if (!davdb) return 0; + + for (open = open_davdbs; open; open = open->next) { + if (davdb == open->db) { + if (--open->refcount) return 0; /* still in use */ + if (prev) + prev->next = open->next; + else + open_davdbs = open->next; + break; + } + prev = open; + } + + assert(open); + + rc = sqlite3_close(open->db); + if (rc != SQLITE_OK) { + syslog(LOG_ERR, "dav_close(%s): %s", open->path, sqlite3_errmsg(open->db)); + r = CYRUSDB_INTERNAL; + } + + free_dav_open(open); + + return r; +} + + +EXPORTED int dav_exec(sqlite3 *davdb, const char *cmd, struct bind_val bval[], + int (*cb)(sqlite3_stmt *stmt, void *rock), void *rock, + sqlite3_stmt **stmt) +{ + int rc, r = 0; + + if (!*stmt) { + /* prepare new statement */ +#if SQLITE_VERSION_NUMBER >= 3006000 + rc = sqlite3_prepare_v2(davdb, cmd, -1, stmt, NULL); +#else + rc = sqlite3_prepare(davdb, cmd, -1, stmt, NULL); +#endif + if (rc != SQLITE_OK) { + syslog(LOG_ERR, "dav_exec() prepare: %s", sqlite3_errmsg(davdb)); + return CYRUSDB_INTERNAL; + } + } + + /* bind values */ + for (; bval && bval->name; bval++) { + int cidx = sqlite3_bind_parameter_index(*stmt, bval->name); + + switch (bval->type) { + case SQLITE_INTEGER: + sqlite3_bind_int(*stmt, cidx, bval->val.i); + break; + + case SQLITE_TEXT: + sqlite3_bind_text(*stmt, cidx, bval->val.s, -1, NULL); + break; + } + } + + /* execute and process the results */ + while ((rc = sqlite3_step(*stmt)) == SQLITE_ROW) { + if (cb && (r = cb(*stmt, rock))) break; + } + + /* reset statement and clear all bindings */ + sqlite3_reset(*stmt); +#if SQLITE_VERSION_NUMBER >= 3006000 + sqlite3_clear_bindings(*stmt); +#endif + + if (!r && rc != SQLITE_DONE) { + syslog(LOG_ERR, "dav_exec() step: %s", sqlite3_errmsg(davdb)); + r = CYRUSDB_INTERNAL; + } + + return r; +} + + +EXPORTED int dav_delete(struct mailbox *mailbox) +{ + struct buf fname = BUF_INITIALIZER; + int r = 0; + + dav_getpath(&fname, mailbox); + if (unlink(buf_cstring(&fname)) && errno != ENOENT) { + syslog(LOG_ERR, "dav_db: error unlinking %s: %m", buf_cstring(&fname)); + r = CYRUSDB_INTERNAL; + } + + buf_free(&fname); + + return r; +}
View file
cyrus-imapd-2.5.tar.gz/imap/dav_db.h
Added
@@ -0,0 +1,92 @@ +/* dav_db.h -- abstract interface for per-user DAV database + * + * Copyright (c) 1994-2012 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 DAV_DB_H +#define DAV_DB_H + +#include <sqlite3.h> +#include "dav_util.h" + +struct dav_data { + unsigned rowid; + time_t creationdate; + const char *mailbox; + const char *resource; + uint32_t imap_uid; /* zero (0) until URL is mapped */ + const char *lock_token; + const char *lock_owner; + const char *lock_ownerid; + time_t lock_expire; +}; + +struct bind_val { + const char *name; + int type; + union { + int i; + const char *s; + } val; +}; + +/* prepare for DAV operations in this process */ +int dav_init(void); + +/* done with all DAV operations for this process */ +int dav_done(void); + +/* get a database handle corresponding to mailbox */ +sqlite3 *dav_open(const char *fname, const char *cmds); + +/* close this handle */ +int dav_close(sqlite3 *davdb); + +/* execute 'cmd' and process results with 'cb' + 'cmd' is prepared as 'stmt' with 'bval' as bound values */ +int dav_exec(sqlite3 *davdb, const char *cmd, struct bind_val bval[], + int (*cb)(sqlite3_stmt *stmt, void *rock), void *rock, + sqlite3_stmt **stmt); + +/* delete database corresponding to mailbox */ +int dav_delete(struct mailbox *mailbox); + +#endif /* DAV_DB_H */
View file
cyrus-imapd-2.5.tar.gz/imap/dav_reconstruct.c
Added
@@ -0,0 +1,217 @@ +/* dav_reconstruct.c - (re)build DAV DB for a user + * + * Copyright (c) 1994-2012 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 <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <syslog.h> +#include <time.h> + +#include <libical/ical.h> + +#include "annotate.h" +#include "caldav_db.h" +#include "carddav_db.h" +#include "exitcodes.h" +#include "global.h" +#include "http_dav.h" +#include "imap_err.h" +#include "mailbox.h" +#include "message.h" +#include "message_guid.h" +#include "mboxname.h" +#include "mboxlist.h" +#include "util.h" +#include "xmalloc.h" +#include "xstrlcat.h" + +extern int optind; +extern char *optarg; + +/* current namespace */ +static struct namespace recon_namespace; + +/* config.c stuff */ +const int config_need_data = 0; + +/* forward declarations */ +static int do_reconstruct(void *rock, + const char *key, + size_t keylen, + const char *data, + size_t datalen); +void usage(void); +void shut_down(int code); + +static int code = 0; +static struct caldav_db *caldavdb = NULL; + + +int main(int argc, char **argv) +{ + int opt, r; + char *alt_config = NULL, *userid; + struct buf fnamebuf = BUF_INITIALIZER; + + if ((geteuid()) == 0 && (become_cyrus(/*is_master*/0) != 0)) { + fatal("must run as the Cyrus user", EC_USAGE); + } + + while ((opt = getopt(argc, argv, "C:")) != EOF) { + switch (opt) { + case 'C': /* alt config file */ + alt_config = optarg; + break; + + default: + usage(); + } + } + + cyrus_init(alt_config, "dav_reconstruct", 0, 0); + + /* Set namespace -- force standard (internal) */ + if ((r = mboxname_init_namespace(&recon_namespace, 1)) != 0) { + syslog(LOG_ERR, "%s", error_message(r)); + fatal(error_message(r), EC_CONFIG); + } + + mboxlist_init(0); + mboxlist_open(NULL); + + signals_set_shutdown(&shut_down); + signals_add_handlers(0); + + if (optind == argc) usage(); + + userid = argv[optind]; + + printf("Reconstructing DAV DB for %s...\n", userid); + caldav_init(); + carddav_init(); + + /* remove existing database entirely */ + /* XXX - build a new file and rename into place? */ + dav_getpath_byuserid(&fnamebuf, userid); + if (buf_len(&fnamebuf)) + unlink(buf_cstring(&fnamebuf)); + + mboxlist_allusermbox(userid, do_reconstruct, NULL, 0); + + caldav_close(caldavdb); + caldav_done(); + + mboxlist_close(); + mboxlist_done(); + + buf_free(&fnamebuf); + + exit(code); +} + + +void usage(void) +{ + fprintf(stderr, + "usage: dav_reconstruct [-C <alt_config>] userid\n"); + exit(EC_USAGE); +} + +/* + * mboxlist_findall() callback function to create DAV DB entries for a mailbox + */ +static int do_reconstruct(void *rock __attribute__((unused)), + const char *key, + size_t keylen, + const char *data __attribute__((unused)), + size_t datalen __attribute__((unused))) +{ + int r = 0; + char ext_name_buf[MAX_MAILBOX_PATH+1]; + mbentry_t *mbentry = NULL; + struct mailbox *mailbox = NULL; + char *name = xstrndup(key, keylen); + + signals_poll(); + + r = mboxlist_lookup(name, &mbentry, NULL); + if (r) goto done; + + /* Convert internal name to external */ + (*recon_namespace.mboxname_toexternal)(&recon_namespace, mbentry->name, + "cyrus", ext_name_buf); + +#ifdef WITH_DAV + if (mbentry->mbtype & (MBTYPE_CALENDAR|MBTYPE_ADDRESSBOOK)) { + printf("Inserting DAV DB entries for %s...\n", ext_name_buf); + + /* Open/lock header */ + r = mailbox_open_irl(mbentry->name, &mailbox); + if (!r) r = mailbox_add_dav(mailbox); + mailbox_close(&mailbox); + } +#endif + +done: + mboxlist_entry_free(&mbentry); + return r; +} + +/* + * Cleanly shut down and exit + */ +void shut_down(int code) __attribute__((noreturn)); +void shut_down(int code) +{ + in_shutdown = 1; + + mboxlist_close(); + mboxlist_done(); + caldav_done(); + exit(code); +}
View file
cyrus-imapd-2.5.tar.gz/imap/dav_util.c
Added
@@ -0,0 +1,84 @@ +/* dav_util.c -- utility functions for dealing with DAV database + * + * Copyright (c) 1994-2014 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 <string.h> + +#include "dav_util.h" +#include "global.h" +#include "mailbox.h" +#include "mboxname.h" +#include "util.h" + +/* Create filename corresponding to DAV DB for mailbox */ +EXPORTED void dav_getpath(struct buf *fname, struct mailbox *mailbox) +{ + const char *userid; + + userid = mboxname_to_userid(mailbox->name); + + if (userid) dav_getpath_byuserid(fname, userid); + else buf_setcstr(fname, mailbox_meta_fname(mailbox, META_DAV)); +} + +/* Create filename corresponding to DAV DB for userid */ +EXPORTED void dav_getpath_byuserid(struct buf *fname, const char *userid) +{ + char c, *domain; + + buf_reset(fname); + if (config_virtdomains && (domain = strchr(userid, '@'))) { + char d = (char) dir_hash_c(domain+1, config_fulldirhash); + *domain = '\0'; /* split user@domain */ + c = (char) dir_hash_c(userid, config_fulldirhash); + buf_printf(fname, "%s%s%c/%s%s%c/%s%s", config_dir, FNAME_DOMAINDIR, d, + domain+1, FNAME_USERDIR, c, userid, FNAME_DAVSUFFIX); + *domain = '@'; /* reassemble user@domain */ + } + else { + c = (char) dir_hash_c(userid, config_fulldirhash); + buf_printf(fname, "%s%s%c/%s%s", config_dir, FNAME_USERDIR, c, userid, + FNAME_DAVSUFFIX); + } +}
View file
cyrus-imapd-2.5.tar.gz/imap/dav_util.h
Added
@@ -0,0 +1,58 @@ +/* dav_util.h -- utility functions for dealing with DAV database + * + * Copyright (c) 1994-2014 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 DAV_UTIL_H +#define DAV_UTIL_H + +#include "mailbox.h" +#include "util.h" + +#define FNAME_DAVSUFFIX ".dav" /* per-user DAV DB extension */ + +/* Create filename corresponding to DAV DB for mailbox */ +void dav_getpath(struct buf *fname, struct mailbox *mailbox); + +/* Create filename corresponding to DAV DB for userid */ +void dav_getpath_byuserid(struct buf *fname, const char *userid); + +#endif /* DAV_UTIL_H */
View file
cyrus-imapd-2.5.tar.gz/imap/global.c
Changed
@@ -51,6 +51,7 @@ #include <sys/types.h> #include <netinet/in.h> #include <sys/stat.h> +#include <openssl/rand.h> #if HAVE_UNISTD_H # include <unistd.h> @@ -100,6 +101,7 @@ EXPORTED const char *config_ptscache_db; EXPORTED const char *config_statuscache_db; HIDDEN const char *config_userdeny_db; +EXPORTED const char *config_zoneinfo_db; EXPORTED int charset_flags; static char session_id_buf[MAX_SESSIONID_SIZE]; @@ -259,6 +261,7 @@ config_ptscache_db = config_getstring(IMAPOPT_PTSCACHE_DB); config_statuscache_db = config_getstring(IMAPOPT_STATUSCACHE_DB); config_userdeny_db = config_getstring(IMAPOPT_USERDENY_DB); + config_zoneinfo_db = config_getstring(IMAPOPT_ZONEINFO_DB); /* configure libcyrus as needed */ libcyrus_config_setstring(CYRUSOPT_CONFIG_DIR, config_dir); @@ -761,16 +764,18 @@ EXPORTED void session_new_id(void) { const char *base; + unsigned char random[8]; int now = time(NULL); if (now != session_id_time) { session_id_time = now; session_id_count = 0; } + RAND_bytes(random, sizeof(random)); ++session_id_count; base = config_getstring(IMAPOPT_SYSLOG_PREFIX); if (!base) base = config_servername; - snprintf(session_id_buf, MAX_SESSIONID_SIZE, "%.128s-%d-%d-%d", - base, getpid(), session_id_time, session_id_count); + snprintf(session_id_buf, MAX_SESSIONID_SIZE, "%.128s-%d-%d-%d-%llu", + base, getpid(), session_id_time, session_id_count, (unsigned long long)(*((uint64_t *)random))); } /* Return the session id */
View file
cyrus-imapd-2.5.tar.gz/imap/global.h
Changed
@@ -185,6 +185,7 @@ extern const char *config_ptscache_db; extern const char *config_statuscache_db; extern const char *config_userdeny_db; +extern const char *config_zoneinfo_db; extern int charset_flags; /* Session ID */
View file
cyrus-imapd-2.5.tar.gz/imap/http_caldav.c
Added
@@ -0,0 +1,7836 @@ +/* http_caldav.c -- Routines for handling CalDAV collections in httpd + * + * Copyright (c) 1994-2011 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. + * + */ +/* + * TODO: + * + * - Make proxying more robust. Currently depends on calendar collections + * residing on same server as user's INBOX. Doesn't handle global/shared + * calendars. + * - Support COPY/MOVE on collections + * - Add more required properties + * - GET/HEAD on collections (iCalendar stream of resources) + * - calendar-query REPORT (handle partial retrieval, prop-filter, timezone?) + * - free-busy-query REPORT (check ACL and transp on all calendars) + * - sync-collection REPORT - need to handle Depth infinity? + */ + +#include <config.h> + +#include <syslog.h> + +#include <libical/ical.h> +#include <libxml/tree.h> +#include <libxml/uri.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include "acl.h" +#include "append.h" +#include "caldav_db.h" +#include "exitcodes.h" +#include "global.h" +#include "hash.h" +#include "httpd.h" +#include "http_caldav_sched.h" +#include "http_dav.h" +#include "http_err.h" +#include "http_proxy.h" +#include "imap_err.h" +#include "index.h" +#include "jcal.h" +#include "xcal.h" +#include "map.h" +#include "mailbox.h" +#include "mboxlist.h" +#include "md5.h" +#include "message.h" +#include "message_guid.h" +#include "proxy.h" +#include "times.h" +#include "smtpclient.h" +#include "spool.h" +#include "strhash.h" +#include "stristr.h" +#include "tok.h" +#include "util.h" +#include "version.h" +#include "xmalloc.h" +#include "xstrlcat.h" +#include "xstrlcpy.h" +#include "zoneinfo_db.h" + + +#define NEW_STAG (1<<8) /* Make sure we skip over PREFER bits */ + + +#ifdef HAVE_RSCALE +#include <unicode/uversion.h> + +static int rscale_cmp(const void *a, const void *b) +{ + /* Convert to uppercase since that's what we prefer to output */ + return strcmp(ucase(*((char **) a)), ucase(*((char **) b))); +} + +static icalarray *rscale_calendars = NULL; +#endif /* HAVE_RSCALE */ + + +#ifndef HAVE_SCHEDULING_PARAMS + +/* Functions to replace those not available in libical < v1.0 */ + +static icalparameter_scheduleagent +icalparameter_get_scheduleagent(icalparameter *param) +{ + const char *agent = NULL; + + if (param) agent = icalparameter_get_iana_value(param); + + if (!agent) return ICAL_SCHEDULEAGENT_NONE; + else if (!strcmp(agent, "SERVER")) return ICAL_SCHEDULEAGENT_SERVER; + else if (!strcmp(agent, "CLIENT")) return ICAL_SCHEDULEAGENT_CLIENT; + else return ICAL_SCHEDULEAGENT_X; +} + +static icalparameter_scheduleforcesend +icalparameter_get_scheduleforcesend(icalparameter *param) +{ + const char *force = NULL; + + if (param) force = icalparameter_get_iana_value(param); + + if (!force) return ICAL_SCHEDULEFORCESEND_NONE; + else if (!strcmp(force, "REQUEST")) return ICAL_SCHEDULEFORCESEND_REQUEST; + else if (!strcmp(force, "REPLY")) return ICAL_SCHEDULEFORCESEND_REPLY; + else return ICAL_SCHEDULEFORCESEND_X; +} + +static icalparameter *icalparameter_new_schedulestatus(const char *stat) +{ + icalparameter *param = icalparameter_new(ICAL_IANA_PARAMETER); + + icalparameter_set_iana_name(param, "SCHEDULE-STATUS"); + icalparameter_set_iana_value(param, stat); + + return param; +} + +/* Wrappers to fetch scheduling parameters by kind */ + +static icalparameter* +icalproperty_get_iana_parameter_by_name(icalproperty *prop, const char *name) +{ + icalparameter *param; + + for (param = icalproperty_get_first_parameter(prop, ICAL_IANA_PARAMETER); + param && strcmp(icalparameter_get_iana_name(param), name); + param = icalproperty_get_next_parameter(prop, ICAL_IANA_PARAMETER)); + + return param; +} + +#define icalproperty_get_scheduleagent_parameter(prop) \ + icalproperty_get_iana_parameter_by_name(prop, "SCHEDULE-AGENT") + +#define icalproperty_get_scheduleforcesend_parameter(prop) \ + icalproperty_get_iana_parameter_by_name(prop, "SCHEDULE-FORCE-SEND") + +#define icalproperty_get_schedulestatus_parameter(prop) \ + icalproperty_get_iana_parameter_by_name(prop, "SCHEDULE-STATUS") + +#else + +/* Wrappers to fetch scheduling parameters by kind */ + +#define icalproperty_get_scheduleagent_parameter(prop) \ + icalproperty_get_first_parameter(prop, ICAL_SCHEDULEAGENT_PARAMETER) + +#define icalproperty_get_scheduleforcesend_parameter(prop) \ + icalproperty_get_first_parameter(prop, ICAL_SCHEDULEFORCESEND_PARAMETER) + +#define icalproperty_get_schedulestatus_parameter(prop) \ + icalproperty_get_first_parameter(prop, ICAL_SCHEDULESTATUS_PARAMETER) + +#endif /* HAVE_SCHEDULING_PARAMS */ + + +struct freebusy { + struct icalperiodtype per; + icalparameter_fbtype type; +}; + +struct freebusy_array { + struct freebusy *fb; + unsigned len; + unsigned alloc; +}; + +struct vavailability { + int priority; + struct icalperiodtype per; + icalcomponent *ical; +}; + +struct vavailability_array { + struct vavailability *vav; + unsigned len; + unsigned alloc; +}; + +struct calquery_filter { + unsigned comp; + unsigned flags; + struct icaltimetype start; + struct icaltimetype end; + icaltimezone *tz; + struct freebusy_array freebusy; /* array of found freebusy periods */ + struct vavailability_array vavail; /* array of found vavail components */ +}; + +/* Bitmask of calquery flags */ +enum { + BUSYTIME_QUERY = (1<<0), + CHECK_CAL_TRANSP = (1<<1), + CHECK_USER_AVAIL = (1<<2) +}; + +static unsigned config_allowsched = IMAP_ENUM_CALDAV_ALLOWSCHEDULING_OFF; +static struct caldav_db *auth_caldavdb = NULL; +static time_t compile_time; +static struct buf ical_prodid_buf = BUF_INITIALIZER; +static const char *ical_prodid = NULL; +static struct strlist *cua_domains = NULL; +icalarray *rscale_calendars = NULL; + +static struct caldav_db *my_caldav_open(struct mailbox *mailbox); +static void my_caldav_close(struct caldav_db *caldavdb); +static void my_caldav_init(struct buf *serverinfo); +static void my_caldav_auth(const char *userid); +static void my_caldav_reset(void); +static void my_caldav_shutdown(void); + +static int caldav_parse_path(const char *path, + struct request_target_t *tgt, const char **errstr); + +static int caldav_check_precond(struct transaction_t *txn, const void *data, + const char *etag, time_t lastmod); + +static int caldav_acl(struct transaction_t *txn, xmlNodePtr priv, int *rights); +static int caldav_copy(struct transaction_t *txn, + struct mailbox *src_mbox, struct index_record *src_rec, + struct mailbox *dest_mbox, const char *dest_rsrc, + struct caldav_db *dest_davdb, + unsigned overwrite, unsigned flags); +static int caldav_delete_sched(struct transaction_t *txn, + struct mailbox *mailbox, + struct index_record *record, void *data); +static int meth_get(struct transaction_t *txn, void *params); +static int caldav_post(struct transaction_t *txn); +static int caldav_put(struct transaction_t *txn, + struct mime_type_t *mime, + struct mailbox *mailbox, + struct caldav_db *caldavdb, + unsigned flags); + +static int propfind_getcontenttype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_restype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_caldata(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_calcompset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int proppatch_calcompset(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], void *rock); +static int propfind_suppcaldata(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_maxsize(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_minmaxdate(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_scheddefault(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_schedtag(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_caltransp(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int proppatch_caltransp(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], void *rock); +static int propfind_timezone(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int proppatch_timezone(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], void *rock); +static int propfind_availability(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int proppatch_availability(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], void *rock); +static int propfind_tzservset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_tzid(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int proppatch_tzid(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], void *rock); +static int propfind_rscaleset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +static int report_cal_query(struct transaction_t *txn, xmlNodePtr inroot, + struct propfind_ctx *fctx); +static int report_cal_multiget(struct transaction_t *txn, xmlNodePtr inroot, + struct propfind_ctx *fctx); +static int report_fb_query(struct transaction_t *txn, xmlNodePtr inroot, + struct propfind_ctx *fctx); + +static int store_resource(struct transaction_t *txn, icalcomponent *ical, + struct mailbox *mailbox, const char *resource, + struct caldav_db *caldavdb, int overwrite, + unsigned flags); + +static void sched_request(const char *organizer, struct sched_param *sparam, + icalcomponent *oldical, icalcomponent *newical, + const char *att_update); +static void sched_reply(const char *userid, + icalcomponent *oldical, icalcomponent *newical); + +static const char *begin_icalendar(struct buf *buf); +static void end_icalendar(struct buf *buf); + +static int apply_calfilter(struct propfind_ctx *fctx, void *data); +static icalcomponent *busytime_query_local(struct transaction_t *txn, + struct propfind_ctx *fctx, + char mailboxname[], + icalproperty_method method, + const char *uid, + const char *organizer, + const char *attendee); + +static struct mime_type_t caldav_mime_types[] = { + /* First item MUST be the default type and storage format */ + { "text/calendar; charset=utf-8", "2.0", "ics", "ifb", + (char* (*)(void *)) &icalcomponent_as_ical_string_r, + (void * (*)(const char*)) &icalparser_parse_string, + (void (*)(void *)) &icalcomponent_free, &begin_icalendar, &end_icalendar + }, + { "application/calendar+xml; charset=utf-8", NULL, "xcs", "xfb", + (char* (*)(void *)) &icalcomponent_as_xcal_string, + (void * (*)(const char*)) &xcal_string_as_icalcomponent, + NULL, &begin_xcal, &end_xcal + }, +#ifdef WITH_JSON + { "application/calendar+json; charset=utf-8", NULL, "jcs", "jfb", + (char* (*)(void *)) &icalcomponent_as_jcal_string, + (void * (*)(const char*)) &jcal_string_as_icalcomponent, + NULL, &begin_jcal, &end_jcal + }, +#endif + { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL } +}; + +/* Array of supported REPORTs */ +static const struct report_type_t caldav_reports[] = { + + /* WebDAV Versioning (RFC 3253) REPORTs */ + { "expand-property", NS_DAV, "multistatus", &report_expand_prop, + DACL_READ, 0 }, + + /* WebDAV Sync (RFC 6578) REPORTs */ + { "sync-collection", NS_DAV, "multistatus", &report_sync_col, + DACL_READ, REPORT_NEED_MBOX | REPORT_NEED_PROPS }, + + /* CalDAV (RFC 4791) REPORTs */ + { "calendar-query", NS_CALDAV, "multistatus", &report_cal_query, + DACL_READ, REPORT_NEED_MBOX }, + { "calendar-multiget", NS_CALDAV, "multistatus", &report_cal_multiget, + DACL_READ, REPORT_NEED_MBOX }, + { "free-busy-query", NS_CALDAV, NULL, &report_fb_query, + DACL_READFB, REPORT_NEED_MBOX }, + + { NULL, 0, NULL, NULL, 0, 0 } +}; + +/* Array of known "live" properties */ +static const struct prop_entry caldav_props[] = { + + /* WebDAV (RFC 4918) properties */ + { "creationdate", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_creationdate, NULL, NULL }, + { "displayname", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_fromdb, proppatch_todb, NULL }, + { "getcontentlanguage", NS_DAV, PROP_ALLPROP | PROP_RESOURCE, + propfind_fromhdr, NULL, "Content-Language" }, + { "getcontentlength", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_getlength, NULL, NULL }, + { "getcontenttype", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_getcontenttype, NULL, "Content-Type" }, + { "getetag", NS_DAV, PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_getetag, NULL, NULL }, + { "getlastmodified", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_getlastmod, NULL, NULL }, + { "lockdiscovery", NS_DAV, PROP_ALLPROP | PROP_RESOURCE, + propfind_lockdisc, NULL, NULL }, + { "resourcetype", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_restype, proppatch_restype, "calendar" }, + { "supportedlock", NS_DAV, PROP_ALLPROP | PROP_RESOURCE, + propfind_suplock, NULL, NULL }, + + /* WebDAV Versioning (RFC 3253) properties */ + { "supported-report-set", NS_DAV, PROP_COLLECTION, + propfind_reportset, NULL, (void *) caldav_reports }, + + /* WebDAV ACL (RFC 3744) properties */ + { "owner", NS_DAV, PROP_COLLECTION | PROP_RESOURCE | PROP_EXPAND, + propfind_owner, NULL, NULL }, + { "group", NS_DAV, 0, NULL, NULL, NULL }, + { "supported-privilege-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE, + propfind_supprivset, NULL, NULL }, + { "current-user-privilege-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE, + propfind_curprivset, NULL, NULL }, + { "acl", NS_DAV, PROP_COLLECTION | PROP_RESOURCE, + propfind_acl, NULL, NULL }, + { "acl-restrictions", NS_DAV, PROP_COLLECTION | PROP_RESOURCE, + propfind_aclrestrict, NULL, NULL }, + { "inherited-acl-set", NS_DAV, 0, NULL, NULL, NULL }, + { "principal-collection-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE, + propfind_princolset, NULL, NULL }, + + /* WebDAV Quota (RFC 4331) properties */ + { "quota-available-bytes", NS_DAV, PROP_COLLECTION, + propfind_quota, NULL, NULL }, + { "quota-used-bytes", NS_DAV, PROP_COLLECTION, + propfind_quota, NULL, NULL }, + + /* WebDAV Current Principal (RFC 5397) properties */ + { "current-user-principal", NS_DAV, + PROP_COLLECTION | PROP_RESOURCE | PROP_EXPAND, + propfind_curprin, NULL, NULL }, + + /* WebDAV POST (RFC 5995) properties */ + { "add-member", NS_DAV, PROP_COLLECTION, + propfind_addmember, NULL, NULL }, + + /* WebDAV Sync (RFC 6578) properties */ + { "sync-token", NS_DAV, PROP_COLLECTION, + propfind_sync_token, NULL, NULL }, + + /* CalDAV (RFC 4791) properties */ + { "calendar-data", NS_CALDAV, + PROP_RESOURCE | PROP_PRESCREEN | PROP_NEEDPROP, + propfind_caldata, NULL, NULL }, + { "calendar-description", NS_CALDAV, PROP_COLLECTION, + propfind_fromdb, proppatch_todb, NULL }, + { "calendar-timezone", NS_CALDAV, + PROP_COLLECTION | PROP_PRESCREEN | PROP_NEEDPROP, + propfind_timezone, proppatch_timezone, NULL }, + { "supported-calendar-component-set", NS_CALDAV, PROP_COLLECTION, + propfind_calcompset, proppatch_calcompset, NULL }, + { "supported-calendar-data", NS_CALDAV, PROP_COLLECTION, + propfind_suppcaldata, NULL, NULL }, + { "max-resource-size", NS_CALDAV, PROP_COLLECTION, + propfind_maxsize, NULL, NULL }, + { "min-date-time", NS_CALDAV, PROP_COLLECTION, + propfind_minmaxdate, NULL, &caldav_epoch }, + { "max-date-time", NS_CALDAV, PROP_COLLECTION, + propfind_minmaxdate, NULL, &caldav_eternity }, + { "max-instances", NS_CALDAV, 0, NULL, NULL, NULL }, + { "max-attendees-per-instance", NS_CALDAV, 0, NULL, NULL, NULL }, + + /* CalDAV Scheduling (RFC 6638) properties */ + { "schedule-tag", NS_CALDAV, PROP_RESOURCE, + propfind_schedtag, NULL, NULL }, + { "schedule-default-calendar-URL", NS_CALDAV, PROP_COLLECTION | PROP_EXPAND, + propfind_scheddefault, NULL, NULL }, + { "schedule-calendar-transp", NS_CALDAV, PROP_COLLECTION, + propfind_caltransp, proppatch_caltransp, NULL }, + + /* Calendar Availability (draft-daboo-calendar-availability) properties */ + { "calendar-availability", NS_CALDAV, + PROP_COLLECTION | PROP_PRESCREEN | PROP_NEEDPROP, + propfind_availability, proppatch_availability, NULL }, + + /* Backwards compatibility with Apple VAVAILABILITY clients */ + { "calendar-availability", NS_CS, + PROP_COLLECTION | PROP_PRESCREEN | PROP_NEEDPROP, + propfind_availability, proppatch_availability, NULL }, + + /* TZ by Ref (draft-ietf-tzdist-caldav-timezone-ref) properties */ + { "timezone-service-set", NS_CALDAV, PROP_COLLECTION, + propfind_tzservset, NULL, NULL }, + { "calendar-timezone-id", NS_CALDAV, PROP_COLLECTION, + propfind_tzid, proppatch_tzid, NULL }, + + /* RSCALE (draft-daboo-icalendar-rscale) properties */ + { "supported-rscale-set", NS_CALDAV, PROP_COLLECTION, + propfind_rscaleset, NULL, NULL }, + + /* CalDAV Extensions (draft-daboo-caldav-extensions) properties */ + { "supported-calendar-component-sets", NS_CALDAV, PROP_COLLECTION, + propfind_calcompset, NULL, NULL }, + + /* Apple Calendar Server properties */ + { "getctag", NS_CS, PROP_ALLPROP | PROP_COLLECTION, + propfind_sync_token, NULL, NULL }, + + { NULL, 0, 0, NULL, NULL, NULL } +}; + + +static struct meth_params caldav_params = { + caldav_mime_types, + &caldav_parse_path, + &caldav_check_precond, + { (db_open_proc_t) &my_caldav_open, + (db_close_proc_t) &my_caldav_close, + (db_lookup_proc_t) &caldav_lookup_resource, + (db_foreach_proc_t) &caldav_foreach, + (db_write_proc_t) &caldav_write, + (db_delete_proc_t) &caldav_delete, + (db_delmbox_proc_t) &caldav_delmbox }, + &caldav_acl, + (copy_proc_t) &caldav_copy, + &caldav_delete_sched, + { MBTYPE_CALENDAR, "mkcalendar", "mkcalendar-response", NS_CALDAV }, + &caldav_post, + { CALDAV_SUPP_DATA, (put_proc_t) &caldav_put }, + caldav_props, + caldav_reports +}; + + +/* Namespace for CalDAV collections */ +struct namespace_t namespace_calendar = { + URL_NS_CALENDAR, 0, "/dav/calendars", "/.well-known/caldav", 1 /* auth */, + (ALLOW_READ | ALLOW_POST | ALLOW_WRITE | ALLOW_DELETE | +#ifdef HAVE_VAVAILABILITY + ALLOW_CAL_AVAIL | +#endif + ALLOW_DAV | ALLOW_WRITECOL | ALLOW_CAL ), + &my_caldav_init, &my_caldav_auth, my_caldav_reset, &my_caldav_shutdown, + { + { &meth_acl, &caldav_params }, /* ACL */ + { &meth_copy, &caldav_params }, /* COPY */ + { &meth_delete, &caldav_params }, /* DELETE */ + { &meth_get, &caldav_params }, /* GET */ + { &meth_get, &caldav_params }, /* HEAD */ + { &meth_lock, &caldav_params }, /* LOCK */ + { &meth_mkcol, &caldav_params }, /* MKCALENDAR */ + { &meth_mkcol, &caldav_params }, /* MKCOL */ + { &meth_copy, &caldav_params }, /* MOVE */ + { &meth_options, &caldav_parse_path }, /* OPTIONS */ + { &meth_post, &caldav_params }, /* POST */ + { &meth_propfind, &caldav_params }, /* PROPFIND */ + { &meth_proppatch, &caldav_params }, /* PROPPATCH */ + { &meth_put, &caldav_params }, /* PUT */ + { &meth_report, &caldav_params }, /* REPORT */ + { &meth_trace, &caldav_parse_path }, /* TRACE */ + { &meth_unlock, &caldav_params } /* UNLOCK */ + } +}; + + +static const struct cal_comp_t { + const char *name; + unsigned long type; +} cal_comps[] = { + { "VEVENT", CAL_COMP_VEVENT }, + { "VTODO", CAL_COMP_VTODO }, + { "VJOURNAL", CAL_COMP_VJOURNAL }, + { "VFREEBUSY", CAL_COMP_VFREEBUSY }, +#ifdef HAVE_VAVAILABILITY + { "VAVAILABILITY", CAL_COMP_VAVAILABILITY }, +#endif +#ifdef HAVE_VPOLL + { "VPOLL", CAL_COMP_VPOLL }, +#endif +// { "VTIMEZONE", CAL_COMP_VTIMEZONE }, +// { "VALARM", CAL_COMP_VALARM }, + { NULL, 0 } +}; + + +static struct caldav_db *my_caldav_open(struct mailbox *mailbox) +{ + if (httpd_userid && mboxname_userownsmailbox(httpd_userid, mailbox->name)) { + return auth_caldavdb; + } + else { + return caldav_open_mailbox(mailbox, CALDAV_CREATE); + } +} + + +static void my_caldav_close(struct caldav_db *caldavdb) +{ + if (caldavdb && (caldavdb != auth_caldavdb)) caldav_close(caldavdb); +} + + +static void my_caldav_init(struct buf *serverinfo) +{ + const char *domains; + char *domain; + tok_t tok; + + buf_printf(serverinfo, " SQLite/%s", sqlite3_libversion()); + buf_printf(serverinfo, " Libical/%s", ICAL_VERSION); +#ifdef HAVE_RSCALE + if ((rscale_calendars = icalrecurrencetype_rscale_supported_calendars())) { + icalarray_sort(rscale_calendars, &rscale_cmp); + + buf_printf(serverinfo, " ICU4C/%s", U_ICU_VERSION); + } +#endif +#ifdef WITH_JSON + buf_printf(serverinfo, " Jansson/%s", JANSSON_VERSION); +#endif + + namespace_calendar.enabled = + config_httpmodules & IMAP_ENUM_HTTPMODULES_CALDAV; + + if (!namespace_calendar.enabled) return; + + if (!config_getstring(IMAPOPT_CALENDARPREFIX)) { + fatal("Required 'calendarprefix' option is not set", EC_CONFIG); + } + + caldav_init(); + + config_allowsched = config_getenum(IMAPOPT_CALDAV_ALLOWSCHEDULING); + if (config_allowsched) { + namespace_calendar.allow |= ALLOW_CAL_SCHED; + +#ifndef HAVE_SCHEDULING_PARAMS + /* Need to set this to parse CalDAV Scheduling parameters */ + ical_set_unknown_token_handling_setting(ICAL_ASSUME_IANA_TOKEN); +#endif + } + +#ifdef HAVE_TZ_BY_REF + if (namespace_timezone.enabled) { + char zonedir[MAX_MAILBOX_PATH+1]; + + snprintf(zonedir, MAX_MAILBOX_PATH, "%s%s", + config_dir, FNAME_ZONEINFODIR); + set_zone_directory(zonedir); + icaltimezone_set_tzid_prefix(""); + icaltimezone_set_builtin_tzdata(1); + + namespace_calendar.allow |= ALLOW_CAL_NOTZ; + } +#endif + + namespace_principal.enabled = 1; + namespace_principal.allow |= namespace_calendar.allow & + (ALLOW_CAL | ALLOW_CAL_AVAIL | ALLOW_CAL_SCHED); + + compile_time = calc_compile_time(__TIME__, __DATE__); + + buf_printf(&ical_prodid_buf, + "-//CyrusIMAP.org/Cyrus %s//EN", cyrus_version()); + ical_prodid = buf_cstring(&ical_prodid_buf); + + /* Create an array of calendar-user-adddress-set domains */ + domains = config_getstring(IMAPOPT_CALENDAR_USER_ADDRESS_SET); + if (!domains) domains = config_servername; + + tok_init(&tok, domains, " \t", TOK_TRIMLEFT|TOK_TRIMRIGHT); + while ((domain = tok_next(&tok))) appendstrlist(&cua_domains, domain); + tok_fini(&tok); +} + + +static void my_caldav_auth(const char *userid) +{ + const char *mailboxname; + int r; + + /* Generate mailboxname of calendar-home-set */ + mailboxname = caldav_mboxname(userid, NULL); + + if (httpd_userisadmin || + global_authisa(httpd_authstate, IMAPOPT_PROXYSERVERS)) { + /* admin or proxy from frontend - won't have DAV database */ + return; + } + else if (config_mupdate_server && !config_getstring(IMAPOPT_PROXYSERVERS)) { + /* proxy-only server - won't have DAV database */ + } + else { + /* Open CalDAV DB for 'userid' */ + my_caldav_reset(); + auth_caldavdb = caldav_open_userid(userid, CALDAV_CREATE); + if (!auth_caldavdb) fatal("Unable to open CalDAV DB", EC_IOERR); + } + + /* Auto-provision calendars for 'userid' */ + + /* calendar-home-set */ + r = mboxlist_lookup(mailboxname, NULL, NULL); + if (r == IMAP_MAILBOX_NONEXISTENT) { + if (config_mupdate_server) { + /* Find location of INBOX */ + const char *inboxname = mboxname_user_mbox(userid, NULL); + mbentry_t *mbentry = NULL; + + r = http_mlookup(inboxname, &mbentry, NULL); + if (!r && mbentry->server) { + proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + return; + } + mboxlist_entry_free(&mbentry); + } + else r = 0; + + /* will have been overwritten */ + mailboxname = caldav_mboxname(userid, NULL); + + /* Create locally */ + r = mboxlist_createmailbox(mailboxname, MBTYPE_CALENDAR, + NULL, 0, + userid, httpd_authstate, + 0, 0, 0, 0, NULL); + if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)", + mailboxname, error_message(r)); + } + + /* Default calendar */ + mailboxname = caldav_mboxname(userid, SCHED_DEFAULT); + r = mboxlist_lookup(mailboxname, NULL, NULL); + if (r == IMAP_MAILBOX_NONEXISTENT) { + r = mboxlist_createmailbox(mailboxname, MBTYPE_CALENDAR, + NULL, 0, + userid, httpd_authstate, + 0, 0, 0, 0, NULL); + if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)", + mailboxname, error_message(r)); + } + + /* Scheduling Inbox */ + mailboxname = caldav_mboxname(userid, SCHED_INBOX); + r = mboxlist_lookup(mailboxname, NULL, NULL); + if (r == IMAP_MAILBOX_NONEXISTENT) { + r = mboxlist_createmailbox(mailboxname, MBTYPE_CALENDAR, + NULL, 0, + userid, httpd_authstate, + 0, 0, 0, 0, NULL); + if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)", + mailboxname, error_message(r)); + } + + /* Scheduling Outbox */ + mailboxname = caldav_mboxname(userid, SCHED_OUTBOX); + r = mboxlist_lookup(mailboxname, NULL, NULL); + if (r == IMAP_MAILBOX_NONEXISTENT) { + r = mboxlist_createmailbox(mailboxname, MBTYPE_CALENDAR, + NULL, 0, + userid, httpd_authstate, + 0, 0, 0, 0, NULL); + if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)", + mailboxname, error_message(r)); + } +} + + +static void my_caldav_reset(void) +{ + if (auth_caldavdb) caldav_close(auth_caldavdb); + auth_caldavdb = NULL; +} + + +static void my_caldav_shutdown(void) +{ + buf_free(&ical_prodid_buf); + freestrlist(cua_domains); + + caldav_done(); +} + + +/* Parse request-target path in CalDAV namespace */ +static int caldav_parse_path(const char *path, + struct request_target_t *tgt, const char **errstr) +{ + char *p; + size_t len; + struct mboxname_parts parts; + struct buf boxbuf = BUF_INITIALIZER; + + /* Make a working copy of target path */ + strlcpy(tgt->path, path, sizeof(tgt->path)); + tgt->tail = tgt->path + strlen(tgt->path); + + p = tgt->path; + + /* Sanity check namespace */ + len = strlen(namespace_calendar.prefix); + if (strlen(p) < len || + strncmp(namespace_calendar.prefix, p, len) || + (path[len] && path[len] != '/')) { + *errstr = "Namespace mismatch request target path"; + return HTTP_FORBIDDEN; + } + + /* Default to bare-bones Allow bits for toplevel collections */ + tgt->allow &= ~(ALLOW_POST|ALLOW_WRITE|ALLOW_DELETE); + + /* Skip namespace */ + p += len; + if (!*p || !*++p) return 0; + + /* Check if we're in user space */ + len = strcspn(p, "/"); + if (!strncmp(p, "user", len)) { + p += len; + if (!*p || !*++p) return 0; + + /* Get user id */ + len = strcspn(p, "/"); + tgt->user = p; + tgt->userlen = len; + + p += len; + if (!*p || !*++p) { + /* Make sure calendar-home-set is terminated with '/' */ + if (p[-1] != '/') *p++ = '/'; + goto done; + } + + len = strcspn(p, "/"); + } + + /* Get collection */ + tgt->collection = p; + tgt->collen = len; + + p += len; + if (!*p || !*++p) { + /* Make sure collection is terminated with '/' */ + if (p[-1] != '/') *p++ = '/'; + goto done; + } + + /* Get resource */ + len = strcspn(p, "/"); + tgt->resource = p; + tgt->reslen = len; + + p += len; + + if (*p) { +// *errstr = "Too many segments in request target path"; + return HTTP_NOT_FOUND; + } + + done: + /* Set proper Allow bits and flags based on path components */ + if (tgt->collection) { + if (!strncmp(tgt->collection, SCHED_INBOX, strlen(SCHED_INBOX))) + tgt->flags = TGT_SCHED_INBOX; + else if (!strncmp(tgt->collection, SCHED_OUTBOX, strlen(SCHED_OUTBOX))) + tgt->flags = TGT_SCHED_OUTBOX; + + if (tgt->resource) { + if (!tgt->flags) tgt->allow |= ALLOW_WRITE; + tgt->allow |= ALLOW_DELETE; + tgt->allow &= ~ALLOW_WRITECOL; + } + else if (tgt->flags != TGT_SCHED_INBOX) { + tgt->allow |= ALLOW_POST; + + if (strcmp(tgt->collection, SCHED_DEFAULT)) + tgt->allow |= ALLOW_DELETE; + } + } + else if (tgt->user) tgt->allow |= ALLOW_DELETE; + + /* Create mailbox name from the parsed path */ + + mboxname_init_parts(&parts); + + if (tgt->user && tgt->userlen) { + /* holy "avoid copying" batman */ + char *userid = xstrndup(tgt->user, tgt->userlen); + mboxname_userid_to_parts(userid, &parts); + free(userid); + } + + buf_setcstr(&boxbuf, config_getstring(IMAPOPT_CALENDARPREFIX)); + if (tgt->collen) { + buf_putc(&boxbuf, '.'); + buf_appendmap(&boxbuf, tgt->collection, tgt->collen); + } + parts.box = buf_release(&boxbuf); + + mboxname_parts_to_internal(&parts, tgt->mboxname); + + mboxname_free_parts(&parts); + + return 0; +} + + +/* Check headers for any preconditions */ +static int caldav_check_precond(struct transaction_t *txn, const void *data, + const char *etag, time_t lastmod) +{ + const struct caldav_data *cdata = (const struct caldav_data *) data; + const char *stag = cdata ? cdata->sched_tag : NULL; + const char **hdr; + int precond; + + /* Do normal WebDAV/HTTP checks (primarily for lock-token via If header) */ + precond = check_precond(txn, data, etag, lastmod); + if (!(precond == HTTP_OK || precond == HTTP_PARTIAL)) return precond; + + /* Per RFC 6638, check Schedule-Tag */ + if ((hdr = spool_getheader(txn->req_hdrs, "If-Schedule-Tag-Match"))) { + /* Special case for Apple 'If-Schedule-Tag-Match:' with no value + * and also no schedule tag on the record - let that match */ + if (cdata && !stag && !hdr[0][0]) return precond; + if (etagcmp(hdr[0], stag)) return HTTP_PRECOND_FAILED; + } + + if (txn->meth == METH_GET || txn->meth == METH_HEAD) { + /* Fill in Schedule-Tag for successful GET/HEAD */ + txn->resp_body.stag = stag; + } + + return precond; +} + + +static int caldav_acl(struct transaction_t *txn, xmlNodePtr priv, int *rights) +{ + if (!xmlStrcmp(priv->ns->href, BAD_CAST XML_NS_CALDAV)) { + /* CalDAV privileges */ + switch (txn->req_tgt.flags) { + case TGT_SCHED_INBOX: + if (!xmlStrcmp(priv->name, BAD_CAST "schedule-deliver")) + *rights |= DACL_SCHED; + else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-deliver-invite")) + *rights |= DACL_INVITE; + else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-deliver-reply")) + *rights |= DACL_REPLY; + else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-query-freebusy")) + *rights |= DACL_SCHEDFB; + else { + /* DAV:not-supported-privilege */ + txn->error.precond = DAV_SUPP_PRIV; + } + break; + case TGT_SCHED_OUTBOX: + if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send")) + *rights |= DACL_SCHED; + else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send-invite")) + *rights |= DACL_INVITE; + else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send-reply")) + *rights |= DACL_REPLY; + else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send-freebusy")) + *rights |= DACL_SCHEDFB; + else { + /* DAV:not-supported-privilege */ + txn->error.precond = DAV_SUPP_PRIV; + } + break; + default: + if (!xmlStrcmp(priv->name, BAD_CAST "read-free-busy")) + *rights |= DACL_READFB; + else { + /* DAV:not-supported-privilege */ + txn->error.precond = DAV_SUPP_PRIV; + } + break; + } + + /* Done processing this priv */ + return 1; + } + else if (!xmlStrcmp(priv->ns->href, BAD_CAST XML_NS_DAV)) { + /* WebDAV privileges */ + if (!xmlStrcmp(priv->name, BAD_CAST "all")) { + switch (txn->req_tgt.flags) { + case TGT_SCHED_INBOX: + /* DAV:all aggregates CALDAV:schedule-deliver */ + *rights |= DACL_SCHED; + break; + case TGT_SCHED_OUTBOX: + /* DAV:all aggregates CALDAV:schedule-send */ + *rights |= DACL_SCHED; + break; + default: + /* DAV:all aggregates CALDAV:read-free-busy */ + *rights |= DACL_READFB; + break; + } + } + else if (!xmlStrcmp(priv->name, BAD_CAST "read")) { + switch (txn->req_tgt.flags) { + case TGT_SCHED_INBOX: + case TGT_SCHED_OUTBOX: + break; + default: + /* DAV:read aggregates CALDAV:read-free-busy */ + *rights |= DACL_READFB; + break; + } + } + } + + /* Process this priv in meth_acl() */ + return 0; +} + + +/* Perform a COPY/MOVE request + * + * preconditions: + * CALDAV:supported-calendar-data + * CALDAV:valid-calendar-data + * CALDAV:valid-calendar-object-resource + * CALDAV:supported-calendar-component + * CALDAV:no-uid-conflict (DAV:href) + * CALDAV:calendar-collection-location-ok + * CALDAV:max-resource-size + * CALDAV:min-date-time + * CALDAV:max-date-time + * CALDAV:max-instances + * CALDAV:max-attendees-per-instance + */ +static int caldav_copy(struct transaction_t *txn, + struct mailbox *src_mbox, struct index_record *src_rec, + struct mailbox *dest_mbox, const char *dest_rsrc, + struct caldav_db *dest_davdb, + unsigned overwrite, unsigned flags) +{ + int r; + + const char *organizer = NULL; + icalcomponent *ical, *comp; + icalproperty *prop; + const char *base; + size_t len; + + /* Load message containing the resource and parse iCal data */ + r = mailbox_map_message(src_mbox, src_rec->uid, &base, &len); + if (r) return r; + ical = icalparser_parse_string(base + src_rec->header_size); + mailbox_unmap_message(src_mbox, src_rec->uid, &base, &len); + + if (!ical) { + txn->error.precond = CALDAV_VALID_DATA; + return HTTP_FORBIDDEN; + } + + /* Finished our initial read of source mailbox */ + mailbox_unlock_index(src_mbox, NULL); + + if (namespace_calendar.allow & ALLOW_CAL_SCHED) { + comp = icalcomponent_get_first_real_component(ical); + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + if (prop) organizer = icalproperty_get_organizer(prop); + if (organizer) flags |= NEW_STAG; + } + + /* Store source resource at destination */ + r = store_resource(txn, ical, dest_mbox, dest_rsrc, dest_davdb, + overwrite, flags); + + icalcomponent_free(ical); + + return r; +} + + +/* Perform scheduling actions for a DELETE request */ +static int caldav_delete_sched(struct transaction_t *txn, + struct mailbox *mailbox, + struct index_record *record, void *data) +{ + struct caldav_data *cdata = (struct caldav_data *) data; + int r = 0; + + if (!(namespace_calendar.allow & ALLOW_CAL_SCHED)) return 0; + + /* Only process deletes on regular calendar collections */ + if (txn->req_tgt.flags) return 0; + + if (!record) { + /* XXX DELETE collection - check all resources for sched objects */ + } + else if (cdata->sched_tag) { + /* Scheduling object resource */ + const char *userid, *organizer, **hdr; + icalcomponent *ical, *comp; + icalproperty *prop; + struct sched_param sparam; + const char *base; + size_t len; + + /* Load message containing the resource and parse iCal data */ + r = mailbox_map_message(mailbox, record->uid, &base, &len); + if (r) return r; + ical = icalparser_parse_string(base + record->header_size); + mailbox_unmap_message(mailbox, record->uid, &base, &len); + + if (!ical) { + syslog(LOG_ERR, + "meth_delete: failed to parse iCalendar object %s:%u", + txn->req_tgt.mboxname, record->uid); + return HTTP_SERVER_ERROR; + } + + /* Construct userid corresponding to mailbox */ + userid = mboxname_to_userid(txn->req_tgt.mboxname); + + /* Grab the organizer */ + comp = icalcomponent_get_first_real_component(ical); + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + organizer = icalproperty_get_organizer(prop); + + if (caladdress_lookup(organizer, &sparam)) { + syslog(LOG_ERR, + "meth_delete: failed to process scheduling message in %s" + " (org=%s, att=%s)", + txn->req_tgt.mboxname, organizer, userid); + txn->error.desc = "Failed to lookup organizer address\r\n"; + r = HTTP_SERVER_ERROR; + goto done; + } + + if (!strcmpsafe(sparam.userid, userid)) { + /* Organizer scheduling object resource */ + sched_request(organizer, &sparam, ical, NULL, 0); + } + else if (!(hdr = spool_getheader(txn->req_hdrs, "Schedule-Reply")) || + strcmp(hdr[0], "F")) { + /* Attendee scheduling object resource */ + sched_reply(userid, ical, NULL); + } + + done: + icalcomponent_free(ical); + } + + return r; +} + +static const char *begin_icalendar(struct buf *buf) +{ + /* Begin iCalendar stream */ + buf_setcstr(buf, "BEGIN:VCALENDAR\r\n"); + buf_printf(buf, "PRODID:%s\r\n", ical_prodid); + buf_appendcstr(buf, "VERSION:2.0\r\n"); + + return ""; +} + +static void end_icalendar(struct buf *buf) +{ + /* End iCalendar stream */ + buf_setcstr(buf, "END:VCALENDAR\r\n"); +} + +static int dump_calendar(struct transaction_t *txn, int rights) +{ + int ret = 0, r, precond; + struct resp_body_t *resp_body = &txn->resp_body; + struct buf *buf = &resp_body->payload; + struct mailbox *mailbox = NULL; + static char etag[33]; + uint32_t recno; + struct index_record record; + struct hash_table tzid_table; + static const char *displayname_annot = + ANNOT_NS "<" XML_NS_DAV ">displayname"; + struct buf attrib = BUF_INITIALIZER; + const char **hdr, *sep; + struct mime_type_t *mime = NULL; + + /* Check rights */ + if ((rights & DACL_READ) != DACL_READ) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_READ; + return HTTP_NO_PRIVS; + } + + /* Check requested MIME type: + 1st entry in caldav_mime_types array MUST be default MIME type */ + if ((hdr = spool_getheader(txn->req_hdrs, "Accept"))) + mime = get_accept_type(hdr, caldav_mime_types); + else mime = caldav_mime_types; + if (!mime) return HTTP_NOT_ACCEPTABLE; + + /* Open mailbox for reading */ + r = mailbox_open_irl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Check any preconditions */ + sprintf(etag, "%u-%u-%u", + mailbox->i.uidvalidity, mailbox->i.last_uid, mailbox->i.exists); + precond = caldav_check_precond(txn, NULL, etag, mailbox->index_mtime); + + switch (precond) { + case HTTP_OK: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, Expires, and Cache-Control */ + txn->resp_body.etag = etag; + txn->resp_body.lastmod = mailbox->index_mtime; + txn->resp_body.maxage = 3600; /* 1 hr */ + txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE; /* don't use stale data */ + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + /* Setup for chunked response */ + txn->flags.te |= TE_CHUNKED; + txn->flags.vary |= VARY_ACCEPT; + txn->resp_body.type = mime->content_type; + + /* Set filename of resource */ + r = annotatemore_lookupmask(mailbox->name, displayname_annot, httpd_userid, &attrib); + /* fall back to last part of mailbox name */ + if (r || !attrib.len) buf_setcstr(&attrib, strrchr(mailbox->name, '.') + 1); + + buf_reset(&txn->buf); + buf_printf(&txn->buf, "%s.%s", buf_cstring(&attrib), mime->file_ext); + txn->resp_body.fname = buf_cstring(&txn->buf); + + /* Short-circuit for HEAD request */ + if (txn->meth == METH_HEAD) { + response_header(HTTP_OK, txn); + return 0; + } + + /* iCalendar data in response should not be transformed */ + txn->flags.cc |= CC_NOTRANSFORM; + + /* Create hash table for TZIDs */ + construct_hash_table(&tzid_table, 10, 1); + + /* Begin (converted) iCalendar stream */ + sep = mime->begin_stream(buf); + write_body(HTTP_OK, txn, buf_cstring(buf), buf_len(buf)); + + for (r = 0, recno = 1; recno <= mailbox->i.num_records; recno++) { + const char *data; + size_t len; + icalcomponent *ical; + + if (mailbox_read_index_record(mailbox, recno, &record)) continue; + + if (record.system_flags & (FLAG_EXPUNGED | FLAG_DELETED)) continue; + + /* Map and parse existing iCalendar resource */ + if (mailbox_map_message(mailbox, record.uid, &data, &len)) continue; + ical = icalparser_parse_string(data + record.header_size); + mailbox_unmap_message(mailbox, record.uid, &data, &len); + + if (ical) { + icalcomponent *comp; + + for (comp = icalcomponent_get_first_component(ical, + ICAL_ANY_COMPONENT); + comp; + comp = icalcomponent_get_next_component(ical, + ICAL_ANY_COMPONENT)) { + char *cal_str; + icalcomponent_kind kind = icalcomponent_isa(comp); + + /* Don't duplicate any TZIDs in our iCalendar */ + if (kind == ICAL_VTIMEZONE_COMPONENT) { + icalproperty *prop = + icalcomponent_get_first_property(comp, + ICAL_TZID_PROPERTY); + const char *tzid = icalproperty_get_tzid(prop); + + if (hash_lookup(tzid, &tzid_table)) continue; + else hash_insert(tzid, (void *)0xDEADBEEF, &tzid_table); + } + + /* Include this component in our iCalendar */ + if (r++ && *sep) { + /* Add separator, if necessary */ + buf_reset(buf); + buf_printf_markup(buf, 0, sep); + write_body(0, txn, buf_cstring(buf), buf_len(buf)); + } + cal_str = mime->to_string(comp); + write_body(0, txn, cal_str, strlen(cal_str)); + free(cal_str); + } + + icalcomponent_free(ical); + } + } + + free_hash_table(&tzid_table, NULL); + + /* End (converted) iCalendar stream */ + mime->end_stream(buf); + write_body(0, txn, buf_cstring(buf), buf_len(buf)); + + /* End of output */ + write_body(0, txn, NULL, 0); + + done: + buf_free(&attrib); + mailbox_close(&mailbox); + + return ret; +} + + +/* + * mboxlist_findall() callback function to list calendars + */ + +struct list_cal_rock { + struct transaction_t *txn; + const char *proto; + const char *host; + const char *base; + unsigned *level; +}; + +static int list_cal_cb(char *name, + int matchlen __attribute__((unused)), + int maycreate __attribute__((unused)), + void *rock) +{ + struct list_cal_rock *lrock = (struct list_cal_rock *) rock; + struct transaction_t *txn = lrock->txn; + struct buf *body = &txn->resp_body.payload; + const char *base_path = txn->req_tgt.path, *calstyle = ""; + unsigned *level = lrock->level; + static size_t inboxlen = 0; + static size_t outboxlen = 0; + static size_t defaultlen = 0; + char *shortname; + mbentry_t *mbentry = NULL; + size_t len; + int r, rights, isdefault = 0; + static const char *displayname_annot = + ANNOT_NS "<" XML_NS_DAV ">displayname"; + struct buf displayname = BUF_INITIALIZER; + + if (!inboxlen) inboxlen = strlen(SCHED_INBOX) - 1; + if (!outboxlen) outboxlen = strlen(SCHED_OUTBOX) - 1; + if (!defaultlen) defaultlen = strlen(SCHED_DEFAULT) - 1; + + shortname = strrchr(name, '.') + 1; + len = strlen(shortname); + + /* Don't list scheduling Inbox/Outbox */ + if ((len == inboxlen && !strncmp(shortname, SCHED_INBOX, inboxlen)) || + (len == outboxlen && !strncmp(shortname, SCHED_OUTBOX, outboxlen))) + goto done; + + /* Don't list deleted mailboxes */ + if (mboxname_isdeletedmailbox(name, 0)) goto done; + + /* Lookup the mailbox and make sure its readable */ + r = http_mlookup(name, &mbentry, NULL); + if (r || !((rights = cyrus_acl_myrights(httpd_authstate, mbentry->acl)) & ACL_READ)) + goto done; + + /* Is this the default calendar? */ + if (len == defaultlen && !strncmp(shortname, SCHED_DEFAULT, defaultlen)) { + isdefault = 1; + calstyle = "<b>"; + } + + /* Send a body chunk once in a while */ + if (buf_len(body) > PROT_BUFSIZE) { + write_body(0, txn, buf_cstring(body), buf_len(body)); + buf_reset(body); + } + + /* Lookup DAV:displayname */ + r = annotatemore_lookupmask(name, displayname_annot, httpd_userid, &displayname); + /* fall back to the last part of the mailbox name */ + if (r || !displayname.len) buf_setcstr(&displayname, shortname); + + /* Add available calendar with link */ + buf_printf_markup(body, (*level)++, "<tr>"); + buf_printf_markup(body, *level, "<td>%s%s%s", + calstyle, buf_cstring(&displayname), calstyle); + + buf_printf_markup(body, *level, + "<td><a href=\"webcal://%s%s%s\">Subscribe</a>", + lrock->host, base_path, shortname); + + buf_printf_markup(body, *level, + "<td><a href=\"%s%s\">Download</a>", + base_path, shortname); + + if (!isdefault && (rights & DACL_RMCOL)) { + buf_printf_markup(body, *level, + "<td><a href=\"%s%s?action=delete\">Delete</a>", + base_path, shortname); + } + + --(*level); +done: + buf_free(&displayname); + mboxlist_entry_free(&mbentry); + + return 0; +} + + +struct list_tzid_rock { + struct buf *body; + unsigned *level; +}; + +int list_tzid_cb(const char *tzid, + int tzidlen __attribute__((unused)), + struct zoneinfo *zi __attribute__((unused)), + void *rock) +{ + struct list_tzid_rock *tzrock = (struct list_tzid_rock *) rock; + + /* Skip Etc and other non-standard zones */ + if (strchr(tzid, '/') && strncmp(tzid, "Etc/", 4)) { + buf_printf_markup(tzrock->body, *tzrock->level, + "<option>%s</option>", tzid); + } + + return 0; +} + + +/* Create a HTML document listing all calendars available to the user */ +static int action_list(struct transaction_t *txn, int rights) +{ + int ret = 0, precond; + char mboxlist[MAX_MAILBOX_PATH+1]; + struct stat sbuf; + time_t lastmod; + const char *etag; + unsigned level = 0; + struct buf *body = &txn->resp_body.payload; + struct list_cal_rock lrock; + + /* Check rights */ + if ((rights & DACL_READ) != DACL_READ) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_READ; + return HTTP_NO_PRIVS; + } + + /* stat() mailboxes.db for Last-Modified and ETag */ + snprintf(mboxlist, MAX_MAILBOX_PATH, "%s%s", config_dir, FNAME_MBOXLIST); + stat(mboxlist, &sbuf); + lastmod = MAX(compile_time, sbuf.st_mtime); + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%ld-%ld-%ld", + compile_time, sbuf.st_mtime, sbuf.st_size); + + /* stat() config file for Last-Modified and ETag */ + stat(config_filename, &sbuf); + lastmod = MAX(lastmod, sbuf.st_mtime); + buf_printf(&txn->buf, "-%ld-%ld", sbuf.st_mtime, sbuf.st_size); + etag = buf_cstring(&txn->buf); + + /* Check any preconditions */ + precond = caldav_check_precond(txn, NULL, etag, lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, and Expires */ + txn->resp_body.etag = etag; + txn->resp_body.lastmod = lastmod; + txn->flags.cc |= CC_REVALIDATE; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + /* Setup for chunked response */ + txn->flags.te |= TE_CHUNKED; + txn->resp_body.type = "text/html; charset=utf-8"; + + /* Short-circuit for HEAD request */ + if (txn->meth == METH_HEAD) { + response_header(HTTP_OK, txn); + goto done; + } + + /* Send HTML header */ + buf_reset(body); + buf_printf_markup(body, level, HTML_DOCTYPE); + buf_printf_markup(body, level++, "<html>"); + buf_printf_markup(body, level++, "<head>"); + buf_printf_markup(body, level, "<title>%s</title>", "Available Calendars"); + buf_printf_markup(body, --level, "</head>"); + buf_printf_markup(body, level++, "<body>"); + buf_printf_markup(body, level, "<h2>%s</h2>", "Available Calendars"); + buf_printf_markup(body, level++, "<table border cellpadding=5>"); + write_body(HTTP_OK, txn, buf_cstring(body), buf_len(body)); + buf_reset(body); + + /* Populate list callback rock */ + http_proto_host(txn->req_hdrs, &lrock.proto, &lrock.host); + lrock.txn = txn; + lrock.level = &level; + + /* Generate list of calendars */ + strlcat(txn->req_tgt.mboxname, ".%", sizeof(txn->req_tgt.mboxname)); + + mboxlist_findall(NULL, txn->req_tgt.mboxname, 1, httpd_userid, + httpd_authstate, list_cal_cb, &lrock); + + if (buf_len(body)) write_body(0, txn, buf_cstring(body), buf_len(body)); + + /* Finish list */ + buf_reset(body); + buf_printf_markup(body, --level, "</table>"); + + if (rights & DACL_MKCOL) { + /* Add "create" form */ + const struct cal_comp_t *comp; + struct list_tzid_rock tzrock = { body, &level }; + + buf_printf_markup(body, level, "<p><hr>"); + buf_printf_markup(body, level, "<h3>%s</h3>", "Create New Calendar"); + buf_printf_markup(body, level++, + "<form method=GET action=\"%s\">", + txn->req_tgt.path); + buf_printf_markup(body, level, + "<input type=hidden name=action value=create>"); + buf_printf_markup(body, level++, "<table cellpadding=5>"); + buf_printf_markup(body, level++, "<tr>"); + buf_printf_markup(body, level, "<td align=right>Name:"); + buf_printf_markup(body, level--, + "<td><input name=name size=30 maxlength=40>"); + buf_printf_markup(body, level++, "<tr>"); + buf_printf_markup(body, level, "<td align=right>Description:"); + buf_printf_markup(body, level--, + "<td><input name=desc size=75 maxlength=120>"); + + buf_printf_markup(body, level++, "<tr>"); + buf_printf_markup(body, level, "<td align=right>Components:" + "<br><sub>(default = ALL)</sub>"); + buf_printf_markup(body, level++, "<td>"); + for (comp = cal_comps; comp->name; comp++) { + buf_printf_markup(body, level, + "<input type=checkbox name=comp value=%s>%s", + comp->name, comp->name); + } + level -= 2; + + if (namespace_calendar.allow & ALLOW_CAL_NOTZ) { + buf_printf_markup(body, level++, "<tr>"); + buf_printf_markup(body, level, "<td align=right>Time Zone:"); + buf_printf_markup(body, level++, "<td>"); + buf_printf_markup(body, level++, "<select name=tzid>"); + buf_printf_markup(body, level, "<option></option>"); + zoneinfo_find(NULL, 1, 0, &list_tzid_cb, &tzrock); + buf_printf_markup(body, --level, "</select>"); + level -= 2; + } + + buf_printf_markup(body, level++, "<tr>"); + buf_printf_markup(body, level, "<td>"); + buf_printf_markup(body, level--, + "<td><input type=submit value=Create>"); + buf_printf_markup(body, --level, "</table>"); + buf_printf_markup(body, --level, "</form>"); + } + + /* Finish HTML */ + buf_printf_markup(body, --level, "</body>"); + buf_printf_markup(body, --level, "</html>"); + write_body(0, txn, buf_cstring(body), buf_len(body)); + + /* End of output */ + write_body(0, txn, NULL, 0); + + done: + return ret; +} + + +/* Redirect client back to the "list" page */ +static void success_redirect(struct transaction_t *txn, const char *op) +{ + struct buf *body = &txn->resp_body.payload; + unsigned level = 0; + + txn->resp_body.type = "text/html; charset=utf-8"; + buf_reset(body); + buf_printf_markup(body, level, HTML_DOCTYPE); + buf_printf_markup(body, level++, "<html>"); + buf_printf_markup(body, level++, "<head>"); + buf_printf_markup(body, level, "<title>%s</title>", "Success"); + buf_printf_markup(body, --level, "</head>"); + buf_printf_markup(body, level++, "<body>"); + buf_printf_markup(body, level, + "<h4>%s of calendar <tt>%s</tt> was successful</h4>", + op, txn->req_tgt.path); + + *txn->req_tgt.collection = '\0'; + buf_reset(&txn->buf); + buf_printf(&txn->buf, "%s?action=list", txn->req_tgt.path); + txn->location = buf_cstring(&txn->buf); + + buf_printf_markup(body, level, + "<p>Return to <a href=\"%s\">Available Calendars</a>", + txn->location); + buf_printf_markup(body, --level, "</body>"); + buf_printf_markup(body, --level, "</html>"); + + write_body(HTTP_SEE_OTHER, txn, buf_cstring(body), buf_len(body)); +} + + +/* Create a new calendar */ +static int action_create(struct transaction_t *txn, int rights) +{ + struct buf *body = &txn->req_body.payload; + struct strlist *name, *desc, *comp, *tzid; + size_t len; + int ret; + + /* Check rights */ + if (!(rights & DACL_MKCOL)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_MKCOL; + return HTTP_NO_PRIVS; + } + + name = hash_lookup("name", &txn->req_qparams); + if (!name || name->next || !*name->s) { + txn->error.desc = "Multiple/missing/empty name parameter(s)"; + return HTTP_BAD_REQUEST; + } + + desc = hash_lookup("desc", &txn->req_qparams); + if (desc && desc->next) { + txn->error.desc = "Multiple desc parameter(s)"; + return HTTP_BAD_REQUEST; + } + + tzid = hash_lookup("tzid", &txn->req_qparams); + if (tzid && tzid->next) { + txn->error.desc = "Multiple tzid parameter(s)"; + return HTTP_BAD_REQUEST; + } + + /* Append a unique resource name to URL path */ + len = strlen(txn->req_tgt.path); + txn->req_tgt.collection = txn->req_tgt.path + len; + txn->req_tgt.collen = + snprintf(txn->req_tgt.collection, MAX_MAILBOX_PATH - len, + "%x-%x-%d-%ld", strhash(txn->req_tgt.path), + strhash(name->s), getpid(), time(0)); + + xmlFreeURI(txn->req_uri); + txn->req_uri = parse_uri(METH_MKCALENDAR, txn->req_tgt.path, 1, + &txn->error.desc); + + /* Construct a MKCALENDAR request body */ + txn->req_body.flags = BODY_DONE; + spool_cache_header(xstrdup("Content-Type"), xstrdup("application/xml"), + txn->req_hdrs); + buf_setcstr(body, XML_DECLARATION); + buf_printf(body, + "<C:mkcalendar xmlns:D=\"%s\" xmlns:C=\"%s\"><D:set><D:prop>", + XML_NS_DAV, XML_NS_CALDAV); + buf_printf(body, "<D:displayname>%s</D:displayname>", name->s); + if (desc && *desc->s) { + buf_printf(body, "<C:calendar-description>%s</C:calendar-description>", + desc->s); + } + + comp = hash_lookup("comp", &txn->req_qparams); + if (comp) { + buf_appendcstr(body, "<C:supported-calendar-component-set>"); + do { + buf_printf(body, "<C:comp name=\"%s\"/>", comp->s); + } while ((comp = comp->next)); + buf_appendcstr(body, "</C:supported-calendar-component-set>"); + } + + if (tzid && *tzid->s) { + buf_printf(body, "<C:calendar-timezone-id>%s</C:calendar-timezone-id>", + tzid->s); + } + buf_appendcstr(body, "</D:prop></D:set></C:mkcalendar>"); + + /* Perform MKCALENDAR */ + ret = meth_mkcol(txn, &caldav_params); + + if (ret == HTTP_CREATED) { + /* Success - tell client to go back to list page */ + success_redirect(txn, "Creation"); + return 0; + } + + return ret; +} + + +/* Delete a calendar */ +static int action_delete(struct transaction_t *txn, int rights) +{ + int ret; + + /* Check rights */ + if (!(rights & DACL_RMCOL)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_RMCOL; + return HTTP_NO_PRIVS; + } + + ret = meth_delete(txn, &caldav_params); + + if (ret == HTTP_NO_CONTENT) { + /* Success - tell client to go back to list page */ + success_redirect(txn, "Deletion"); + return 0; + } + + return ret; +} + + +/* Create a HTML document listing all actions available on the cal-home-set */ +static int list_actions(struct transaction_t *txn, int rights) +{ + int ret = 0, precond; + time_t lastmod = compile_time; + static char etag[21]; + unsigned level = 0; + struct buf *body = &txn->resp_body.payload; + const char *proto = NULL, *host = NULL; + + /* Check rights */ + if ((rights & DACL_READ) != DACL_READ) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_READ; + return HTTP_NO_PRIVS; + } + + sprintf(etag, "%ld", compile_time); + + /* Check any preconditions */ + precond = caldav_check_precond(txn, NULL, etag, lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, and Expires */ + txn->resp_body.etag = etag; + txn->resp_body.lastmod = lastmod; + txn->resp_body.maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + /* Setup for chunked response */ + txn->flags.te |= TE_CHUNKED; + txn->resp_body.type = "text/html; charset=utf-8"; + + /* Short-circuit for HEAD request */ + if (txn->meth == METH_HEAD) { + response_header(HTTP_OK, txn); + goto done; + } + + /* Send HTML header */ + buf_reset(body); + buf_printf_markup(body, level, HTML_DOCTYPE); + buf_printf_markup(body, level++, "<html>"); + buf_printf_markup(body, level++, "<head>"); + buf_printf_markup(body, level, "<title>%s</title>", "Available Actions"); + buf_printf_markup(body, --level, "</head>"); + buf_printf_markup(body, level++, "<body>"); + buf_printf_markup(body, level, "<h2>%s</h2>", "Available Actions"); + buf_printf_markup(body, level++, "<ul>"); + + /* Generate list of actions */ + http_proto_host(txn->req_hdrs, &proto, &host); + buf_printf_markup(body, level, + "<li><a href=\"%s://%s%s?action=%s\">%s</a></li>", + proto, host, txn->req_tgt.path, + "list", "Available Calendars"); + buf_printf_markup(body, level, + "<li><a href=\"%s://%s%s?action=%s\">%s</a></li>", + proto, host, txn->req_tgt.path, + "freebusy", "Free/Busy Query"); + + /* Finish HTML */ + buf_printf_markup(body, --level, "</ul>"); + buf_printf_markup(body, --level, "</body>"); + buf_printf_markup(body, --level, "</html>"); + write_body(HTTP_OK, txn, buf_cstring(body), buf_len(body)); + + /* End of output */ + write_body(0, txn, NULL, 0); + + done: + return ret; +} + + +/* Parse an RFC3339 date/time per + http://www.calconnect.org/pubdocs/CD0903%20Freebusy%20Read%20URL.pdf */ +static struct icaltimetype icaltime_from_rfc3339_string(const char *str) +{ + struct icaltimetype tt = icaltime_null_time(); + size_t size; + + size = strlen(str); + + if (size == 20) { + /* UTC */ + if (sscanf(str, "%4u-%02u-%02uT%02u:%02u:%02uZ", + &tt.year, &tt.month, &tt.day, + &tt.hour, &tt.minute, &tt.second) < 6) { + goto fail; + } + + tt = icaltime_normalize(tt); + } + else if (size == 25) { + /* TZ offset */ + int offset_hour, offset_minute; + char offset_sign; + + if (sscanf(str, "%4u-%02u-%02uT%02u:%02u:%02u%c%02u:%02u", + &tt.year, &tt.month, &tt.day, + &tt.hour, &tt.minute, &tt.second, + &offset_sign, &offset_hour, &offset_minute) < 9) { + goto fail; + } + + if (offset_sign == '-') { + /* negative offset */ + offset_hour *= -1; + offset_minute *= -1; + } + else if (offset_sign != '+') { + goto fail; + } + + icaltime_adjust(&tt, 0, -offset_hour, -offset_minute, 0); + } + else { + goto fail; + } + + tt.is_utc = 1; + return tt; + + fail: + return icaltime_null_time(); +} + + +/* Execute a free/busy query per + http://www.calconnect.org/pubdocs/CD0903%20Freebusy%20Read%20URL.pdf */ +static int action_freebusy(struct transaction_t *txn, int rights) +{ + int ret = 0; + struct tm *tm; + struct strlist *param; + struct mime_type_t *mime = NULL; + struct propfind_ctx fctx; + struct calquery_filter calfilter; + time_t start; + struct icaldurationtype period = icaldurationtype_null_duration(); + icaltimezone *utc = icaltimezone_get_utc_timezone(); + icalcomponent *cal; + + /* Check rights */ + if (!(rights & DACL_READFB)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_READFB; + return HTTP_NO_PRIVS; + } + + /* Check/find 'format' */ + param = hash_lookup("format", &txn->req_qparams); + if (param) { + if (param->next /* once only */) return HTTP_BAD_REQUEST; + + for (mime = caldav_mime_types; mime->content_type; mime++) { + if (is_mediatype(param->s, mime->content_type)) break; + } + } + else mime = caldav_mime_types; + + if (!mime || !mime->content_type) return HTTP_NOT_ACCEPTABLE; + + memset(&calfilter, 0, sizeof(struct calquery_filter)); + calfilter.comp = + CAL_COMP_VEVENT | CAL_COMP_VFREEBUSY | CAL_COMP_VAVAILABILITY; + calfilter.flags = BUSYTIME_QUERY | CHECK_CAL_TRANSP | CHECK_USER_AVAIL; + + /* Check for 'start' */ + param = hash_lookup("start", &txn->req_qparams); + if (param) { + if (param->next /* once only */) return HTTP_BAD_REQUEST; + + calfilter.start = icaltime_from_rfc3339_string(param->s); + if (icaltime_is_null_time(calfilter.start)) return HTTP_BAD_REQUEST; + + /* Default to end of given day */ + start = icaltime_as_timet_with_zone(calfilter.start, utc); + tm = localtime(&start); + + period.seconds = 60 - tm->tm_sec; + period.minutes = 59 - tm->tm_min; + period.hours = 23 - tm->tm_hour; + } + else { + /* Default to start of current day */ + start = time(0); + tm = localtime(&start); + tm->tm_hour = tm->tm_min = tm->tm_sec = 0; + calfilter.start = icaltime_from_timet_with_zone(mktime(tm), 0, utc); + + /* Default to 42 day period */ + period.days = 42; + } + + /* Check for 'period' */ + param = hash_lookup("period", &txn->req_qparams); + if (param) { + if (param->next /* once only */ || + hash_lookup("end", &txn->req_qparams) /* can't use with 'end' */) + return HTTP_BAD_REQUEST; + + period = icaldurationtype_from_string(param->s); + if (icaldurationtype_is_bad_duration(period)) return HTTP_BAD_REQUEST; + } + + /* Check for 'end' */ + param = hash_lookup("end", &txn->req_qparams); + if (param) { + if (param->next /* once only */) return HTTP_BAD_REQUEST; + + calfilter.end = icaltime_from_rfc3339_string(param->s); + if (icaltime_is_null_time(calfilter.end)) return HTTP_BAD_REQUEST; + } + else { + /* Set end based on period */ + calfilter.end = icaltime_add(calfilter.start, period); + } + + + memset(&fctx, 0, sizeof(struct propfind_ctx)); + fctx.req_tgt = &txn->req_tgt; + fctx.depth = 2; + fctx.userid = proxy_userid; + fctx.userisadmin = httpd_userisadmin; + fctx.authstate = httpd_authstate; + fctx.reqd_privs = 0; /* handled by CALDAV:schedule-deliver on Inbox */ + fctx.filter = apply_calfilter; + fctx.filter_crit = &calfilter; + fctx.err = &txn->error; + fctx.ret = &ret; + fctx.fetcheddata = 0; + + cal = busytime_query_local(txn, &fctx, txn->req_tgt.mboxname, + 0, NULL, NULL, NULL); + + if (calfilter.freebusy.fb) free(calfilter.freebusy.fb); + + if (cal) { + const char *proto, *host; + icalcomponent *fb; + icalproperty *url; + char *cal_str; + + /* Construct URL */ + buf_reset(&txn->buf); + http_proto_host(txn->req_hdrs, &proto, &host); + buf_printf(&txn->buf, "%s://%s%s/user/%.*s/?%s", + proto, host, namespace_calendar.prefix, + (int) txn->req_tgt.userlen, txn->req_tgt.user, + URI_QUERY(txn->req_uri)); + + /* Set URL property */ + fb = icalcomponent_get_first_component(cal, ICAL_VFREEBUSY_COMPONENT); + url = icalproperty_new_url(buf_cstring(&txn->buf)); + icalcomponent_add_property(fb, url); + + /* Set filename of resource */ + buf_reset(&txn->buf); + buf_printf(&txn->buf, "%.*s.%s", + (int) txn->req_tgt.userlen, txn->req_tgt.user, + mime->file_ext2); + txn->resp_body.fname = buf_cstring(&txn->buf); + + txn->resp_body.type = mime->content_type; + + /* iCalendar data in response should not be transformed */ + txn->flags.cc |= CC_NOTRANSFORM; + + /* Output the iCalendar object */ + cal_str = mime->to_string(cal); + icalcomponent_free(cal); + + write_body(HTTP_OK, txn, cal_str, strlen(cal_str)); + free(cal_str); + } + else ret = HTTP_NOT_FOUND; + + return ret; +} + + +static int server_info(struct transaction_t *txn) +{ + int precond; + static struct message_guid prev_guid; + struct message_guid guid; + const char *etag; + static time_t lastmod = 0; + struct stat sbuf; + static xmlChar *buf = NULL; + static int bufsiz = 0; + + if (!httpd_userid) return HTTP_UNAUTHORIZED; + + /* Initialize */ + if (!lastmod) message_guid_set_null(&prev_guid); + + /* Generate ETag based on compile date/time of this source file, + the number of available RSCALEs and the config file size/mtime */ + stat(config_filename, &sbuf); + lastmod = MAX(compile_time, sbuf.st_mtime); + + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%ld-%ld-%ld", (long) compile_time, + sbuf.st_mtime, sbuf.st_size); + + message_guid_generate(&guid, buf_cstring(&txn->buf), buf_len(&txn->buf)); + etag = message_guid_encode(&guid); + + /* Check any preconditions, including range request */ + txn->flags.ranges = 1; + precond = check_precond(txn, NULL, etag, lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_PARTIAL: + case HTTP_NOT_MODIFIED: + /* Fill in Etag, Last-Modified, and Expires */ + txn->resp_body.etag = etag; + txn->resp_body.lastmod = lastmod; + txn->resp_body.maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + return precond; + } + + if (!message_guid_equal(&prev_guid, &guid)) { + xmlNodePtr root, node, service; + xmlNsPtr ns[NUM_NAMESPACE]; + + /* Start construction of our query-result */ + if (!(root = init_xml_response("server-info", NS_DAV, NULL, ns))) { + txn->error.desc = "Unable to create XML response"; + return HTTP_SERVER_ERROR; + } + + node = xmlNewChild(root, NULL, BAD_CAST "services", NULL); + + service = xmlNewChild(node, NULL, BAD_CAST "service", NULL); + ensure_ns(ns, NS_CALDAV, service, XML_NS_CALDAV, "C"); + xmlNewChild(service, ns[NS_CALDAV], BAD_CAST "name", BAD_CAST "caldav"); + + /* Add href */ + { + const char **hdr = spool_getheader(txn->req_hdrs, "Host"); + buf_reset(&txn->buf); + buf_printf(&txn->buf, "%s://%s", + https? "https" : "http", hdr[0]); + buf_appendcstr(&txn->buf, namespace_calendar.prefix); + xml_add_href(service, ns[NS_DAV], buf_cstring(&txn->buf)); + } + + /* Add features */ + node = xmlNewChild(service, NULL, BAD_CAST "features", NULL); + xmlNewChild(node, ns[NS_DAV], BAD_CAST "feature", BAD_CAST "1"); + xmlNewChild(node, ns[NS_DAV], BAD_CAST "feature", BAD_CAST "2"); + xmlNewChild(node, ns[NS_DAV], BAD_CAST "feature", BAD_CAST "3"); + xmlNewChild(node, ns[NS_DAV], BAD_CAST "feature", + BAD_CAST "access-control"); + xmlNewChild(node, ns[NS_DAV], BAD_CAST "feature", + BAD_CAST "extended-mkcol"); + xmlNewChild(node, ns[NS_CALDAV], BAD_CAST "feature", + BAD_CAST "calendar-access"); + if (namespace_calendar.allow & ALLOW_CAL_AVAIL) + xmlNewChild(node, ns[NS_CALDAV], BAD_CAST "feature", + BAD_CAST "calendar-availability"); + if (namespace_calendar.allow & ALLOW_CAL_SCHED) + xmlNewChild(node, ns[NS_CALDAV], BAD_CAST "feature", + BAD_CAST "calendar-auto-schedule"); + + /* Add properties */ + { + struct propfind_ctx fctx; + struct request_target_t req_tgt; + struct mailbox mailbox; + struct index_record record; + struct propstat propstat[PROPSTAT_OK+1]; + + /* Setup a dummy propfind_ctx and propstat */ + memset(&fctx, 0, sizeof(struct propfind_ctx)); + memset(&req_tgt, 0, sizeof(struct request_target_t)); + fctx.req_tgt = &req_tgt; + fctx.req_tgt->collection = ""; + fctx.mailbox = &mailbox; + fctx.mailbox->name = ""; + fctx.record = &record; + fctx.ns = ns; + + propstat[PROPSTAT_OK].root = xmlNewNode(NULL, BAD_CAST ""); + node = xmlNewChild(propstat[PROPSTAT_OK].root, + ns[NS_DAV], BAD_CAST "properties", NULL); + + propfind_suplock(BAD_CAST "supportedlock", ns[NS_DAV], + &fctx, NULL, &propstat[PROPSTAT_OK], NULL); + propfind_aclrestrict(BAD_CAST "acl-restrictions", ns[NS_DAV], + &fctx, NULL, &propstat[PROPSTAT_OK], NULL); + propfind_suppcaldata(BAD_CAST "supported-calendar-data", + ns[NS_CALDAV], + &fctx, NULL, &propstat[PROPSTAT_OK], NULL); + propfind_calcompset(BAD_CAST "supported-calendar-component-sets", + ns[NS_CALDAV], + &fctx, NULL, &propstat[PROPSTAT_OK], NULL); + propfind_rscaleset(BAD_CAST "supported-rscale-set", ns[NS_CALDAV], + &fctx, NULL, &propstat[PROPSTAT_OK], NULL); + propfind_maxsize(BAD_CAST "max-resource-size", ns[NS_CALDAV], + &fctx, NULL, &propstat[PROPSTAT_OK], NULL); + propfind_minmaxdate(BAD_CAST "min-date-time", ns[NS_CALDAV], + &fctx, NULL, &propstat[PROPSTAT_OK], + &caldav_epoch); + propfind_minmaxdate(BAD_CAST "max-date-time", ns[NS_CALDAV], + &fctx, NULL, &propstat[PROPSTAT_OK], + &caldav_eternity); + + /* Unlink properties from propstat and link to service */ + xmlUnlinkNode(node); + xmlAddChild(service, node); + + /* Cleanup */ + xmlFreeNode(propstat[PROPSTAT_OK].root); + buf_free(&fctx.buf); + } + + /* Dump XML response tree into a text buffer */ + if (buf) xmlFree(buf); + xmlDocDumpFormatMemoryEnc(root->doc, &buf, &bufsiz, "utf-8", 1); + xmlFreeDoc(root->doc); + + if (!buf) { + txn->error.desc = "Error dumping XML tree"; + return HTTP_SERVER_ERROR; + } + + message_guid_copy(&prev_guid, &guid); + } + + /* Output the XML response */ + txn->resp_body.type = "application/xml; charset=utf-8"; + write_body(precond, txn, (char *) buf, bufsiz); + + return 0; +} + + +/* Perform a GET/HEAD request on a CalDAV resource */ +static int meth_get(struct transaction_t *txn, void *params) +{ + struct meth_params *gparams = (struct meth_params *) params; + struct strlist *action; + int r, rights; + mbentry_t *mbentry = NULL; + + action = hash_lookup("action", &txn->req_qparams); + if (action) { + if (action->next) return HTTP_BAD_REQUEST; + + /* GET server-info resource */ + if (!strcmp(action->s, "server-info")) return server_info(txn); + } + + /* Parse the path */ + if ((r = gparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) return r; + + /* GET an individual resource */ + if (txn->req_tgt.resource) return meth_get_dav(txn, gparams); + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + /* Local Mailbox */ + + /* Check ACL for current user */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + mboxlist_entry_free(&mbentry); + + if (txn->req_tgt.collection) { + /* Download an entire calendar collection */ + if (!action) return dump_calendar(txn, rights); + + /* Delete a calendar collection */ + if (!strcmp(action->s, "delete")) return action_delete(txn, rights); + } + else { + /* Display available actions HTML page */ + if (!action) return list_actions(txn, rights); + + /* GET a list of calendars under calendar-home-set */ + if (!strcmp(action->s, "list")) return action_list(txn, rights); + + /* Create a new calendar under calendar-home-set */ + if (!strcmp(action->s, "create")) return action_create(txn, rights); + + /* GET busytime of the user */ + if (!strcmp(action->s, "freebusy")) return action_freebusy(txn, rights); + } + + /* Unknown action */ + return HTTP_BAD_REQUEST; +} + + +/* Perform a busy time request, if necessary */ +static int caldav_post(struct transaction_t *txn) +{ + int ret = 0, r, rights; + char orgid[MAX_MAILBOX_NAME+1] = ""; + mbentry_t *mbentry = NULL; + const char **hdr; + struct mime_type_t *mime = NULL; + icalcomponent *ical = NULL, *comp; + icalcomponent_kind kind = 0; + icalproperty_method meth = 0; + icalproperty *prop = NULL; + const char *uid = NULL, *organizer = NULL; + struct sched_param sparam; + + if (!(namespace_calendar.allow & ALLOW_CAL_SCHED) || !txn->req_tgt.flags) { + /* POST to regular calendar collection */ + return HTTP_CONTINUE; + } + else if (txn->req_tgt.flags == TGT_SCHED_INBOX) { + /* Don't allow POST to schedule-inbox */ + return HTTP_NOT_ALLOWED; + } + + /* POST to schedule-outbox */ + + /* Check Content-Type */ + if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) { + for (mime = caldav_mime_types; mime->content_type; mime++) { + if (is_mediatype(mime->content_type, hdr[0])) break; + } + } + if (!mime || !mime->content_type) { + txn->error.precond = CALDAV_SUPP_DATA; + return HTTP_BAD_REQUEST; + } + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Get rights for current user */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + mboxlist_entry_free(&mbentry); + + /* Read body */ + txn->req_body.flags |= BODY_DECODE; + r = http_read_body(httpd_in, httpd_out, + txn->req_hdrs, &txn->req_body, &txn->error.desc); + if (r) { + txn->flags.conn = CONN_CLOSE; + return r; + } + + /* Make sure we have a body */ + if (!buf_len(&txn->req_body.payload)) { + txn->error.desc = "Missing request body\r\n"; + return HTTP_BAD_REQUEST; + } + + /* Parse the iCal data for important properties */ + ical = mime->from_string(buf_cstring(&txn->req_body.payload)); + if (!ical || !icalrestriction_check(ical)) { + txn->error.precond = CALDAV_VALID_DATA; + ret = HTTP_BAD_REQUEST; + goto done; + } + + meth = icalcomponent_get_method(ical); + comp = icalcomponent_get_first_real_component(ical); + if (comp) { + uid = icalcomponent_get_uid(comp); + kind = icalcomponent_isa(comp); + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + } + + /* Check method preconditions */ + if (!meth || !uid || !prop) { + txn->error.precond = CALDAV_VALID_SCHED; + ret = HTTP_BAD_REQUEST; + goto done; + } + + /* Organizer MUST be local to use CalDAV Scheduling */ + organizer = icalproperty_get_organizer(prop); + if (organizer) { + if (!caladdress_lookup(organizer, &sparam) && + !(sparam.flags & SCHEDTYPE_REMOTE)) { + strlcpy(orgid, sparam.userid, sizeof(orgid)); + mboxname_hiersep_toexternal(&httpd_namespace, orgid, 0); + } + } + + if (strncmp(orgid, txn->req_tgt.user, txn->req_tgt.userlen)) { + txn->error.precond = CALDAV_VALID_ORGANIZER; + ret = HTTP_FORBIDDEN; + goto done; + } + + switch (kind) { + case ICAL_VFREEBUSY_COMPONENT: + if (meth == ICAL_METHOD_REQUEST) + if (!(rights & DACL_SCHEDFB)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_SCHEDFB; + ret = HTTP_NO_PRIVS; + } + else ret = sched_busytime_query(txn, mime, ical); + else { + txn->error.precond = CALDAV_VALID_SCHED; + ret = HTTP_BAD_REQUEST; + } + break; + + default: + txn->error.precond = CALDAV_VALID_SCHED; + ret = HTTP_BAD_REQUEST; + } + + done: + if (ical) icalcomponent_free(ical); + + return ret; +} + + +const char *get_icalcomponent_errstr(icalcomponent *ical) +{ + icalcomponent *comp; + + for (comp = icalcomponent_get_first_component(ical, ICAL_ANY_COMPONENT); + comp; + comp = icalcomponent_get_next_component(ical, ICAL_ANY_COMPONENT)) { + icalproperty *prop; + + for (prop = icalcomponent_get_first_property(comp, ICAL_ANY_PROPERTY); + prop; + prop = icalcomponent_get_next_property(comp, ICAL_ANY_PROPERTY)) { + + if (icalproperty_isa(prop) == ICAL_XLICERROR_PROPERTY) { + icalparameter *param; + const char *errstr = icalproperty_get_xlicerror(prop); + + if (!errstr) return "Unknown iCal parsing error"; + + param = icalproperty_get_first_parameter( + prop, ICAL_XLICERRORTYPE_PARAMETER); + + if (icalparameter_get_xlicerrortype(param) == + ICAL_XLICERRORTYPE_VALUEPARSEERROR) { + /* Check if this is an empty property error */ + char propname[256]; + if (sscanf(errstr, + "No value for %s property", propname) == 1) { + /* Empty LOCATION is OK */ + if (!strcmp(propname, "LOCATION")) continue; + } + } + + return errstr; + } + } + } + + return NULL; +} + + +/* Perform a PUT request + * + * preconditions: + * CALDAV:valid-calendar-data + * CALDAV:valid-calendar-object-resource + * CALDAV:supported-calendar-component + * CALDAV:no-uid-conflict (DAV:href) + * CALDAV:max-resource-size + * CALDAV:min-date-time + * CALDAV:max-date-time + * CALDAV:max-instances + * CALDAV:max-attendees-per-instance + */ +static int caldav_put(struct transaction_t *txn, + struct mime_type_t *mime, + struct mailbox *mailbox, + struct caldav_db *davdb, + unsigned flags) +{ + int ret = 0; + icalcomponent *ical = NULL, *comp, *nextcomp; + icalcomponent_kind kind; + icalproperty_kind recip_kind; + icalproperty *prop; + const char *uid, *organizer = NULL; + + /* Parse and validate the iCal data */ + ical = mime->from_string(buf_cstring(&txn->req_body.payload)); + if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) { + txn->error.precond = CALDAV_VALID_DATA; + ret = HTTP_FORBIDDEN; + goto done; + } + + icalrestriction_check(ical); + if ((txn->error.desc = get_icalcomponent_errstr(ical))) { + assert(!buf_len(&txn->buf)); + buf_setcstr(&txn->buf, txn->error.desc); + txn->error.desc = buf_cstring(&txn->buf); + txn->error.precond = CALDAV_VALID_DATA; + ret = HTTP_FORBIDDEN; + goto done; + } + + comp = icalcomponent_get_first_real_component(ical); + +#ifdef HAVE_RSCALE + /* Make sure we support the provided RSCALE in an RRULE */ + prop = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY); + if (prop) { + struct icalrecurrencetype rt = icalproperty_get_rrule(prop); + + if (*rt.rscale) { + UEnumeration *en; + UErrorCode stat = U_ZERO_ERROR; + const char *rscale; + + ucase(rt.rscale); + while (!found && start < end) { + unsigned mid = start + (end - start) / 2; + const char **rscale = icalarray_element_at(rscale_calendars, mid); + int r = strcmp(rt.rscale, *rscale); + + if (r == 0) found = 1; + else if (r < 0) end = mid; + else start = mid + 1; + } + uenum_close(en); + + if (!rscale) { + txn->error.precond = CALDAV_SUPP_RSCALE; + ret = HTTP_FORBIDDEN; + goto done; + } + } + } +#endif /* HAVE_RSCALE */ + + /* Make sure iCal UIDs [and ORGANIZERs] in all components are the same */ + kind = icalcomponent_isa(comp); + uid = icalcomponent_get_uid(comp); + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + if (prop) organizer = icalproperty_get_organizer(prop); + while ((nextcomp = + icalcomponent_get_next_component(ical, kind))) { + const char *nextuid = icalcomponent_get_uid(nextcomp); + + if (!nextuid || strcmp(uid, nextuid)) { + txn->error.precond = CALDAV_VALID_OBJECT; + ret = HTTP_FORBIDDEN; + goto done; + } + + if (organizer) { + const char *nextorg = NULL; + + prop = icalcomponent_get_first_property(nextcomp, + ICAL_ORGANIZER_PROPERTY); + if (prop) nextorg = icalproperty_get_organizer(prop); + if (!nextorg || strcmp(organizer, nextorg)) { + txn->error.precond = CALDAV_SAME_ORGANIZER; + ret = HTTP_FORBIDDEN; + goto done; + } + } + } + + switch (kind) { + case ICAL_VEVENT_COMPONENT: + case ICAL_VTODO_COMPONENT: + case ICAL_VPOLL_COMPONENT: + recip_kind = (kind == ICAL_VPOLL_COMPONENT) ? + ICAL_VOTER_PROPERTY : ICAL_ATTENDEE_PROPERTY; + + if ((namespace_calendar.allow & ALLOW_CAL_SCHED) && organizer + /* XXX Hack for Outlook */ + && icalcomponent_get_first_property(comp, recip_kind)) { + /* Scheduling object resource */ + const char *userid; + struct caldav_data *cdata; + struct sched_param sparam; + icalcomponent *oldical = NULL; + + /* Construct userid corresponding to mailbox */ + userid = mboxname_to_userid(txn->req_tgt.mboxname); + + /* Make sure iCal UID is unique for this user */ + caldav_lookup_uid(davdb, uid, 0, &cdata); + /* XXX Check errors */ + + if (cdata->dav.mailbox && + (strcmp(cdata->dav.mailbox, txn->req_tgt.mboxname) || + strcmp(cdata->dav.resource, txn->req_tgt.resource))) { + + buf_printf(&txn->buf, "%s/user/%s/%s/%s", + namespace_calendar.prefix, + userid, strrchr(cdata->dav.mailbox, '.')+1, + cdata->dav.resource); + txn->error.resource = buf_cstring(&txn->buf); + ret = HTTP_FORBIDDEN; + goto done; + } + + /* Lookup the organizer */ + if (caladdress_lookup(organizer, &sparam)) { + syslog(LOG_ERR, + "meth_put: failed to process scheduling message in %s" + " (org=%s)", + txn->req_tgt.mboxname, organizer); + txn->error.desc = "Failed to lookup organizer address\r\n"; + ret = HTTP_SERVER_ERROR; + goto done; + } + + if (cdata->dav.imap_uid) { + /* Update existing object */ + struct index_record record; + const char *data; + size_t len; + int r; + + /* Load message containing the resource and parse iCal data */ + r = mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &record); + if (!r) r = mailbox_map_message(mailbox, record.uid, &data, &len); + if (r) { + txn->error.desc = "Failed to read record \r\n"; + ret = HTTP_SERVER_ERROR; + goto done; + } + oldical = icalparser_parse_string(data + record.header_size); + mailbox_unmap_message(mailbox, record.uid, &data, &len); + } + + if (cdata->organizer) { + /* Don't allow ORGANIZER to be changed */ + const char *p = organizer; + + if (!strncasecmp(p, "mailto:", 7)) p += 7; + if (strcmp(cdata->organizer, p)) { + txn->error.desc = "Can not change organizer address"; + ret = HTTP_FORBIDDEN; + } + } + + if (!strcmpsafe(sparam.userid, userid)) { + /* Organizer scheduling object resource */ + if (ret) { + txn->error.precond = CALDAV_ALLOWED_ORG_CHANGE; + goto done; + } + sched_request(organizer, &sparam, oldical, ical, 0); + } + else { + /* Attendee scheduling object resource */ + if (ret) { + txn->error.precond = CALDAV_ALLOWED_ATT_CHANGE; + goto done; + } +#if 0 + if (!oldical) { + /* Can't reply to a non-existent invitation */ + /* XXX But what about invites over iMIP? */ + txn->error.desc = "Can not reply to non-existent resource"; + ret = HTTP_FORBIDDEN; + goto done; + } +#endif + sched_reply(userid, oldical, ical); + } + + if (oldical) icalcomponent_free(oldical); + + flags |= NEW_STAG; + } + break; + + case ICAL_VJOURNAL_COMPONENT: + case ICAL_VFREEBUSY_COMPONENT: + case ICAL_VAVAILABILITY_COMPONENT: + /* Nothing else to do */ + break; + + default: + txn->error.precond = CALDAV_SUPP_COMP; + return HTTP_FORBIDDEN; + break; + } + + /* Store resource at target */ + ret = store_resource(txn, ical, mailbox, txn->req_tgt.resource, + davdb, OVERWRITE_CHECK, flags); + + if (flags & PREFER_REP) { + struct resp_body_t *resp_body = &txn->resp_body; + const char **hdr; + char *data; + + if ((hdr = spool_getheader(txn->req_hdrs, "Accept"))) + mime = get_accept_type(hdr, caldav_mime_types); + if (!mime) goto done; + + switch (ret) { + case HTTP_NO_CONTENT: + ret = HTTP_OK; + + case HTTP_CREATED: + /* Convert into requested MIME type */ + data = mime->to_string(ical); + + /* Fill in Content-Type, Content-Length */ + resp_body->type = mime->content_type; + resp_body->len = strlen(data); + + /* Fill in Content-Location */ + resp_body->loc = txn->req_tgt.path; + + /* Fill in Expires and Cache-Control */ + resp_body->maxage = 3600; /* 1 hr */ + txn->flags.cc = CC_MAXAGE + | CC_REVALIDATE /* don't use stale data */ + | CC_NOTRANSFORM; /* don't alter iCal data */ + + /* Output current representation */ + write_body(ret, txn, data, resp_body->len); + + free(data); + ret = 0; + break; + + default: + /* failure - do nothing */ + break; + } + } + + done: + if (ical) icalcomponent_free(ical); + + return ret; +} + + +/* Append a new busytime period to the busytime array */ +static void add_freebusy(struct icaltimetype *start, + struct icaltimetype *end, + icalparameter_fbtype fbtype, + struct calquery_filter *calfilter) +{ + struct freebusy_array *freebusy = &calfilter->freebusy; + struct freebusy *newfb; + + /* Grow the array, if necessary */ + if (freebusy->len == freebusy->alloc) { + freebusy->alloc += 100; /* XXX arbitrary */ + freebusy->fb = xrealloc(freebusy->fb, + freebusy->alloc * sizeof(struct freebusy)); + } + + /* Add new freebusy */ + newfb = &freebusy->fb[freebusy->len++]; + if (icaltime_is_date(*start)) { + newfb->per.duration = icaltime_subtract(*end, *start); + newfb->per.end = icaltime_null_time(); + start->is_date = 0; /* MUST be DATE-TIME */ + newfb->per.start = + icaltime_convert_to_zone(*start, icaltimezone_get_utc_timezone()); + } + else { + newfb->per.duration = icaldurationtype_null_duration(); + newfb->per.end = (icaltime_compare(calfilter->end, *end) < 0) ? + calfilter->end : *end; + newfb->per.start = (icaltime_compare(calfilter->start, *start) > 0) ? + calfilter->start : *start; + } + newfb->type = fbtype; +} + + +/* Append a new busytime period for recurring comp to the busytime array */ +static void add_freebusy_comp(icalcomponent *comp, struct icaltime_span *span, + void *rock) +{ + struct calquery_filter *calfilter = (struct calquery_filter *) rock; + int is_date = icaltime_is_date(icalcomponent_get_dtstart(comp)); + icaltimezone *utc = icaltimezone_get_utc_timezone(); + struct icaltimetype start, end; + icalparameter_fbtype fbtype; + + /* Set start and end times */ + start = icaltime_from_timet_with_zone(span->start, is_date, utc); + end = icaltime_from_timet_with_zone(span->end, is_date, utc); + + /* Set FBTYPE */ + switch (icalcomponent_isa(comp)) { + case ICAL_VEVENT_COMPONENT: + fbtype = icalcomponent_get_status(comp) == ICAL_STATUS_TENTATIVE ? + ICAL_FBTYPE_BUSYTENTATIVE : ICAL_FBTYPE_BUSY; + break; + + case ICAL_VFREEBUSY_COMPONENT: + /* XXX Need to do something better here */ + fbtype = ICAL_FBTYPE_BUSY; + break; + +#ifdef HAVE_VAVAILABILITY + case ICAL_VAVAILABILITY_COMPONENT: { + enum icalproperty_busytype busytype = ICAL_BUSYTYPE_BUSYUNAVAILABLE; + icalproperty *prop = + icalcomponent_get_first_property(comp, ICAL_BUSYTYPE_PROPERTY); + + if (prop) busytype = icalproperty_get_busytype(prop); + + switch (busytype) { + case ICAL_BUSYTYPE_BUSYUNAVAILABLE: + fbtype = ICAL_FBTYPE_BUSYUNAVAILABLE; + break; + case ICAL_BUSYTYPE_BUSYTENTATIVE: + fbtype = ICAL_FBTYPE_BUSYTENTATIVE; + break; + default: + fbtype = ICAL_FBTYPE_BUSY; + break; + } + break; + } + + case ICAL_XAVAILABLE_COMPONENT: + fbtype = ICAL_FBTYPE_FREE; + break; +#endif /* HAVE_VAVAILABILITY */ + + default: + fbtype = ICAL_FBTYPE_NONE; + break; + } + + add_freebusy(&start, &end, fbtype, calfilter); +} + + +static int is_busytime(struct calquery_filter *calfilter, icalcomponent *comp) +{ + if (calfilter->flags & BUSYTIME_QUERY) { + /* Check TRANSP and STATUS per RFC 4791, section 7.10 */ + const icalproperty *prop; + + /* Skip transparent events */ + prop = icalcomponent_get_first_property(comp, ICAL_TRANSP_PROPERTY); + if (prop && icalproperty_get_transp(prop) == ICAL_TRANSP_TRANSPARENT) + return 0; + + /* Skip cancelled events */ + if (icalcomponent_get_status(comp) == ICAL_STATUS_CANCELLED) return 0; + } + + return 1; +} + + +/* Compare recurid to start time of busytime periods -- used for searching */ +static int compare_recurid(const void *key, const void *mem) +{ + struct icaltimetype *recurid = (struct icaltimetype *) key; + struct freebusy *fb = (struct freebusy *) mem; + + return icaltime_compare(*recurid, fb->per.start); +} + + +/* Compare start/end times of freebusy periods -- used for sorting */ +static int compare_freebusy(const void *fb1, const void *fb2) +{ + struct freebusy *a = (struct freebusy *) fb1; + struct freebusy *b = (struct freebusy *) fb2; + + int r = icaltime_compare(a->per.start, b->per.start); + + if (r == 0) r = icaltime_compare(a->per.end, b->per.end); + + return r; +} + + +static int expand_occurrences(icalcomponent *ical, icalcomponent_kind kind, + struct calquery_filter *calfilter) +{ + struct freebusy_array *freebusy = &calfilter->freebusy; + icalcomponent *comp; + icaltimezone *utc = icaltimezone_get_utc_timezone(); + icaltime_span rangespan; + unsigned firstr, lastr; + + /* If not saving busytime, reset our array */ + if (!(calfilter->flags & BUSYTIME_QUERY)) freebusy->len = 0; + + /* Create a span for the given time-range */ + rangespan.start = + icaltime_as_timet_with_zone(calfilter->start, utc); + rangespan.end = + icaltime_as_timet_with_zone(calfilter->end, utc); + + /* Mark start of where recurrences will be added */ + firstr = freebusy->len; + + /* Find the master component */ + for (comp = icalcomponent_get_first_component(ical, kind); + comp && + icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY); + comp = icalcomponent_get_next_component(ical, kind)); + + if (is_busytime(calfilter, comp)) { + /* Add all recurring busytime in specified time-range */ + icalcomponent_foreach_recurrence(comp, + calfilter->start, + calfilter->end, + add_freebusy_comp, calfilter); + } + + /* Mark end of where recurrences were added */ + lastr = freebusy->len; + + /* Sort freebusy periods by start time */ + qsort(freebusy->fb + firstr, freebusy->len - firstr, + sizeof(struct freebusy), compare_freebusy); + + /* Handle overridden recurrences */ + for (comp = icalcomponent_get_first_component(ical, kind); + comp; comp = icalcomponent_get_next_component(ical, kind)) { + icalproperty *prop; + struct icaltimetype recurid; + icalparameter *param; + struct freebusy *overridden; + icaltime_span recurspan; + + /* The *_get_recurrenceid() functions don't appear + to deal with timezones properly, so we do it ourselves */ + prop = + icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY); + if (!prop) continue; + + recurid = icalproperty_get_recurrenceid(prop); + param = icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER); + + if (param) { + const char *tzid = icalparameter_get_tzid(param); + icaltimezone *tz = NULL; + + tz = icalcomponent_get_timezone(ical, tzid); + if (!tz) { + tz = icaltimezone_get_builtin_timezone_from_tzid(tzid); + } + if (tz) icaltime_set_timezone(&recurid, tz); + } + + recurid = + icaltime_convert_to_zone(recurid, + icaltimezone_get_utc_timezone()); + recurid.is_date = 0; /* make DATE-TIME for comparison */ + + /* Check if this overridden instance is in our array */ + overridden = bsearch(&recurid, freebusy->fb + firstr, lastr - firstr, + sizeof(struct freebusy), compare_recurid); + if (overridden) { + /* "Remove" the instance + by setting fbtype to NONE (we ignore these later) + NOTE: MUST keep period.start otherwise bsearch() breaks */ + /* XXX Doesn't handle the RANGE=THISANDFUTURE param */ + overridden->type = ICAL_FBTYPE_NONE; + } + + /* If overriding component isn't busytime, skip it */ + if (!is_busytime(calfilter, comp)) continue; + + /* Check if the new instance is in our time-range */ + recurspan = icaltime_span_new(icalcomponent_get_dtstart(comp), + icalcomponent_get_dtend(comp), 1); + + if (icaltime_span_overlaps(&recurspan, &rangespan)) { + /* Add this instance to the array */ + add_freebusy_comp(comp, &recurspan, calfilter); + } + } + + return (freebusy->len - firstr); +} + + +/* See if the current resource matches the specified filter + * (comp-type and/or time-range). Returns 1 if match, 0 otherwise. + */ +static int apply_calfilter(struct propfind_ctx *fctx, void *data) +{ + struct calquery_filter *calfilter = + (struct calquery_filter *) fctx->filter_crit; + struct caldav_data *cdata = (struct caldav_data *) data; + int match = 1; + + if (calfilter->comp) { + /* Perform CALDAV:comp-filter filtering */ + if (!(cdata->comp_type & calfilter->comp)) return 0; + } + + if (!icaltime_is_null_time(calfilter->start)) { + /* Perform CALDAV:time-range filtering */ + struct icaltimetype dtstart = icaltime_from_string(cdata->dtstart); + struct icaltimetype dtend = icaltime_from_string(cdata->dtend); + + if (icaltime_compare(dtend, calfilter->start) <= 0) { + /* Component ends earlier than range */ + return 0; + } + else if (icaltime_compare(dtstart, calfilter->end) >= 0) { + /* Component starts later than range */ + return 0; + } + else if (cdata->comp_type == CAL_COMP_VAVAILABILITY) { + /* Don't try to expand VAVAILABILITY, just mark it as in range */ + return 1; + } + else if (cdata->comp_flags.recurring) { + /* Component is recurring. + * Need to mmap() and parse iCalendar object + * to perform complete check of each recurrence. + */ + const char *data; + size_t len; + icalcomponent *ical; + icalcomponent_kind kind; + + /* XXX - error */ + if (mailbox_map_message(fctx->mailbox, fctx->record->uid, &data, &len)) + return 0; + + ical = icalparser_parse_string(data + fctx->record->header_size); + mailbox_unmap_message(fctx->mailbox, fctx->record->uid, &data, &len); + + kind = + icalcomponent_isa(icalcomponent_get_first_real_component(ical)); + + match = expand_occurrences(ical, kind, calfilter); + + icalcomponent_free(ical); + } + else if (calfilter->flags & BUSYTIME_QUERY) { + icalparameter_fbtype fbtype; + /* Component is non-recurring and we need to save busytime */ + if (cdata->comp_flags.transp) { + /* Don't include transparent events in freebusy */ + return 0; + } + if (cdata->comp_flags.status == CAL_STATUS_CANCELED) { + /* Don't include canceled events in freebusy */ + return 0; + } + + switch (cdata->comp_flags.status) { + case CAL_STATUS_UNAVAILABLE: + fbtype = ICAL_FBTYPE_BUSYUNAVAILABLE; break; + + case CAL_STATUS_TENTATIVE: + fbtype = ICAL_FBTYPE_BUSYTENTATIVE; break; + + default: + fbtype = ICAL_FBTYPE_BUSY; break; + } + + add_freebusy(&dtstart, &dtend, fbtype, calfilter); + } + } + + return match; +} + + +static int is_valid_timerange(const struct icaltimetype start, + const struct icaltimetype end) +{ + return (icaltime_is_valid_time(start) && icaltime_is_valid_time(end) && + !icaltime_is_date(start) && !icaltime_is_date(end) && + (icaltime_is_utc(start) || start.zone) && + (icaltime_is_utc(end) || end.zone)); +} + + +static int parse_comp_filter(xmlNodePtr root, struct calquery_filter *filter, + struct error_t *error) +{ + int ret = 0; + xmlNodePtr node; + + /* Parse elements of filter */ + for (node = root; node; node = node->next) { + if (node->type == XML_ELEMENT_NODE) { + if (!xmlStrcmp(node->name, BAD_CAST "comp-filter")) { + xmlChar *name = xmlGetProp(node, BAD_CAST "name"); + + if (!filter->comp) { + if (!xmlStrcmp(name, BAD_CAST "VCALENDAR")) + filter->comp = CAL_COMP_VCALENDAR; + else { + error->precond = CALDAV_VALID_FILTER; + ret = HTTP_FORBIDDEN; + } + } + else if (filter->comp == CAL_COMP_VCALENDAR) { + if (!xmlStrcmp(name, BAD_CAST "VCALENDAR") || + !xmlStrcmp(name, BAD_CAST "VALARM")) { + error->precond = CALDAV_VALID_FILTER; + ret = HTTP_FORBIDDEN; + } + else if (!xmlStrcmp(name, BAD_CAST "VEVENT")) + filter->comp |= CAL_COMP_VEVENT; + else if (!xmlStrcmp(name, BAD_CAST "VTODO")) + filter->comp |= CAL_COMP_VTODO; + else if (!xmlStrcmp(name, BAD_CAST "VJOURNAL")) + filter->comp |= CAL_COMP_VJOURNAL; + else if (!xmlStrcmp(name, BAD_CAST "VFREEBUSY")) + filter->comp |= CAL_COMP_VFREEBUSY; + else if (!xmlStrcmp(name, BAD_CAST "VTIMEZONE")) + filter->comp |= CAL_COMP_VTIMEZONE; + else if (!xmlStrcmp(name, BAD_CAST "VAVAILABILITY")) + filter->comp |= CAL_COMP_VAVAILABILITY; + else if (!xmlStrcmp(name, BAD_CAST "VPOLL")) + filter->comp |= CAL_COMP_VPOLL; + else { + error->precond = CALDAV_SUPP_FILTER; + ret = HTTP_FORBIDDEN; + } + } + else if (filter->comp & (CAL_COMP_VEVENT | CAL_COMP_VTODO)) { + if (!xmlStrcmp(name, BAD_CAST "VALARM")) + filter->comp |= CAL_COMP_VALARM; + else { + error->precond = CALDAV_VALID_FILTER; + ret = HTTP_FORBIDDEN; + } + } + else { + error->precond = CALDAV_SUPP_FILTER; + ret = HTTP_FORBIDDEN; + } + + xmlFree(name); + + if (!ret) + ret = parse_comp_filter(node->children, filter, error); + if (ret) return ret; + } + else if (!xmlStrcmp(node->name, BAD_CAST "time-range")) { + icaltimezone *utc = icaltimezone_get_utc_timezone(); + xmlChar *start, *end; + + if (!(filter->comp & (CAL_COMP_VEVENT | CAL_COMP_VTODO))) { + error->precond = CALDAV_VALID_FILTER; + return HTTP_FORBIDDEN; + } + + start = xmlGetProp(node, BAD_CAST "start"); + if (start) { + filter->start = icaltime_from_string((char *) start); + xmlFree(start); + } + else { + filter->start = + icaltime_from_timet_with_zone(caldav_epoch, 0, utc); + } + + end = xmlGetProp(node, BAD_CAST "end"); + if (end) { + filter->end = icaltime_from_string((char *) end); + xmlFree(end); + } + else { + filter->end = + icaltime_from_timet_with_zone(caldav_eternity, 0, utc); + } + + if (!is_valid_timerange(filter->start, filter->end)) { + error->precond = CALDAV_VALID_FILTER; + return HTTP_FORBIDDEN; + } + } + else { + error->precond = CALDAV_SUPP_FILTER; + return HTTP_FORBIDDEN; + } + } + } + + return ret; +} + + +/* Callback to fetch DAV:getcontenttype */ +static int propfind_getcontenttype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + buf_setcstr(&fctx->buf, "text/calendar; charset=utf-8"); + + if (fctx->data) { + struct caldav_data *cdata = (struct caldav_data *) fctx->data; + const char *comp = NULL; + + switch (cdata->comp_type) { + case CAL_COMP_VEVENT: comp = "VEVENT"; break; + case CAL_COMP_VTODO: comp = "VTODO"; break; + case CAL_COMP_VJOURNAL: comp = "VJOURNAL"; break; + case CAL_COMP_VFREEBUSY: comp = "VFREEBUSY"; break; + case CAL_COMP_VAVAILABILITY: comp = "VAVAILABILITY"; break; + case CAL_COMP_VPOLL: comp = "VPOLL"; break; + } + + if (comp) buf_printf(&fctx->buf, "; component=%s", comp); + } + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); + + return 0; +} + + +/* Callback to fetch DAV:resourcetype */ +static int propfind_restype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + if (!fctx->record) { + xmlNewChild(node, NULL, BAD_CAST "collection", NULL); + + if (fctx->req_tgt->collection) { + ensure_ns(fctx->ns, NS_CALDAV, resp->parent, XML_NS_CALDAV, "C"); + if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX)) { + xmlNewChild(node, fctx->ns[NS_CALDAV], + BAD_CAST "schedule-inbox", NULL); + } + else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX)) { + xmlNewChild(node, fctx->ns[NS_CALDAV], + BAD_CAST "schedule-outbox", NULL); + } + else { + xmlNewChild(node, fctx->ns[NS_CALDAV], + BAD_CAST "calendar", NULL); + } + } + } + + return 0; +} + + +/* Callback to prescreen/fetch CALDAV:calendar-data */ +static int propfind_caldata(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock) +{ + xmlNodePtr prop = (xmlNodePtr) rock; + const char *data = NULL; + size_t datalen = 0; + const char *map = NULL; + size_t maplen = 0; + int r; + + if (propstat) { + if (!fctx->record) return HTTP_NOT_FOUND; + + mailbox_map_message(fctx->mailbox, fctx->record->uid, &map, &maplen); + + data = map + fctx->record->header_size; + datalen = maplen - fctx->record->header_size; + } + + r = propfind_getdata(name, ns, fctx, propstat, prop, caldav_mime_types, + CALDAV_SUPP_DATA, data, datalen); + + if (propstat) + mailbox_unmap_message(fctx->mailbox, fctx->record->uid, &map, &maplen); + + return r; +} + + +/* Helper function to fetch CALDAV:calendar-home-set, + * CALDAV:schedule-inbox-URL, CALDAV:schedule-outbox-URL, + * and CALDAV:schedule-default-calendar-URL + */ +static int propfind_calurl(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + xmlNodePtr expand, + const char *cal) +{ + xmlNodePtr node; + + if (!(namespace_calendar.enabled && fctx->req_tgt->user)) + return HTTP_NOT_FOUND; + + node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%s/user/%.*s/%s", namespace_calendar.prefix, + (int) fctx->req_tgt->userlen, fctx->req_tgt->user, + cal ? cal : ""); + + if (expand) { + /* Return properties for this URL */ + expand_property(expand, fctx, buf_cstring(&fctx->buf), + &caldav_parse_path, caldav_props, node, cal ? 1 : 0); + + } + else { + /* Return just the URL */ + xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf)); + } + + return 0; +} + + +/* Callback to fetch CALDAV:calendar-home-set */ +int propfind_calhome(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock) +{ + return propfind_calurl(name, ns, fctx, resp, propstat, rock, NULL); +} + + +/* Callback to fetch CALDAV:schedule-inbox-URL */ +int propfind_schedinbox(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock) +{ + return propfind_calurl(name, ns, fctx, resp, propstat, rock, SCHED_INBOX); +} + + +/* Callback to fetch CALDAV:schedule-outbox-URL */ +int propfind_schedoutbox(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock) +{ + return propfind_calurl(name, ns, fctx, resp, propstat, rock, SCHED_OUTBOX); +} + + +/* Callback to fetch CALDAV:schedule-default-calendar-URL */ +static int propfind_scheddefault(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock) +{ + /* Only defined on CALDAV:schedule-inbox-URL */ + if (!fctx->req_tgt->collection || + strcmp(fctx->req_tgt->collection, SCHED_INBOX)) + return HTTP_NOT_FOUND; + + return propfind_calurl(name, ns, fctx, resp, propstat, rock, SCHED_DEFAULT); +} + + +/* Callback to fetch CALDAV:supported-calendar-component-set[s] */ +static int propfind_calcompset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + struct buf attrib = BUF_INITIALIZER; + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set"; + unsigned long types = 0; + xmlNodePtr set, node; + const struct cal_comp_t *comp; + int r = 0; + + if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND; + + r = annotatemore_lookupmask(fctx->mailbox->name, prop_annot, httpd_userid, &attrib); + if (r) return HTTP_SERVER_ERROR; + + if (attrib.len) { + types = strtoul(buf_cstring(&attrib), NULL, 10); + } + else { + const char **hdr = spool_getheader(fctx->req_hdrs, "User-Agent"); + + types = -1; /* ALL components types */ + + /* XXX iOS/8 doesn't like VPOLL */ + if (hdr && strstr(hdr[0], "iOS/8")) types &= ~CAL_COMP_VPOLL; + } + + buf_free(&attrib); + + if (!types) return HTTP_NOT_FOUND; + + set = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + /* Create "comp" elements from the stored bitmask */ + for (comp = cal_comps; comp->name; comp++) { + if (types & comp->type) { + node = xmlNewChild(set, fctx->ns[NS_CALDAV], + BAD_CAST "comp", NULL); + xmlNewProp(node, BAD_CAST "name", BAD_CAST comp->name); + } + } + + return 0; +} + + +/* Callback to write supported-calendar-component-set property */ +static int proppatch_calcompset(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + int r = 0; + unsigned precond = 0; + + if (set && (pctx->meth != METH_PROPPATCH)) { + /* "Writeable" for MKCOL/MKCALENDAR only */ + xmlNodePtr cur; + unsigned long types = 0; + + /* Work through the given list of components */ + for (cur = prop->children; cur; cur = cur->next) { + xmlChar *name; + const struct cal_comp_t *comp; + + /* Make sure its a "comp" element with a "name" */ + if (cur->type != XML_ELEMENT_NODE) continue; + if (xmlStrcmp(cur->name, BAD_CAST "comp") || + !(name = xmlGetProp(cur, BAD_CAST "name"))) break; + + /* Make sure we have a valid component type */ + for (comp = cal_comps; + comp->name && xmlStrcmp(name, BAD_CAST comp->name); comp++); + xmlFree(name); + + if (comp->name) types |= comp->type; /* found match in our list */ + else break; /* no match - invalid type */ + } + + if (!cur) { + /* All component types are valid */ + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set"; + const char *userid = NULL; + annotate_state_t *astate = NULL; + + buf_reset(&pctx->buf); + buf_printf(&pctx->buf, "%lu", types); + + if (!mboxname_userownsmailbox(httpd_userid, pctx->mailbox->name)) + userid = httpd_userid; + + r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate); + if (!r) r = annotate_state_write(astate, prop_annot, userid, &pctx->buf); + + if (!r) { + xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + prop->name, prop->ns, NULL, 0); + } + else { + xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV], + &propstat[PROPSTAT_ERROR], + prop->name, prop->ns, NULL, 0); + } + + return 0; + } + + /* Invalid component type */ + precond = CALDAV_SUPP_COMP; + } + else { + /* Protected property */ + precond = DAV_PROT_PROP; + } + + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, precond); + + *pctx->ret = HTTP_FORBIDDEN; + + return 0; +} + +/* Callback to fetch CALDAV:supported-calendar-data */ +static int propfind_suppcaldata(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node; + struct mime_type_t *mime; + + if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND; + + node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + for (mime = caldav_mime_types; mime->content_type; mime++) { + xmlNodePtr type = xmlNewChild(node, fctx->ns[NS_CALDAV], + BAD_CAST "calendar-data", NULL); + + /* Trim any charset from content-type */ + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%.*s", + (int) strcspn(mime->content_type, ";"), mime->content_type); + + xmlNewProp(type, BAD_CAST "content-type", + BAD_CAST buf_cstring(&fctx->buf)); + + if (mime->version) + xmlNewProp(type, BAD_CAST "version", BAD_CAST mime->version); + } + + return 0; +} + + +/* Callback to fetch CALDAV:max-resource-size */ +static int propfind_maxsize(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + static int maxsize = 0; + + if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND; + + if (!maxsize) { + maxsize = config_getint(IMAPOPT_MAXMESSAGESIZE); + if (!maxsize) maxsize = INT_MAX; + } + + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%d", maxsize); + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); + + return 0; +} + + +/* Callback to fetch CALDAV:min/max-date-time */ +static int propfind_minmaxdate(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock) +{ + struct icaltimetype date; + + if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND; + + date = icaltime_from_timet_with_zone(*((time_t *) rock), 0, + icaltimezone_get_utc_timezone()); + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST icaltime_as_ical_string(date), 0); + + return 0; +} + + +/* Callback to fetch CALDAV:schedule-tag */ +static int propfind_schedtag(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + struct caldav_data *cdata = (struct caldav_data *) fctx->data; + + if (!cdata->sched_tag) return HTTP_NOT_FOUND; + + /* add DQUOTEs */ + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "\"%s\"", cdata->sched_tag); + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); + + return 0; +} + + +/* Callback to fetch CALDAV:calendar-user-address-set */ +int propfind_caluseraddr(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node; + struct strlist *domains; + + if (!(namespace_calendar.enabled && fctx->req_tgt->user)) + return HTTP_NOT_FOUND; + + node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + /* XXX This needs to be done via an LDAP/DB lookup */ + for (domains = cua_domains; domains; domains = domains->next) { + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "mailto:%.*s@%s", (int) fctx->req_tgt->userlen, + fctx->req_tgt->user, domains->s); + + xmlNewChild(node, fctx->ns[NS_DAV], BAD_CAST "href", + BAD_CAST buf_cstring(&fctx->buf)); + } + + return 0; +} + + +/* Callback to fetch CALDAV:calendar-user-type */ +int propfind_calusertype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + const char *type = fctx->req_tgt->user ? "INDIVIDUAL" : "UNKNOWN"; + + if (!namespace_calendar.enabled) return HTTP_NOT_FOUND; + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST type, 0); + + return 0; +} + + +/* Callback to fetch CALDAV:schedule-calendar-transp */ +static int propfind_caltransp(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + struct buf attrib = BUF_INITIALIZER; + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">schedule-calendar-transp"; + xmlNodePtr node; + int r = 0; + + if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND; + + r = annotatemore_lookupmask(fctx->mailbox->name, prop_annot, httpd_userid, &attrib); + + if (r) return HTTP_SERVER_ERROR; + if (!attrib.len) return HTTP_NOT_FOUND; + + node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + xmlNewChild(node, fctx->ns[NS_CALDAV], BAD_CAST buf_cstring(&attrib), NULL); + + buf_free(&attrib); + + return 0; +} + + +/* Callback to write schedule-calendar-transp property */ +static int proppatch_caltransp(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + int r; + + if (pctx->req_tgt->collection && !pctx->req_tgt->resource) { + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">schedule-calendar-transp"; + const char *userid = NULL; + annotate_state_t *astate = NULL; + buf_reset(&pctx->buf); + + if (set) { + xmlNodePtr cur; + + /* Find the value */ + for (cur = prop->children; cur; cur = cur->next) { + + /* Make sure its a value we understand */ + if (cur->type != XML_ELEMENT_NODE) continue; + if (!xmlStrcmp(cur->name, BAD_CAST "opaque") || + !xmlStrcmp(cur->name, BAD_CAST "transparent")) { + buf_setcstr(&pctx->buf, (const char *)cur->name); + break; + } + else { + /* Unknown value */ + xml_add_prop(HTTP_CONFLICT, pctx->ns[NS_DAV], + &propstat[PROPSTAT_CONFLICT], + prop->name, prop->ns, NULL, 0); + + *pctx->ret = HTTP_FORBIDDEN; + + return 0; + } + } + } + + if (!mboxname_userownsmailbox(httpd_userid, pctx->mailbox->name)) + userid = httpd_userid; + + r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate); + if (!r) r = annotate_state_write(astate, prop_annot, userid, &pctx->buf); + if (!r) { + xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], prop->name, prop->ns, NULL, 0); + } + else { + xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV], + &propstat[PROPSTAT_ERROR], + prop->name, prop->ns, NULL, 0); + } + } + else { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, 0); + + *pctx->ret = HTTP_FORBIDDEN; + } + + return 0; +} + + +/* Callback to prescreen/fetch CALDAV:calendar-timezone */ +static int propfind_timezone(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock) +{ + xmlNodePtr prop = (xmlNodePtr) rock; + const char *data = NULL, *msg_base = NULL; + unsigned long datalen = 0; + int r = 0; + + if (propstat) { + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone"; + struct buf attrib = BUF_INITIALIZER; + + if (fctx->mailbox && !fctx->record) { + r = annotatemore_lookupmask(fctx->mailbox->name, + buf_cstring(&fctx->buf), + httpd_userid, &attrib); + } + + if (r) return HTTP_SERVER_ERROR; + if (attrib.len) { + data = buf_cstring(&attrib); + datalen = attrib.len; + } + else if ((namespace_calendar.allow & ALLOW_CAL_NOTZ)) { + /* Check for CALDAV:calendar-timezone-id */ + prop_annot = ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone-id"; + + r = annotatemore_lookupmask(fctx->mailbox->name, prop_annot, + httpd_userid, &attrib); + + if (r) return HTTP_SERVER_ERROR; + if (!attrib.len) return HTTP_NOT_FOUND; + else { + const char *path; + int fd; + + /* Open and mmap the timezone file */ + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%s%s/%s.ics", + config_dir, FNAME_ZONEINFODIR, buf_cstring(&attrib)); + + path = buf_cstring(&fctx->buf); + if ((fd = open(path, O_RDONLY)) == -1) return HTTP_SERVER_ERROR; + + map_refresh(fd, 1, &msg_base, &datalen, + MAP_UNKNOWN_LEN, path, NULL); + close(fd); + + if (!msg_base) return HTTP_SERVER_ERROR; + + data = msg_base; + } + } + else return HTTP_NOT_FOUND; + } + + r = propfind_getdata(name, ns, fctx, propstat, prop, caldav_mime_types, + CALDAV_SUPP_DATA, data, datalen); + + if (msg_base) map_free(&msg_base, &datalen); + + return r; +} + + +/* Callback to write calendar-timezone property */ +static int proppatch_timezone(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + int r; + + if (pctx->req_tgt->collection && !pctx->req_tgt->resource) { + xmlChar *type, *ver = NULL, *freeme = NULL; + struct buf buf = BUF_INITIALIZER; + struct mime_type_t *mime; + unsigned valid = 1; + + type = xmlGetProp(prop, BAD_CAST "content-type"); + if (type) ver = xmlGetProp(prop, BAD_CAST "version"); + + /* Check/find requested MIME type */ + for (mime = caldav_mime_types; type && mime->content_type; mime++) { + if (is_mediatype(mime->content_type, (const char *) type)) { + if (ver && + (!mime->version || xmlStrcmp(ver, BAD_CAST mime->version))) { + continue; + } + break; + } + } + + if (!mime) { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, + CALDAV_SUPP_DATA); + *pctx->ret = HTTP_FORBIDDEN; + valid = 0; + } + else if (!mime->content_type) { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, + CALDAV_SUPP_DATA); + *pctx->ret = HTTP_FORBIDDEN; + valid = 0; + } + else if (set) { + icalcomponent *ical = NULL; + + freeme = xmlNodeGetContent(prop); + + /* Parse and validate the iCal data */ + ical = mime->from_string((const char *)freeme); + if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, + CALDAV_VALID_DATA); + *pctx->ret = HTTP_FORBIDDEN; + valid = 0; + } + else if (!icalcomponent_get_first_component(ical, + ICAL_VTIMEZONE_COMPONENT) + || icalcomponent_get_first_real_component(ical)) { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, + CALDAV_VALID_OBJECT); + *pctx->ret = HTTP_FORBIDDEN; + valid = 0; + } + else if (mime != caldav_mime_types) { + buf_setcstr(&buf, icalcomponent_as_ical_string(ical)); + } + else { + buf_setcstr(&buf, (const char *)freeme); + } + } + + if (valid) { + /* Remove CALDAV:calendar-timezone-id */ + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone-id"; + const char *userid = NULL; + annotate_state_t *astate = NULL; + + if (!mboxname_userownsmailbox(httpd_userid, pctx->mailbox->name)) + userid = httpd_userid; + + r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate); + if (!r) r = annotate_state_write(astate, prop_annot, userid, &buf); + + if (r) { + xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV], + &propstat[PROPSTAT_ERROR], + prop->name, prop->ns, NULL, 0); + *pctx->ret = HTTP_SERVER_ERROR; + } + else { + /* Set CALDAV:calendar-timezone */ + proppatch_todb(prop, set, pctx, propstat, (void *) buf_cstring(&buf)); + } + } + + if (freeme) xmlFree(freeme); + if (type) xmlFree(type); + if (ver) xmlFree(ver); + buf_free(&buf); + } + else { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, 0); + + *pctx->ret = HTTP_FORBIDDEN; + } + + return 0; +} + + +/* Callback to prescreen/fetch CALDAV:calendar-availability */ +static int propfind_availability(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock) +{ + xmlNodePtr prop = (xmlNodePtr) rock; + const char *data = NULL; + unsigned long datalen = 0; + + if (propstat) { + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">calendar-availability"; + struct buf attrib = BUF_INITIALIZER; + int r = 0; + + if (fctx->mailbox && !fctx->record) { + r = annotatemore_lookup(fctx->mailbox->name, prop_annot, + /* shared */ NULL, &attrib); + } + + if (r) return HTTP_SERVER_ERROR; + if (!attrib.len) return HTTP_NOT_FOUND; + + data = buf_cstring(&attrib); + datalen = attrib.len; + } + + return propfind_getdata(name, ns, fctx, propstat, prop, caldav_mime_types, + CALDAV_SUPP_DATA, data, datalen); +} + + +/* Callback to write calendar-availability property */ +static int proppatch_availability(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + if (config_allowsched && pctx->req_tgt->flags == TGT_SCHED_INBOX) { + struct buf buf = BUF_INITIALIZER; + xmlChar *type, *ver = NULL, *freeme = NULL; + struct mime_type_t *mime; + icalcomponent *ical = NULL; + const char *value = NULL; + unsigned valid = 1; + + type = xmlGetProp(prop, BAD_CAST "content-type"); + if (type) ver = xmlGetProp(prop, BAD_CAST "version"); + + /* Check/find requested MIME type */ + for (mime = caldav_mime_types; type && mime->content_type; mime++) { + if (is_mediatype(mime->content_type, (const char *) type)) { + if (ver && + (!mime->version || xmlStrcmp(ver, BAD_CAST mime->version))) { + continue; + } + break; + } + } + + if (!mime->content_type) { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, + CALDAV_SUPP_DATA); + *pctx->ret = HTTP_FORBIDDEN; + valid = 0; + } + else if (set) { + freeme = xmlNodeGetContent(prop); + + /* Parse and validate the iCal data */ + ical = mime->from_string((const char *)freeme); + if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, + CALDAV_VALID_DATA); + *pctx->ret = HTTP_FORBIDDEN; + valid = 0; + } + else if (!icalcomponent_get_first_component(ical, + ICAL_VAVAILABILITY_COMPONENT)) { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, + CALDAV_VALID_OBJECT); + *pctx->ret = HTTP_FORBIDDEN; + valid = 0; + } + else if (mime != caldav_mime_types) { + buf_setcstr(&buf, icalcomponent_as_ical_string(ical)); + } + else { + buf_setcstr(&buf, (const char *)freeme); + } + } + + if (valid) { + proppatch_todb(prop, set, pctx, propstat, (void *) value); + } + + if (ical) icalcomponent_free(ical); + if (freeme) xmlFree(freeme); + if (type) xmlFree(type); + if (ver) xmlFree(ver); + } + else { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, 0); + + *pctx->ret = HTTP_FORBIDDEN; + } + + return 0; +} + + +/* Callback to fetch CALDAV:timezone-service-set */ +static int propfind_tzservset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + assert(name && ns && fctx && propstat); + +#ifdef HAVE_TZ_BY_REF + if (fctx->req_tgt->resource) return HTTP_NOT_FOUND; + + if (namespace_calendar.allow & ALLOW_CAL_NOTZ) { + xmlNodePtr node; + const char *proto = NULL, *host = NULL; + + node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + http_proto_host(fctx->req_hdrs, &proto, &host); + + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%s://%s%s", + proto, host, namespace_timezone.prefix); + + xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf)); + + return 0; + } +#endif /* HAVE_TZ_BY_REF */ + + return HTTP_NOT_FOUND; +} + + +/* Callback to fetch CALDAV:calendar-timezone-id property */ +static int propfind_tzid(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone-id"; + struct buf attrib = BUF_INITIALIZER; + const char *value = NULL; + int r = 0; + + if (!(namespace_calendar.allow & ALLOW_CAL_NOTZ) || + !fctx->req_tgt->collection || fctx->req_tgt->resource) + return HTTP_NOT_FOUND; + + r = annotatemore_lookupmask(fctx->mailbox->name, prop_annot, + httpd_userid, &attrib); + + if (r) return HTTP_SERVER_ERROR; + if (attrib.len) { + value = buf_cstring(&attrib); + } + if (!value) { + /* Check for CALDAV:calendar-timezone */ + prop_annot = ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone"; + + if (fctx->mailbox && !fctx->record) { + r = annotatemore_lookupmask(fctx->mailbox->name, prop_annot, + httpd_userid, &attrib); + } + + if (r) return HTTP_SERVER_ERROR; + if (!attrib.len) return HTTP_NOT_FOUND; + else { + icalcomponent *ical, *vtz; + icalproperty *tzid; + + ical = icalparser_parse_string(buf_cstring(&attrib)); + vtz = icalcomponent_get_first_component(ical, + ICAL_VTIMEZONE_COMPONENT); + tzid = icalcomponent_get_first_property(vtz, ICAL_TZID_PROPERTY); + value = icalmemory_tmp_copy(icalproperty_get_tzid(tzid)); + icalcomponent_free(ical); + } + } + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST value, 0); + + return 0; +} + + +/* Callback to write CALDAV:calendar-timezone-id property */ +static int proppatch_tzid(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ +#ifdef HAVE_TZ_BY_REF + if ((namespace_calendar.allow & ALLOW_CAL_NOTZ) && + pctx->req_tgt->collection && !pctx->req_tgt->resource) { + xmlChar *freeme = NULL; + const char *tzid = NULL; + unsigned valid = 1; + int r; + + if (set) { + freeme = xmlNodeGetContent(prop); + tzid = (const char *) freeme; + + /* Verify we have tzid record in the database */ + r = zoneinfo_lookup(tzid, NULL); + if (r) { + if (r == CYRUSDB_NOTFOUND) { + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, + CALDAV_VALID_TIMEZONE); + } + else { + xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV], + &propstat[PROPSTAT_ERROR], + prop->name, prop->ns, NULL, 0); + } + *pctx->ret = HTTP_FORBIDDEN; + valid = 0; + } + } + + if (valid) { + /* Remove CALDAV:calendar-timezone */ + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">calendar-timezone"; + annotate_state_t *astate = NULL; + + r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate); + if (!r) r = annotate_state_write(astate, prop_annot, /*userid*/NULL, NULL); + + if (r) { + xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV], + &propstat[PROPSTAT_ERROR], + prop->name, prop->ns, NULL, 0); + *pctx->ret = HTTP_SERVER_ERROR; + } + else { + /* Set CALDAV:calendar-timezone-id */ + proppatch_todb(prop, set, pctx, propstat, (void *) tzid); + } + } + + if (freeme) xmlFree(freeme); + + return 0; + } +#else + (void) set; /* squash compiler warning */ +#endif /* HAVE_TZ_BY_REF */ + + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, 0); + + *pctx->ret = HTTP_FORBIDDEN; + + return 0; +} + + +/* Callback to fetch CALDAV:supported-rscale-set */ +static int propfind_rscaleset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + assert(name && ns && fctx && propstat); + + if (fctx->req_tgt->resource) return HTTP_NOT_FOUND; + +#ifdef HAVE_RSCALE + if (icalrecur_rscale_token_handling_is_supported()) { + xmlNodePtr top; + UEnumeration *en; + UErrorCode status = U_ZERO_ERROR; + const char *rscale; + + top = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + en = ucal_getKeywordValuesForLocale("calendar", NULL, FALSE, &status); + while ((rscale = uenum_next(en, NULL, &status))) { + xmlNewChild(top, fctx->ns[NS_CALDAV], + BAD_CAST "supported-rscale", BAD_CAST rscale); + } + uenum_close(en); + + return 0; + } +#endif + + return HTTP_NOT_FOUND; +} + + +static int report_cal_query(struct transaction_t *txn, + xmlNodePtr inroot, struct propfind_ctx *fctx) +{ + int ret = 0; + xmlNodePtr node; + struct calquery_filter calfilter; + + memset(&calfilter, 0, sizeof(struct calquery_filter)); + + fctx->filter_crit = &calfilter; + fctx->open_db = (db_open_proc_t) &my_caldav_open; + fctx->close_db = (db_close_proc_t) &my_caldav_close; + fctx->lookup_resource = (db_lookup_proc_t) &caldav_lookup_resource; + fctx->foreach_resource = (db_foreach_proc_t) &caldav_foreach; + fctx->proc_by_resource = &propfind_by_resource; + + /* Parse children element of report */ + for (node = inroot->children; node; node = node->next) { + if (node->type == XML_ELEMENT_NODE) { + if (!xmlStrcmp(node->name, BAD_CAST "filter")) { + ret = parse_comp_filter(node->children, &calfilter, &txn->error); + if (ret) return ret; + else fctx->filter = apply_calfilter; + } + else if (!xmlStrcmp(node->name, BAD_CAST "timezone")) { + xmlChar *tzdata = NULL; + icalcomponent *ical = NULL, *tz = NULL; + + /* XXX Need to do pass this to query for floating time */ + syslog(LOG_WARNING, "REPORT calendar-query w/timezone"); + tzdata = xmlNodeGetContent(node); + ical = icalparser_parse_string((const char *) tz); + if (ical) tz = icalcomponent_get_first_component(ical, + ICAL_VTIMEZONE_COMPONENT); + if (!tz || icalcomponent_get_first_real_component(ical)) { + txn->error.precond = CALDAV_VALID_DATA; + ret = HTTP_FORBIDDEN; + } + else { + icalcomponent_remove_component(ical, tz); + calfilter.tz = icaltimezone_new(); + icaltimezone_set_component(calfilter.tz, tz); + } + + if (tzdata) xmlFree(tzdata); + if (ical) icalcomponent_free(ical); + if (ret) return ret; + } + else if (!xmlStrcmp(node->name, BAD_CAST "timezone-id")) { + xmlChar *tzid = NULL; + + /* XXX Need to pass this to query for floating time */ + syslog(LOG_WARNING, "REPORT calendar-query w/tzid"); + tzid = xmlNodeGetContent(node); + + /* XXX Need to load timezone */ + + if (tzid) xmlFree(tzid); + if (ret) return ret; + } + } + } + + if (fctx->depth++ > 0) { + /* Calendar collection(s) */ + if (txn->req_tgt.collection) { + /* Add response for target calendar collection */ + propfind_by_collection(txn->req_tgt.mboxname, 0, 0, fctx); + } + else { + /* Add responses for all contained calendar collections */ + strlcat(txn->req_tgt.mboxname, ".%", sizeof(txn->req_tgt.mboxname)); + mboxlist_findall(NULL, /* internal namespace */ + txn->req_tgt.mboxname, 1, httpd_userid, + httpd_authstate, propfind_by_collection, fctx); + } + + if (fctx->davdb) my_caldav_close(fctx->davdb); + + ret = *fctx->ret; + } + + if (calfilter.tz) icaltimezone_free(calfilter.tz, 1); + + /* Expanded recurrences still populate busytime array */ + if (calfilter.freebusy.fb) free(calfilter.freebusy.fb); + + return (ret ? ret : HTTP_MULTI_STATUS); +} + + +static int report_cal_multiget(struct transaction_t *txn, + xmlNodePtr inroot, struct propfind_ctx *fctx) +{ + int r, ret = 0; + struct mailbox *mailbox = NULL; + xmlNodePtr node; + struct buf uri = BUF_INITIALIZER; + + /* Get props for each href */ + for (node = inroot->children; node; node = node->next) { + if ((node->type == XML_ELEMENT_NODE) && + !xmlStrcmp(node->name, BAD_CAST "href")) { + xmlChar *href = xmlNodeListGetString(inroot->doc, node->children, 1); + int len = xmlStrlen(href); + struct request_target_t tgt; + struct caldav_data *cdata; + + buf_ensure(&uri, len); + xmlURIUnescapeString((const char *) href, len, uri.s); + xmlFree(href); + + /* Parse the path */ + memset(&tgt, 0, sizeof(struct request_target_t)); + tgt.namespace = URL_NS_CALENDAR; + + if ((r = caldav_parse_path(uri.s, &tgt, &fctx->err->desc))) { + ret = r; + goto done; + } + + fctx->req_tgt = &tgt; + + /* Check if we already have this mailbox open */ + if (!mailbox || strcmp(mailbox->name, tgt.mboxname)) { + if (mailbox) mailbox_close(&mailbox); + + /* Open mailbox for reading */ + r = mailbox_open_irl(tgt.mboxname, &mailbox); + if (r && r != IMAP_MAILBOX_NONEXISTENT) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + fctx->mailbox = mailbox; + } + + if (!fctx->mailbox || !tgt.resource) { + /* Add response for missing target */ + xml_add_response(fctx, HTTP_NOT_FOUND, 0); + continue; + } + + /* Open the DAV DB corresponding to the mailbox */ + fctx->davdb = my_caldav_open(fctx->mailbox); + + /* Find message UID for the resource */ + r = caldav_lookup_resource(fctx->davdb, + tgt.mboxname, tgt.resource, 0, &cdata); + if (r) { + ret = HTTP_NOT_FOUND; + goto done; + } + cdata->dav.resource = tgt.resource; + /* XXX Check errors */ + + propfind_by_resource(fctx, cdata); + + my_caldav_close(fctx->davdb); + } + } + + done: + mailbox_close(&mailbox); + buf_free(&uri); + + return (ret ? ret : HTTP_MULTI_STATUS); +} + + +/* Append a new vavailability period to the vavail array */ +static void +add_vavailability(struct vavailability_array *vavail, icalcomponent *ical) +{ + icaltimezone *utc = icaltimezone_get_utc_timezone(); + struct vavailability *newav; + icalcomponent *vav; + icalproperty *prop; + + /* Grow the array, if necessary */ + if (vavail->len == vavail->alloc) { + vavail->alloc += 10; /* XXX arbitrary */ + vavail->vav = xrealloc(vavail->vav, + vavail->alloc * sizeof(struct vavailability)); + } + + /* Add new vavailability */ + newav = &vavail->vav[vavail->len++]; + newav->ical = ical; + + vav = icalcomponent_get_first_real_component(ical); + + /* Set period */ + newav->per.start = icalcomponent_get_dtstart(vav); + if (icaltime_is_null_time(newav->per.start)) + newav->per.start = icaltime_from_timet_with_zone(caldav_epoch, 0, utc); + else + newav->per.start = icaltime_convert_to_zone(newav->per.start, utc), + newav->per.end = icalcomponent_get_dtend(vav); + if (icaltime_is_null_time(newav->per.end)) + newav->per.end = icaltime_from_timet_with_zone(caldav_eternity, 0, utc); + else + newav->per.end = icaltime_convert_to_zone(newav->per.end, utc), + newav->per.duration = icaldurationtype_null_duration(); + + /* Set PRIORITY - 0 (or none) has lower priority than 9 */ + prop = icalcomponent_get_first_property(vav, ICAL_PRIORITY_PROPERTY); + if (prop) newav->priority = icalproperty_get_priority(prop); + if (!prop || !newav->priority) newav->priority = 10; +} + + +/* caldav_foreach() callback to find busytime of a resource */ +static int busytime_by_resource(void *rock, void *data) +{ + struct propfind_ctx *fctx = (struct propfind_ctx *) rock; + struct caldav_data *cdata = (struct caldav_data *) data; + struct index_record record; + int r; + + if (!cdata->dav.imap_uid) return 0; + + /* Fetch index record for the resource */ + r = mailbox_find_index_record(fctx->mailbox, cdata->dav.imap_uid, &record); + if (r) return 0; + + fctx->record = &record; + if (apply_calfilter(fctx, data) && + cdata->comp_type == CAL_COMP_VAVAILABILITY) { + /* Add VAVAIL to our array for later use */ + struct calquery_filter *calfilter = + (struct calquery_filter *) fctx->filter_crit; + icalcomponent *vav; + + mailbox_map_record(fctx->mailbox, fctx->record, &fctx->msg_buf); + + vav = + icalparser_parse_string(buf_cstring(&fctx->msg_buf) + fctx->record->header_size); + + add_vavailability(&calfilter->vavail, vav); + } + + buf_free(&fctx->msg_buf); + fctx->record = NULL; + + return 0; +} + + +/* mboxlist_findall() callback to find busytime of a collection */ +static int busytime_by_collection(char *mboxname, int matchlen, + int maycreate, void *rock) +{ + struct propfind_ctx *fctx = (struct propfind_ctx *) rock; + struct calquery_filter *calfilter = + (struct calquery_filter *) fctx->filter_crit; + + if (calfilter && (calfilter->flags & CHECK_CAL_TRANSP)) { + /* Check if the collection is marked as transparent */ + struct buf attrib = BUF_INITIALIZER; + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">schedule-calendar-transp"; + + if (!annotatemore_lookupmask(mboxname, prop_annot, httpd_userid, &attrib)) { + if (!strcmp(buf_cstring(&attrib), "transparent")) { + buf_free(&attrib); + return 0; + } + buf_free(&attrib); + } + } + + return propfind_by_collection(mboxname, matchlen, maycreate, rock); +} + + +/* Compare type and start/end times of freebusy periods -- used for sorting */ +static int compare_freebusy_with_type(const void *fb1, const void *fb2) +{ + struct freebusy *a = (struct freebusy *) fb1; + struct freebusy *b = (struct freebusy *) fb2; + + int r = a->type - b->type; + + if (r == 0) r = compare_freebusy(fb1, fb2); + + return r; +} + + +/* Compare priority, start/end times, and type of vavail periods for sorting */ +static int compare_vavail(const void *v1, const void *v2) +{ + struct vavailability *a = (struct vavailability *) v1; + struct vavailability *b = (struct vavailability *) v2; + + int r = a->priority - b->priority; + + if (r == 0) { + r = icaltime_compare(a->per.start, b->per.start); + + if (r == 0) r = icaltime_compare(a->per.end, b->per.end); + } + + return r; +} + + +static void combine_vavailability(struct calquery_filter *calfilter) +{ + struct vavailability_array *vavail = &calfilter->vavail; + struct calquery_filter availfilter; + struct query_range { + struct icalperiodtype per; + struct query_range *next; + } *ranges, *range, *prev, *next; + unsigned i, j; + + memset(&availfilter, 0, sizeof(struct calquery_filter)); + + /* Sort VAVAILBILITY periods by priority and start time */ + qsort(vavail->vav, vavail->len, + sizeof(struct vavailability), compare_vavail); + + /* Maintain a linked list of remaining time ranges + * to be filled in by lower priority VAV components + * starting with the time range in the freebusy query. + * Ranges are removed, clipped, or split as they get filled. + * Quit when no ranges or VAVAILABILITY components remain. + */ + ranges = xmalloc(sizeof(struct query_range)); + ranges->per.start = calfilter->start; + ranges->per.end = calfilter->end; + ranges->next = NULL; + + for (i = 0; i < vavail->len; i++) { + struct vavailability *vav = &vavail->vav[i]; + icalcomponent *comp; + + comp = icalcomponent_get_first_component(vav->ical, + ICAL_VAVAILABILITY_COMPONENT); + + for (range = ranges, prev = NULL; range; prev = range, range = next) { + struct icalperiodtype period; + icaltime_span span; + + next = range->next; + + if (icaltime_compare(vav->per.end, range->per.start) <= 0 || + icaltime_compare(vav->per.start, range->per.end) >= 0) { + /* Skip VAVAILABILITY outside our range */ + continue; + } + + /* Set filter range (maximum start time and minimum end time) + and adjust current range as necessary */ + if (icaltime_compare(vav->per.start, range->per.start) <= 0) { + /* VAV starts before range - filter using range start */ + availfilter.start = range->per.start; + + if (icaltime_compare(vav->per.end, range->per.end) >= 0) { + /* VAV ends after range - filter using range end */ + availfilter.end = range->per.end; + + /* Filling entire range - remove it */ + if (prev) prev->next = next; + else ranges = NULL; + + free(range); + range = NULL; + } + else { + /* VAV ends before range - filter using VAV end */ + availfilter.end = vav->per.end; + + /* Filling front part - adjust start to back remainder */ + range->per.start = vav->per.end; + } + } + else { + /* VAV starts after range - filter using VAV start */ + availfilter.start = vav->per.start; + + if (icaltime_compare(vav->per.end, range->per.end) >= 0) { + /* VAV ends after range - filter using range end */ + availfilter.end = range->per.end; + } + else { + /* VAV ends before range - filter using VAV end */ + availfilter.end = vav->per.end; + + /* Splitting range - insert new range for back remainder */ + struct query_range *newr = + xmalloc(sizeof(struct query_range)); + newr->per.start = vav->per.end; + newr->per.end = range->per.end; + newr->next = next; + range->next = newr; + } + + /* Adjust end to front remainder */ + range->per.end = vav->per.start; + } + + /* Expand available time occurrences */ + expand_occurrences(comp, ICAL_XAVAILABLE_COMPONENT, &availfilter); + + /* Calculate unavailable periods and add to busytime */ + period.start = availfilter.start; + for (j = 0; j < availfilter.freebusy.len; j++) { + struct freebusy *fb = &availfilter.freebusy.fb[j]; + + /* Ignore overridden instances */ + if (fb->type == ICAL_FBTYPE_NONE) continue; + + period.end = fb->per.start; + if (icaltime_compare(period.end, period.start) > 0) { + span = icaltime_span_new(period.start, period.end, 1); + add_freebusy_comp(comp, &span, calfilter); + } + period.start = fb->per.end; + } + period.end = availfilter.end; + if (icaltime_compare(period.end, period.start) > 0) { + span = icaltime_span_new(period.start, period.end, 1); + add_freebusy_comp(comp, &span, calfilter); + } + } + + /* Done with this ical component */ + icalcomponent_free(vav->ical); + } + + /* Cleanup the vavailability array */ + free(vavail->vav); + + /* Cleanup the availability array */ + if (availfilter.freebusy.fb) free(availfilter.freebusy.fb); + + /* Remove any unfilled ranges */ + for (; ranges; ranges = next) { + next = ranges->next; + free(ranges); + } +} + + +/* Create an iCalendar object containing busytime of all specified resources */ +static icalcomponent *busytime_query_local(struct transaction_t *txn, + struct propfind_ctx *fctx, + char mailboxname[], + icalproperty_method method, + const char *uid, + const char *organizer, + const char *attendee) +{ + struct calquery_filter *calfilter = + (struct calquery_filter *) fctx->filter_crit; + struct freebusy_array *freebusy = &calfilter->freebusy; + struct vavailability_array *vavail = &calfilter->vavail; + icalcomponent *ical = NULL; + icalcomponent *fbcomp; + icalproperty *prop; + unsigned n; + + fctx->open_db = (db_open_proc_t) &my_caldav_open; + fctx->close_db = (db_close_proc_t) &my_caldav_close; + fctx->lookup_resource = (db_lookup_proc_t) &caldav_lookup_resource; + fctx->foreach_resource = (db_foreach_proc_t) &caldav_foreach; + fctx->proc_by_resource = &busytime_by_resource; + + /* Gather up all of the busytime and VAVAILABILITY periods */ + if (fctx->depth > 0) { + /* Calendar collection(s) */ + + if (txn->req_tgt.collection) { + /* Get busytime for target calendar collection */ + busytime_by_collection(mailboxname, 0, 0, fctx); + } + else { + /* Get busytime for all contained calendar collections */ + strlcat(mailboxname, ".%", MAX_MAILBOX_PATH); + mboxlist_findall(NULL, /* internal namespace */ + mailboxname, 1, httpd_userid, + httpd_authstate, busytime_by_collection, fctx); + } + + if (fctx->davdb) my_caldav_close(fctx->davdb); + } + + if (*fctx->ret) return NULL; + + if (calfilter->flags & CHECK_USER_AVAIL) { + /* Check for CALDAV:calendar-availability on user's Inbox */ + struct buf attrib = BUF_INITIALIZER; + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">calendar-availability"; + const char *userid = mboxname_to_userid(mailboxname); + const char *mboxname = caldav_mboxname(userid, SCHED_INBOX); + if (!annotatemore_lookup(mboxname, prop_annot, + /* shared */ "", &attrib) && attrib.len) { + add_vavailability(vavail, icalparser_parse_string(buf_cstring(&attrib))); + } + } + + /* Combine VAVAILABILITY components into busytime */ + if (vavail->len) combine_vavailability(calfilter); + + /* Sort busytime periods by type and start/end times for coalescing */ + qsort(freebusy->fb, freebusy->len, + sizeof(struct freebusy), compare_freebusy_with_type); + + /* Coalesce busytime periods of same type into one */ + for (n = 0; n + 1 < freebusy->len; n++) { + struct freebusy *fb, *next_fb; + icaltimetype end, next_end; + int isdur, next_isdur; + + fb = &freebusy->fb[n]; + next_fb = &freebusy->fb[n+1]; + + /* Ignore overridden instances */ + if (fb->type == ICAL_FBTYPE_NONE) continue; + + isdur = !icaldurationtype_is_null_duration(fb->per.duration); + end = !isdur ? fb->per.end : + icaltime_add(fb->per.start, fb->per.duration); + + /* Skip periods of different type or that don't overlap */ + if ((fb->type != next_fb->type) || + icaltime_compare(end, next_fb->per.start) < 0) continue; + + /* Coalesce into next busytime */ + next_isdur = !icaldurationtype_is_null_duration(next_fb->per.duration); + next_end = !next_isdur ? next_fb->per.end : + icaltime_add(next_fb->per.start, next_fb->per.duration); + + if (icaltime_compare(end, next_end) >= 0) { + /* Current period subsumes next */ + next_fb->per.end = fb->per.end; + next_fb->per.duration = fb->per.duration; + } + else if (isdur && next_isdur) { + /* Both periods are durations */ + struct icaldurationtype overlap = + icaltime_subtract(end, next_fb->per.start); + + next_fb->per.duration.days += fb->per.duration.days - overlap.days; + } + else { + /* Need to use explicit period */ + next_fb->per.end = next_end; + next_fb->per.duration = icaldurationtype_null_duration(); + } + + next_fb->per.start = fb->per.start; + + /* "Remove" the instance + by setting fbtype to NONE (we ignore these later) */ + fb->type = ICAL_FBTYPE_NONE; + } + + /* Sort busytime periods by start/end times for addition to VFREEBUSY */ + qsort(freebusy->fb, freebusy->len, + sizeof(struct freebusy), compare_freebusy); + + /* Construct iCalendar object with VFREEBUSY component */ + ical = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT, + icalproperty_new_version("2.0"), + icalproperty_new_prodid(ical_prodid), + 0); + + if (method) icalcomponent_set_method(ical, method); + + fbcomp = icalcomponent_vanew(ICAL_VFREEBUSY_COMPONENT, + icalproperty_new_dtstamp( + icaltime_from_timet_with_zone( + time(0), + 0, + icaltimezone_get_utc_timezone())), + icalproperty_new_dtstart(calfilter->start), + icalproperty_new_dtend(calfilter->end), + 0); + + icalcomponent_add_component(ical, fbcomp); + + if (uid) icalcomponent_set_uid(fbcomp, uid); + if (organizer) { + prop = icalproperty_new_organizer(organizer); + icalcomponent_add_property(fbcomp, prop); + } + if (attendee) { + prop = icalproperty_new_attendee(attendee); + icalcomponent_add_property(fbcomp, prop); + } + + /* Add busytime periods to VFREEBUSY component */ + for (n = 0; n < freebusy->len; n++) { + struct freebusy *fb = &freebusy->fb[n]; + icalproperty *busy; + + /* Ignore overridden instances */ + if (fb->type == ICAL_FBTYPE_NONE) continue; + + /* Create new FREEBUSY property with FBTYPE and add to component */ + busy = icalproperty_new_freebusy(fb->per); + icalproperty_add_parameter(busy, icalparameter_new_fbtype(fb->type)); + icalcomponent_add_property(fbcomp, busy); + } + + return ical; +} + + +static int report_fb_query(struct transaction_t *txn, + xmlNodePtr inroot, struct propfind_ctx *fctx) +{ + int ret = 0; + const char **hdr; + struct mime_type_t *mime; + struct calquery_filter calfilter; + xmlNodePtr node; + icalcomponent *cal; + icaltimezone *utc = icaltimezone_get_utc_timezone(); + + /* Can not be run against a resource */ + if (txn->req_tgt.resource) return HTTP_FORBIDDEN; + + /* Check requested MIME type: + 1st entry in caldav_mime_types array MUST be default MIME type */ + if ((hdr = spool_getheader(txn->req_hdrs, "Accept"))) + mime = get_accept_type(hdr, caldav_mime_types); + else mime = caldav_mime_types; + if (!mime) return HTTP_NOT_ACCEPTABLE; + + memset(&calfilter, 0, sizeof(struct calquery_filter)); + calfilter.comp = + CAL_COMP_VEVENT | CAL_COMP_VFREEBUSY | CAL_COMP_VAVAILABILITY; + calfilter.start = icaltime_from_timet_with_zone(caldav_epoch, 0, utc); + calfilter.end = icaltime_from_timet_with_zone(caldav_eternity, 0, utc); + calfilter.flags = BUSYTIME_QUERY; + fctx->filter = apply_calfilter; + fctx->filter_crit = &calfilter; + + /* Parse children element of report */ + for (node = inroot->children; node; node = node->next) { + if (node->type == XML_ELEMENT_NODE) { + if (!xmlStrcmp(node->name, BAD_CAST "time-range")) { + xmlChar *start, *end; + + start = xmlGetProp(node, BAD_CAST "start"); + if (start) { + calfilter.start = icaltime_from_string((char *) start); + xmlFree(start); + } + + end = xmlGetProp(node, BAD_CAST "end"); + if (end) { + calfilter.end = icaltime_from_string((char *) end); + xmlFree(end); + } + + if (!is_valid_timerange(calfilter.start, calfilter.end)) { + return HTTP_BAD_REQUEST; + } + } + } + } + + cal = busytime_query_local(txn, fctx, txn->req_tgt.mboxname, + 0, NULL, NULL, NULL); + + if (calfilter.freebusy.fb) free(calfilter.freebusy.fb); + + if (cal) { + /* Output the iCalendar object as text/calendar */ + char *cal_str = mime->to_string(cal); + icalcomponent_free(cal); + + txn->resp_body.type = mime->content_type; + + /* iCalendar data in response should not be transformed */ + txn->flags.cc |= CC_NOTRANSFORM; + + write_body(HTTP_OK, txn, cal_str, strlen(cal_str)); + free(cal_str); + } + else ret = HTTP_NOT_FOUND; + + return ret; +} + + +/* Store the iCal data in the specified calendar/resource */ +static int store_resource(struct transaction_t *txn, icalcomponent *ical, + struct mailbox *mailbox, const char *resource, + struct caldav_db *caldavdb, int overwrite, + unsigned flags) +{ + int ret = HTTP_CREATED, r; + icalcomponent *comp; + icalcomponent_kind kind; + icalproperty_method meth; + icalproperty *prop; + unsigned mykind = 0; + char *header; + const char *organizer = NULL; + const char *prop_annot = + ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set"; + struct buf attrib = BUF_INITIALIZER; + struct buf headbuf = BUF_INITIALIZER; + struct index_record oldrecord; + struct caldav_data *cdata; + quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_DONTCARE_INITIALIZER; + FILE *f = NULL; + struct stagemsg *stage; + const char *uid, *ics; + uint32_t expunge_uid = 0; + time_t now = time(NULL); + char datestr[80]; + struct appendstate as; + static char sched_tag[64]; + static unsigned store_count = 0; + + /* Check for supported component type */ + comp = icalcomponent_get_first_real_component(ical); + kind = icalcomponent_isa(comp); + switch (kind) { + case ICAL_VEVENT_COMPONENT: mykind = CAL_COMP_VEVENT; break; + case ICAL_VTODO_COMPONENT: mykind = CAL_COMP_VTODO; break; + case ICAL_VJOURNAL_COMPONENT: mykind = CAL_COMP_VJOURNAL; break; + case ICAL_VFREEBUSY_COMPONENT: mykind = CAL_COMP_VFREEBUSY; break; + case ICAL_VAVAILABILITY_COMPONENT: mykind = CAL_COMP_VAVAILABILITY; break; +#ifdef HAVE_VPOLL + case ICAL_VPOLL_COMPONENT: mykind = CAL_COMP_VPOLL; break; +#endif + default: + txn->error.precond = CALDAV_SUPP_COMP; + return HTTP_FORBIDDEN; + } + + if (!annotatemore_lookupmask(mailbox->name, prop_annot, httpd_userid, &attrib) + && attrib.len) { + unsigned long supp_comp = strtoul(buf_cstring(&attrib), NULL, 10); + + buf_free(&attrib); + + if (!(mykind & supp_comp)) { + txn->error.precond = CALDAV_SUPP_COMP; + return HTTP_FORBIDDEN; + } + } + + /* Find iCalendar UID for the current resource, if exists */ + uid = icalcomponent_get_uid(comp); + caldav_lookup_resource(caldavdb, + mailbox->name, resource, 0, &cdata); + if (cdata->ical_uid && strcmp(cdata->ical_uid, uid)) { + /* CALDAV:no-uid-conflict */ + txn->error.precond = CALDAV_UID_CONFLICT; + return HTTP_FORBIDDEN; + } + + /* XXX - theoretical race, but the mailbox is locked, so nothing + * else can ACTUALLY change it */ + if (cdata->dav.imap_uid) { + /* Fetch index record for the resource */ + r = mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &oldrecord); + if (r) return HTTP_SERVER_ERROR; + + if (overwrite == OVERWRITE_CHECK) { + /* Check any preconditions */ + const char *etag = message_guid_encode(&oldrecord.guid); + time_t lastmod = oldrecord.internaldate; + int precond = caldav_check_precond(txn, cdata, + etag, lastmod); + + if (precond != HTTP_OK) + return HTTP_PRECOND_FAILED; + } + + expunge_uid = cdata->dav.imap_uid; + } + + /* Check for existing iCalendar UID */ + caldav_lookup_uid(caldavdb, uid, 0, &cdata); + if (!(flags & NO_DUP_CHECK) && + cdata->dav.mailbox && !strcmp(cdata->dav.mailbox, mailbox->name) && + strcmp(cdata->dav.resource, resource)) { + /* CALDAV:no-uid-conflict */ + const char *owner = mboxname_to_userid(cdata->dav.mailbox); + + txn->error.precond = CALDAV_UID_CONFLICT; + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%s/user/%s/%s/%s", + namespace_calendar.prefix, owner, + strrchr(cdata->dav.mailbox, '.')+1, cdata->dav.resource); + txn->error.resource = buf_cstring(&txn->buf); + return HTTP_FORBIDDEN; + } + + /* 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\r\n"; + return HTTP_SERVER_ERROR; + } + + /* Remove all X-LIC-ERROR properties*/ + icalcomponent_strip_errors(ical); + + ics = icalcomponent_as_ical_string(ical); + + /* Create iMIP header for resource */ + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + if (prop) { + organizer = icalproperty_get_organizer(prop)+7; + header = charset_encode_mimeheader(organizer, 0); + fprintf(f, "From: %s\r\n", header); + free(header); + } + else if (strchr(proxy_userid, '@')) { + /* XXX This needs to be done via an LDAP/DB lookup */ + header = charset_encode_mimeheader(proxy_userid, 0); + fprintf(f, "From: %s\r\n", header); + free(header); + } + else { + struct buf *headbuf = &txn->buf; + + assert(!headbuf->len); + buf_printf(headbuf, "%s@%s", proxy_userid, config_servername); + header = charset_encode_mimeheader(headbuf->s, headbuf->len); + buf_reset(headbuf); + + fprintf(f, "From: %s\r\n", header); + free(header); + } + + prop = icalcomponent_get_first_property(comp, ICAL_SUMMARY_PROPERTY); + if (prop) { + header = charset_encode_mimeheader(icalproperty_get_summary(prop), 0); + fprintf(f, "Subject: %s\r\n", header); + free(header); + } + else fprintf(f, "Subject: %s\r\n", icalcomponent_kind_to_string(kind)); + + time_to_rfc822(icaltime_as_timet_with_zone(icalcomponent_get_dtstamp(comp), + icaltimezone_get_utc_timezone()), + datestr, sizeof(datestr)); + fprintf(f, "Date: %s\r\n", datestr); + + /* XXX - validate uid for mime safety? */ + if (strchr(uid, '@')) { + fprintf(f, "Message-ID: <%s>\r\n", uid); + } + else { + fprintf(f, "Message-ID: <%s@%s>\r\n", uid, config_servername); + } + + fprintf(f, "Content-Type: text/calendar; charset=utf-8"); + if ((meth = icalcomponent_get_method(ical)) != ICAL_METHOD_NONE) { + fprintf(f, "; method=%s", icalproperty_method_to_string(meth)); + } + fprintf(f, "; component=%s\r\n", icalcomponent_kind_to_string(kind)); + + fprintf(f, "Content-Length: %u\r\n", (unsigned) strlen(ics)); + fprintf(f, "Content-Disposition: inline; filename=\"%s\"", resource); + if (organizer) { + const char *stag; + if (flags & NEW_STAG) { + sprintf(sched_tag, "%d-%ld-%u", getpid(), now, store_count++); + stag = sched_tag; + } + else stag = cdata->sched_tag; + if (stag) fprintf(f, ";\r\n\tschedule-tag=%s", stag); + } + fprintf(f, "\r\n"); + + /* XXX Check domain of data and use appropriate CTE */ + + fprintf(f, "MIME-Version: 1.0\r\n"); + fprintf(f, "\r\n"); + + /* Write the iCal data to the file */ + fprintf(f, "%s", ics); + qdiffs[QUOTA_STORAGE] = ftell(f); + + fclose(f); + + qdiffs[QUOTA_MESSAGE] = 1; + + /* Prepare to append the iMIP message to calendar mailbox */ + if ((r = append_setup_mbox(&as, mailbox, NULL, NULL, 0, qdiffs, 0, 0, EVENT_MESSAGE_NEW|EVENT_CALENDAR))) { + syslog(LOG_ERR, "append_setup(%s) failed: %s", + mailbox->name, error_message(r)); + ret = HTTP_SERVER_ERROR; + txn->error.desc = "append_setup() failed\r\n"; + } + else { + struct body *body = NULL; + + /* Append the iMIP file to the calendar mailbox */ + if ((r = append_fromstage(&as, &body, stage, now, NULL, 0, 0))) { + syslog(LOG_ERR, "append_fromstage() failed"); + ret = HTTP_SERVER_ERROR; + txn->error.desc = "append_fromstage() failed\r\n"; + } + if (body) { + message_free_body(body); + free(body); + } + + if (r) append_abort(&as); + else { + /* Commit the append to the calendar mailbox */ + if ((r = append_commit(&as))) { + syslog(LOG_ERR, "append_commit() failed"); + ret = HTTP_SERVER_ERROR; + txn->error.desc = "append_commit() failed\r\n"; + } + else { + /* append_commit() returns a write-locked index */ + struct index_record newrecord; + + /* Read index record for new message (always the last one) */ + mailbox_read_index_record(mailbox, mailbox->i.num_records, + &newrecord); + + if (expunge_uid) { + /* Now that we have the replacement message in place + and the mailbox locked, re-read the old record + and see if we should overwrite it. Either way, + one of our records will have to be expunged. + */ + int userflag; + + ret = HTTP_NO_CONTENT; + + /* Perform the actual expunge */ + r = mailbox_user_flag(mailbox, DFLAG_UNBIND, &userflag, 1); + if (!r) { + oldrecord.user_flags[userflag/32] |= 1<<(userflag&31); + oldrecord.system_flags |= FLAG_EXPUNGED; + r = mailbox_rewrite_index_record(mailbox, &oldrecord); + } + if (r) { + syslog(LOG_ERR, "expunging record (%s) failed: %s", + mailbox->name, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + } + } + + if (!r) { + struct resp_body_t *resp_body = &txn->resp_body; + + if (cdata->organizer && (flags & NEW_STAG)) { + resp_body->stag = sched_tag; + } + + if ((flags & PREFER_REP) || !(flags & NEW_STAG)) { + /* Tell client about the new resource */ + resp_body->lastmod = newrecord.internaldate; + resp_body->etag = message_guid_encode(&newrecord.guid); + } + } + } + } + } + + append_removestage(stage); + + buf_free(&headbuf); + + return ret; +} + + +int caladdress_lookup(const char *addr, struct sched_param *param) +{ + char *p; + int islocal = 1, found = 1; + static char userid[MAX_MAILBOX_BUFFER]; + + memset(param, 0, sizeof(struct sched_param)); + + if (!addr) return HTTP_NOT_FOUND; + + p = (char *) addr; + if (!strncasecmp(addr, "mailto:", 7)) p += 7; + + /* XXX Do LDAP/DB/socket lookup to see if user is local */ + /* XXX Hack until real lookup stuff is written */ + strlcpy(userid, p, sizeof(userid)); + if ((p = strchr(userid, '@')) && !(*p = '\0') && *++p) { + struct strlist *domains = cua_domains; + + for (; domains && strcmp(p, domains->s); domains = domains->next); + + if (!domains) islocal = 0; + } + + if (islocal) { + /* User is in a local domain */ + int r; + char mailboxname[MAX_MAILBOX_NAME]; + mbentry_t *mbentry = NULL; + struct mboxname_parts parts; + + if (!found) return HTTP_NOT_FOUND; + else param->userid = userid; + + /* Lookup user's cal-home-set to see if its on this server */ + + mboxname_userid_to_parts(userid, &parts); + parts.box = xstrdupnull(config_getstring(IMAPOPT_CALENDARPREFIX)); + mboxname_parts_to_internal(&parts, mailboxname); + mboxname_free_parts(&parts); + + r = http_mlookup(mailboxname, &mbentry, NULL); + if (!r) { + param->server = xstrdupnull(mbentry->server); /* XXX - memory leak */ + mboxlist_entry_free(&mbentry); + if (param->server) param->flags |= SCHEDTYPE_ISCHEDULE; + } + /* Fall through and try remote */ + } + + /* User is outside of our domain(s) - + Do remote scheduling (default = iMIP) */ + param->flags |= SCHEDTYPE_REMOTE; + +#ifdef WITH_DKIM + /* Do iSchedule DNS SRV lookup */ + + /* XXX If success, set server, port, + and flags |= SCHEDTYPE_ISCHEDULE [ | SCHEDTYPE_SSL ] */ + +#ifdef IOPTEST /* CalConnect ioptest */ + if (!strcmp(p, "example.com")) { + param->userid = userid; + param->server = "ischedule.example.com"; + param->port = 8008; + param->flags |= SCHEDTYPE_ISCHEDULE; + } + else if (!strcmp(p, "mysite.edu")) { + param->userid = userid; + param->server = "ischedule.mysite.edu"; + param->port = 8080; + param->flags |= SCHEDTYPE_ISCHEDULE; + } + else if (!strcmp(p, "bedework.org")) { + param->userid = userid; + param->server = "www.bedework.org"; + param->port = 80; + param->flags |= SCHEDTYPE_ISCHEDULE; + } +#endif /* IOPTEST */ + +#endif /* WITH_DKIM */ + + return 0; +} + + +/* Send an iMIP request for attendees in 'ical' */ +static int imip_send(icalcomponent *ical) +{ + icalcomponent *comp; + icalproperty *prop; + icalproperty_method meth; + icalcomponent_kind kind; + const char *argv[8], *originator, *subject; + FILE *sm; + pid_t pid; + int r; + time_t t = time(NULL); + char datestr[80]; + static unsigned send_count = 0; + icalproperty_kind recip_kind; + const char *(*get_recipient)(const icalproperty *); + + meth = icalcomponent_get_method(ical); + comp = icalcomponent_get_first_real_component(ical); + kind = icalcomponent_isa(comp); + + /* Determine Originator and Recipient(s) based on methond and component */ + if (meth == ICAL_METHOD_REPLY) { + recip_kind = ICAL_ORGANIZER_PROPERTY; + get_recipient = &icalproperty_get_organizer; + + if (kind == ICAL_VPOLL_COMPONENT) { + prop = icalcomponent_get_first_property(comp, ICAL_VOTER_PROPERTY); + originator = icalproperty_get_voter(prop) + 7; + } + else { + prop = + icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY); + originator = icalproperty_get_attendee(prop) + 7; + } + } + else { + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + originator = icalproperty_get_organizer(prop) + 7; + + if (kind == ICAL_VPOLL_COMPONENT) { + recip_kind = ICAL_VOTER_PROPERTY; + get_recipient = &icalproperty_get_voter; + } + else { + recip_kind = ICAL_ATTENDEE_PROPERTY; + get_recipient = &icalproperty_get_attendee; + } + } + + argv[0] = "sendmail"; + argv[1] = "-f"; + argv[2] = originator; + argv[3] = "-i"; + argv[4] = "-N"; + argv[5] = "failure,delay"; + argv[6] = "-t"; + argv[7] = NULL; + pid = open_sendmail(argv, &sm); + + if (sm == NULL) return HTTP_UNAVAILABLE; + + /* Create iMIP message */ + fprintf(sm, "From: %s\r\n", originator); + + for (prop = icalcomponent_get_first_property(comp, recip_kind); + prop; + prop = icalcomponent_get_next_property(comp, recip_kind)) { + fprintf(sm, "To: %s\r\n", get_recipient(prop) + 7); + } + + subject = icalcomponent_get_summary(comp); + if (!subject) { + fprintf(sm, "Subject: %s %s\r\n", icalcomponent_kind_to_string(kind), + icalproperty_method_to_string(meth)); + } + else fprintf(sm, "Subject: %s\r\n", subject); + + time_to_rfc822(t, datestr, sizeof(datestr)); + fprintf(sm, "Date: %s\r\n", datestr); + + fprintf(sm, "Message-ID: <cmu-httpd-%u-%ld-%u@%s>\r\n", + getpid(), t, send_count++, config_servername); + + fprintf(sm, "Content-Type: text/calendar; charset=utf-8"); + fprintf(sm, "; method=%s; component=%s \r\n", + icalproperty_method_to_string(meth), + icalcomponent_kind_to_string(kind)); + + fputs("Content-Disposition: inline\r\n", sm); + + fputs("MIME-Version: 1.0\r\n", sm); + fputs("\r\n", sm); + + fputs(icalcomponent_as_ical_string(ical), sm); + + fclose(sm); + + while (waitpid(pid, &r, 0) < 0); + + return r; +} + + +/* Add a <response> XML element for 'recipient' to 'root' */ +xmlNodePtr xml_add_schedresponse(xmlNodePtr root, xmlNsPtr dav_ns, + xmlChar *recipient, xmlChar *status) +{ + xmlNodePtr resp, recip; + + resp = xmlNewChild(root, NULL, BAD_CAST "response", NULL); + recip = xmlNewChild(resp, NULL, BAD_CAST "recipient", NULL); + + if (dav_ns) xml_add_href(recip, dav_ns, (const char *) recipient); + else xmlNodeAddContent(recip, recipient); + + if (status) + xmlNewChild(resp, NULL, BAD_CAST "request-status", status); + + return resp; +} + + +struct remote_rock { + struct transaction_t *txn; + icalcomponent *ical; + xmlNodePtr root; + xmlNsPtr *ns; +}; + +/* Send an iTIP busytime request to remote attendees via iMIP or iSchedule */ +static void busytime_query_remote(const char *server __attribute__((unused)), + void *data, void *rock) +{ + struct sched_param *remote = (struct sched_param *) data; + struct remote_rock *rrock = (struct remote_rock *) rock; + icalcomponent *comp; + struct proplist *list; + xmlNodePtr resp; + const char *status = NULL; + int r; + + comp = icalcomponent_get_first_real_component(rrock->ical); + + /* Add the attendees to the iTIP request */ + for (list = remote->props; list; list = list->next) { + icalcomponent_add_property(comp, list->prop); + } + + if (remote->flags == SCHEDTYPE_REMOTE) { + /* Use iMIP */ + + r = imip_send(rrock->ical); + + if (!r) status = REQSTAT_SENT; + else status = REQSTAT_TEMPFAIL; + } + else { + /* Use iSchedule */ + xmlNodePtr xml; + + r = isched_send(remote, NULL, rrock->ical, &xml); + if (r) status = REQSTAT_TEMPFAIL; + else if (xmlStrcmp(xml->name, BAD_CAST "schedule-response")) { + if (r) status = REQSTAT_TEMPFAIL; + } + else { + xmlNodePtr cur; + + /* Process each response element */ + for (cur = xml->children; cur; cur = cur->next) { + xmlNodePtr node; + xmlChar *recip = NULL, *status = NULL, *content = NULL; + + if (cur->type != XML_ELEMENT_NODE) continue; + + for (node = cur->children; node; node = node->next) { + if (node->type != XML_ELEMENT_NODE) continue; + + if (!xmlStrcmp(node->name, BAD_CAST "recipient")) + recip = xmlNodeGetContent(node); + else if (!xmlStrcmp(node->name, BAD_CAST "request-status")) + status = xmlNodeGetContent(node); + else if (!xmlStrcmp(node->name, BAD_CAST "calendar-data")) + content = xmlNodeGetContent(node); + } + + resp = + xml_add_schedresponse(rrock->root, + !(rrock->txn->req_tgt.allow & ALLOW_ISCHEDULE) ? + rrock->ns[NS_DAV] : NULL, + recip, status); + + xmlFree(status); + xmlFree(recip); + + if (content) { + xmlNodePtr cdata = + xmlNewTextChild(resp, NULL, + BAD_CAST "calendar-data", NULL); + xmlAddChild(cdata, + xmlNewCDataBlock(rrock->root->doc, + content, + xmlStrlen(content))); + xmlFree(content); + + /* iCal data in resp SHOULD NOT be transformed */ + rrock->txn->flags.cc |= CC_NOTRANSFORM; + } + } + + xmlFreeDoc(xml->doc); + } + } + + /* Report request-status (if necesary) + * Remove the attendees from the iTIP request and hash bucket + */ + for (list = remote->props; list; list = list->next) { + if (status) { + const char *attendee = icalproperty_get_attendee(list->prop); + xml_add_schedresponse(rrock->root, + !(rrock->txn->req_tgt.allow & ALLOW_ISCHEDULE) ? + rrock->ns[NS_DAV] : NULL, + BAD_CAST attendee, + BAD_CAST status); + } + + icalcomponent_remove_property(comp, list->prop); + icalproperty_free(list->prop); + } + + if (remote->server) free(remote->server); +} + + +static void free_sched_param(void *data) +{ + struct sched_param *sched_param = (struct sched_param *) data; + + if (sched_param) { + struct proplist *prop, *next; + + for (prop = sched_param->props; prop; prop = next) { + next = prop->next; + free(prop); + } + free(sched_param); + } +} + + +/* Perform a Busy Time query based on given VFREEBUSY component */ +/* NOTE: This function is destructive of 'ical' */ +int sched_busytime_query(struct transaction_t *txn, + struct mime_type_t *mime, icalcomponent *ical) +{ + int ret = 0; + static const char *calendarprefix = NULL; + icalcomponent *comp; + char mailboxname[MAX_MAILBOX_BUFFER]; + icalproperty *prop = NULL, *next; + const char *uid = NULL, *organizer = NULL; + struct sched_param sparam; + struct auth_state *org_authstate = NULL; + xmlNodePtr root = NULL; + xmlNsPtr ns[NUM_NAMESPACE]; + struct propfind_ctx fctx; + struct calquery_filter calfilter; + struct hash_table remote_table; + struct sched_param *remote = NULL; + + if (!calendarprefix) { + calendarprefix = config_getstring(IMAPOPT_CALENDARPREFIX); + } + + comp = icalcomponent_get_first_real_component(ical); + uid = icalcomponent_get_uid(comp); + + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + organizer = icalproperty_get_organizer(prop); + + /* XXX Do we need to do more checks here? */ + if (caladdress_lookup(organizer, &sparam) || + (sparam.flags & SCHEDTYPE_REMOTE)) + org_authstate = auth_newstate("anonymous"); + else + org_authstate = auth_newstate(sparam.userid); + + /* Start construction of our schedule-response */ + if (!(root = + init_xml_response("schedule-response", + (txn->req_tgt.allow & ALLOW_ISCHEDULE) ? NS_ISCHED : + NS_CALDAV, NULL, ns))) { + ret = HTTP_SERVER_ERROR; + txn->error.desc = "Unable to create XML response\r\n"; + goto done; + } + + /* Need DAV for hrefs */ + ensure_ns(ns, NS_DAV, root, XML_NS_DAV, "D"); + + /* Populate our filter and propfind context for local attendees */ + memset(&calfilter, 0, sizeof(struct calquery_filter)); + calfilter.comp = + CAL_COMP_VEVENT | CAL_COMP_VFREEBUSY | CAL_COMP_VAVAILABILITY; + calfilter.start = icalcomponent_get_dtstart(comp); + calfilter.end = icalcomponent_get_dtend(comp); + calfilter.flags = BUSYTIME_QUERY | CHECK_CAL_TRANSP | CHECK_USER_AVAIL; + + memset(&fctx, 0, sizeof(struct propfind_ctx)); + fctx.req_tgt = &txn->req_tgt; + fctx.depth = 2; + fctx.userid = proxy_userid; + fctx.userisadmin = httpd_userisadmin; + fctx.authstate = org_authstate; + fctx.reqd_privs = 0; /* handled by CALDAV:schedule-deliver on Inbox */ + fctx.filter = apply_calfilter; + fctx.filter_crit = &calfilter; + fctx.err = &txn->error; + fctx.ret = &ret; + fctx.fetcheddata = 0; + + /* Create hash table for any remote attendee servers */ + construct_hash_table(&remote_table, 10, 1); + + assert(!buf_len(&txn->buf)); + + /* Process each attendee */ + for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY); + prop; + prop = next) { + const char *attendee; + int r; + + next = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY); + + /* Remove each attendee so we can add in only those + that reside on a given remote server later */ + icalcomponent_remove_property(comp, prop); + + /* Is attendee remote or local? */ + attendee = icalproperty_get_attendee(prop); + r = caladdress_lookup(attendee, &sparam); + + /* Don't allow scheduling of remote users via an iSchedule request */ + if ((sparam.flags & SCHEDTYPE_REMOTE) && + (txn->req_tgt.allow & ALLOW_ISCHEDULE)) { + r = HTTP_FORBIDDEN; + } + + if (r) { + xml_add_schedresponse(root, + !(txn->req_tgt.allow & ALLOW_ISCHEDULE) ? + ns[NS_DAV] : NULL, + BAD_CAST attendee, BAD_CAST REQSTAT_NOUSER); + + icalproperty_free(prop); + } + else if (sparam.flags) { + /* Remote attendee */ + struct proplist *newprop; + const char *key; + + if (sparam.flags == SCHEDTYPE_REMOTE) { + /* iMIP - collect attendees under empty key (no server) */ + key = ""; + } + else { + /* iSchedule - collect attendees by server */ + key = sparam.server; + } + + remote = hash_lookup(key, &remote_table); + if (!remote) { + /* New remote - add it to the hash table */ + remote = xzmalloc(sizeof(struct sched_param)); + if (sparam.server) remote->server = xstrdup(sparam.server); + remote->port = sparam.port; + remote->flags = sparam.flags; + hash_insert(key, remote, &remote_table); + } + newprop = xmalloc(sizeof(struct proplist)); + newprop->prop = prop; + newprop->next = remote->props; + remote->props = newprop; + } + else { + /* Local attendee on this server */ + xmlNodePtr resp; + const char *userid = sparam.userid; + mbentry_t *mbentry = NULL; + icalcomponent *busy = NULL; + + resp = + xml_add_schedresponse(root, + !(txn->req_tgt.allow & ALLOW_ISCHEDULE) ? + ns[NS_DAV] : NULL, + BAD_CAST attendee, NULL); + + /* XXX - BROKEN WITH DOMAIN SPLIT, POS */ + /* Check ACL of ORGANIZER on attendee's Scheduling Inbox */ + snprintf(mailboxname, sizeof(mailboxname), + "user.%s.%s.Inbox", userid, calendarprefix); + + r = mboxlist_lookup(mailboxname, &mbentry, NULL); + if (r) { + syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s", + mailboxname, error_message(r)); + xmlNewChild(resp, NULL, BAD_CAST "request-status", + BAD_CAST REQSTAT_REJECTED); + } + else { + /* Start query at attendee's calendar-home-set */ + snprintf(mailboxname, sizeof(mailboxname), + "user.%s.%s", userid, calendarprefix); + + fctx.davdb = NULL; + fctx.req_tgt->collection = NULL; + calfilter.freebusy.len = 0; + busy = busytime_query_local(txn, &fctx, mailboxname, + ICAL_METHOD_REPLY, uid, + organizer, attendee); + } + + if (busy) { + xmlNodePtr cdata; + char *fb_str = mime->to_string(busy); + icalcomponent_free(busy); + + xmlNewChild(resp, NULL, BAD_CAST "request-status", + BAD_CAST REQSTAT_SUCCESS); + + cdata = xmlNewTextChild(resp, NULL, + BAD_CAST "calendar-data", NULL); + + /* Trim any charset from content-type */ + buf_reset(&txn->buf); + buf_printf(&txn->buf, "%.*s", + (int) strcspn(mime->content_type, ";"), + mime->content_type); + + xmlNewProp(cdata, BAD_CAST "content-type", + BAD_CAST buf_cstring(&txn->buf)); + + if (mime->version) + xmlNewProp(cdata, BAD_CAST "version", + BAD_CAST mime->version); + + xmlAddChild(cdata, + xmlNewCDataBlock(root->doc, BAD_CAST fb_str, + strlen(fb_str))); + free(fb_str); + + /* iCalendar data in response should not be transformed */ + txn->flags.cc |= CC_NOTRANSFORM; + } + else { + xmlNewChild(resp, NULL, BAD_CAST "request-status", + BAD_CAST REQSTAT_NOUSER); + } + + icalproperty_free(prop); + } + } + + buf_reset(&txn->buf); + + if (remote) { + struct remote_rock rrock = { txn, ical, root, ns }; + hash_enumerate(&remote_table, busytime_query_remote, &rrock); + } + free_hash_table(&remote_table, free_sched_param); + + /* Output the XML response */ + if (!ret) xml_response(HTTP_OK, txn, root->doc); + + done: + if (org_authstate) auth_freestate(org_authstate); + if (calfilter.freebusy.fb) free(calfilter.freebusy.fb); + if (root) xmlFreeDoc(root->doc); + + return ret; +} + + +static void free_sched_data(void *data) +{ + struct sched_data *sched_data = (struct sched_data *) data; + + if (sched_data) { + if (sched_data->itip) icalcomponent_free(sched_data->itip); + free(sched_data); + } +} + + +#define SCHEDSTAT_PENDING "1.0" +#define SCHEDSTAT_SENT "1.1" +#define SCHEDSTAT_DELIVERED "1.2" +#define SCHEDSTAT_SUCCESS "2.0" +#define SCHEDSTAT_PARAM "2.3" +#define SCHEDSTAT_NOUSER "3.7" +#define SCHEDSTAT_NOPRIVS "3.8" +#define SCHEDSTAT_TEMPFAIL "5.1" +#define SCHEDSTAT_PERMFAIL "5.2" +#define SCHEDSTAT_REJECTED "5.3" + +/* Deliver scheduling object to a remote recipient */ +static void sched_deliver_remote(const char *recipient, + struct sched_param *sparam, + struct sched_data *sched_data) +{ + int r; + + if (sparam->flags == SCHEDTYPE_REMOTE) { + /* Use iMIP */ + r = imip_send(sched_data->itip); + if (!r) { + sched_data->status = + sched_data->ischedule ? REQSTAT_SENT : SCHEDSTAT_SENT; + } + else { + sched_data->status = sched_data->ischedule ? + REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL; + } + } + else { + /* Use iSchedule */ + xmlNodePtr xml; + + r = isched_send(sparam, recipient, sched_data->itip, &xml); + if (r) { + sched_data->status = sched_data->ischedule ? + REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL; + } + else if (xmlStrcmp(xml->name, BAD_CAST "schedule-response")) { + sched_data->status = sched_data->ischedule ? + REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL; + } + else { + xmlNodePtr cur; + + /* Process each response element */ + for (cur = xml->children; cur; cur = cur->next) { + xmlNodePtr node; + xmlChar *recip = NULL, *status = NULL; + static char statbuf[1024]; + + if (cur->type != XML_ELEMENT_NODE) continue; + + for (node = cur->children; node; node = node->next) { + if (node->type != XML_ELEMENT_NODE) continue; + + if (!xmlStrcmp(node->name, BAD_CAST "recipient")) + recip = xmlNodeGetContent(node); + else if (!xmlStrcmp(node->name, + BAD_CAST "request-status")) + status = xmlNodeGetContent(node); + } + + if (!strncmp((const char *) status, "2.0", 3)) { + sched_data->status = sched_data->ischedule ? + REQSTAT_DELIVERED : SCHEDSTAT_DELIVERED; + } + else { + if (sched_data->ischedule) + strlcpy(statbuf, (const char *) status, sizeof(statbuf)); + else + strlcpy(statbuf, (const char *) status, 4); + + sched_data->status = statbuf; + } + + xmlFree(status); + xmlFree(recip); + } + } + } +} + + +#ifdef HAVE_VPOLL +/* + * deliver_merge_reply() helper function + * + * Merge VOTER responses into VPOLL subcomponents + */ +static void deliver_merge_vpoll_reply(icalcomponent *ical, icalcomponent *reply) +{ + icalproperty *mastervoterp; + const char *voter; + icalcomponent *comp; + + mastervoterp = icalcomponent_get_first_property(reply, ICAL_VOTER_PROPERTY); + voter = icalproperty_get_voter(mastervoterp); + + /* Process each existing VPOLL subcomponent */ + for (comp = icalcomponent_get_first_component(ical, ICAL_ANY_COMPONENT); + comp; + comp = icalcomponent_get_next_component(ical, ICAL_ANY_COMPONENT)) { + + icalproperty *itemid, *voterp; + int id; + + itemid = + icalcomponent_get_first_property(comp, ICAL_POLLITEMID_PROPERTY); + if (!itemid) continue; + + id = icalproperty_get_pollitemid(itemid); + + /* Remove any existing voter property from the subcomponent */ + for (voterp = icalcomponent_get_first_property(comp, + ICAL_VOTER_PROPERTY); + voterp && strcmp(voter, icalproperty_get_voter(voterp)); + voterp = icalcomponent_get_next_property(comp, + ICAL_VOTER_PROPERTY)); + + if (voterp) { + icalcomponent_remove_property(comp, voterp); + icalproperty_free(voterp); + } + + + /* Find matching poll-item-id in the reply */ + for (itemid = icalcomponent_get_first_property(reply, + ICAL_POLLITEMID_PROPERTY); + itemid && (id != icalproperty_get_pollitemid(itemid)); + itemid = icalcomponent_get_next_property(reply, + ICAL_POLLITEMID_PROPERTY)); + if (itemid) { + icalparameter *param; + + /* Add a VOTER property with params from the reply */ + voterp = icalproperty_new_clone(mastervoterp); + + for (param = + icalproperty_get_first_parameter(itemid, + ICAL_ANY_PARAMETER); + param; + param = + icalproperty_get_next_parameter(itemid, + ICAL_ANY_PARAMETER)) { + switch (icalparameter_isa(param)) { + case ICAL_PUBLICCOMMENT_PARAMETER: + case ICAL_RESPONSE_PARAMETER: + icalproperty_add_parameter(voterp, + icalparameter_new_clone(param)); + break; + + default: + break; + } + } + + icalcomponent_add_property(comp, voterp); + } + } +} + + +/* sched_reply() helper function + * + * Add voter responses to VPOLL reply and remove subcomponents + * + */ +static void sched_vpoll_reply(icalcomponent *poll, const char *voter) +{ + icalcomponent *item, *next; + icalproperty *prop; + + for (item = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT); + + item; + item = next) { + + next = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT); + + prop = icalcomponent_get_first_property(item, ICAL_POLLITEMID_PROPERTY); + if (prop) { + int id = icalproperty_get_pollitemid(prop); + + for (prop = icalcomponent_get_first_property(item, + ICAL_VOTER_PROPERTY); + prop; + prop = + icalcomponent_get_next_property(item, + ICAL_VOTER_PROPERTY)) { + if (!strcmp(voter, icalproperty_get_voter(prop))) { + icalproperty *itemid = icalproperty_new_pollitemid(id); + icalparameter *param; + + for (param = + icalproperty_get_first_parameter(prop, + ICAL_ANY_PARAMETER); + param; + param = + icalproperty_get_next_parameter(prop, + ICAL_ANY_PARAMETER)) { + switch (icalparameter_isa(param)) { + case ICAL_PUBLICCOMMENT_PARAMETER: + case ICAL_RESPONSE_PARAMETER: + icalproperty_add_parameter(itemid, + icalparameter_new_clone(param)); + break; + + default: + break; + } + } + + icalcomponent_add_property(poll, itemid); + } + } + } + + icalcomponent_remove_component(poll, item); + icalcomponent_free(item); + } +} + + +struct pollstatus { + icalcomponent *item; + struct hash_table voter_table; +}; + +static void free_pollstatus(void *data) +{ + struct pollstatus *status = (struct pollstatus *) data; + + if (status) { + free_hash_table(&status->voter_table, NULL); + free(status); + } +} + +static int deliver_merge_pollstatus(icalcomponent *ical, icalcomponent *request) +{ + int deliver_inbox = 0; + struct hash_table comp_table; + icalcomponent *poll, *sub; + icalproperty *prop; + const char *itemid, *voter; + + /* Add each sub-component of old object to hash table for comparison */ + construct_hash_table(&comp_table, 10, 1); + poll = icalcomponent_get_first_component(ical, ICAL_VPOLL_COMPONENT); + for (sub = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT); + sub; + sub = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT)) { + struct pollstatus *status = xmalloc(sizeof(struct pollstatus)); + + status->item = sub; + + prop = icalcomponent_get_first_property(sub, ICAL_POLLITEMID_PROPERTY); + itemid = icalproperty_get_value_as_string(prop); + + hash_insert(itemid, status, &comp_table); + + /* Add each VOTER to voter hash table */ + construct_hash_table(&status->voter_table, 10, 1); + for (prop = icalcomponent_get_first_property(sub, ICAL_VOTER_PROPERTY); + prop; + prop = + icalcomponent_get_next_property(sub, ICAL_VOTER_PROPERTY)) { + voter = icalproperty_get_voter(prop); + + hash_insert(voter, prop, &status->voter_table); + } + } + + /* Process each sub-component in the iTIP request */ + poll = icalcomponent_get_first_component(request, ICAL_VPOLL_COMPONENT); + for (sub = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT); + sub; + sub = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT)) { + struct pollstatus *status; + + prop = icalcomponent_get_first_property(sub, ICAL_POLLITEMID_PROPERTY); + itemid = icalproperty_get_value_as_string(prop); + + status = hash_del(itemid, &comp_table); + if (status) { + for (prop = icalcomponent_get_first_property(sub, + ICAL_VOTER_PROPERTY); + prop; + prop = icalcomponent_get_next_property(sub, + ICAL_VOTER_PROPERTY)) { + + icalproperty *oldvoter; + + voter = icalproperty_get_voter(prop); + + oldvoter = hash_del(voter, &status->voter_table); + if (oldvoter) { + icalcomponent_remove_property(status->item, oldvoter); + icalproperty_free(oldvoter); + } + + icalcomponent_add_property(status->item, + icalproperty_new_clone(prop)); + } + + free_pollstatus(status); + } + } + + free_hash_table(&comp_table, free_pollstatus); + + return deliver_inbox; +} + + +static void sched_pollstatus(const char *organizer, + struct sched_param *sparam, icalcomponent *ical, + const char *voter) +{ + struct auth_state *authstate; + struct sched_data sched_data; + icalcomponent *itip, *comp; + icalproperty *prop; + + /* XXX Do we need to do more checks here? */ + if (sparam->flags & SCHEDTYPE_REMOTE) + authstate = auth_newstate("anonymous"); + else + authstate = auth_newstate(sparam->userid); + + memset(&sched_data, 0, sizeof(struct sched_data)); + sched_data.force_send = ICAL_SCHEDULEFORCESEND_NONE; + + /* Create a shell for our iTIP request objects */ + itip = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT, + icalproperty_new_version("2.0"), + icalproperty_new_prodid(ical_prodid), + icalproperty_new_method(ICAL_METHOD_POLLSTATUS), + 0); + + /* Copy over any CALSCALE property */ + prop = icalcomponent_get_first_property(ical, ICAL_CALSCALE_PROPERTY); + if (prop) icalcomponent_add_property(itip, icalproperty_new_clone(prop)); + + /* Process each VPOLL in resource */ + for (comp = icalcomponent_get_first_component(ical, ICAL_VPOLL_COMPONENT); + comp; + comp =icalcomponent_get_next_component(ical, ICAL_VPOLL_COMPONENT)) { + + icalcomponent *stat, *poll, *sub; + struct strlist *voters = NULL; + + /* Make a working copy of the iTIP */ + stat = icalcomponent_new_clone(itip); + + /* Make a working copy of the VPOLL and add to pollstatus */ + poll = icalcomponent_new_clone(comp); + icalcomponent_add_component(stat, poll); + + /* Make list of VOTERs (stripping SCHEDULE-STATUS */ + for (prop = icalcomponent_get_first_property(poll, ICAL_VOTER_PROPERTY); + prop; + prop = + icalcomponent_get_next_property(poll, ICAL_VOTER_PROPERTY)) { + const char *voter = icalproperty_get_voter(prop); + + if (strcmp(voter, organizer)) + appendstrlist(&voters, (char *) icalproperty_get_voter(prop)); + + icalproperty_remove_parameter_by_name(prop, "SCHEDULE-STATUS"); + } + + /* Process each sub-component of VPOLL */ + for (sub = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT); + sub; + sub = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT)) { + + icalproperty *next; + + /* Strip all properties other than POLL-ITEM-ID and VOTER */ + for (prop = + icalcomponent_get_first_property(sub, ICAL_ANY_PROPERTY); + prop; prop = next) { + + next = icalcomponent_get_next_property(sub, ICAL_ANY_PROPERTY); + + switch (icalproperty_isa(prop)) { + case ICAL_POLLITEMID_PROPERTY: + case ICAL_VOTER_PROPERTY: + break; + + default: + icalcomponent_remove_property(sub, prop); + icalproperty_free(prop); + break; + } + } + } + + /* Attempt to deliver to each voter in the list - removing as we go */ + while (voters) { + struct strlist *next = voters->next; + + /* Don't send status back to VOTER that triggered POLLSTATUS */ + if (strcmp(voters->s, voter)) { + sched_data.itip = stat; + sched_deliver(voters->s, &sched_data, authstate); + } + + free(voters->s); + free(voters); + voters = next; + } + + icalcomponent_free(stat); + } + + icalcomponent_free(itip); + auth_freestate(authstate); +} +#else /* HAVE_VPOLL */ +static void +deliver_merge_vpoll_reply(icalcomponent *ical __attribute__((unused)), + icalcomponent *reply __attribute__((unused))) +{ + return; +} + +static void sched_vpoll_reply(icalcomponent *poll __attribute__((unused)), + const char *voter __attribute__((unused))) +{ + return; +} + +static int +deliver_merge_pollstatus(icalcomponent *ical __attribute__((unused)), + icalcomponent *request __attribute__((unused))) +{ + return 0; +} + +static void sched_pollstatus(const char *organizer __attribute__((unused)), + struct sched_param *sparam __attribute__((unused)), + icalcomponent *ical __attribute__((unused)), + const char *voter __attribute__((unused))) +{ + return; +} +#endif /* HAVE_VPOLL */ + + +static const char *deliver_merge_reply(icalcomponent *ical, + icalcomponent *reply) +{ + struct hash_table comp_table; + icalcomponent *comp, *itip; + icalcomponent_kind kind; + icalproperty *prop, *att; + icalparameter *param; + icalparameter_partstat partstat = ICAL_PARTSTAT_NONE; + icalparameter_rsvp rsvp = ICAL_RSVP_NONE; + const char *recurid, *attendee = NULL, *req_stat = SCHEDSTAT_SUCCESS; + icalproperty_kind recip_kind; + const char *(*get_recipient)(const icalproperty *); + + /* Add each component of old object to hash table for comparison */ + construct_hash_table(&comp_table, 10, 1); + comp = icalcomponent_get_first_real_component(ical); + kind = icalcomponent_isa(comp); + do { + prop = + icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY); + if (prop) recurid = icalproperty_get_value_as_string(prop); + else recurid = ""; + + hash_insert(recurid, comp, &comp_table); + + } while ((comp = icalcomponent_get_next_component(ical, kind))); + + + if (kind == ICAL_VPOLL_COMPONENT) { + recip_kind = ICAL_VOTER_PROPERTY; + get_recipient = &icalproperty_get_voter; + } + else { + recip_kind = ICAL_ATTENDEE_PROPERTY; + get_recipient = &icalproperty_get_attendee; + } + + /* Process each component in the iTIP reply */ + for (itip = icalcomponent_get_first_component(reply, kind); + itip; + itip = icalcomponent_get_next_component(reply, kind)) { + + /* Lookup this comp in the hash table */ + prop = + icalcomponent_get_first_property(itip, ICAL_RECURRENCEID_PROPERTY); + if (prop) recurid = icalproperty_get_value_as_string(prop); + else recurid = ""; + + comp = hash_lookup(recurid, &comp_table); + if (!comp) { + /* New recurrence overridden by attendee. + Create a new recurrence from master component. */ + comp = icalcomponent_new_clone(hash_lookup("", &comp_table)); + + /* Add RECURRENCE-ID */ + icalcomponent_add_property(comp, icalproperty_new_clone(prop)); + + /* Remove RRULE */ + prop = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY); + if (prop) { + icalcomponent_remove_property(comp, prop); + icalproperty_free(prop); + } + + /* Replace DTSTART, DTEND, SEQUENCE */ + prop = + icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY); + if (prop) { + icalcomponent_remove_property(comp, prop); + icalproperty_free(prop); + } + prop = + icalcomponent_get_first_property(itip, ICAL_DTSTART_PROPERTY); + if (prop) + icalcomponent_add_property(comp, icalproperty_new_clone(prop)); + + prop = + icalcomponent_get_first_property(comp, ICAL_DTEND_PROPERTY); + if (prop) { + icalcomponent_remove_property(comp, prop); + icalproperty_free(prop); + } + prop = + icalcomponent_get_first_property(itip, ICAL_DTEND_PROPERTY); + if (prop) + icalcomponent_add_property(comp, icalproperty_new_clone(prop)); + + prop = + icalcomponent_get_first_property(comp, ICAL_SEQUENCE_PROPERTY); + if (prop) { + icalcomponent_remove_property(comp, prop); + icalproperty_free(prop); + } + prop = + icalcomponent_get_first_property(itip, ICAL_SEQUENCE_PROPERTY); + if (prop) + icalcomponent_add_property(comp, icalproperty_new_clone(prop)); + + icalcomponent_add_component(ical, comp); + } + + /* Get the sending attendee */ + att = icalcomponent_get_first_property(itip, recip_kind); + attendee = get_recipient(att); + param = icalproperty_get_first_parameter(att, ICAL_PARTSTAT_PARAMETER); + if (param) partstat = icalparameter_get_partstat(param); + param = icalproperty_get_first_parameter(att, ICAL_RSVP_PARAMETER); + if (param) rsvp = icalparameter_get_rsvp(param); + + prop = + icalcomponent_get_first_property(itip, ICAL_REQUESTSTATUS_PROPERTY); + if (prop) { + struct icalreqstattype rq = icalproperty_get_requeststatus(prop); + req_stat = icalenum_reqstat_code(rq.code); + } + + /* Find matching attendee in existing object */ + for (prop = + icalcomponent_get_first_property(comp, recip_kind); + prop && strcmp(attendee, get_recipient(prop)); + prop = + icalcomponent_get_next_property(comp, recip_kind)); + if (!prop) { + /* Attendee added themselves to this recurrence */ + prop = icalproperty_new_clone(att); + icalcomponent_add_property(comp, prop); + } + + /* Set PARTSTAT */ + if (partstat != ICAL_PARTSTAT_NONE) { + param = icalparameter_new_partstat(partstat); + icalproperty_set_parameter(prop, param); + } + + /* Set RSVP */ + icalproperty_remove_parameter_by_kind(prop, ICAL_RSVP_PARAMETER); + if (rsvp != ICAL_RSVP_NONE) { + param = icalparameter_new_rsvp(rsvp); + icalproperty_add_parameter(prop, param); + } + + /* Set SCHEDULE-STATUS */ + param = icalparameter_new_schedulestatus(req_stat); + icalproperty_set_parameter(prop, param); + + /* Handle VPOLL reply */ + if (kind == ICAL_VPOLL_COMPONENT) deliver_merge_vpoll_reply(comp, itip); + } + + free_hash_table(&comp_table, NULL); + + return attendee; +} + + +static int deliver_merge_request(const char *attendee, + icalcomponent *ical, icalcomponent *request) +{ + int deliver_inbox = 0; + struct hash_table comp_table; + icalcomponent *comp, *itip; + icalcomponent_kind kind = ICAL_NO_COMPONENT; + icalproperty *prop; + icalparameter *param; + const char *tzid, *recurid; + + /* Add each VTIMEZONE of old object to hash table for comparison */ + construct_hash_table(&comp_table, 10, 1); + for (comp = + icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT); + comp; + comp = + icalcomponent_get_next_component(ical, ICAL_VTIMEZONE_COMPONENT)) { + prop = icalcomponent_get_first_property(comp, ICAL_TZID_PROPERTY); + tzid = icalproperty_get_tzid(prop); + + hash_insert(tzid, comp, &comp_table); + } + + /* Process each VTIMEZONE in the iTIP request */ + for (itip = icalcomponent_get_first_component(request, + ICAL_VTIMEZONE_COMPONENT); + itip; + itip = icalcomponent_get_next_component(request, + ICAL_VTIMEZONE_COMPONENT)) { + /* Lookup this TZID in the hash table */ + prop = icalcomponent_get_first_property(itip, ICAL_TZID_PROPERTY); + tzid = icalproperty_get_tzid(prop); + + comp = hash_lookup(tzid, &comp_table); + if (comp) { + /* Remove component from old object */ + icalcomponent_remove_component(ical, comp); + icalcomponent_free(comp); + } + + /* Add new/modified component from iTIP request */ + icalcomponent_add_component(ical, icalcomponent_new_clone(itip)); + } + + free_hash_table(&comp_table, NULL); + + /* Add each component of old object to hash table for comparison */ + construct_hash_table(&comp_table, 10, 1); + comp = icalcomponent_get_first_real_component(ical); + if (comp) kind = icalcomponent_isa(comp); + for (; comp; comp = icalcomponent_get_next_component(ical, kind)) { + prop = + icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY); + if (prop) recurid = icalproperty_get_value_as_string(prop); + else recurid = ""; + + hash_insert(recurid, comp, &comp_table); + } + + /* Process each component in the iTIP request */ + itip = icalcomponent_get_first_real_component(request); + if (kind == ICAL_NO_COMPONENT) kind = icalcomponent_isa(itip); + for (; itip; itip = icalcomponent_get_next_component(request, kind)) { + icalcomponent *new_comp = icalcomponent_new_clone(itip); + + /* Lookup this comp in the hash table */ + prop = + icalcomponent_get_first_property(itip, ICAL_RECURRENCEID_PROPERTY); + if (prop) recurid = icalproperty_get_value_as_string(prop); + else recurid = ""; + + comp = hash_lookup(recurid, &comp_table); + if (comp) { + int old_seq, new_seq; + + /* Check if this is something more than an update */ + /* XXX Probably need to check PARTSTAT=NEEDS-ACTION + and RSVP=TRUE as well */ + old_seq = icalcomponent_get_sequence(comp); + new_seq = icalcomponent_get_sequence(itip); + if (new_seq > old_seq) deliver_inbox = 1; + + /* Copy over any COMPLETED, PERCENT-COMPLETE, + or TRANSP properties */ + prop = + icalcomponent_get_first_property(comp, ICAL_COMPLETED_PROPERTY); + if (prop) { + icalcomponent_add_property(new_comp, + icalproperty_new_clone(prop)); + } + prop = + icalcomponent_get_first_property(comp, + ICAL_PERCENTCOMPLETE_PROPERTY); + if (prop) { + icalcomponent_add_property(new_comp, + icalproperty_new_clone(prop)); + } + prop = + icalcomponent_get_first_property(comp, ICAL_TRANSP_PROPERTY); + if (prop) { + icalcomponent_add_property(new_comp, + icalproperty_new_clone(prop)); + } + + /* Copy over any ORGANIZER;SCHEDULE-STATUS */ + /* XXX Do we only do this iff PARTSTAT!=NEEDS-ACTION */ + prop = + icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + param = icalproperty_get_schedulestatus_parameter(prop); + if (param) { + param = icalparameter_new_clone(param); + prop = + icalcomponent_get_first_property(new_comp, + ICAL_ORGANIZER_PROPERTY); + icalproperty_add_parameter(prop, param); + } + + /* Remove component from old object */ + icalcomponent_remove_component(ical, comp); + icalcomponent_free(comp); + } + else { + /* New component */ + deliver_inbox = 1; + } + + if (config_allowsched == IMAP_ENUM_CALDAV_ALLOWSCHEDULING_APPLE && + kind == ICAL_VEVENT_COMPONENT) { + /* Make VEVENT component transparent if recipient ATTENDEE + PARTSTAT=NEEDS-ACTION (for compatibility with CalendarServer) */ + for (prop = + icalcomponent_get_first_property(new_comp, + ICAL_ATTENDEE_PROPERTY); + prop && strcmp(icalproperty_get_attendee(prop), attendee); + prop = + icalcomponent_get_next_property(new_comp, + ICAL_ATTENDEE_PROPERTY)); + param = + icalproperty_get_first_parameter(prop, ICAL_PARTSTAT_PARAMETER); + if (param && + icalparameter_get_partstat(param) == + ICAL_PARTSTAT_NEEDSACTION) { + prop = + icalcomponent_get_first_property(new_comp, + ICAL_TRANSP_PROPERTY); + if (prop) + icalproperty_set_transp(prop, ICAL_TRANSP_TRANSPARENT); + else { + prop = icalproperty_new_transp(ICAL_TRANSP_TRANSPARENT); + icalcomponent_add_property(new_comp, prop); + } + } + } + + /* Add new/modified component from iTIP request */ + icalcomponent_add_component(ical, new_comp); + } + + free_hash_table(&comp_table, NULL); + + return deliver_inbox; +} + + +/* Deliver scheduling object to local recipient */ +static void sched_deliver_local(const char *recipient, + struct sched_param *sparam, + struct sched_data *sched_data, + struct auth_state *authstate) +{ + int r = 0, rights, reqd_privs, deliver_inbox = 1; + const char *userid = sparam->userid, *attendee = NULL; + static struct buf resource = BUF_INITIALIZER; + static unsigned sched_count = 0; + const char *mailboxname = NULL; + mbentry_t *mbentry = NULL; + struct mailbox *mailbox = NULL, *inbox = NULL; + struct caldav_db *caldavdb = NULL; + struct caldav_data *cdata; + icalcomponent *ical = NULL; + icalproperty_method method; + icalcomponent_kind kind; + icalcomponent *comp; + icalproperty *prop; + struct transaction_t txn; + + /* Check ACL of sender on recipient's Scheduling Inbox */ + mailboxname = caldav_mboxname(userid, SCHED_INBOX); + r = mboxlist_lookup(mailboxname, &mbentry, NULL); + if (r) { + syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s", + mailboxname, error_message(r)); + sched_data->status = + sched_data->ischedule ? REQSTAT_REJECTED : SCHEDSTAT_REJECTED; + goto done; + } + + rights = cyrus_acl_myrights(authstate, mbentry->acl); + mboxlist_entry_free(&mbentry); + + reqd_privs = sched_data->is_reply ? DACL_REPLY : DACL_INVITE; + if (!(rights & reqd_privs)) { + sched_data->status = + sched_data->ischedule ? REQSTAT_NOPRIVS : SCHEDSTAT_NOPRIVS; + syslog(LOG_DEBUG, "No scheduling receive ACL for user %s on Inbox %s", + httpd_userid, userid); + goto done; + } + + /* Open recipient's Inbox for reading */ + if ((r = mailbox_open_irl(mailboxname, &inbox))) { + syslog(LOG_ERR, "mailbox_open_irl(%s) failed: %s", + mailboxname, error_message(r)); + sched_data->status = + sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL; + goto done; + } + + /* Get METHOD of the iTIP message */ + method = icalcomponent_get_method(sched_data->itip); + + /* Search for iCal UID in recipient's calendars */ + caldavdb = caldav_open_userid(userid, CALDAV_CREATE); + if (!caldavdb) { + sched_data->status = + sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL; + goto done; + } + + caldav_lookup_uid(caldavdb, + icalcomponent_get_uid(sched_data->itip), 0, &cdata); + + if (cdata->dav.mailbox) { + mailboxname = cdata->dav.mailbox; + buf_setcstr(&resource, cdata->dav.resource); + } + else if (sched_data->is_reply) { + /* Can't find object belonging to organizer - ignore reply */ + sched_data->status = + sched_data->ischedule ? REQSTAT_PERMFAIL : SCHEDSTAT_PERMFAIL; + goto done; + } + else if (method == ICAL_METHOD_CANCEL || method == ICAL_METHOD_POLLSTATUS) { + /* Can't find object belonging to attendee - we're done */ + sched_data->status = + sched_data->ischedule ? REQSTAT_SUCCESS : SCHEDSTAT_DELIVERED; + goto done; + } + else { + /* Can't find object belonging to attendee - use default calendar */ + mailboxname = caldav_mboxname(userid, SCHED_DEFAULT); + buf_reset(&resource); + buf_printf(&resource, "%s.ics", + icalcomponent_get_uid(sched_data->itip)); + + /* Create new attendee object */ + ical = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT, 0); + + /* Copy over VERSION property */ + prop = icalcomponent_get_first_property(sched_data->itip, + ICAL_VERSION_PROPERTY); + icalcomponent_add_property(ical, icalproperty_new_clone(prop)); + + /* Copy over PRODID property */ + prop = icalcomponent_get_first_property(sched_data->itip, + ICAL_PRODID_PROPERTY); + icalcomponent_add_property(ical, icalproperty_new_clone(prop)); + + /* Copy over any CALSCALE property */ + prop = icalcomponent_get_first_property(sched_data->itip, + ICAL_CALSCALE_PROPERTY); + if (prop) { + icalcomponent_add_property(ical, + icalproperty_new_clone(prop)); + } + } + + /* Open recipient's calendar for reading */ + r = mailbox_open_irl(mailboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "mailbox_open_irl(%s) failed: %s", + mailboxname, error_message(r)); + sched_data->status = + sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL; + goto done; + } + + if (cdata->dav.imap_uid) { + struct index_record record; + struct buf msg_buf = BUF_INITIALIZER; + + /* Load message containing the resource and parse iCal data */ + r = mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &record); + if (!r) r = mailbox_map_record(mailbox, &record, &msg_buf); + if (r) { + syslog(LOG_ERR, "mailbox_map_record(%s, %u) failed: %s", + mailbox->name, record.uid, error_message(r)); + goto done; + } + ical = icalparser_parse_string(buf_base(&msg_buf) + record.header_size); + buf_free(&msg_buf); + + for (comp = icalcomponent_get_first_component(sched_data->itip, + ICAL_ANY_COMPONENT); + comp; + comp = icalcomponent_get_next_component(sched_data->itip, + ICAL_ANY_COMPONENT)) { + /* Don't allow component type to be changed */ + int reject = 0; + kind = icalcomponent_isa(comp); + switch (kind) { + case ICAL_VEVENT_COMPONENT: + if (cdata->comp_type != CAL_COMP_VEVENT) reject = 1; + break; + case ICAL_VTODO_COMPONENT: + if (cdata->comp_type != CAL_COMP_VTODO) reject = 1; + break; + case ICAL_VJOURNAL_COMPONENT: + if (cdata->comp_type != CAL_COMP_VJOURNAL) reject = 1; + break; + case ICAL_VFREEBUSY_COMPONENT: + if (cdata->comp_type != CAL_COMP_VFREEBUSY) reject = 1; + break; + case ICAL_VAVAILABILITY_COMPONENT: + if (cdata->comp_type != CAL_COMP_VAVAILABILITY) reject = 1; + break; +#ifdef HAVE_VPOLL + case ICAL_VPOLL_COMPONENT: + if (cdata->comp_type != CAL_COMP_VPOLL) reject = 1; + break; +#endif + default: + break; + } + + /* Don't allow ORGANIZER to be changed */ + if (!reject && cdata->organizer) { + prop = + icalcomponent_get_first_property(comp, + ICAL_ORGANIZER_PROPERTY); + if (prop) { + const char *organizer = + organizer = icalproperty_get_organizer(prop); + + if (!strncasecmp(organizer, "mailto:", 7)) organizer += 7; + if (strcmp(cdata->organizer, organizer)) reject = 1; + } + } + + if (reject) { + sched_data->status = sched_data->ischedule ? + REQSTAT_REJECTED : SCHEDSTAT_REJECTED; + goto done; + } + } + } + + switch (method) { + case ICAL_METHOD_CANCEL: + /* Get component type */ + comp = icalcomponent_get_first_real_component(ical); + kind = icalcomponent_isa(comp); + + /* Set STATUS:CANCELLED on all components */ + do { + icalcomponent_set_status(comp, ICAL_STATUS_CANCELLED); + icalcomponent_set_sequence(comp, + icalcomponent_get_sequence(comp)+1); + } while ((comp = icalcomponent_get_next_component(ical, kind))); + + break; + + case ICAL_METHOD_REPLY: + attendee = deliver_merge_reply(ical, sched_data->itip); + + break; + + case ICAL_METHOD_REQUEST: + deliver_inbox = deliver_merge_request(recipient, + ical, sched_data->itip); + break; + + case ICAL_METHOD_POLLSTATUS: + deliver_inbox = deliver_merge_pollstatus(ical, sched_data->itip); + break; + + default: + /* Unknown METHOD -- ignore it */ + syslog(LOG_ERR, "Unknown iTIP method: %s", + icalenum_method_to_string(method)); + + sched_data->is_reply = 0; + goto inbox; + } + + /* Store the (updated) object in the recipients's calendar */ + mailbox_unlock_index(mailbox, NULL); + + r = store_resource(&txn, ical, mailbox, buf_cstring(&resource), + caldavdb, OVERWRITE_YES, NEW_STAG); + + if (r == HTTP_CREATED || r == HTTP_NO_CONTENT) { + sched_data->status = + sched_data->ischedule ? REQSTAT_SUCCESS : SCHEDSTAT_DELIVERED; + } + else { + syslog(LOG_ERR, "store_resource(%s) failed: %s (%s)", + mailbox->name, error_message(r), txn.error.resource); + sched_data->status = + sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL; + goto done; + } + + inbox: + if (deliver_inbox) { + /* Create a name for the new iTIP message resource */ + buf_reset(&resource); + buf_printf(&resource, "%x-%d-%ld-%u.ics", + strhash(icalcomponent_get_uid(sched_data->itip)), getpid(), + time(0), sched_count++); + + /* Store the message in the recipient's Inbox */ + mailbox_unlock_index(inbox, NULL); + + r = store_resource(&txn, sched_data->itip, inbox, + buf_cstring(&resource), caldavdb, OVERWRITE_NO, 0); + /* XXX What do we do if storing to Inbox fails? */ + } + + /* XXX Should this be a config option? - it might have perf implications */ + if (sched_data->is_reply) { + /* Send updates to attendees - skipping sender of reply */ + comp = icalcomponent_get_first_real_component(ical); + if (icalcomponent_isa(comp) == ICAL_VPOLL_COMPONENT) + sched_pollstatus(recipient, sparam, ical, attendee); + else + sched_request(recipient, sparam, NULL, ical, attendee); + } + + done: + if (ical) icalcomponent_free(ical); + mailbox_close(&inbox); + mailbox_close(&mailbox); + if (caldavdb) caldav_close(caldavdb); +} + + +/* Deliver scheduling object to recipient's Inbox */ +void sched_deliver(const char *recipient, void *data, void *rock) +{ + struct sched_data *sched_data = (struct sched_data *) data; + struct auth_state *authstate = (struct auth_state *) rock; + struct sched_param sparam; + int islegal; + + /* Check SCHEDULE-FORCE-SEND value */ + switch (sched_data->force_send) { + case ICAL_SCHEDULEFORCESEND_NONE: + islegal = 1; + break; + + case ICAL_SCHEDULEFORCESEND_REPLY: + islegal = sched_data->is_reply; + break; + + case ICAL_SCHEDULEFORCESEND_REQUEST: + islegal = !sched_data->is_reply; + break; + + default: + islegal = 0; + break; + } + + if (!islegal) { + sched_data->status = SCHEDSTAT_PARAM; + return; + } + + if (caladdress_lookup(recipient, &sparam)) { + sched_data->status = + sched_data->ischedule ? REQSTAT_NOUSER : SCHEDSTAT_NOUSER; + /* Unknown user */ + return; + } + + if (sparam.flags) { + /* Remote recipient */ + sched_deliver_remote(recipient, &sparam, sched_data); + } + else { + /* Local recipient */ + sched_deliver_local(recipient, &sparam, sched_data, authstate); + } +} + + +struct comp_data { + icalcomponent *comp; + icalparameter_partstat partstat; + int sequence; +}; + +static void free_comp_data(void *data) { + struct comp_data *comp_data = (struct comp_data *) data; + + if (comp_data) { + if (comp_data->comp) icalcomponent_free(comp_data->comp); + free(comp_data); + } +} + + +/* + * sched_request/reply() helper function + * + * Update DTSTAMP, remove VALARMs, + * optionally remove scheduling params from ORGANIZER + */ +static void clean_component(icalcomponent *comp, int clean_org) +{ + icalcomponent *alarm, *next; + icalproperty *prop; + icaltimezone *utc = icaltimezone_get_utc_timezone(); + time_t now = time(NULL); + + /* Replace DTSTAMP on component */ + prop = icalcomponent_get_first_property(comp, + ICAL_DTSTAMP_PROPERTY); + icalcomponent_remove_property(comp, prop); + icalproperty_free(prop); + prop = + icalproperty_new_dtstamp(icaltime_from_timet_with_zone(now, 0, utc)); + icalcomponent_add_property(comp, prop); + + /* Remove any VALARM components */ + for (alarm = icalcomponent_get_first_component(comp, ICAL_VALARM_COMPONENT); + alarm; alarm = next) { + next = icalcomponent_get_next_component(comp, ICAL_VALARM_COMPONENT); + icalcomponent_remove_component(comp, alarm); + icalcomponent_free(alarm); + } + + if (clean_org) { + /* Grab the organizer */ + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + + /* Remove CalDAV Scheduling parameters from organizer */ + icalproperty_remove_parameter_by_name(prop, "SCHEDULE-AGENT"); + icalproperty_remove_parameter_by_name(prop, "SCHEDULE-FORCE-SEND"); + } +} + + +/* + * sched_request() helper function + * + * Add EXDATE to master component if attendee is excluded from recurrence + */ +struct exclude_rock { + unsigned ncomp; + icalcomponent *comp; +}; + +static void sched_exclude(const char *attendee __attribute__((unused)), + void *data, void *rock) +{ + struct sched_data *sched_data = (struct sched_data *) data; + struct exclude_rock *erock = (struct exclude_rock *) rock; + + if (!(sched_data->comp_mask & (1<<erock->ncomp))) { + icalproperty *recurid, *exdate; + struct icaltimetype exdt; + icalparameter *param; + + /* Fetch the RECURRENCE-ID and use it to create a new EXDATE */ + recurid = icalcomponent_get_first_property(erock->comp, + ICAL_RECURRENCEID_PROPERTY); + exdt = icalproperty_get_recurrenceid(recurid); + + exdate = icalproperty_new_exdate(exdt); + + /* Copy any parameters from RECURRENCE-ID to EXDATE */ + param = icalproperty_get_first_parameter(recurid, ICAL_TZID_PARAMETER); + if (param) { + icalproperty_add_parameter(exdate, icalparameter_new_clone(param)); + } + param = icalproperty_get_first_parameter(recurid, ICAL_VALUE_PARAMETER); + if (param) { + icalproperty_add_parameter(exdate, icalparameter_new_clone(param)); + } + /* XXX Need to handle RANGE parameter */ + + /* Add the EXDATE to the master component for this attendee */ + icalcomponent_add_property(sched_data->master, exdate); + } +} + + +/* + * sched_request() helper function + * + * Process all attendees in the given component and add a + * properly modified component to the attendee's iTIP request if necessary + */ +static void process_attendees(icalcomponent *comp, unsigned ncomp, + const char *organizer, const char *att_update, + struct hash_table *att_table, + icalcomponent *itip, unsigned needs_action) +{ + icalcomponent *copy; + icalproperty *prop; + icalparameter *param; + icalcomponent_kind kind = icalcomponent_isa(comp); + icalproperty_kind recip_kind; + const char *(*get_recipient)(const icalproperty *); + + if (kind == ICAL_VPOLL_COMPONENT) { + recip_kind = ICAL_VOTER_PROPERTY; + get_recipient = &icalproperty_get_voter; + } + else { + recip_kind = ICAL_ATTENDEE_PROPERTY; + get_recipient = &icalproperty_get_attendee; + } + + /* Strip SCHEDULE-STATUS from each attendee + and optionally set PROPSTAT=NEEDS-ACTION */ + for (prop = icalcomponent_get_first_property(comp, recip_kind); + prop; + prop = icalcomponent_get_next_property(comp, recip_kind)) { + + const char *attendee = get_recipient(prop); + + /* Don't modify attendee == organizer */ + if (!strcmp(attendee, organizer)) continue; + + icalproperty_remove_parameter_by_name(prop, "SCHEDULE-STATUS"); + + if (needs_action) { + /* Set PARTSTAT */ + param = icalparameter_new_partstat(ICAL_PARTSTAT_NEEDSACTION); + icalproperty_set_parameter(prop, param); + } + } + + /* Clone a working copy of the component */ + copy = icalcomponent_new_clone(comp); + + clean_component(copy, 0); + + /* Process each attendee */ + for (prop = icalcomponent_get_first_property(copy, recip_kind); + prop; + prop = icalcomponent_get_next_property(copy, recip_kind)) { + unsigned do_sched = 1; + icalparameter_scheduleforcesend force_send = + ICAL_SCHEDULEFORCESEND_NONE; + const char *attendee = get_recipient(prop); + + /* Don't schedule attendee == organizer */ + if (!strcmp(attendee, organizer)) continue; + + /* Don't send an update to the attendee that just sent a reply */ + if (att_update && !strcmp(attendee, att_update)) continue; + + /* Check CalDAV Scheduling parameters */ + param = icalproperty_get_scheduleagent_parameter(prop); + if (param) { + icalparameter_scheduleagent agent = + icalparameter_get_scheduleagent(param); + + if (agent != ICAL_SCHEDULEAGENT_SERVER) do_sched = 0; + icalproperty_remove_parameter_by_ref(prop, param); + } + + param = icalproperty_get_scheduleforcesend_parameter(prop); + if (param) { + force_send = icalparameter_get_scheduleforcesend(param); + icalproperty_remove_parameter_by_ref(prop, param); + } + + /* Create/update iTIP request for this attendee */ + if (do_sched) { + struct sched_data *sched_data; + icalcomponent *new_comp; + + sched_data = hash_lookup(attendee, att_table); + if (!sched_data) { + /* New attendee - add it to the hash table */ + sched_data = xzmalloc(sizeof(struct sched_data)); + sched_data->itip = icalcomponent_new_clone(itip); + sched_data->force_send = force_send; + hash_insert(attendee, sched_data, att_table); + } + new_comp = icalcomponent_new_clone(copy); + icalcomponent_add_component(sched_data->itip, new_comp); + sched_data->comp_mask |= (1 << ncomp); + + /* XXX We assume that the master component is always first */ + if (!ncomp) sched_data->master = new_comp; + } + } + + /* XXX We assume that the master component is always first */ + if (ncomp) { + /* Handle attendees that are excluded from this recurrence */ + struct exclude_rock erock = { ncomp, copy }; + + hash_enumerate(att_table, sched_exclude, &erock); + } + + icalcomponent_free(copy); +} + + +/* + * sched_request() helper function + * + * Organizer removed this component, mark it as cancelled for all attendees + */ +struct cancel_rock { + const char *organizer; + struct hash_table *att_table; + icalcomponent *itip; +}; + +static void sched_cancel(const char *recurid __attribute__((unused)), + void *data, void *rock) +{ + struct comp_data *old_data = (struct comp_data *) data; + struct cancel_rock *crock = (struct cancel_rock *) rock; + + /* Deleting the object -- set STATUS to CANCELLED for component */ + icalcomponent_set_status(old_data->comp, ICAL_STATUS_CANCELLED); +// icalcomponent_set_sequence(old_data->comp, old_data->sequence+1); + + process_attendees(old_data->comp, 0, crock->organizer, NULL, + crock->att_table, crock->itip, 0); +} + + +/* + * Compare the properties of the given kind in two components. + * Returns 0 if equal, 1 otherwise. + * + * If the property exists in neither comp, then they are equal. + * If the property exists in only 1 comp, then they are not equal. + * if the property is RDATE or EXDATE, create an MD5 hash of all + * property strings for each component and compare the hashes. + * Otherwise compare the two property strings. + */ +static unsigned propcmp(icalcomponent *oldical, icalcomponent *newical, + icalproperty_kind kind) +{ + icalproperty *oldprop = icalcomponent_get_first_property(oldical, kind); + icalproperty *newprop = icalcomponent_get_first_property(newical, kind); + + if (!oldprop) return (newprop != NULL); + else if (!newprop) return 1; + else if ((kind == ICAL_RDATE_PROPERTY) || (kind == ICAL_EXDATE_PROPERTY)) { + MD5_CTX ctx; + const char *str; + unsigned char old_md5[MD5_DIGEST_LENGTH], new_md5[MD5_DIGEST_LENGTH]; + + MD5Init(&ctx); + do { + str = icalproperty_get_value_as_string(oldprop); + MD5Update(&ctx, str, strlen(str)); + } while ((oldprop = icalcomponent_get_next_property(oldical, kind))); + + MD5Final(old_md5, &ctx); + + MD5Init(&ctx); + do { + str = icalproperty_get_value_as_string(newprop); + MD5Update(&ctx, str, strlen(str)); + } while ((newprop = icalcomponent_get_next_property(newical, kind))); + + MD5Final(new_md5, &ctx); + + return (memcmp(old_md5, new_md5, MD5_DIGEST_LENGTH) != 0); + } + else { + return (strcmp(icalproperty_get_value_as_string(oldprop), + icalproperty_get_value_as_string(newprop)) != 0); + } +} + + +/* Create and deliver an organizer scheduling request */ +static void sched_request(const char *organizer, struct sched_param *sparam, + icalcomponent *oldical, icalcomponent *newical, + const char *att_update) +{ + int r; + icalproperty_method method; + struct auth_state *authstate; + icalcomponent *ical, *req, *comp; + icalproperty *prop; + icalcomponent_kind kind; + struct hash_table att_table, comp_table; + const char *sched_stat = NULL, *recurid; + struct comp_data *old_data; + + /* Check what kind of action we are dealing with */ + if (!newical) { + /* Remove */ + ical = oldical; + method = ICAL_METHOD_CANCEL; + } + else { + /* Create / Modify */ + ical = newical; + method = ICAL_METHOD_REQUEST; + } + + if (!att_update) { + int rights = 0; + mbentry_t *mbentry = NULL; + /* Check ACL of auth'd user on userid's Scheduling Outbox */ + const char *outboxname = caldav_mboxname(sparam->userid, SCHED_OUTBOX); + + r = mboxlist_lookup(outboxname, &mbentry, NULL); + if (r) { + syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s", + outboxname, error_message(r)); + } + else { + rights = cyrus_acl_myrights(httpd_authstate, mbentry->acl); + mboxlist_entry_free(&mbentry); + } + + if (!(rights & DACL_INVITE)) { + /* DAV:need-privileges */ + sched_stat = SCHEDSTAT_NOPRIVS; + syslog(LOG_DEBUG, "No scheduling send ACL for user %s on Outbox %s", + httpd_userid, sparam->userid); + + goto done; + } + } + + /* Create a shell for our iTIP request objects */ + req = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT, + icalproperty_new_version("2.0"), + icalproperty_new_prodid(ical_prodid), + icalproperty_new_method(method), + 0); + + /* XXX Make sure SEQUENCE is incremented */ + + /* Copy over any CALSCALE property */ + prop = icalcomponent_get_first_property(ical, ICAL_CALSCALE_PROPERTY); + if (prop) { + icalcomponent_add_property(req, + icalproperty_new_clone(prop)); + } + + /* Copy over any VTIMEZONE components */ + for (comp = icalcomponent_get_first_component(ical, + ICAL_VTIMEZONE_COMPONENT); + comp; + comp = icalcomponent_get_next_component(ical, + ICAL_VTIMEZONE_COMPONENT)) { + icalcomponent_add_component(req, + icalcomponent_new_clone(comp)); + } + + comp = icalcomponent_get_first_real_component(ical); + kind = icalcomponent_isa(comp); + + /* Add each component of old object to hash table for comparison */ + construct_hash_table(&comp_table, 10, 1); + + if (!att_update && oldical) { + comp = icalcomponent_get_first_real_component(oldical); + + /* If the existing object isn't a scheduling object, + we don't need to compare components, treat them as new */ + if (icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY)) { + do { + old_data = xzmalloc(sizeof(struct comp_data)); + old_data->comp = comp; + old_data->sequence = icalcomponent_get_sequence(comp); + + prop = + icalcomponent_get_first_property(comp, + ICAL_RECURRENCEID_PROPERTY); + if (prop) recurid = icalproperty_get_value_as_string(prop); + else recurid = ""; + + hash_insert(recurid, old_data, &comp_table); + + } while ((comp = icalcomponent_get_next_component(oldical, kind))); + } + } + + /* Create hash table of attendees */ + construct_hash_table(&att_table, 10, 1); + + /* Process each component of new object */ + if (newical) { + unsigned ncomp = 0; + + comp = icalcomponent_get_first_real_component(newical); + do { + unsigned changed = 1, needs_action = 0; + + prop = icalcomponent_get_first_property(comp, + ICAL_RECURRENCEID_PROPERTY); + if (prop) recurid = icalproperty_get_value_as_string(prop); + else recurid = ""; + + old_data = hash_del(recurid, &comp_table); + + if (old_data) { + /* Per RFC 6638, Section 3.2.8: We need to compare + DTSTART, DTEND, DURATION, DUE, RRULE, RDATE, EXDATE */ + if (propcmp(old_data->comp, comp, ICAL_DTSTART_PROPERTY)) + needs_action = 1; + else if (propcmp(old_data->comp, comp, ICAL_DTEND_PROPERTY)) + needs_action = 1; + else if (propcmp(old_data->comp, comp, ICAL_DURATION_PROPERTY)) + needs_action = 1; + else if (propcmp(old_data->comp, comp, ICAL_DUE_PROPERTY)) + needs_action = 1; + else if (propcmp(old_data->comp, comp, ICAL_RRULE_PROPERTY)) + needs_action = 1; + else if (propcmp(old_data->comp, comp, ICAL_RDATE_PROPERTY)) + needs_action = 1; + else if (propcmp(old_data->comp, comp, ICAL_EXDATE_PROPERTY)) + needs_action = 1; + else if (kind == ICAL_VPOLL_COMPONENT) { + } + + if (needs_action && + (old_data->sequence >= icalcomponent_get_sequence(comp))) { + /* Make sure SEQUENCE is set properly */ + icalcomponent_set_sequence(comp, + old_data->sequence + 1); + } + + free(old_data); + } + + if (changed) { + /* Process all attendees in created/modified components */ + process_attendees(comp, ncomp++, organizer, att_update, + &att_table, req, needs_action); + } + + } while ((comp = icalcomponent_get_next_component(newical, kind))); + } + + if (oldical) { + /* Cancel any components that have been left behind in the old obj */ + struct cancel_rock crock = { organizer, &att_table, req }; + + hash_enumerate(&comp_table, sched_cancel, &crock); + } + free_hash_table(&comp_table, free); + + icalcomponent_free(req); + + /* Attempt to deliver requests to attendees */ + /* XXX Do we need to do more checks here? */ + if (sparam->flags & SCHEDTYPE_REMOTE) + authstate = auth_newstate("anonymous"); + else + authstate = auth_newstate(sparam->userid); + + hash_enumerate(&att_table, sched_deliver, authstate); + auth_freestate(authstate); + + done: + if (newical) { + unsigned ncomp = 0; + icalproperty_kind recip_kind; + const char *(*get_recipient)(const icalproperty *); + + /* Set SCHEDULE-STATUS for each attendee in organizer object */ + comp = icalcomponent_get_first_real_component(newical); + kind = icalcomponent_isa(comp); + + if (kind == ICAL_VPOLL_COMPONENT) { + recip_kind = ICAL_VOTER_PROPERTY; + get_recipient = &icalproperty_get_voter; + } + else { + recip_kind = ICAL_ATTENDEE_PROPERTY; + get_recipient = &icalproperty_get_attendee; + } + + do { + for (prop = + icalcomponent_get_first_property(comp, recip_kind); + prop; + prop = + icalcomponent_get_next_property(comp, recip_kind)) { + const char *stat = NULL; + const char *attendee = get_recipient(prop); + + /* Don't set status if attendee == organizer */ + if (!strcmp(attendee, organizer)) continue; + + if (sched_stat) stat = sched_stat; + else { + struct sched_data *sched_data; + + sched_data = hash_lookup(attendee, &att_table); + if (sched_data && (sched_data->comp_mask & (1 << ncomp))) + stat = sched_data->status; + } + + if (stat) { + /* Set SCHEDULE-STATUS */ + icalparameter *param; + + param = icalparameter_new_schedulestatus(stat); + icalproperty_set_parameter(prop, param); + } + } + + ncomp++; + } while ((comp = icalcomponent_get_next_component(newical, kind))); + } + + /* Cleanup */ + if (!sched_stat) free_hash_table(&att_table, free_sched_data); +} + + +/* + * sched_reply() helper function + * + * Remove all attendees from 'comp' other than the one corresponding to 'userid' + * + * Returns the new trimmed component (must be freed by caller) + * Optionally returns the 'attendee' property, his/her 'propstat', + * and the 'recurid' of the component + */ +static icalcomponent *trim_attendees(icalcomponent *comp, const char *userid, + icalproperty **attendee, + icalparameter_partstat *partstat, + const char **recurid) +{ + icalcomponent *copy; + icalproperty *prop, *nextprop, *myattendee = NULL; + icalcomponent_kind kind; + icalproperty_kind recip_kind; + const char *(*get_recipient)(const icalproperty *); + + if (partstat) *partstat = ICAL_PARTSTAT_NONE; + + /* Clone a working copy of the component */ + copy = icalcomponent_new_clone(comp); + + kind = icalcomponent_isa(comp); + if (kind == ICAL_VPOLL_COMPONENT) { + recip_kind = ICAL_VOTER_PROPERTY; + get_recipient = &icalproperty_get_voter; + } + else { + recip_kind = ICAL_ATTENDEE_PROPERTY; + get_recipient = &icalproperty_get_attendee; + } + + /* Locate userid in the attendee list (stripping others) */ + for (prop = icalcomponent_get_first_property(copy, recip_kind); + prop; + prop = nextprop) { + const char *att = get_recipient(prop); + struct sched_param sparam; + + nextprop = icalcomponent_get_next_property(copy, recip_kind); + + if (!myattendee && + !caladdress_lookup(att, &sparam) && + !(sparam.flags & SCHEDTYPE_REMOTE) && + !strcmpsafe(sparam.userid, userid)) { + /* Found it */ + myattendee = prop; + + if (partstat) { + /* Get the PARTSTAT */ + icalparameter *param = + icalproperty_get_first_parameter(myattendee, + ICAL_PARTSTAT_PARAMETER); + if (param) *partstat = icalparameter_get_partstat(param); + } + } + else { + /* Some other attendee, remove it */ + icalcomponent_remove_property(copy, prop); + icalproperty_free(prop); + } + } + + if (attendee) *attendee = myattendee; + + if (recurid) { + prop = icalcomponent_get_first_property(copy, + ICAL_RECURRENCEID_PROPERTY); + if (prop) *recurid = icalproperty_get_value_as_string(prop); + else *recurid = ""; + } + + return copy; +} + + +/* + * sched_reply() helper function + * + * Attendee removed this component, mark it as declined for the organizer. + */ +static void sched_decline(const char *recurid __attribute__((unused)), + void *data, void *rock) +{ + struct comp_data *old_data = (struct comp_data *) data; + icalcomponent *itip = (icalcomponent *) rock; + icalproperty *myattendee; + icalparameter *param; + + /* Don't send a decline for cancelled components */ + if (icalcomponent_get_status(old_data->comp) == ICAL_STATUS_CANCELLED) + return; + + myattendee = icalcomponent_get_first_property(old_data->comp, + ICAL_ATTENDEE_PROPERTY); + + param = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED); + icalproperty_set_parameter(myattendee, param); + + clean_component(old_data->comp, 1); + + icalcomponent_add_component(itip, old_data->comp); +} + + +/* Create and deliver an attendee scheduling reply */ +static void sched_reply(const char *userid, + icalcomponent *oldical, icalcomponent *newical) +{ + int r, rights = 0; + mbentry_t *mbentry = NULL; + const char *outboxname; + icalcomponent *ical; + struct sched_data *sched_data; + struct auth_state *authstate; + icalcomponent *comp; + icalproperty *prop; + icalparameter *param; + icalcomponent_kind kind; + icalparameter_scheduleforcesend force_send = ICAL_SCHEDULEFORCESEND_NONE; + const char *organizer, *recurid; + struct hash_table comp_table; + struct comp_data *old_data; + + /* Check what kind of action we are dealing with */ + if (!newical) { + /* Remove */ + ical = oldical; + } + else { + /* Create / Modify */ + ical = newical; + } + + /* Check CalDAV Scheduling parameters on the organizer */ + comp = icalcomponent_get_first_real_component(ical); + kind = icalcomponent_isa(comp); + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + organizer = icalproperty_get_organizer(prop); + + param = icalproperty_get_scheduleagent_parameter(prop); + if (param && + icalparameter_get_scheduleagent(param) != ICAL_SCHEDULEAGENT_SERVER) { + /* We are not supposed to send replies to the organizer */ + return; + } + + param = icalproperty_get_scheduleforcesend_parameter(prop); + if (param) force_send = icalparameter_get_scheduleforcesend(param); + + sched_data = xzmalloc(sizeof(struct sched_data)); + sched_data->is_reply = 1; + sched_data->force_send = force_send; + + /* Check ACL of auth'd user on userid's Scheduling Outbox */ + outboxname = caldav_mboxname(userid, SCHED_OUTBOX); + + r = mboxlist_lookup(outboxname, &mbentry, NULL); + if (r) { + syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s", + outboxname, error_message(r)); + } + else { + rights = cyrus_acl_myrights(httpd_authstate, mbentry->acl); + mboxlist_entry_free(&mbentry); + } + + if (!(rights & DACL_REPLY)) { + /* DAV:need-privileges */ + if (newical) sched_data->status = SCHEDSTAT_NOPRIVS; + syslog(LOG_DEBUG, "No scheduling send ACL for user %s on Outbox %s", + httpd_userid, userid); + + goto done; + } + + /* Create our reply iCal object */ + sched_data->itip = + icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT, + icalproperty_new_version("2.0"), + icalproperty_new_prodid(ical_prodid), + icalproperty_new_method(ICAL_METHOD_REPLY), + 0); + + /* XXX Make sure SEQUENCE is incremented */ + + /* Copy over any CALSCALE property */ + prop = icalcomponent_get_first_property(ical, ICAL_CALSCALE_PROPERTY); + if (prop) { + icalcomponent_add_property(sched_data->itip, + icalproperty_new_clone(prop)); + } + + /* Copy over any VTIMEZONE components */ + for (comp = icalcomponent_get_first_component(ical, + ICAL_VTIMEZONE_COMPONENT); + comp; + comp = icalcomponent_get_next_component(ical, + ICAL_VTIMEZONE_COMPONENT)) { + icalcomponent_add_component(sched_data->itip, + icalcomponent_new_clone(comp)); + } + + /* Add each component of old object to hash table for comparison */ + construct_hash_table(&comp_table, 10, 1); + + if (oldical) { + comp = icalcomponent_get_first_real_component(oldical); + do { + old_data = xzmalloc(sizeof(struct comp_data)); + + old_data->comp = trim_attendees(comp, userid, NULL, + &old_data->partstat, &recurid); + + hash_insert(recurid, old_data, &comp_table); + + } while ((comp = icalcomponent_get_next_component(oldical, kind))); + } + + /* Process each component of new object */ + if (newical) { + unsigned ncomp = 0; + + comp = icalcomponent_get_first_real_component(newical); + do { + icalcomponent *copy; + icalproperty *myattendee; + icalparameter_partstat partstat; + int changed = 1; + + copy = trim_attendees(comp, userid, + &myattendee, &partstat, &recurid); + if (myattendee) { + /* Found our userid */ + old_data = hash_del(recurid, &comp_table); + + if (old_data) { + if (kind == ICAL_VPOLL_COMPONENT) { + /* VPOLL replies always override existing votes */ + sched_vpoll_reply(copy, + icalproperty_get_voter(myattendee)); + } + else { + /* XXX Need to check EXDATE */ + + /* Compare PARTSTAT in the two components */ + if (old_data->partstat == partstat) { + changed = 0; + } + } + + free_comp_data(old_data); + } + } + else { + /* Our user isn't in this component */ + /* XXX Can this actually happen? */ + changed = 0; + } + + if (changed) { + clean_component(copy, 1); + + icalcomponent_add_component(sched_data->itip, copy); + sched_data->comp_mask |= (1 << ncomp); + } + else icalcomponent_free(copy); + + ncomp++; + } while ((comp = icalcomponent_get_next_component(newical, kind))); + } + + /* Decline any components that have been left behind in the old obj */ + hash_enumerate(&comp_table, sched_decline, sched_data->itip); + free_hash_table(&comp_table, free_comp_data); + + done: + if (sched_data->itip && + icalcomponent_get_first_real_component(sched_data->itip)) { + /* We built a reply object */ + + if (!sched_data->status) { + /* Attempt to deliver reply to organizer */ + authstate = auth_newstate(userid); + sched_deliver(organizer, sched_data, authstate); + auth_freestate(authstate); + } + + if (newical) { + unsigned ncomp = 0; + + /* Set SCHEDULE-STATUS for organizer in attendee object */ + comp = icalcomponent_get_first_real_component(newical); + do { + if (sched_data->comp_mask & (1 << ncomp)) { + prop = + icalcomponent_get_first_property(comp, + ICAL_ORGANIZER_PROPERTY); + param = + icalparameter_new_schedulestatus(sched_data->status); + icalproperty_add_parameter(prop, param); + } + + ncomp++; + } while ((comp = icalcomponent_get_next_component(newical, kind))); + } + } + + /* Cleanup */ + free_sched_data(sched_data); +}
View file
cyrus-imapd-2.5.tar.gz/imap/http_caldav_sched.h
Added
@@ -0,0 +1,132 @@ +/* http_caldav_sched.h -- Routines for dealing with CALDAV scheduling in httpd + * + * Copyright (c) 1994-2013 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 HTTP_CALDAV_SCHED_H +#define HTTP_CALDAV_SCHED_H + +#define IOPTEST + +#include <libical/ical.h> + +#ifdef WITH_DKIM +#include <dkim.h> + +#ifndef DKIM_CANON_ISCHEDULE +#undef WITH_DKIM +#endif + +#endif /* WITH_DKIM */ + +#include "http_dav.h" + + +#ifndef HAVE_SCHEDULING_PARAMS + +typedef enum { + ICAL_SCHEDULEAGENT_X, + ICAL_SCHEDULEAGENT_SERVER, + ICAL_SCHEDULEAGENT_CLIENT, + ICAL_SCHEDULEAGENT_NONE +} icalparameter_scheduleagent; + +typedef enum { + ICAL_SCHEDULEFORCESEND_X, + ICAL_SCHEDULEFORCESEND_REQUEST, + ICAL_SCHEDULEFORCESEND_REPLY, + ICAL_SCHEDULEFORCESEND_NONE +} icalparameter_scheduleforcesend; + +#endif /* !HAVE_SCHEDULING_PARAMS */ + +#define REQSTAT_PENDING "1.0;Pending" +#define REQSTAT_SENT "1.1;Sent" +#define REQSTAT_DELIVERED "1.2;Delivered" +#define REQSTAT_SUCCESS "2.0;Success" +#define REQSTAT_NOUSER "3.7;Invalid calendar user" +#define REQSTAT_NOPRIVS "3.8;Noauthority" +#define REQSTAT_TEMPFAIL "5.1;Service unavailable" +#define REQSTAT_PERMFAIL "5.2;Invalid calendar service" +#define REQSTAT_REJECTED "5.3;No scheduling support for user" + +struct sched_data { + unsigned ischedule; + unsigned is_reply; + icalcomponent *itip; + icalcomponent *master; + unsigned comp_mask; + icalparameter_scheduleforcesend force_send; + const char *status; +}; + +/* Scheduling protocol flags */ +#define SCHEDTYPE_REMOTE (1<<0) +#define SCHEDTYPE_ISCHEDULE (1<<1) +#define SCHEDTYPE_SSL (1<<2) + +struct proplist { + icalproperty *prop; + struct proplist *next; +}; + +/* Each calendar user address has the following scheduling protocol params */ +struct sched_param { + char *userid; /* Userid corresponding to calendar address */ + char *server; /* Remote server user lives on */ + unsigned port; /* Remote server port, default = 80 */ + unsigned flags; /* Flags dictating protocol to use for scheduling */ + struct proplist *props; /* List of attendee iCal properties */ +}; + +extern icalarray *rscale_calendars; +extern const char *get_icalcomponent_errstr(icalcomponent *ical); +extern int isched_send(struct sched_param *sparam, const char *recipient, + icalcomponent *ical, xmlNodePtr *xml); + +extern int sched_busytime_query(struct transaction_t *txn, + struct mime_type_t *mime, icalcomponent *comp); +extern void sched_deliver(const char *recipient, void *data, void *rock); +extern xmlNodePtr xml_add_schedresponse(xmlNodePtr root, xmlNsPtr dav_ns, + xmlChar *recipient, xmlChar *status); +extern int caladdress_lookup(const char *addr, struct sched_param *param); + +#endif /* HTTP_CALDAV_SCHED_H */
View file
cyrus-imapd-2.5.tar.gz/imap/http_carddav.c
Added
@@ -0,0 +1,1134 @@ +/* http_carddav.c -- Routines for handling CardDAV collections in httpd + * + * Copyright (c) 1994-2013 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. + * + */ +/* + * TODO: + * Support <filter> for addressbook-query Report + * + */ + +#include <config.h> + +#include <syslog.h> + +#include <libical/vcc.h> +#include <libxml/tree.h> +#include <libxml/uri.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include "acl.h" +#include "append.h" +#include "carddav_db.h" +#include "exitcodes.h" +#include "global.h" +#include "hash.h" +#include "httpd.h" +#include "http_dav.h" +#include "http_err.h" +#include "http_proxy.h" +#include "imap_err.h" +#include "index.h" +#include "mailbox.h" +#include "mboxlist.h" +#include "message.h" +#include "message_guid.h" +#include "proxy.h" +#include "smtpclient.h" +#include "spool.h" +#include "stristr.h" +#include "times.h" +#include "util.h" +#include "version.h" +#include "xmalloc.h" +#include "xstrlcat.h" +#include "xstrlcpy.h" + +static struct carddav_db *auth_carddavdb = NULL; + +static struct carddav_db *my_carddav_open(struct mailbox *mailbox); +static void my_carddav_close(struct carddav_db *carddavdb); +static void my_carddav_init(struct buf *serverinfo); +static void my_carddav_auth(const char *userid); +static void my_carddav_reset(void); +static void my_carddav_shutdown(void); + +static int carddav_parse_path(const char *path, + struct request_target_t *tgt, const char **errstr); + +static int carddav_copy(struct transaction_t *txn, + struct mailbox *src_mbox, struct index_record *src_rec, + struct mailbox *dest_mbox, const char *dest_rsrc, + struct carddav_db *dest_davdb, + unsigned overwrite, unsigned flags); +static int carddav_put(struct transaction_t *txn, + struct mime_type_t *mime, + struct mailbox *mailbox, + struct carddav_db *carddavdb, + unsigned flags); +static VObject *vcard_string_as_vobject(const char *str) +{ + return Parse_MIME(str, strlen(str)); +} + +static int propfind_getcontenttype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_restype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_addrdata(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_suppaddrdata(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +static int report_card_query(struct transaction_t *txn, xmlNodePtr inroot, + struct propfind_ctx *fctx); +static int report_card_multiget(struct transaction_t *txn, xmlNodePtr inroot, + struct propfind_ctx *fctx); + +static int store_resource(struct transaction_t *txn, VObject *vcard, + struct mailbox *mailbox, const char *resource, + struct carddav_db *carddavdb, int overwrite, + unsigned flags); + +static struct mime_type_t carddav_mime_types[] = { + /* First item MUST be the default type and storage format */ + { "text/vcard; charset=utf-8", "3.0", NULL, "vcf", NULL, + (void * (*)(const char*)) &vcard_string_as_vobject, + (void (*)(void *)) &cleanVObject, NULL, NULL + }, + { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL } +}; + +/* Array of supported REPORTs */ +static const struct report_type_t carddav_reports[] = { + + /* WebDAV Versioning (RFC 3253) REPORTs */ + { "expand-property", NS_DAV, "multistatus", &report_expand_prop, + DACL_READ, 0 }, + + /* WebDAV Sync (RFC 6578) REPORTs */ + { "sync-collection", NS_DAV, "multistatus", &report_sync_col, + DACL_READ, REPORT_NEED_MBOX | REPORT_NEED_PROPS }, + + /* CardDAV (RFC 6352) REPORTs */ + { "addressbook-query", NS_CARDDAV, "multistatus", &report_card_query, + DACL_READ, REPORT_NEED_MBOX }, + { "addressbook-multiget", NS_CARDDAV, "multistatus", &report_card_multiget, + DACL_READ, REPORT_NEED_MBOX }, + + { NULL, 0, NULL, NULL, 0, 0 } +}; + +/* Array of known "live" properties */ +static const struct prop_entry carddav_props[] = { + + /* WebDAV (RFC 4918) properties */ + { "creationdate", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_creationdate, NULL, NULL }, + { "displayname", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_fromdb, proppatch_todb, NULL }, + { "getcontentlanguage", NS_DAV, PROP_ALLPROP | PROP_RESOURCE, + propfind_fromhdr, NULL, "Content-Language" }, + { "getcontentlength", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_getlength, NULL, NULL }, + { "getcontenttype", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_getcontenttype, NULL, "Content-Type" }, + { "getetag", NS_DAV, PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_getetag, NULL, NULL }, + { "getlastmodified", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_getlastmod, NULL, NULL }, + { "lockdiscovery", NS_DAV, PROP_ALLPROP | PROP_RESOURCE, + propfind_lockdisc, NULL, NULL }, + { "resourcetype", NS_DAV, + PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE, + propfind_restype, proppatch_restype, "addressbook" }, + { "supportedlock", NS_DAV, PROP_ALLPROP | PROP_RESOURCE, + propfind_suplock, NULL, NULL }, + + /* WebDAV Versioning (RFC 3253) properties */ + { "supported-report-set", NS_DAV, PROP_COLLECTION, + propfind_reportset, NULL, (void *) carddav_reports }, + + /* WebDAV ACL (RFC 3744) properties */ + { "owner", NS_DAV, PROP_COLLECTION | PROP_RESOURCE | PROP_EXPAND, + propfind_owner, NULL, NULL }, + { "group", NS_DAV, 0, NULL, NULL, NULL }, + { "supported-privilege-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE, + propfind_supprivset, NULL, NULL }, + { "current-user-privilege-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE, + propfind_curprivset, NULL, NULL }, + { "acl", NS_DAV, PROP_COLLECTION | PROP_RESOURCE, + propfind_acl, NULL, NULL }, + { "acl-restrictions", NS_DAV, PROP_COLLECTION | PROP_RESOURCE, + propfind_aclrestrict, NULL, NULL }, + { "inherited-acl-set", NS_DAV, 0, NULL, NULL, NULL }, + { "principal-collection-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE, + propfind_princolset, NULL, NULL }, + + /* WebDAV Quota (RFC 4331) properties */ + { "quota-available-bytes", NS_DAV, PROP_COLLECTION, + propfind_quota, NULL, NULL }, + { "quota-used-bytes", NS_DAV, PROP_COLLECTION, + propfind_quota, NULL, NULL }, + + /* WebDAV Current Principal (RFC 5397) properties */ + { "current-user-principal", NS_DAV, + PROP_COLLECTION | PROP_RESOURCE | PROP_EXPAND, + propfind_curprin, NULL, NULL }, + + /* WebDAV POST (RFC 5995) properties */ + { "add-member", NS_DAV, PROP_COLLECTION, + NULL, /* Until Apple Contacts is fixed */ NULL, NULL }, + + /* WebDAV Sync (RFC 6578) properties */ + { "sync-token", NS_DAV, PROP_COLLECTION, + propfind_sync_token, NULL, NULL }, + + /* CardDAV (RFC 6352) properties */ + { "address-data", NS_CARDDAV, + PROP_RESOURCE | PROP_PRESCREEN | PROP_NEEDPROP, + propfind_addrdata, NULL, NULL }, + { "addressbook-description", NS_CARDDAV, PROP_COLLECTION, + propfind_fromdb, proppatch_todb, NULL }, + { "supported-address-data", NS_CARDDAV, PROP_COLLECTION, + propfind_suppaddrdata, NULL, NULL }, + { "max-resource-size", NS_CARDDAV, 0, NULL, NULL, NULL }, + + /* Apple Calendar Server properties */ + { "getctag", NS_CS, PROP_ALLPROP | PROP_COLLECTION, + propfind_sync_token, NULL, NULL }, + + { NULL, 0, 0, NULL, NULL, NULL } +}; + +static struct meth_params carddav_params = { + carddav_mime_types, + &carddav_parse_path, + &check_precond, + { (db_open_proc_t) &my_carddav_open, + (db_close_proc_t) &my_carddav_close, + (db_lookup_proc_t) &carddav_lookup_resource, + (db_foreach_proc_t) &carddav_foreach, + (db_write_proc_t) &carddav_write, + (db_delete_proc_t) &carddav_delete, + (db_delmbox_proc_t) &carddav_delmbox }, + NULL, /* No ACL extensions */ + (copy_proc_t) &carddav_copy, + NULL, /* No special DELETE handling */ + { MBTYPE_ADDRESSBOOK, NULL, NULL, 0 }, /* No special MK* method */ + NULL, /* No special POST handling */ + { CARDDAV_SUPP_DATA, (put_proc_t) &carddav_put }, + carddav_props, + carddav_reports +}; + + +/* Namespace for Carddav collections */ +struct namespace_t namespace_addressbook = { + URL_NS_ADDRESSBOOK, 0, "/dav/addressbooks", "/.well-known/carddav", + 1 /* auth */, +#if 0 /* Until Apple Contacts fixes their add-member implementation */ + (ALLOW_READ | ALLOW_POST | ALLOW_WRITE | ALLOW_DELETE | + ALLOW_DAV | ALLOW_WRITECOL | ALLOW_CARD), +#else + (ALLOW_READ | ALLOW_WRITE | ALLOW_DELETE | + ALLOW_DAV | ALLOW_WRITECOL | ALLOW_CARD), +#endif + &my_carddav_init, &my_carddav_auth, my_carddav_reset, &my_carddav_shutdown, + { + { &meth_acl, &carddav_params }, /* ACL */ + { &meth_copy, &carddav_params }, /* COPY */ + { &meth_delete, &carddav_params }, /* DELETE */ + { &meth_get_dav, &carddav_params }, /* GET */ + { &meth_get_dav, &carddav_params }, /* HEAD */ + { &meth_lock, &carddav_params }, /* LOCK */ + { NULL, NULL }, /* MKCALENDAR */ + { &meth_mkcol, &carddav_params }, /* MKCOL */ + { &meth_copy, &carddav_params }, /* MOVE */ + { &meth_options, &carddav_parse_path }, /* OPTIONS */ +#if 0 /* Until Apple Contacts fixes their add-member implementation */ + { &meth_post, &carddav_params }, /* POST */ +#else + { NULL, NULL }, /* POST */ +#endif + { &meth_propfind, &carddav_params }, /* PROPFIND */ + { &meth_proppatch, &carddav_params }, /* PROPPATCH */ + { &meth_put, &carddav_params }, /* PUT */ + { &meth_report, &carddav_params }, /* REPORT */ + { &meth_trace, &carddav_parse_path }, /* TRACE */ + { &meth_unlock, &carddav_params } /* UNLOCK */ + } +}; + + +static struct carddav_db *my_carddav_open(struct mailbox *mailbox) +{ + if (httpd_userid && mboxname_userownsmailbox(httpd_userid, mailbox->name)) { + return auth_carddavdb; + } + else { + return carddav_open_mailbox(mailbox, CALDAV_CREATE); + } +} + + +static void my_carddav_close(struct carddav_db *carddavdb) +{ + if (carddavdb && (carddavdb != auth_carddavdb)) carddav_close(carddavdb); +} + + +static void my_carddav_init(struct buf *serverinfo __attribute__((unused))) +{ + namespace_addressbook.enabled = + config_httpmodules & IMAP_ENUM_HTTPMODULES_CARDDAV; + + if (!namespace_addressbook.enabled) return; + + if (!config_getstring(IMAPOPT_ADDRESSBOOKPREFIX)) { + fatal("Required 'addressbookprefix' option is not set", EC_CONFIG); + } + + carddav_init(); + + namespace_principal.enabled = 1; + namespace_principal.allow |= ALLOW_CARD; +} + + +#define DEFAULT_ADDRBOOK "Default" + +static void my_carddav_auth(const char *userid) +{ + int r; + struct buf boxbuf = BUF_INITIALIZER; + const char *mailboxname; + + mailboxname = mboxname_user_mbox(userid, NULL); + + if (httpd_userisadmin || + global_authisa(httpd_authstate, IMAPOPT_PROXYSERVERS)) { + /* admin or proxy from frontend - won't have DAV database */ + return; + } + else if (config_mupdate_server && !config_getstring(IMAPOPT_PROXYSERVERS)) { + /* proxy-only server - won't have DAV databases */ + } + else { + /* Open CardDAV DB for 'userid' */ + my_carddav_reset(); + auth_carddavdb = carddav_open_userid(userid, CARDDAV_CREATE); + if (!auth_carddavdb) fatal("Unable to open CardDAV DB", EC_IOERR); + } + + buf_setcstr(&boxbuf, config_getstring(IMAPOPT_ADDRESSBOOKPREFIX)); + + mailboxname = mboxname_user_mbox(userid, buf_cstring(&boxbuf)); + + /* Auto-provision an addressbook for 'userid' */ + r = mboxlist_lookup(mailboxname, NULL, NULL); + if (r == IMAP_MAILBOX_NONEXISTENT) { + if (config_mupdate_server) { + /* Find location of INBOX */ + const char *inboxname = mboxname_user_mbox(userid, NULL); + mbentry_t *mbentry = NULL; + + r = http_mlookup(inboxname, &mbentry, NULL); + if (!r && mbentry->server) { + proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + return; + } + mboxlist_entry_free(&mbentry); + } + else r = 0; + + mailboxname = mboxname_user_mbox(userid, buf_cstring(&boxbuf)); + + /* XXX - set rights */ + r = mboxlist_createmailbox(mailboxname, MBTYPE_ADDRESSBOOK, + NULL, 0, + userid, httpd_authstate, + 0, 0, 0, 0, NULL); + if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)", + mailboxname, error_message(r)); + } + if (r) return; + + /* Default addressbook */ + buf_setcstr(&boxbuf, config_getstring(IMAPOPT_ADDRESSBOOKPREFIX)); + buf_printf(&boxbuf, ".%s", DEFAULT_ADDRBOOK); + mailboxname = mboxname_user_mbox(userid, buf_cstring(&boxbuf)); + r = mboxlist_lookup(mailboxname, NULL, NULL); + if (r == IMAP_MAILBOX_NONEXISTENT) { + /* XXX - set rights */ + r = mboxlist_createmailbox(mailboxname, MBTYPE_ADDRESSBOOK, + NULL, 0, + userid, httpd_authstate, + 0, 0, 0, 0, NULL); + if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)", + mailboxname, error_message(r)); + } +} + + +static void my_carddav_reset(void) +{ + if (auth_carddavdb) carddav_close(auth_carddavdb); + auth_carddavdb = NULL; +} + + +static void my_carddav_shutdown(void) +{ + carddav_done(); +} + + +/* Parse request-target path in CardDAV namespace */ +static int carddav_parse_path(const char *path, + struct request_target_t *tgt, const char **errstr) +{ + char *p; + size_t len; + struct mboxname_parts parts; + struct buf boxbuf = BUF_INITIALIZER; + + /* Make a working copy of target path */ + strlcpy(tgt->path, path, sizeof(tgt->path)); + tgt->tail = tgt->path + strlen(tgt->path); + + p = tgt->path; + + /* Sanity check namespace */ + len = strlen(namespace_addressbook.prefix); + if (strlen(p) < len || + strncmp(namespace_addressbook.prefix, p, len) || + (path[len] && path[len] != '/')) { + *errstr = "Namespace mismatch request target path"; + return HTTP_FORBIDDEN; + } + + /* Default to bare-bones Allow bits for toplevel collections */ + tgt->allow &= ~(ALLOW_POST|ALLOW_WRITE|ALLOW_DELETE); + + /* Skip namespace */ + p += len; + if (!*p || !*++p) return 0; + + /* Check if we're in user space */ + len = strcspn(p, "/"); + if (!strncmp(p, "user", len)) { + p += len; + if (!*p || !*++p) return 0; + + /* Get user id */ + len = strcspn(p, "/"); + tgt->user = p; + tgt->userlen = len; + + p += len; + if (!*p || !*++p) goto done; + + len = strcspn(p, "/"); + } + + /* Get collection */ + tgt->collection = p; + tgt->collen = len; + + p += len; + if (!*p || !*++p) { + /* Make sure collection is terminated with '/' */ + if (p[-1] != '/') *p++ = '/'; + goto done; + } + + /* Get resource */ + len = strcspn(p, "/"); + tgt->resource = p; + tgt->reslen = len; + + p += len; + + if (*p) { +// *errstr = "Too many segments in request target path"; + return HTTP_NOT_FOUND; + } + + done: + /* Set proper Allow bits based on path components */ + if (tgt->collection) { + if (tgt->resource) { + tgt->allow &= ~ALLOW_WRITECOL; + tgt->allow |= (ALLOW_WRITE|ALLOW_DELETE); + } +#if 0 /* Until Apple Contacts fixes their add-member implementation */ + else tgt->allow |= (ALLOW_POST|ALLOW_DELETE); +#else + else tgt->allow |= ALLOW_DELETE; +#endif + } + else if (tgt->user) tgt->allow |= ALLOW_DELETE; + + + /* Create mailbox name from the parsed path */ + + mboxname_init_parts(&parts); + + if (tgt->user && tgt->userlen) { + /* holy "avoid copying" batman */ + char *userid = xstrndup(tgt->user, tgt->userlen); + mboxname_userid_to_parts(userid, &parts); + free(userid); + } + + buf_setcstr(&boxbuf, config_getstring(IMAPOPT_ADDRESSBOOKPREFIX)); + if (tgt->collen) { + buf_putc(&boxbuf, '.'); + buf_appendmap(&boxbuf, tgt->collection, tgt->collen); + } + parts.box = buf_release(&boxbuf); + + mboxname_parts_to_internal(&parts, tgt->mboxname); + + mboxname_free_parts(&parts); + + return 0; +} + + +/* Perform a COPY/MOVE request + * + * preconditions: + * CARDDAV:supported-address-data + * CARDDAV:valid-address-data + * CARDDAV:no-uid-conflict (DAV:href) + * CARDDAV:addressbook-collection-location-ok + * CARDDAV:max-resource-size + */ +static int carddav_copy(struct transaction_t *txn, + struct mailbox *src_mbox, struct index_record *src_rec, + struct mailbox *dest_mbox, const char *dest_rsrc, + struct carddav_db *dest_davdb, + unsigned overwrite, unsigned flags) +{ + int r; + struct buf msg_buf = BUF_INITIALIZER; + VObject *vcard; + + /* Load message containing the resource and parse vCard data */ + r = mailbox_map_record(src_mbox, src_rec, &msg_buf); + if (r) return r; + vcard = Parse_MIME(buf_base(&msg_buf) + src_rec->header_size, + src_rec->size - src_rec->header_size); + buf_free(&msg_buf); + + if (!vcard) { + txn->error.precond = CARDDAV_VALID_DATA; + return HTTP_FORBIDDEN; + } + + /* Finished our initial read of source mailbox */ + mailbox_unlock_index(src_mbox, NULL); + + /* Store source resource at destination */ + r = store_resource(txn, vcard, dest_mbox, dest_rsrc, dest_davdb, + overwrite, flags); + + cleanVObject(vcard); + cleanStrTbl(); + + return r; +} + + +/* Perform a PUT request + * + * preconditions: + * CARDDAV:valid-address-data + * CARDDAV:no-uid-conflict (DAV:href) + * CARDDAV:max-resource-size + */ +static int carddav_put(struct transaction_t *txn, + struct mime_type_t *mime, + struct mailbox *mailbox, + struct carddav_db *davdb, + unsigned flags) +{ + int ret; + VObject *vcard = NULL; + + /* Parse and validate the vCard data */ + vcard = mime->from_string(buf_cstring(&txn->req_body.payload)); + if (!vcard || strcmp(vObjectName(vcard), "VCARD")) { + txn->error.precond = CARDDAV_VALID_DATA; + ret = HTTP_FORBIDDEN; + goto done; + } + + /* Store resource at target */ + ret = store_resource(txn, vcard, mailbox, txn->req_tgt.resource, + davdb, OVERWRITE_CHECK, flags); + + if (flags & PREFER_REP) { + struct resp_body_t *resp_body = &txn->resp_body; + const char *data; + + switch (ret) { + case HTTP_NO_CONTENT: + ret = HTTP_OK; + + case HTTP_CREATED: + /* Use the request data */ + data = buf_cstring(&txn->req_body.payload); + + /* Fill in Content-Type, Content-Length */ + resp_body->type = mime->content_type; + resp_body->len = strlen(data); + + /* Fill in Content-Location */ + resp_body->loc = txn->req_tgt.path; + + /* Fill in Expires and Cache-Control */ + resp_body->maxage = 3600; /* 1 hr */ + txn->flags.cc = CC_MAXAGE + | CC_REVALIDATE /* don't use stale data */ + | CC_NOTRANSFORM; /* don't alter vCard data */ + + /* Output current representation */ + write_body(ret, txn, data, resp_body->len); + ret = 0; + break; + + default: + /* failure - do nothing */ + break; + } + } + + done: + if (vcard) { + cleanVObject(vcard); + cleanStrTbl(); + } + + return ret; +} + + +/* Callback to fetch DAV:getcontenttype */ +static int propfind_getcontenttype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + buf_setcstr(&fctx->buf, "text/vcard; charset=utf-8"); + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); + + return 0; +} + + +/* Callback to fetch DAV:resourcetype */ +static int propfind_restype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + if (!fctx->record) { + xmlNewChild(node, NULL, BAD_CAST "collection", NULL); + + if (fctx->req_tgt->collection) { + ensure_ns(fctx->ns, NS_CARDDAV, resp->parent, + XML_NS_CARDDAV, "C"); + xmlNewChild(node, fctx->ns[NS_CARDDAV], + BAD_CAST "addressbook", NULL); + } + } + + return 0; +} + + +/* Callback to prescreen/fetch CARDDAV:address-data */ +static int propfind_addrdata(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock) +{ + xmlNodePtr prop = (xmlNodePtr) rock; + const char *data = NULL; + size_t datalen = 0; + + if (propstat) { + if (!fctx->record) return HTTP_NOT_FOUND; + + if (!fctx->msg_buf.len) + mailbox_map_record(fctx->mailbox, fctx->record, &fctx->msg_buf); + if (!fctx->msg_buf.len) return HTTP_SERVER_ERROR; + + data = fctx->msg_buf.s + fctx->record->header_size; + datalen = fctx->record->size - fctx->record->header_size; + } + + return propfind_getdata(name, ns, fctx, propstat, prop, carddav_mime_types, + CARDDAV_SUPP_DATA, data, datalen); +} + + +/* Callback to fetch CARDDAV:addressbook-home-set */ +int propfind_abookhome(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock) +{ + xmlNodePtr node; + xmlNodePtr expand = (xmlNodePtr) rock; + + if (!(namespace_addressbook.enabled && fctx->req_tgt->user)) + return HTTP_NOT_FOUND; + + node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%s/user/%.*s/", namespace_addressbook.prefix, + (int) fctx->req_tgt->userlen, fctx->req_tgt->user); + + if (expand) { + /* Return properties for this URL */ + expand_property(expand, fctx, buf_cstring(&fctx->buf), + &carddav_parse_path, carddav_props, node, 0); + + } + else { + /* Return just the URL */ + xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf)); + } + + return 0; +} + + +/* Callback to fetch CARDDAV:supported-address-data */ +static int propfind_suppaddrdata(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node; + struct mime_type_t *mime; + + if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND; + + node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + for (mime = carddav_mime_types; mime->content_type; mime++) { + xmlNodePtr type = xmlNewChild(node, fctx->ns[NS_CARDDAV], + BAD_CAST "address-data-type", NULL); + + /* Trim any charset from content-type */ + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%.*s", + (int) strcspn(mime->content_type, ";"), mime->content_type); + + xmlNewProp(type, BAD_CAST "content-type", + BAD_CAST buf_cstring(&fctx->buf)); + + if (mime->version) + xmlNewProp(type, BAD_CAST "version", BAD_CAST mime->version); + } + + buf_reset(&fctx->buf); + + return 0; +} + + +static int report_card_query(struct transaction_t *txn, + xmlNodePtr inroot, struct propfind_ctx *fctx) +{ + int ret = 0; + xmlNodePtr node; + + fctx->filter_crit = (void *) 0xDEADBEEF; /* placeholder until we filter */ + fctx->open_db = (db_open_proc_t) &my_carddav_open; + fctx->close_db = (db_close_proc_t) &my_carddav_close; + fctx->lookup_resource = (db_lookup_proc_t) &carddav_lookup_resource; + fctx->foreach_resource = (db_foreach_proc_t) &carddav_foreach; + fctx->proc_by_resource = &propfind_by_resource; + + /* Parse children element of report */ + for (node = inroot->children; node; node = node->next) { + if (node->type == XML_ELEMENT_NODE) { + if (!xmlStrcmp(node->name, BAD_CAST "filter")) { + txn->error.precond = CARDDAV_SUPP_FILTER; + return HTTP_FORBIDDEN; + } + } + } + + if (fctx->depth++ > 0) { + /* Calendar collection(s) */ + if (txn->req_tgt.collection) { + /* Add response for target calendar collection */ + propfind_by_collection(txn->req_tgt.mboxname, 0, 0, fctx); + } + else { + /* Add responses for all contained calendar collections */ + strlcat(txn->req_tgt.mboxname, ".%", sizeof(txn->req_tgt.mboxname)); + mboxlist_findall(NULL, /* internal namespace */ + txn->req_tgt.mboxname, 1, httpd_userid, + httpd_authstate, propfind_by_collection, fctx); + } + + if (fctx->davdb) my_carddav_close(fctx->davdb); + + ret = *fctx->ret; + } + + return (ret ? ret : HTTP_MULTI_STATUS); +} + + +static int report_card_multiget(struct transaction_t *txn, + xmlNodePtr inroot, struct propfind_ctx *fctx) +{ + int r, ret = 0; + struct mailbox *mailbox = NULL; + xmlNodePtr node; + struct buf uri = BUF_INITIALIZER; + + /* Get props for each href */ + for (node = inroot->children; node; node = node->next) { + if ((node->type == XML_ELEMENT_NODE) && + !xmlStrcmp(node->name, BAD_CAST "href")) { + xmlChar *href = xmlNodeListGetString(inroot->doc, node->children, 1); + int len = xmlStrlen(href); + struct request_target_t tgt; + struct carddav_data *cdata; + + buf_ensure(&uri, len); + xmlURIUnescapeString((const char *) href, len, uri.s); + xmlFree(href); + + /* Parse the path */ + memset(&tgt, 0, sizeof(struct request_target_t)); + tgt.namespace = URL_NS_CALENDAR; + + if ((r = carddav_parse_path(uri.s, &tgt, &fctx->err->desc))) { + ret = r; + goto done; + } + + fctx->req_tgt = &tgt; + + /* Check if we already have this mailbox open */ + if (!mailbox || strcmp(mailbox->name, tgt.mboxname)) { + if (mailbox) mailbox_unlock_index(mailbox, NULL); + + /* Open mailbox for reading */ + r = mailbox_open_irl(tgt.mboxname, &mailbox); + if (r && r != IMAP_MAILBOX_NONEXISTENT) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + fctx->mailbox = mailbox; + } + + if (!fctx->mailbox || !tgt.resource) { + /* Add response for missing target */ + xml_add_response(fctx, HTTP_NOT_FOUND, 0); + continue; + } + + /* Open the DAV DB corresponding to the mailbox */ + fctx->davdb = my_carddav_open(fctx->mailbox); + + /* Find message UID for the resource */ + carddav_lookup_resource(fctx->davdb, + tgt.mboxname, tgt.resource, 0, &cdata); + cdata->dav.resource = tgt.resource; + /* XXX Check errors */ + + propfind_by_resource(fctx, cdata); + + my_carddav_close(fctx->davdb); + } + } + + done: + mailbox_close(&mailbox); + buf_free(&uri); + + return (ret ? ret : HTTP_MULTI_STATUS); +} + + + +/* Store the vCard data in the specified addressbook/resource */ +static int store_resource(struct transaction_t *txn, VObject *vcard, + struct mailbox *mailbox, const char *resource, + struct carddav_db *carddavdb, int overwrite, + unsigned flags) +{ + int ret = HTTP_CREATED, r; + VObjectIterator i; + struct carddav_data *cdata; + FILE *f = NULL; + struct index_record oldrecord; + struct stagemsg *stage; + char *header; + const char *version = NULL, *uid = NULL, *fullname = NULL; + quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_DONTCARE_INITIALIZER; + uint32_t expunge_uid = 0; + time_t now = time(NULL); + char datestr[80]; + struct appendstate as; + + /* Fetch some important properties */ + initPropIterator(&i, vcard); + while (moreIteration(&i)) { + VObject *prop = nextVObject(&i); + const char *name = vObjectName(prop); + + if (!strcmp(name, "VERSION")) { + version = fakeCString(vObjectUStringZValue(prop)); + if (strcmp(version, "3.0")) { + txn->error.precond = CARDDAV_SUPP_DATA; + return HTTP_FORBIDDEN; + } + } + else if (!strcmp(name, "UID")) { + uid = fakeCString(vObjectUStringZValue(prop)); + } + else if (!strcmp(name, "FN")) { + fullname = fakeCString(vObjectUStringZValue(prop)); + } + } + + /* Sanity check data */ + if (!version || !uid || !fullname) { + txn->error.precond = CARDDAV_VALID_DATA; + return HTTP_FORBIDDEN; + } + + /* Check for existing vCard UID */ + carddav_lookup_uid(carddavdb, uid, 0, &cdata); + if (!(flags & NO_DUP_CHECK) && + cdata->dav.mailbox && !strcmp(cdata->dav.mailbox, mailbox->name) && + strcmp(cdata->dav.resource, resource)) { + /* CARDDAV:no-uid-conflict */ + const char *owner = mboxname_to_userid(cdata->dav.mailbox); + + txn->error.precond = CARDDAV_UID_CONFLICT; + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%s/user/%s/%s/%s", + namespace_addressbook.prefix, owner, + strrchr(cdata->dav.mailbox, '.')+1, cdata->dav.resource); + txn->error.resource = buf_cstring(&txn->buf); + return HTTP_FORBIDDEN; + } + + if (cdata->dav.imap_uid) { + /* Fetch index record for the resource */ + r = mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &oldrecord); + + if (overwrite == OVERWRITE_CHECK) { + /* Check any preconditions */ + const char *etag = message_guid_encode(&oldrecord.guid); + time_t lastmod = oldrecord.internaldate; + int precond = check_precond(txn, cdata, etag, lastmod); + + if (precond != HTTP_OK) + return HTTP_PRECOND_FAILED; + } + + expunge_uid = oldrecord.uid; + } + + /* 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\r\n"; + return HTTP_SERVER_ERROR; + } + + /* Create iMIP header for resource */ + + /* XXX This needs to be done via an LDAP/DB lookup */ + header = charset_encode_mimeheader(proxy_userid, 0); + fprintf(f, "From: %s <>\r\n", header); + free(header); + + header = charset_encode_mimeheader(fullname, 0); + fprintf(f, "Subject: %s\r\n", header); + free(header); + + time_to_rfc822(now, datestr, sizeof(datestr)); + + fprintf(f, "Date: %s\r\n", datestr); + + fprintf(f, "Message-ID: <%s@%s>\r\n", uid, config_servername); + + fprintf(f, "Content-Type: text/vcard; charset=utf-8\r\n"); + + fprintf(f, "Content-Length: %u\r\n", (unsigned)buf_len(&txn->req_body.payload)); + fprintf(f, "Content-Disposition: inline; filename=\"%s\"\r\n", resource); + + /* XXX Check domain of data and use appropriate CTE */ + + fprintf(f, "MIME-Version: 1.0\r\n"); + fprintf(f, "\r\n"); + + /* Write the vCard data to the file */ + fprintf(f, "%s", buf_cstring(&txn->req_body.payload)); + + qdiffs[QUOTA_STORAGE] = ftell(f); + qdiffs[QUOTA_MESSAGE] = 1; + + fclose(f); + + /* Prepare to append the iMIP message to calendar mailbox */ + if ((r = append_setup_mbox(&as, mailbox, NULL, NULL, 0, qdiffs, 0, 0, EVENT_MESSAGE_NEW|EVENT_CALENDAR))) { + syslog(LOG_ERR, "append_setup(%s) failed: %s", + mailbox->name, error_message(r)); + ret = HTTP_SERVER_ERROR; + txn->error.desc = "append_setup() failed\r\n"; + } + else { + struct body *body = NULL; + + /* Append the iMIP file to the calendar mailbox */ + if ((r = append_fromstage(&as, &body, stage, now, NULL, 0, 0))) { + syslog(LOG_ERR, "append_fromstage() failed"); + ret = HTTP_SERVER_ERROR; + txn->error.desc = "append_fromstage() failed\r\n"; + } + if (body) { + message_free_body(body); + free(body); + } + + if (r) append_abort(&as); + else { + /* Commit the append to the calendar mailbox */ + r = append_commit(&as); + if (r) { + syslog(LOG_ERR, "append_commit() failed"); + ret = HTTP_SERVER_ERROR; + txn->error.desc = "append_commit() failed\r\n"; + } + else { + /* append_commit() returns a write-locked index */ + struct index_record newrecord; + + /* Read index record for new message (always the last one) */ + mailbox_read_index_record(mailbox, mailbox->i.num_records, + &newrecord); + + if (expunge_uid) { + /* Now that we have the replacement message in place + and the mailbox locked, re-read the old record + and see if we should overwrite it. Either way, + one of our records will have to be expunged. + */ + int userflag; + + ret = HTTP_NO_CONTENT; + + /* Perform the actual expunge */ + r = mailbox_user_flag(mailbox, DFLAG_UNBIND, &userflag, 1); + if (!r) { + oldrecord.user_flags[userflag/32] |= 1<<(userflag&31); + oldrecord.system_flags |= FLAG_EXPUNGED; + r = mailbox_rewrite_index_record(mailbox, &oldrecord); + } + if (r) { + syslog(LOG_ERR, "expunging record (%s) failed: %s", + mailbox->name, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + } + } + + if (!r) { + struct resp_body_t *resp_body = &txn->resp_body; + + /* Tell client about the new resource */ + resp_body->lastmod = newrecord.internaldate; + resp_body->etag = message_guid_encode(&newrecord.guid); + } + } + } + } + + append_removestage(stage); + + return ret; +}
View file
cyrus-imapd-2.5.tar.gz/imap/http_client.c
Added
@@ -0,0 +1,449 @@ +/* http_client.c - HTTP client-side support functions + * + * Copyright (c) 1994-2014 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 <string.h> +#include <syslog.h> + +#include "http_err.h" +#include "http_client.h" +#include "prot.h" +#include "tok.h" + + +/* Compare Content-Types */ +int is_mediatype(const char *pat, const char *type) +{ + const char *psep = strchr(pat, '/'); + const char *tsep = strchr(type, '/'); + size_t plen; + size_t tlen; + int alltypes; + + /* Check type */ + if (!psep || !tsep) return 0; + plen = psep - pat; + tlen = tsep - type; + + alltypes = !strncmp(pat, "*", plen); + + if (!alltypes && ((tlen != plen) || strncasecmp(pat, type, tlen))) return 0; + + /* Check subtype */ + pat = ++psep; + plen = strcspn(pat, "; \r\n\0"); + type = ++tsep; + tlen = strcspn(type, "; \r\n\0"); + + return (!strncmp(pat, "*", plen) || + (!alltypes && (tlen == plen) && !strncasecmp(pat, type, tlen))); +} + + +/* + * Parse the framing of a request or response message. + * Handles chunked, gzip, deflate TE only. + * Handles close-delimited response bodies (no Content-Length specified) + */ +int http_parse_framing(hdrcache_t hdrs, struct body_t *body, + const char **errstr) +{ + static unsigned max_msgsize = 0; + const char **hdr; + + if (!max_msgsize) { + max_msgsize = config_getint(IMAPOPT_MAXMESSAGESIZE); + + /* If max_msgsize is 0, allow any size */ + if (!max_msgsize) max_msgsize = INT_MAX; + } + + body->framing = FRAMING_LENGTH; + body->te = TE_NONE; + body->len = 0; + body->max = max_msgsize; + + /* Check for Transfer-Encoding */ + if ((hdr = spool_getheader(hdrs, "Transfer-Encoding"))) { + for (; *hdr; hdr++) { + tok_t tok = TOK_INITIALIZER(*hdr, ",", TOK_TRIMLEFT|TOK_TRIMRIGHT); + char *token; + + while ((token = tok_next(&tok))) { + if (body->te & TE_CHUNKED) { + /* "chunked" MUST only appear once and MUST be last */ + break; + } + else if (!strcasecmp(token, "chunked")) { + body->te |= TE_CHUNKED; + body->framing = FRAMING_CHUNKED; + } + else if (body->te & ~TE_CHUNKED) { + /* can't combine compression codings */ + break; + } +#ifdef HAVE_ZLIB + else if (!strcasecmp(token, "deflate")) + body->te = TE_DEFLATE; + else if (!strcasecmp(token, "gzip") || + !strcasecmp(token, "x-gzip")) + body->te = TE_GZIP; +#endif + else if (!(body->flags & BODY_DISCARD)) { + /* unknown/unsupported TE */ + break; + } + } + tok_fini(&tok); + if (token) break; /* error */ + } + + if (*hdr) { + body->te = TE_UNKNOWN; + *errstr = "Specified Transfer-Encoding not implemented"; + return HTTP_BAD_MEDIATYPE; + } + + /* Check if this is a non-chunked response */ + else if (!(body->te & TE_CHUNKED)) { + if ((body->flags & BODY_RESPONSE) && (body->flags & BODY_CLOSE)) { + body->framing = FRAMING_CLOSE; + } + else { + body->te = TE_UNKNOWN; + *errstr = "Final Transfer-Encoding MUST be \"chunked\""; + return HTTP_BAD_MEDIATYPE; + } + } + } + + /* Check for Content-Length */ + else if ((hdr = spool_getheader(hdrs, "Content-Length"))) { + if (hdr[1]) { + *errstr = "Multiple Content-Length header fields"; + return HTTP_BAD_REQUEST; + } + + body->len = strtoul(hdr[0], NULL, 10); + if (body->len > max_msgsize) return HTTP_TOO_LARGE; + + body->framing = FRAMING_LENGTH; + } + + /* Check if this is a close-delimited response */ + else if (body->flags & BODY_RESPONSE) { + if (body->flags & BODY_CLOSE) body->framing = FRAMING_CLOSE; + else return HTTP_LENGTH_REQUIRED; + } + + return 0; +} + + +/* + * Read the body of a request or response. + * Handles chunked, gzip, deflate TE only. + * Handles close-delimited response bodies (no Content-Length specified) + * Handles gzip and deflate CE only. + */ +int http_read_body(struct protstream *pin, struct protstream *pout, + hdrcache_t hdrs, struct body_t *body, const char **errstr) +{ + char buf[PROT_BUFSIZE]; + unsigned n; + int r = 0; + + syslog(LOG_DEBUG, "read_body(%#x)", body->flags); + + if (body->flags & BODY_DONE) return 0; + body->flags |= BODY_DONE; + + if (!(body->flags & BODY_DISCARD)) buf_reset(&body->payload); + else if (body->flags & BODY_CONTINUE) { + /* Don't care about the body and client hasn't sent it, we're done */ + return 0; + } + + if (body->framing == FRAMING_UNKNOWN) { + /* Get message framing */ + r = http_parse_framing(hdrs, body, errstr); + if (r) return r; + } + + if (body->flags & BODY_CONTINUE) { + /* Tell client to send the body */ + prot_printf(pout, "%s %s\r\n\r\n", + HTTP_VERSION, error_message(HTTP_CONTINUE)); + prot_flush(pout); + } + + /* Read and buffer the body */ + switch (body->framing) { + case FRAMING_LENGTH: + /* Read 'len' octets */ + for (; body->len; body->len -= n) { + if (body->flags & BODY_DISCARD) + n = prot_read(pin, buf, MIN(body->len, PROT_BUFSIZE)); + else + n = prot_readbuf(pin, &body->payload, body->len); + + if (!n) { + syslog(LOG_ERR, "prot_read() error"); + *errstr = "Unable to read body data"; + goto read_failure; + } + } + + break; + + case FRAMING_CHUNKED: + { + unsigned last = 0; + + /* Read chunks until last-chunk (zero chunk-size) */ + do { + unsigned chunk; + + /* Read chunk-size */ + if (!prot_fgets(buf, PROT_BUFSIZE, pin) || + sscanf(buf, "%x", &chunk) != 1) { + *errstr = "Unable to read chunk size"; + goto read_failure; + + /* XXX Do we need to parse chunk-ext? */ + } + else if (chunk > body->max - body->len) return HTTP_TOO_LARGE; + + if (!chunk) { + /* last-chunk */ + last = 1; + + /* Read/parse any trailing headers */ + spool_fill_hdrcache(pin, NULL, hdrs, NULL); + } + + /* Read 'chunk' octets */ + for (; chunk; chunk -= n) { + if (body->flags & BODY_DISCARD) + n = prot_read(pin, buf, MIN(chunk, PROT_BUFSIZE)); + else + n = prot_readbuf(pin, &body->payload, chunk); + + if (!n) { + syslog(LOG_ERR, "prot_read() error"); + *errstr = "Unable to read chunk data"; + goto read_failure; + } + body->len += n; + } + + /* Read CRLF terminating the chunk/trailer */ + if (!prot_fgets(buf, sizeof(buf), pin)) { + *errstr = "Missing CRLF following chunk/trailer"; + goto read_failure; + } + + } while (!last); + + body->te &= ~TE_CHUNKED; + + break; + } + + case FRAMING_CLOSE: + /* Read until EOF */ + do { + if (body->flags & BODY_DISCARD) + n = prot_read(pin, buf, PROT_BUFSIZE); + else + n = prot_readbuf(pin, &body->payload, PROT_BUFSIZE); + + if (n > body->max - body->len) return HTTP_TOO_LARGE; + body->len += n; + + } while (n); + + if (!pin->eof) goto read_failure; + + break; + + default: + /* XXX Should never get here */ + *errstr = "Unknown length of read body data"; + goto read_failure; + } + + + if (!(body->flags & BODY_DISCARD) && buf_len(&body->payload)) { +#ifdef HAVE_ZLIB + /* Decode the payload, if necessary */ + if (body->te == TE_DEFLATE) + r = buf_inflate(&body->payload, DEFLATE_ZLIB); + else if (body->te == TE_GZIP) + r = buf_inflate(&body->payload, DEFLATE_GZIP); + + if (r) { + *errstr = "Error decoding payload"; + return HTTP_BAD_REQUEST; + } +#endif + + /* Decode the representation, if necessary */ + if (body->flags & BODY_DECODE) { + const char **hdr; + + if (!(hdr = spool_getheader(hdrs, "Content-Encoding"))) { + /* nothing to see here */ + } + +#ifdef HAVE_ZLIB + else if (!strcasecmp(hdr[0], "deflate")) { + const char **ua = spool_getheader(hdrs, "User-Agent"); + + /* Try to detect Microsoft's broken deflate */ + if (ua && strstr(ua[0], "; MSIE ")) + r = buf_inflate(&body->payload, DEFLATE_RAW); + else + r = buf_inflate(&body->payload, DEFLATE_ZLIB); + } + else if (!strcasecmp(hdr[0], "gzip") || + !strcasecmp(hdr[0], "x-gzip")) + r = buf_inflate(&body->payload, DEFLATE_GZIP); +#endif + else { + *errstr = "Specified Content-Encoding not accepted"; + return HTTP_BAD_MEDIATYPE; + } + + if (r) { + *errstr = "Error decoding content"; + return HTTP_BAD_REQUEST; + } + } + } + + return 0; + + read_failure: + if (strcmpsafe(prot_error(pin), PROT_EOF_STRING)) { + /* client timed out */ + *errstr = prot_error(pin); + syslog(LOG_WARNING, "%s, closing connection", *errstr); + return HTTP_TIMEOUT; + } + else return HTTP_BAD_REQUEST; +} + + +/* Read a response from backend */ +int http_read_response(struct backend *be, unsigned meth, unsigned *code, + const char **statline, hdrcache_t *hdrs, + struct body_t *body, const char **errstr) +{ + static char statbuf[2048]; + const char **conn; + int c = EOF; + + if (statline) *statline = statbuf; + *errstr = NULL; + *code = HTTP_BAD_GATEWAY; + + if (*hdrs) spool_free_hdrcache(*hdrs); + if (!(*hdrs = spool_new_hdrcache())) { + *errstr = "Unable to create header cache for backend response"; + return HTTP_SERVER_ERROR; + } + if (!prot_fgets(statbuf, sizeof(statbuf), be->in) || + (sscanf(statbuf, HTTP_VERSION " %u ", code) != 1) || + spool_fill_hdrcache(be->in, NULL, *hdrs, NULL)) { + *errstr = "Unable to read status-line/headers from backend"; + return HTTP_BAD_GATEWAY; + } + eatline(be->in, c); /* CRLF separating headers & body */ + + /* 1xx (provisional) response - nothing else to do */ + if (*code < 200) return 0; + + /* Final response */ + if (!body) return 0; /* body will be piped */ + if (!(body->flags & BODY_DISCARD)) buf_reset(&body->payload); + + /* Check connection persistence */ + if (!strncmp(statbuf, "HTTP/1.0 ", 9)) body->flags |= BODY_CLOSE; + for (conn = spool_getheader(*hdrs, "Connection"); conn && *conn; conn++) { + tok_t tok = + TOK_INITIALIZER(*conn, ",", TOK_TRIMLEFT|TOK_TRIMRIGHT); + char *token; + + while ((token = tok_next(&tok))) { + if (!strcasecmp(token, "keep-alive")) body->flags &= ~BODY_CLOSE; + else if (!strcasecmp(token, "close")) body->flags |= BODY_CLOSE; + } + tok_fini(&tok); + } + + /* Not expecting a body for 204/304 response or any HEAD response */ + switch (*code){ + case 204: /* No Content */ + case 304: /* Not Modified */ + break; + + default: + if (meth == METH_HEAD) break; + + else { + body->flags |= BODY_RESPONSE; + body->framing = FRAMING_UNKNOWN; + + if (http_read_body(be->in, be->out, *hdrs, body, errstr)) { + return HTTP_BAD_GATEWAY; + } + } + } + + return 0; +}
View file
cyrus-imapd-2.5.tar.gz/imap/http_client.h
Added
@@ -0,0 +1,125 @@ +/* http_client.h - HTTP client-side support functions + * + * Copyright (c) 1994-2014 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 _HTTP_CLIENT_H +#define _HTTP_CLIENT_H + +#include "backend.h" +#include "spool.h" + +/* Supported HTTP version */ +#define HTTP_VERSION "HTTP/1.1" +#define HTTP_VERSION_LEN 8 + +/* Context for reading request/response body */ +struct body_t { + unsigned char flags; /* Disposition flags */ + unsigned char framing; /* Message framing */ + unsigned char te; /* Transfer-Encoding */ + unsigned max; /* Max allowed len */ + ulong len; /* Content-Length */ + struct buf payload; /* Payload */ +}; + +/* Message Framing flags */ +enum { + FRAMING_UNKNOWN = 0, + FRAMING_LENGTH, + FRAMING_CHUNKED, + FRAMING_CLOSE +}; + +/* Transfer-Encoding flags (coding of response payload) */ +enum { + TE_NONE = 0, + TE_DEFLATE = (1<<0), /* Implies TE_CHUNKED as final coding */ + TE_GZIP = (1<<1), /* Implies TE_CHUNKED as final coding */ + TE_CHUNKED = (1<<2), /* MUST be last */ + TE_UNKNOWN = 0xff +}; + +/* http_read_body() flags */ +enum { + BODY_RESPONSE = (1<<0), /* Response body, otherwise request */ + BODY_CONTINUE = (1<<1), /* Expect:100-continue request */ + BODY_CLOSE = (1<<2), /* Close-delimited response body */ + BODY_DECODE = (1<<3), /* Decode any Content-Encoding */ + BODY_DISCARD = (1<<4), /* Discard body (don't buffer or decode) */ + BODY_DONE = (1<<5) /* Body has been read */ +}; + +/* Index into known HTTP methods - needs to stay in sync with array */ +enum { + METH_ACL = 0, + METH_COPY, + METH_DELETE, + METH_GET, + METH_HEAD, + METH_LOCK, + METH_MKCALENDAR, + METH_MKCOL, + METH_MOVE, + METH_OPTIONS, + METH_POST, + METH_PROPFIND, + METH_PROPPATCH, + METH_PUT, + METH_REPORT, + METH_TRACE, + METH_UNLOCK, + + METH_UNKNOWN, /* MUST be last */ +}; + + +extern int is_mediatype(const char *pat, const char *type); +extern int http_parse_framing(hdrcache_t hdrs, struct body_t *body, + const char **errstr); +extern int http_read_body(struct protstream *pin, struct protstream *pout, + hdrcache_t hdrs, struct body_t *body, + const char **errstr); +extern int http_read_response(struct backend *be, unsigned meth, unsigned *code, + const char **statline, hdrcache_t *hdrs, + struct body_t *body, const char **errstr); + +#endif /* _HTTP_CLIENT_H */
View file
cyrus-imapd-2.5.tar.gz/imap/http_dav.c
Added
@@ -0,0 +1,5808 @@ +/* http_dav.c -- Routines for dealing with DAV properties in httpd + * + * Copyright (c) 1994-2011 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. + * + */ +/* + * TODO: + * + * - CALDAV:supported-calendar-component-set should be a bitmask in + * cyrus.index header Mailbox Options field + * + * - CALDAV:schedule-calendar-transp should be a flag in + * cyrus.index header (Mailbox Options) + * + * - DAV:creationdate sould be added to cyrus.header since it only + * gets set at creation time + * + * - Should add a last_metadata_update field to cyrus.index header + * for use in PROPFIND, PROPPATCH, and possibly REPORT. + * This would get updated any time a mailbox annotation, mailbox + * acl, or quota root limit is changed + * + * - Should we use cyrus.index header Format field to indicate + * CalDAV mailbox? + * + */ + + +#include "http_dav.h" +#include "annotate.h" +#include "acl.h" +#include "append.h" +#include "caldav_db.h" +#include "global.h" +#include "http_err.h" +#include "http_proxy.h" +#include "imap_err.h" +#include "index.h" +#include "proxy.h" +#include "times.h" +#include "syslog.h" +#include "strhash.h" +#include "tok.h" +#include "xmalloc.h" +#include "xstrlcat.h" +#include "xstrlcpy.h" + +#include <libxml/uri.h> + + +#define SYNC_TOKEN_URL_SCHEME "data:," + +static const struct dav_namespace_t { + const char *href; + const char *prefix; +} known_namespaces[] = { + { XML_NS_DAV, "D" }, + { XML_NS_CALDAV, "C" }, + { XML_NS_CARDDAV, "C" }, + { XML_NS_ISCHED, NULL }, + { XML_NS_CS, "CS" }, + { XML_NS_CYRUS, "CY" }, +}; + +/* PROPFIND modes */ +enum { + PROPFIND_NONE = 0, /* only used with REPORT */ + PROPFIND_ALL, + PROPFIND_NAME, + PROPFIND_PROP, + PROPFIND_EXPAND /* only used with expand-prop REPORT */ +}; + +static int prin_parse_path(const char *path, + struct request_target_t *tgt, const char **errstr); +static int propfind_displayname(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_restype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_alturiset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +static int propfind_principalurl(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +static int report_prin_prop_search(struct transaction_t *txn, + xmlNodePtr inroot, + struct propfind_ctx *fctx); +static int report_prin_search_prop_set(struct transaction_t *txn, + xmlNodePtr inroot, + struct propfind_ctx *fctx); + +static int allprop_cb(const char *mailbox __attribute__((unused)), + uint32_t uid __attribute__((unused)), + const char *entry, + const char *userid, const struct buf *attrib, + void *rock); + +/* Array of supported REPORTs */ +static const struct report_type_t dav_reports[] = { + + /* WebDAV Versioning (RFC 3253) REPORTs */ + { "expand-property", NS_DAV, "multistatus", &report_expand_prop, + DACL_READ, 0 }, + + /* WebDAV ACL (RFC 3744) REPORTs */ + { "principal-property-search", NS_DAV, "multistatus", + &report_prin_prop_search, 0, 0 }, + { "principal-search-property-set", NS_DAV, "principal-search-property-set", + &report_prin_search_prop_set, 0, 0 }, + + { NULL, 0, NULL, NULL, 0, 0 } +}; + +/* Array of known "live" properties */ +static const struct prop_entry dav_props[] = { + + /* WebDAV (RFC 4918) properties */ + { "creationdate", NS_DAV, PROP_ALLPROP, NULL, NULL, NULL }, + { "displayname", NS_DAV, PROP_ALLPROP | PROP_COLLECTION, + propfind_displayname, NULL, NULL }, + { "getcontentlanguage", NS_DAV, PROP_ALLPROP, NULL, NULL, NULL }, + { "getcontentlength", NS_DAV, PROP_ALLPROP | PROP_COLLECTION, + propfind_getlength, NULL, NULL }, + { "getcontenttype", NS_DAV, PROP_ALLPROP, NULL, NULL, NULL }, + { "getetag", NS_DAV, PROP_ALLPROP, NULL, NULL, NULL }, + { "getlastmodified", NS_DAV, PROP_ALLPROP, NULL, NULL, NULL }, + { "lockdiscovery", NS_DAV, PROP_ALLPROP | PROP_COLLECTION, + propfind_lockdisc, NULL, NULL }, + { "resourcetype", NS_DAV, PROP_ALLPROP | PROP_COLLECTION, + propfind_restype, NULL, NULL }, + { "supportedlock", NS_DAV, PROP_ALLPROP | PROP_COLLECTION, + propfind_suplock, NULL, NULL }, + + /* WebDAV Versioning (RFC 3253) properties */ + { "supported-report-set", NS_DAV, PROP_COLLECTION, + propfind_reportset, NULL, (void *) dav_reports }, + + /* WebDAV ACL (RFC 3744) properties */ + { "alternate-URI-set", NS_DAV, PROP_COLLECTION, + propfind_alturiset, NULL, NULL }, + { "principal-URL", NS_DAV, PROP_COLLECTION | PROP_EXPAND, + propfind_principalurl, NULL, NULL }, + { "group-member-set", NS_DAV, 0, NULL, NULL, NULL }, + { "group-membership", NS_DAV, 0, NULL, NULL, NULL }, + { "principal-collection-set", NS_DAV, PROP_COLLECTION, + propfind_princolset, NULL, NULL }, + + /* WebDAV Current Principal (RFC 5397) properties */ + { "current-user-principal", NS_DAV, PROP_COLLECTION | PROP_EXPAND, + propfind_curprin, NULL, NULL }, + + /* CalDAV (RFC 4791) properties */ + { "calendar-home-set", NS_CALDAV, PROP_COLLECTION | PROP_EXPAND, + propfind_calhome, NULL, NULL }, + + /* CalDAV Scheduling (RFC 6638) properties */ + { "schedule-inbox-URL", NS_CALDAV, PROP_COLLECTION | PROP_EXPAND, + propfind_schedinbox, NULL, NULL }, + { "schedule-outbox-URL", NS_CALDAV, PROP_COLLECTION | PROP_EXPAND, + propfind_schedoutbox, NULL, NULL }, + { "calendar-user-address-set", NS_CALDAV, PROP_COLLECTION, + propfind_caluseraddr, NULL, NULL }, + { "calendar-user-type", NS_CALDAV, PROP_COLLECTION, + propfind_calusertype, NULL, NULL }, + + /* CardDAV (RFC 6352) properties */ + { "addressbook-home-set", NS_CARDDAV, PROP_COLLECTION | PROP_EXPAND, + propfind_abookhome, NULL, NULL }, + + /* Apple Calendar Server properties */ + { "getctag", NS_CS, PROP_ALLPROP, NULL, NULL, NULL }, + + { NULL, 0, 0, NULL, NULL, NULL } +}; + + +static struct meth_params princ_params = { + .parse_path = &prin_parse_path, + .lprops = dav_props, + .reports = dav_reports +}; + +/* Namespace for WebDAV principals */ +struct namespace_t namespace_principal = { + URL_NS_PRINCIPAL, 0, "/dav/principals", NULL, 1 /* auth */, + ALLOW_READ | ALLOW_DAV, + NULL, NULL, NULL, NULL, + { + { NULL, NULL }, /* ACL */ + { NULL, NULL }, /* COPY */ + { NULL, NULL }, /* DELETE */ + { &meth_get_dav, &princ_params }, /* GET */ + { &meth_get_dav, &princ_params }, /* HEAD */ + { NULL, NULL }, /* LOCK */ + { NULL, NULL }, /* MKCALENDAR */ + { NULL, NULL }, /* MKCOL */ + { NULL, NULL }, /* MOVE */ + { &meth_options, NULL }, /* OPTIONS */ + { NULL, NULL }, /* POST */ + { &meth_propfind, &princ_params }, /* PROPFIND */ + { NULL, NULL }, /* PROPPATCH */ + { NULL, NULL }, /* PUT */ + { &meth_report, &princ_params }, /* REPORT */ + { &meth_trace, NULL }, /* TRACE */ + { NULL, NULL } /* UNLOCK */ + } +}; + + +/* Linked-list of properties for fetching */ +struct propfind_entry_list { + xmlChar *name; /* Property name (needs to be freed) */ + xmlNsPtr ns; /* Property namespace */ + unsigned char flags; /* Flags for how/where prop apply */ + int (*get)(const xmlChar *name, /* Callback to fetch property */ + xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + void *rock; /* Add'l data to pass to callback */ + struct propfind_entry_list *next; +}; + + +/* Bitmask of privilege flags */ +enum { + PRIV_IMPLICIT = (1<<0), + PRIV_INBOX = (1<<1), + PRIV_OUTBOX = (1<<2) +}; + + +/* Array of precondition/postcondition errors */ +static const struct precond_t { + const char *name; /* Property name */ + unsigned ns; /* Index into known namespace array */ +} preconds[] = { + /* Placeholder for zero (no) precondition code */ + { NULL, 0 }, + + /* WebDAV (RFC 4918) preconditons */ + { "cannot-modify-protected-property", NS_DAV }, + { "lock-token-matches-request-uri", NS_DAV }, + { "lock-token-submitted", NS_DAV }, + { "no-conflicting-lock", NS_DAV }, + + /* WebDAV Versioning (RFC 3253) preconditions */ + { "supported-report", NS_DAV }, + { "resource-must-be-null", NS_DAV }, + + /* WebDAV ACL (RFC 3744) preconditions */ + { "need-privileges", NS_DAV }, + { "no-invert", NS_DAV }, + { "no-abstract", NS_DAV }, + { "not-supported-privilege", NS_DAV }, + { "recognized-principal", NS_DAV }, + + /* WebDAV Quota (RFC 4331) preconditions */ + { "quota-not-exceeded", NS_DAV }, + { "sufficient-disk-space", NS_DAV }, + + /* WebDAV Extended MKCOL (RFC 5689) preconditions */ + { "valid-resourcetype", NS_DAV }, + + /* WebDAV Sync (RFC 6578) preconditions */ + { "valid-sync-token", NS_DAV }, + { "number-of-matches-within-limits", NS_DAV }, + + /* CalDAV (RFC 4791) preconditions */ + { "supported-calendar-data", NS_CALDAV }, + { "valid-calendar-data", NS_CALDAV }, + { "valid-calendar-object-resource", NS_CALDAV }, + { "supported-calendar-component", NS_CALDAV }, + { "calendar-collection-location-ok", NS_CALDAV }, + { "no-uid-conflict", NS_CALDAV }, + { "supported-filter", NS_CALDAV }, + { "valid-filter", NS_CALDAV }, + + /* RSCALE (draft-daboo-icalendar-rscale) preconditions */ + { "supported-rscale", NS_CALDAV }, + + /* TZ by Ref (draft-ietf-tzdist-caldav-timezone-ref) preconditions */ + { "valid-timezone", NS_CALDAV }, + + /* CalDAV Scheduling (RFC 6638) preconditions */ + { "valid-scheduling-message", NS_CALDAV }, + { "valid-organizer", NS_CALDAV }, + { "unique-scheduling-object-resource", NS_CALDAV }, + { "same-organizer-in-all-components", NS_CALDAV }, + { "allowed-organizer-scheduling-object-change", NS_CALDAV }, + { "allowed-attendee-scheduling-object-change", NS_CALDAV }, + + /* iSchedule (draft-desruisseaux-ischedule) preconditions */ + { "version-not-supported", NS_ISCHED }, + { "invalid-calendar-data-type", NS_ISCHED }, + { "invalid-calendar-data", NS_ISCHED }, + { "invalid-scheduling-message", NS_ISCHED }, + { "originator-missing", NS_ISCHED }, + { "too-many-originators", NS_ISCHED }, + { "originator-invalid", NS_ISCHED }, + { "originator-denied", NS_ISCHED }, + { "recipient-missing", NS_ISCHED }, + { "recipient-mismatch", NS_ISCHED }, + { "verification-failed", NS_ISCHED }, + + /* CardDAV (RFC 6352) preconditions */ + { "supported-address-data", NS_CARDDAV }, + { "valid-address-data", NS_CARDDAV }, + { "no-uid-conflict", NS_CARDDAV }, + { "addressbook-collection-location-ok", NS_CARDDAV }, + { "supported-filter", NS_CARDDAV } +}; + + +/* Parse request-target path in DAV principals namespace */ +static int prin_parse_path(const char *path, + struct request_target_t *tgt, const char **errstr) +{ + char *p; + size_t len; + + /* Make a working copy of target path */ + strlcpy(tgt->path, path, sizeof(tgt->path)); + tgt->tail = tgt->path + strlen(tgt->path); + + p = tgt->path; + + /* Sanity check namespace */ + len = strlen(namespace_principal.prefix); + if (strlen(p) < len || + strncmp(namespace_principal.prefix, p, len) || + (path[len] && path[len] != '/')) { + *errstr = "Namespace mismatch request target path"; + return HTTP_FORBIDDEN; + } + + /* Skip namespace */ + p += len; + if (!*p || !*++p) return 0; + + /* Check if we're in user space */ + len = strcspn(p, "/"); + if (!strncmp(p, "user", len)) { + p += len; + if (!*p || !*++p) return 0; + + /* Get user id */ + len = strcspn(p, "/"); + tgt->user = p; + tgt->userlen = len; + + p += len; + if (!*p || !*++p) return 0; + } + else return HTTP_NOT_FOUND; /* need to specify a userid */ + + if (*p) { +// *errstr = "Too many segments in request target path"; + return HTTP_NOT_FOUND; + } + + return 0; +} + + +unsigned get_preferences(struct transaction_t *txn) +{ + unsigned mask = 0, prefs = 0; + const char **hdr; + + /* Create a mask for preferences honored by method */ + switch (txn->meth) { + case METH_COPY: + case METH_MOVE: + case METH_POST: + case METH_PUT: + mask = PREFER_REP; + break; + + case METH_MKCALENDAR: + case METH_MKCOL: + case METH_PROPPATCH: + mask = PREFER_MIN; + break; + + case METH_PROPFIND: + case METH_REPORT: + mask = (PREFER_MIN | PREFER_NOROOT); + break; + } + + if (!mask) return 0; + else { + txn->flags.vary |= VARY_PREFER; + if (mask & PREFER_MIN) txn->flags.vary |= VARY_BRIEF; + } + + /* Check for Prefer header(s) */ + if ((hdr = spool_getheader(txn->req_hdrs, "Prefer"))) { + int i; + for (i = 0; hdr[i]; i++) { + tok_t tok; + char *token; + + tok_init(&tok, hdr[i], ",\r\n", TOK_TRIMLEFT|TOK_TRIMRIGHT); + while ((token = tok_next(&tok))) { + if ((mask & PREFER_MIN) && + !strcmp(token, "return=minimal")) + prefs |= PREFER_MIN; + else if ((mask & PREFER_REP) && + !strcmp(token, "return=representation")) + prefs |= PREFER_REP; + else if ((mask & PREFER_NOROOT) && + !strcmp(token, "depth-noroot")) + prefs |= PREFER_NOROOT; + } + tok_fini(&tok); + } + + txn->resp_body.prefs = prefs; + } + + /* Check for Brief header */ + if ((mask & PREFER_MIN) && + (hdr = spool_getheader(txn->req_hdrs, "Brief")) && + !strcasecmp(hdr[0], "t")) { + prefs |= PREFER_MIN; + } + + return prefs; +} + + +/* Check requested MIME type */ +struct mime_type_t *get_accept_type(const char **hdr, struct mime_type_t *types) +{ + struct mime_type_t *ret = NULL; + struct accept *e, *enc = parse_accept(hdr); + + for (e = enc; e && e->token; e++) { + if (!ret && e->qual > 0.0) { + struct mime_type_t *m; + + for (m = types; !ret && m->content_type; m++) { + if (is_mediatype(e->token, m->content_type)) ret = m; + } + } + + free(e->token); + } + if (enc) free(enc); + + return ret; +} + + +static int add_privs(int rights, unsigned flags, + xmlNodePtr parent, xmlNodePtr root, xmlNsPtr *ns); + + +/* Ensure that we have a given namespace. If it doesn't exist in what we + * parsed in the request, create it and attach to 'node'. + */ +int ensure_ns(xmlNsPtr *respNs, int ns, xmlNodePtr node, + const char *url, const char *prefix) +{ + if (!respNs[ns]) { + xmlNsPtr nsDef; + char myprefix[20]; + + /* Search for existing namespace using our prefix */ + for (nsDef = node->nsDef; nsDef; nsDef = nsDef->next) { + if ((!nsDef->prefix && !prefix) || + (nsDef->prefix && prefix && + !strcmp((const char *) nsDef->prefix, prefix))) break; + } + + if (nsDef) { + /* Prefix is already used - generate a new one */ + snprintf(myprefix, sizeof(myprefix), "X%X", strhash(url) & 0xffff); + prefix = myprefix; + } + + respNs[ns] = xmlNewNs(node, BAD_CAST url, BAD_CAST prefix); + } + + /* XXX check for errors */ + return 0; +} + + +/* Add namespaces declared in the request to our root node and Ns array */ +static int xml_add_ns(xmlNodePtr req, xmlNsPtr *respNs, xmlNodePtr root) +{ + for (; req; req = req->next) { + if (req->type == XML_ELEMENT_NODE) { + xmlNsPtr nsDef; + + for (nsDef = req->nsDef; nsDef; nsDef = nsDef->next) { + if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_DAV)) + ensure_ns(respNs, NS_DAV, root, + (const char *) nsDef->href, + (const char *) nsDef->prefix); + else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CALDAV)) + ensure_ns(respNs, NS_CALDAV, root, + (const char *) nsDef->href, + (const char *) nsDef->prefix); + else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CARDDAV)) + ensure_ns(respNs, NS_CARDDAV, root, + (const char *) nsDef->href, + (const char *) nsDef->prefix); + else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CS)) + ensure_ns(respNs, NS_CS, root, + (const char *) nsDef->href, + (const char *) nsDef->prefix); + else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CYRUS)) + ensure_ns(respNs, NS_CYRUS, root, + (const char *) nsDef->href, + (const char *) nsDef->prefix); + else + xmlNewNs(root, nsDef->href, nsDef->prefix); + } + } + + xml_add_ns(req->children, respNs, root); + } + + /* XXX check for errors */ + return 0; +} + + +/* Initialize an XML tree for a property response */ +xmlNodePtr init_xml_response(const char *resp, int ns, + xmlNodePtr req, xmlNsPtr *respNs) +{ + /* Start construction of our XML response tree */ + xmlDocPtr doc = xmlNewDoc(BAD_CAST "1.0"); + xmlNodePtr root = NULL; + + if (!doc) return NULL; + if (!(root = xmlNewNode(NULL, BAD_CAST resp))) return NULL; + + xmlDocSetRootElement(doc, root); + + /* Add namespaces from request to our response, + * creating array of known namespaces that we can reference later. + */ + memset(respNs, 0, NUM_NAMESPACE * sizeof(xmlNsPtr)); + xml_add_ns(req, respNs, root); + + /* Set namespace of root node */ + ensure_ns(respNs, ns, root, + known_namespaces[ns].href, known_namespaces[ns].prefix); + xmlSetNs(root, respNs[ns]); + + return root; +} + +xmlNodePtr xml_add_href(xmlNodePtr parent, xmlNsPtr ns, const char *href) +{ + xmlChar *uri = xmlURIEscapeStr(BAD_CAST href, BAD_CAST ":/?="); + xmlNodePtr node = xmlNewChild(parent, ns, BAD_CAST "href", uri); + + free(uri); + return node; +} + +xmlNodePtr xml_add_error(xmlNodePtr root, struct error_t *err, + xmlNsPtr *avail_ns) +{ + xmlNsPtr ns[NUM_NAMESPACE]; + xmlNodePtr error, node; + const struct precond_t *precond = &preconds[err->precond]; + unsigned err_ns = NS_DAV; + const char *resp_desc = "responsedescription"; + + if (precond->ns == NS_ISCHED) { + err_ns = NS_ISCHED; + resp_desc = "response-description"; + } + + if (!root) { + error = root = init_xml_response("error", err_ns, NULL, ns); + avail_ns = ns; + } + else error = xmlNewChild(root, NULL, BAD_CAST "error", NULL); + + ensure_ns(avail_ns, precond->ns, root, known_namespaces[precond->ns].href, + known_namespaces[precond->ns].prefix); + node = xmlNewChild(error, avail_ns[precond->ns], + BAD_CAST precond->name, NULL); + + switch (err->precond) { + case DAV_NEED_PRIVS: + if (err->resource && err->rights) { + unsigned flags = 0; + size_t rlen = strlen(err->resource); + const char *p = err->resource + rlen; + + node = xmlNewChild(node, NULL, BAD_CAST "resource", NULL); + xml_add_href(node, NULL, err->resource); + + if (rlen > 6 && !strcmp(p-6, SCHED_INBOX)) + flags = PRIV_INBOX; + else if (rlen > 7 && !strcmp(p-7, SCHED_OUTBOX)) + flags = PRIV_OUTBOX; + + add_privs(err->rights, flags, node, root, avail_ns); + } + break; + + default: + if (err->resource) xml_add_href(node, avail_ns[NS_DAV], err->resource); + break; + } + + if (err->desc) { + xmlNewTextChild(error, NULL, BAD_CAST resp_desc, BAD_CAST err->desc); + } + + return root; +} + + +void xml_add_lockdisc(xmlNodePtr node, const char *root, struct dav_data *data) +{ + time_t now = time(NULL); + + if (data->lock_expire > now) { + xmlNodePtr active, node1; + char tbuf[30]; /* "Second-" + long int + NUL */ + + active = xmlNewChild(node, NULL, BAD_CAST "activelock", NULL); + node1 = xmlNewChild(active, NULL, BAD_CAST "lockscope", NULL); + xmlNewChild(node1, NULL, BAD_CAST "exclusive", NULL); + + node1 = xmlNewChild(active, NULL, BAD_CAST "locktype", NULL); + xmlNewChild(node1, NULL, BAD_CAST "write", NULL); + + xmlNewChild(active, NULL, BAD_CAST "depth", BAD_CAST "0"); + + if (data->lock_owner) { + /* Last char of token signals href (1) or text (0) */ + if (data->lock_token[strlen(data->lock_token)-1] == '1') { + node1 = xmlNewChild(active, NULL, BAD_CAST "owner", NULL); + xml_add_href(node1, NULL, data->lock_owner); + } + else { + xmlNewTextChild(active, NULL, BAD_CAST "owner", + BAD_CAST data->lock_owner); + } + } + + snprintf(tbuf, sizeof(tbuf), "Second-%lu", data->lock_expire - now); + xmlNewChild(active, NULL, BAD_CAST "timeout", BAD_CAST tbuf); + + node1 = xmlNewChild(active, NULL, BAD_CAST "locktoken", NULL); + xml_add_href(node1, NULL, data->lock_token); + + node1 = xmlNewChild(active, NULL, BAD_CAST "lockroot", NULL); + xml_add_href(node1, NULL, root); + } +} + + +/* Add a property 'name', of namespace 'ns', with content 'content', + * and status code/string 'status' to propstat element 'stat'. + * 'stat' will be created as necessary. + */ +xmlNodePtr xml_add_prop(long status, xmlNsPtr davns, + struct propstat *propstat, + const xmlChar *name, xmlNsPtr ns, + xmlChar *content, + unsigned precond) +{ + xmlNodePtr newprop = NULL; + + if (!propstat->root) { + propstat->root = xmlNewNode(davns, BAD_CAST "propstat"); + xmlNewChild(propstat->root, NULL, BAD_CAST "prop", NULL); + } + + if (name) newprop = xmlNewTextChild(propstat->root->children, + ns, name, content); + propstat->status = status; + propstat->precond = precond; + + return newprop; +} + + +struct allprop_rock { + struct propfind_ctx *fctx; + struct propstat *propstat; +}; + +/* Add a response tree to 'root' for the specified href and + either error code or property list */ +int xml_add_response(struct propfind_ctx *fctx, long code, unsigned precond) +{ + xmlNodePtr resp; + + resp = xmlNewChild(fctx->root, fctx->ns[NS_DAV], BAD_CAST "response", NULL); + if (!resp) { + fctx->err->desc = "Unable to add response XML element"; + *fctx->ret = HTTP_SERVER_ERROR; + return HTTP_SERVER_ERROR; + } + xml_add_href(resp, NULL, fctx->req_tgt->path); + + if (code) { + xmlNewChild(resp, NULL, BAD_CAST "status", + BAD_CAST http_statusline(code)); + + if (precond) { + xmlNodePtr error = xmlNewChild(resp, NULL, BAD_CAST "error", NULL); + + xmlNewChild(error, NULL, BAD_CAST preconds[precond].name, NULL); + } + } + else { + struct propstat propstat[NUM_PROPSTAT], *stat; + struct propfind_entry_list *e; + int i; + + memset(propstat, 0, NUM_PROPSTAT * sizeof(struct propstat)); + + /* Process each property in the linked list */ + for (e = fctx->elist; e; e = e->next) { + int r = HTTP_NOT_FOUND; + + if (e->get) { + r = 0; + + /* Pre-screen request based on prop flags */ + if (fctx->req_tgt->resource) { + if (!(e->flags & PROP_RESOURCE)) r = HTTP_NOT_FOUND; + } + else if (!(e->flags & PROP_COLLECTION)) r = HTTP_NOT_FOUND; + + if (!r) { + if (fctx->mode == PROPFIND_NAME) { + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], + e->name, e->ns, NULL, 0); + } + else { + r = e->get(e->name, e->ns, + fctx, resp, propstat, e->rock); + } + } + } + + switch (r) { + case 0: + case HTTP_OK: + /* Nothing to do - property handled in callback */ + break; + + case HTTP_UNAUTHORIZED: + xml_add_prop(HTTP_UNAUTHORIZED, fctx->ns[NS_DAV], + &propstat[PROPSTAT_UNAUTH], + e->name, e->ns, NULL, 0); + break; + + case HTTP_FORBIDDEN: + xml_add_prop(HTTP_FORBIDDEN, fctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + e->name, e->ns, NULL, 0); + break; + + case HTTP_NOT_FOUND: + if (!(fctx->prefer & PREFER_MIN)) { + xml_add_prop(HTTP_NOT_FOUND, fctx->ns[NS_DAV], + &propstat[PROPSTAT_NOTFOUND], + e->name, e->ns, NULL, 0); + } + break; + + default: + xml_add_prop(r, fctx->ns[NS_DAV], &propstat[PROPSTAT_ERROR], + e->name, e->ns, NULL, 0); + break; + + } + } + + /* Process dead properties for allprop/propname */ + if (fctx->mailbox && !fctx->req_tgt->resource && + (fctx->mode == PROPFIND_ALL || fctx->mode == PROPFIND_NAME)) { + struct allprop_rock arock = { fctx, propstat }; + + annotatemore_findall(fctx->mailbox->name, 0, "*", allprop_cb, &arock); + } + + /* Check if we have any propstat elements */ + for (i = 0; i < NUM_PROPSTAT && !propstat[i].root; i++); + if (i == NUM_PROPSTAT) { + /* Add an empty propstat 200 */ + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], NULL, NULL, NULL, 0); + } + + /* Add status and optional error to the propstat elements + and then add them to response element */ + for (i = 0; i < NUM_PROPSTAT; i++) { + stat = &propstat[i]; + + if (stat->root) { + xmlNewChild(stat->root, NULL, BAD_CAST "status", + BAD_CAST http_statusline(stat->status)); + if (stat->precond) { + struct error_t error = { NULL, stat->precond, NULL, 0 }; + xml_add_error(stat->root, &error, fctx->ns); + } + + xmlAddChild(resp, stat->root); + } + } + } + + fctx->record = NULL; + + return 0; +} + + +/* Helper function to prescreen/fetch resource data */ +int propfind_getdata(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + struct propstat propstat[], xmlNodePtr prop, + struct mime_type_t *mime_types, int precond, + const char *data, unsigned long datalen) +{ + int ret = 0; + xmlChar *type, *ver = NULL; + struct mime_type_t *mime; + + type = xmlGetProp(prop, BAD_CAST "content-type"); + if (type) ver = xmlGetProp(prop, BAD_CAST "version"); + + /* Check/find requested MIME type */ + for (mime = mime_types; type && mime->content_type; mime++) { + if (is_mediatype((const char *) type, mime->content_type)) { + if (ver && + (!mime->version || xmlStrcmp(ver, BAD_CAST mime->version))) { + continue; + } + break; + } + } + + if (!propstat) { + /* Prescreen "property" request */ + if (!mime->content_type) { + fctx->err->precond = precond; + ret = *fctx->ret = HTTP_FORBIDDEN; + } + } + else { + /* Add "property" */ + char *freeme = NULL; + + prop = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + if (mime != mime_types) { + /* Not the storage format - convert into requested MIME type */ + void *obj = mime_types->from_string(data); + + data = freeme = mime->to_string(obj); + datalen = strlen(data); + mime_types->free(obj); + } + + if (type) { + xmlSetProp(prop, BAD_CAST "content-type", type); + if (ver) xmlSetProp(prop, BAD_CAST "version", ver); + } + + xmlAddChild(prop, + xmlNewCDataBlock(fctx->root->doc, BAD_CAST data, datalen)); + + fctx->fetcheddata = 1; + + if (freeme) free(freeme); + } + + if (type) xmlFree(type); + if (ver) xmlFree(ver); + + return ret; +} + + +/* Callback to fetch DAV:creationdate */ +int propfind_creationdate(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + time_t t = 0; + char datestr[21]; + + if (fctx->data) { + struct dav_data *ddata = (struct dav_data *) fctx->data; + + t = ddata->creationdate; + } + else if (fctx->mailbox) { + struct stat sbuf; + + fstat(fctx->mailbox->header_fd, &sbuf); + + t = sbuf.st_ctime; + } + + if (!t) return HTTP_NOT_FOUND; + + rfc3339date_gen(datestr, sizeof(datestr), t); + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST datestr, 0); + + return 0; +} + + +/* Callback to fetch DAV:displayname */ +static int propfind_displayname(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + /* XXX Do LDAP/SQL lookup here */ + + buf_reset(&fctx->buf); + if (fctx->req_tgt->user) { + buf_printf(&fctx->buf, "%.*s", + (int) fctx->req_tgt->userlen, fctx->req_tgt->user); + } + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); + + return 0; +} + + +/* Callback to fetch DAV:getcontentlength */ +int propfind_getlength(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + buf_reset(&fctx->buf); + + if (fctx->record) { + buf_printf(&fctx->buf, "%u", + fctx->record->size - fctx->record->header_size); + } + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); + + return 0; +} + + +/* Callback to fetch DAV:getetag */ +int propfind_getetag(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + if (fctx->req_tgt->resource && !fctx->record) return HTTP_NOT_FOUND; + if (!fctx->mailbox) return HTTP_NOT_FOUND; + + buf_reset(&fctx->buf); + + if (fctx->record) { + /* add DQUOTEs */ + buf_printf(&fctx->buf, "\"%s\"", + message_guid_encode(&fctx->record->guid)); + } + else { + buf_printf(&fctx->buf, "\"%u-%u-%u\"", fctx->mailbox->i.uidvalidity, + fctx->mailbox->i.last_uid, fctx->mailbox->i.exists); + } + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); + + return 0; +} + + +/* Callback to fetch DAV:getlastmodified */ +int propfind_getlastmod(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + if (!fctx->mailbox || + (fctx->req_tgt->resource && !fctx->record)) return HTTP_NOT_FOUND; + + buf_ensure(&fctx->buf, 30); + httpdate_gen(fctx->buf.s, fctx->buf.alloc, + fctx->record ? fctx->record->internaldate : + fctx->mailbox->index_mtime); + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST fctx->buf.s, 0); + + return 0; +} + + +/* Callback to fetch DAV:lockdiscovery */ +int propfind_lockdisc(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + if (fctx->mailbox && fctx->record) { + struct dav_data *ddata = (struct dav_data *) fctx->data; + + xml_add_lockdisc(node, fctx->req_tgt->path, ddata); + } + + return 0; +} + + +/* Callback to fetch DAV:resourcetype */ +static int propfind_restype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + if (fctx->req_tgt->user) + xmlNewChild(node, NULL, BAD_CAST "principal", NULL); + + return 0; +} + + +/* Callback to "write" resourcetype property */ +int proppatch_restype(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], + void *rock) +{ + const char *coltype = (const char *) rock; + unsigned precond = 0; + + if (set && (pctx->meth != METH_PROPPATCH)) { + /* "Writeable" for MKCOL/MKCALENDAR only */ + xmlNodePtr cur; + + for (cur = prop->children; cur; cur = cur->next) { + if (cur->type != XML_ELEMENT_NODE) continue; + /* Make sure we have valid resourcetypes for the collection */ + if (xmlStrcmp(cur->name, BAD_CAST "collection") && + xmlStrcmp(cur->name, BAD_CAST coltype)) break; + } + + if (!cur) { + /* All resourcetypes are valid */ + xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + prop->name, prop->ns, NULL, 0); + + return 0; + } + + /* Invalid resourcetype */ + precond = DAV_VALID_RESTYPE; + } + else { + /* Protected property */ + precond = DAV_PROT_PROP; + } + + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, precond); + + *pctx->ret = HTTP_FORBIDDEN; + + return 0; +} + + +/* Callback to fetch DAV:supportedlock */ +int propfind_suplock(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + if (fctx->mailbox && fctx->record) { + xmlNodePtr entry = xmlNewChild(node, NULL, BAD_CAST "lockentry", NULL); + xmlNodePtr scope = xmlNewChild(entry, NULL, BAD_CAST "lockscope", NULL); + xmlNodePtr type = xmlNewChild(entry, NULL, BAD_CAST "locktype", NULL); + + xmlNewChild(scope, NULL, BAD_CAST "exclusive", NULL); + xmlNewChild(type, NULL, BAD_CAST "write", NULL); + } + + return 0; +} + + +/* Callback to fetch DAV:supported-report-set */ +int propfind_reportset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr top, node; + const struct report_type_t *report; + + top = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + for (report = (const struct report_type_t *) rock; + report && report->name; report++) { + node = xmlNewChild(top, NULL, BAD_CAST "supported-report", NULL); + node = xmlNewChild(node, NULL, BAD_CAST "report", NULL); + + ensure_ns(fctx->ns, report->ns, resp->parent, + known_namespaces[report->ns].href, + known_namespaces[report->ns].prefix); + xmlNewChild(node, fctx->ns[report->ns], BAD_CAST report->name, NULL); + } + + return 0; +} + + +/* Callback to fetch DAV:alternate-URI-set */ +static int propfind_alturiset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + return 0; +} + + +/* Callback to fetch DAV:principal-URL */ +static int propfind_principalurl(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + buf_reset(&fctx->buf); + if (fctx->req_tgt->user) { + xmlNodePtr expand = (xmlNodePtr) rock; + + buf_printf(&fctx->buf, "%s/user/%.*s/", + namespace_principal.prefix, + (int) fctx->req_tgt->userlen, fctx->req_tgt->user); + + if (expand) { + /* Return properties for this URL */ + expand_property(expand, fctx, buf_cstring(&fctx->buf), + &prin_parse_path, dav_props, node, 0); + return 0; + } + } + + /* Return just the URL */ + xml_add_href(node, NULL, buf_cstring(&fctx->buf)); + + return 0; +} + + +/* Callback to fetch DAV:owner */ +int propfind_owner(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + if (fctx->req_tgt->user) { + xmlNodePtr expand = (xmlNodePtr) rock; + + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%s/user/%.*s/", + namespace_principal.prefix, + (int) fctx->req_tgt->userlen, fctx->req_tgt->user); + + if (expand) { + /* Return properties for this URL */ + expand_property(expand, fctx, buf_cstring(&fctx->buf), + &prin_parse_path, dav_props, node, 0); + } + else { + /* Return just the URL */ + xml_add_href(node, NULL, buf_cstring(&fctx->buf)); + } + } + + return 0; +} + + +/* Add possibly 'abstract' supported-privilege 'priv_name', of namespace 'ns', + * with description 'desc_str' to node 'root'. For now, we alssume all + * descriptions are English. + */ +static xmlNodePtr add_suppriv(xmlNodePtr root, const char *priv_name, + xmlNsPtr ns, int abstract, const char *desc_str) +{ + xmlNodePtr supp, priv, desc; + + supp = xmlNewChild(root, NULL, BAD_CAST "supported-privilege", NULL); + priv = xmlNewChild(supp, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, ns, BAD_CAST priv_name, NULL); + if (abstract) xmlNewChild(supp, NULL, BAD_CAST "abstract", NULL); + desc = xmlNewChild(supp, NULL, BAD_CAST "description", BAD_CAST desc_str); + xmlNodeSetLang(desc, BAD_CAST "en"); + + return supp; +} + + +/* Callback to fetch DAV:supported-privilege-set */ +int propfind_supprivset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr set, all, agg, write; + unsigned tgt_flags = 0; + + set = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + all = add_suppriv(set, "all", NULL, 0, "Any operation"); + + agg = add_suppriv(all, "read", NULL, 0, "Read any object"); + add_suppriv(agg, "read-current-user-privilege-set", NULL, 1, + "Read current user privilege set"); + + if (fctx->req_tgt->namespace == URL_NS_CALENDAR) { + if (fctx->req_tgt->collection) { + ensure_ns(fctx->ns, NS_CALDAV, resp->parent, XML_NS_CALDAV, "C"); + + if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX)) + tgt_flags = TGT_SCHED_INBOX; + else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX)) + tgt_flags = TGT_SCHED_OUTBOX; + else { + add_suppriv(agg, "read-free-busy", fctx->ns[NS_CALDAV], 0, + "Read free/busy time"); + } + } + } + + write = add_suppriv(all, "write", NULL, 0, "Write any object"); + add_suppriv(write, "write-content", NULL, 0, "Write resource content"); + add_suppriv(write, "write-properties", NULL, 0, "Write properties"); + + agg = add_suppriv(write, "bind", NULL, 0, "Add new member to collection"); + ensure_ns(fctx->ns, NS_CYRUS, resp->parent, XML_NS_CYRUS, "CY"); + add_suppriv(agg, "make-collection", fctx->ns[NS_CYRUS], 0, + "Make new collection"); + add_suppriv(agg, "add-resource", fctx->ns[NS_CYRUS], 0, + "Add new resource"); + + agg = add_suppriv(write, "unbind", NULL, 0, + "Remove member from collection"); + add_suppriv(agg, "remove-collection", fctx->ns[NS_CYRUS], 0, + "Remove collection"); + add_suppriv(agg, "remove-resource", fctx->ns[NS_CYRUS], 0, + "Remove resource"); + + agg = add_suppriv(all, "admin", fctx->ns[NS_CYRUS], 0, + "Perform administrative operations"); + add_suppriv(agg, "read-acl", NULL, 1, "Read ACL"); + add_suppriv(agg, "write-acl", NULL, 1, "Write ACL"); + add_suppriv(agg, "unlock", NULL, 1, "Unlock resource"); + + if (tgt_flags == TGT_SCHED_INBOX) { + agg = add_suppriv(all, "schedule-deliver", fctx->ns[NS_CALDAV], 0, + "Deliver scheduling messages"); + add_suppriv(agg, "schedule-deliver-invite", fctx->ns[NS_CALDAV], 0, + "Deliver scheduling messages from Organizers"); + add_suppriv(agg, "schedule-deliver-reply", fctx->ns[NS_CALDAV], 0, + "Deliver scheduling messages from Attendees"); + add_suppriv(agg, "schedule-query-freebusy", fctx->ns[NS_CALDAV], 0, + "Accept free/busy requests"); + } + else if (tgt_flags == TGT_SCHED_OUTBOX) { + agg = add_suppriv(all, "schedule-send", fctx->ns[NS_CALDAV], 0, + "Send scheduling messages"); + add_suppriv(agg, "schedule-send-invite", fctx->ns[NS_CALDAV], 0, + "Send scheduling messages by Organizers"); + add_suppriv(agg, "schedule-send-reply", fctx->ns[NS_CALDAV], 0, + "Send scheduling messages by Attendees"); + add_suppriv(agg, "schedule-send-freebusy", fctx->ns[NS_CALDAV], 0, + "Submit free/busy requests"); + } + + return 0; +} + + +static int add_privs(int rights, unsigned flags, + xmlNodePtr parent, xmlNodePtr root, xmlNsPtr *ns) +{ + xmlNodePtr priv; + + if ((rights & DACL_ALL) == DACL_ALL && + /* DAV:all on CALDAV:schedule-in/outbox MUST include CALDAV:schedule */ + (!(flags & (PRIV_INBOX|PRIV_OUTBOX)) || + (rights & DACL_SCHED) == DACL_SCHED)) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, NULL, BAD_CAST "all", NULL); + } + if ((rights & DACL_READ) == DACL_READ) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, NULL, BAD_CAST "read", NULL); + if (flags & PRIV_IMPLICIT) rights |= DACL_READFB; + } + if ((rights & DACL_READFB) && + /* CALDAV:read-free-busy does not apply to CALDAV:schedule-in/outbox */ + !(flags & (PRIV_INBOX|PRIV_OUTBOX))) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + ensure_ns(ns, NS_CALDAV, root, XML_NS_CALDAV, "C"); + xmlNewChild(priv, ns[NS_CALDAV], BAD_CAST "read-free-busy", NULL); + } + if ((rights & DACL_WRITE) == DACL_WRITE) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, NULL, BAD_CAST "write", NULL); + } + if (rights & DACL_WRITECONT) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, NULL, BAD_CAST "write-content", NULL); + } + if (rights & DACL_WRITEPROPS) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, NULL, BAD_CAST "write-properties", NULL); + } + + if (rights & (DACL_BIND|DACL_UNBIND|DACL_ADMIN)) { + ensure_ns(ns, NS_CYRUS, root, XML_NS_CYRUS, "CY"); + } + + if ((rights & DACL_BIND) == DACL_BIND) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, NULL, BAD_CAST "bind", NULL); + } + if (rights & DACL_MKCOL) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "make-collection", NULL); + } + if (rights & DACL_ADDRSRC) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "add-resource", NULL); + } + if ((rights & DACL_UNBIND) == DACL_UNBIND) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, NULL, BAD_CAST "unbind", NULL); + } + if (rights & DACL_RMCOL) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "remove-collection", NULL); + } + if ((rights & DACL_RMRSRC) == DACL_RMRSRC) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "remove-resource", NULL); + } + if (rights & DACL_ADMIN) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST "admin", NULL); + } + + if (rights & DACL_SCHED) { + ensure_ns(ns, NS_CALDAV, root, XML_NS_CALDAV, "C"); + } + if ((rights & DACL_SCHED) == DACL_SCHED) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + if (flags & PRIV_INBOX) + xmlNewChild(priv, ns[NS_CALDAV], BAD_CAST "schedule-deliver", NULL); + else if (flags & PRIV_OUTBOX) + xmlNewChild(priv, ns[NS_CALDAV], BAD_CAST "schedule-send", NULL); + } + if (rights & DACL_INVITE) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + if (flags & PRIV_INBOX) + xmlNewChild(priv, ns[NS_CALDAV], + BAD_CAST "schedule-deliver-invite", NULL); + else if (flags & PRIV_OUTBOX) + xmlNewChild(priv, ns[NS_CALDAV], + BAD_CAST "schedule-send-invite", NULL); + } + if (rights & DACL_REPLY) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + if (flags & PRIV_INBOX) + xmlNewChild(priv, ns[NS_CALDAV], + BAD_CAST "schedule-deliver-reply", NULL); + else if (flags & PRIV_OUTBOX) + xmlNewChild(priv, ns[NS_CALDAV], + BAD_CAST "schedule-send-reply", NULL); + } + if (rights & DACL_SCHEDFB) { + priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL); + if (flags & PRIV_INBOX) + xmlNewChild(priv, ns[NS_CALDAV], + BAD_CAST "schedule-query-freebusy", NULL); + else if (flags & PRIV_OUTBOX) + xmlNewChild(priv, ns[NS_CALDAV], + BAD_CAST "schedule-send-freebusy", NULL); + } + + return 0; +} + + +/* Callback to fetch DAV:current-user-privilege-set */ +int propfind_curprivset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + int rights; + unsigned flags = 0; + xmlNodePtr set; + + if (!fctx->mailbox) return HTTP_NOT_FOUND; + if (((rights = cyrus_acl_myrights(fctx->authstate, fctx->mailbox->acl)) + & DACL_READ) != DACL_READ) { + return HTTP_UNAUTHORIZED; + } + + /* Add in implicit rights */ + if (fctx->userisadmin) { + rights |= DACL_ADMIN; + } + else if (mboxname_userownsmailbox(httpd_userid, fctx->mailbox->name)) { + rights |= config_implicitrights; + } + + /* Build the rest of the XML response */ + set = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + if (!fctx->req_tgt->resource) { + if (fctx->req_tgt->namespace == URL_NS_CALENDAR) { + flags = PRIV_IMPLICIT; + + if (fctx->req_tgt->collection) { + if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX)) + flags = PRIV_INBOX; + else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX)) + flags = PRIV_OUTBOX; + } + } + + add_privs(rights, flags, set, resp->parent, fctx->ns); + } + + return 0; +} + + +/* Callback to fetch DAV:acl */ +int propfind_acl(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + int rights; + xmlNodePtr acl; + char *aclstr, *userid; + unsigned flags = 0; + + if (!fctx->mailbox) return HTTP_NOT_FOUND; + /* owner has explicit admin rights */ + if (!mboxname_userownsmailbox(httpd_userid, fctx->mailbox->name)) { + if (!((rights = cyrus_acl_myrights(fctx->authstate, fctx->mailbox->acl)) + & DACL_ADMIN)) { + return HTTP_UNAUTHORIZED; + } + } + + if (fctx->req_tgt->namespace == URL_NS_CALENDAR) { + flags = PRIV_IMPLICIT; + + if (fctx->req_tgt->collection) { + if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX)) + flags = PRIV_INBOX; + else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX)) + flags = PRIV_OUTBOX; + } + } + + /* Start the acl XML response */ + acl = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + /* Parse the ACL string (userid/rights pairs) */ + userid = aclstr = xstrdup(fctx->mailbox->acl); + + while (userid) { + char *rightstr, *nextid; + xmlNodePtr ace, node; + int deny = 0; + + rightstr = strchr(userid, '\t'); + if (!rightstr) break; + *rightstr++ = '\0'; + + nextid = strchr(rightstr, '\t'); + if (!nextid) break; + *nextid++ = '\0'; + + /* Check for negative rights */ + /* XXX Does this correspond to DAV:deny? */ + if (*userid == '-') { + deny = 1; + userid++; + } + + rights = cyrus_acl_strtomask(rightstr); + + /* Add ace XML element for this userid/right pair */ + ace = xmlNewChild(acl, NULL, BAD_CAST "ace", NULL); + + /* XXX Need to check for groups. + * Is there any IMAP equivalent to "unauthenticated"? + * Is there any DAV equivalent to "anonymous"? + */ + + node = xmlNewChild(ace, NULL, BAD_CAST "principal", NULL); + if (!strcmp(userid, fctx->userid)) + xmlNewChild(node, NULL, BAD_CAST "self", NULL); + else if (mboxname_userownsmailbox(userid, fctx->mailbox->name)) + xmlNewChild(node, NULL, BAD_CAST "owner", NULL); + else if (!strcmp(userid, "anyone")) + xmlNewChild(node, NULL, BAD_CAST "authenticated", NULL); + /* XXX - well, it's better than a user called 'anonymous' */ + else if (!strcmp(userid, "anonymous")) + xmlNewChild(node, NULL, BAD_CAST "unauthenticated", NULL); + else { + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%s/user/%s/", + namespace_principal.prefix, userid); + xml_add_href(node, NULL, buf_cstring(&fctx->buf)); + } + + node = xmlNewChild(ace, NULL, + BAD_CAST (deny ? "deny" : "grant"), NULL); + add_privs(rights, flags, node, resp->parent, fctx->ns); + + if (fctx->req_tgt->resource) { + node = xmlNewChild(ace, NULL, BAD_CAST "inherited", NULL); + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%.*s", + (int)(fctx->req_tgt->resource - fctx->req_tgt->path), + fctx->req_tgt->path); + xml_add_href(node, NULL, buf_cstring(&fctx->buf)); + } + + userid = nextid; + } + + if (aclstr) free(aclstr); + + return 0; +} + + +/* Callback to fetch DAV:acl-restrictions */ +int propfind_aclrestrict(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + xmlNewChild(node, NULL, BAD_CAST "no-invert", NULL); + + return 0; +} + + +/* Callback to fetch DAV:principal-collection-set */ +EXPORTED int propfind_princolset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%s/", namespace_principal.prefix); + xmlNewChild(node, NULL, BAD_CAST "href", BAD_CAST buf_cstring(&fctx->buf)); + + return 0; +} + + +/* Callback to fetch DAV:quota-available-bytes and DAV:quota-used-bytes */ +EXPORTED int propfind_quota(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + static char prevroot[MAX_MAILBOX_BUFFER]; + char foundroot[MAX_MAILBOX_BUFFER], *qr = NULL; + + if (fctx->mailbox) { + /* Use the quotaroot as specified in mailbox header */ + qr = fctx->mailbox->quotaroot; + } + else { + /* Find the quotaroot governing this hierarchy */ + if (quota_findroot(foundroot, sizeof(foundroot), fctx->req_tgt->mboxname)) { + qr = foundroot; + } + } + + if (!qr) return HTTP_NOT_FOUND; + + if (!fctx->quota.root || + strcmp(fctx->quota.root, qr)) { + /* Different quotaroot - read it */ + + syslog(LOG_DEBUG, "reading quota for '%s'", qr); + + fctx->quota.root = strcpy(prevroot, qr); + + quota_read(&fctx->quota, NULL, 0); + } + + buf_reset(&fctx->buf); + if (!xmlStrcmp(name, BAD_CAST "quota-available-bytes")) { + /* Calculate limit in bytes and subtract usage */ + quota_t limit = fctx->quota.limits[QUOTA_STORAGE] * quota_units[QUOTA_STORAGE]; + + buf_printf(&fctx->buf, QUOTA_T_FMT, limit - fctx->quota.useds[QUOTA_STORAGE]); + } + else if (fctx->record) { + /* Bytes used by resource */ + buf_printf(&fctx->buf, "%u", fctx->record->size); + } + else if (fctx->mailbox) { + /* Bytes used by calendar collection */ + buf_printf(&fctx->buf, QUOTA_T_FMT, + fctx->mailbox->i.quota_mailbox_used); + } + else { + /* Bytes used by entire hierarchy */ + buf_printf(&fctx->buf, QUOTA_T_FMT, fctx->quota.useds[QUOTA_STORAGE]); + } + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); + + return 0; +} + + +/* Callback to fetch DAV:current-user-principal */ +EXPORTED int propfind_curprin(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock) +{ + xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], + &propstat[PROPSTAT_OK], name, ns, NULL, 0); + + if (fctx->userid) { + xmlNodePtr expand = (xmlNodePtr) rock; + + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%s/user/%s/", + namespace_principal.prefix, fctx->userid); + + if (expand) { + /* Return properties for this URL */ + expand_property(expand, fctx, buf_cstring(&fctx->buf), + &prin_parse_path, dav_props, node, 0); + } + else { + /* Return just the URL */ + xml_add_href(node, NULL, buf_cstring(&fctx->buf)); + } + } + else { + xmlNewChild(node, NULL, BAD_CAST "unauthenticated", NULL); + } + + return 0; +} + + +/* Callback to fetch DAV:add-member */ +int propfind_addmember(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlNodePtr node; + int len; + + if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND; + + node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + + len = fctx->req_tgt->resource ? + (size_t) (fctx->req_tgt->resource - fctx->req_tgt->path) : + strlen(fctx->req_tgt->path); + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, "%.*s?action=add-member", len, fctx->req_tgt->path); + + xml_add_href(node, NULL, buf_cstring(&fctx->buf)); + + return 0; +} + + +/* Callback to fetch DAV:sync-token and CS:getctag */ +int propfind_sync_token(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + if (!fctx->req_tgt->collection || /* until we support sync on cal-home */ + !fctx->mailbox || fctx->record) return HTTP_NOT_FOUND; + + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, SYNC_TOKEN_URL_SCHEME "%u-" MODSEQ_FMT, + fctx->mailbox->i.uidvalidity, + fctx->mailbox->i.highestmodseq); + + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST buf_cstring(&fctx->buf), 0); + + return 0; +} + + +/* Callback to fetch properties from resource header */ +int propfind_fromhdr(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock) +{ + const char *hdrname = (const char *) rock; + int r = HTTP_NOT_FOUND; + + if (fctx->record && + (mailbox_cached_header(hdrname) != BIT32_MAX) && + !mailbox_cacherecord(fctx->mailbox, fctx->record)) { + unsigned size; + struct protstream *stream; + hdrcache_t hdrs = NULL; + const char **hdr; + + size = cacheitem_size(fctx->record, CACHE_HEADERS); + stream = prot_readmap(cacheitem_base(fctx->record, + CACHE_HEADERS), size); + hdrs = spool_new_hdrcache(); + spool_fill_hdrcache(stream, NULL, hdrs, NULL); + prot_free(stream); + + if ((hdr = spool_getheader(hdrs, (const char *) hdrname))) { + xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, BAD_CAST hdr[0], 0); + r = 0; + } + + spool_free_hdrcache(hdrs); + } + + return r; +} + +static struct flaggedresources { + const char *name; + int flag; +} fres[] = { + { "answered", FLAG_ANSWERED }, + { "flagged", FLAG_FLAGGED }, + { "seen", FLAG_SEEN }, + { NULL, 0 } /* last is always NULL */ +}; + +/* Callback to write a property to annotation DB */ +static int proppatch_toresource(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + xmlChar *freeme = NULL; + annotate_state_t *astate = NULL; + struct buf value = BUF_INITIALIZER; + const char *userid = NULL; + int r = 1; /* default to error */ + + /* flags only store "exists" */ + + if (!strcmp((const char *)prop->ns->href, XML_NS_CYRUS "sysflag/")) { + struct flaggedresources *frp; + int isset; + for (frp = fres; frp->name; frp++) { + if (strcasecmp((const char *)prop->name, frp->name)) continue; + r = 0; /* ok to do nothing */ + isset = pctx->record->system_flags & frp->flag; + if (set) { + if (isset) goto done; + pctx->record->system_flags |= frp->flag; + } + else { + if (!isset) goto done; + pctx->record->system_flags &= ~frp->flag; + } + r = mailbox_rewrite_index_record(pctx->mailbox, pctx->record); + goto done; + } + goto done; + } + + if (!strcmp((const char *)prop->ns->href, XML_NS_CYRUS "userflag/")) { + int userflag = 0; + int isset; + r = mailbox_user_flag(pctx->mailbox, (const char *)prop->name, &userflag, 1); + if (r) goto done; + isset = pctx->record->user_flags[userflag/32] & (1<<userflag%31); + if (set) { + if (isset) goto done; + pctx->record->user_flags[userflag/32] |= (1<<userflag%31); + } + else { + if (!isset) goto done; + pctx->record->user_flags[userflag/32] &= ~(1<<userflag%31); + } + r = mailbox_rewrite_index_record(pctx->mailbox, pctx->record); + goto done; + } + + /* otherwise it's a database annotation */ + + buf_reset(&pctx->buf); + buf_printf(&pctx->buf, ANNOT_NS "<%s>%s", + (const char *) prop->ns->href, prop->name); + + if (set) { + freeme = xmlNodeGetContent(prop); + buf_init_ro_cstr(&value, (const char *)freeme); + } + + if (!mboxname_userownsmailbox(pctx->mailbox->name, httpd_userid)) + userid = httpd_userid; + + r = mailbox_get_annotate_state(pctx->mailbox, pctx->record->uid, &astate); + if (!r) r = annotate_state_write(astate, buf_cstring(&pctx->buf), userid, &value); + + done: + + if (!r) { + xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + prop->name, prop->ns, NULL, 0); + } + else { + xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV], + &propstat[PROPSTAT_ERROR], prop->name, prop->ns, NULL, 0); + } + + if (freeme) xmlFree(freeme); + + return 0; +} + + +/* Callback to read a property from annotation DB */ +static int propfind_fromresource(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + struct buf attrib = BUF_INITIALIZER; + xmlNodePtr node; + int r = 0; /* default no error */ + + if (!strcmp((const char *)ns->href, XML_NS_CYRUS "sysflag/")) { + struct flaggedresources *frp; + int isset; + for (frp = fres; frp->name; frp++) { + if (strcasecmp((const char *)name, frp->name)) continue; + isset = fctx->record->system_flags & frp->flag; + if (isset) + buf_setcstr(&attrib, "1"); + goto done; + } + goto done; + } + + if (!strcmp((const char *)ns->href, XML_NS_CYRUS "userflag/")) { + int userflag = 0; + int isset; + r = mailbox_user_flag(fctx->mailbox, (const char *)name, &userflag, 0); + if (r) goto done; + isset = fctx->record->user_flags[userflag/32] & (1<<userflag%31); + if (isset) + buf_setcstr(&attrib, "1"); + goto done; + } + + /* otherwise it's a DB annotation */ + + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, ANNOT_NS "<%s>%s", + (const char *) ns->href, name); + + r = annotatemore_msg_lookupmask(fctx->mailbox->name, fctx->record->uid, + buf_cstring(&fctx->buf), httpd_userid, &attrib); + +done: + if (r) return HTTP_SERVER_ERROR; + if (!buf_len(&attrib)) return HTTP_NOT_FOUND; + + node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + xmlAddChild(node, xmlNewCDataBlock(fctx->root->doc, + BAD_CAST buf_cstring(&attrib), buf_len(&attrib))); + + return 0; +} + + +/* Callback to read a property from annotation DB */ +int propfind_fromdb(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + xmlNodePtr resp __attribute__((unused)), + struct propstat propstat[], + void *rock __attribute__((unused))) +{ + struct buf attrib = BUF_INITIALIZER; + xmlNodePtr node; + int r = 0; + + if (fctx->req_tgt->resource) + return propfind_fromresource(name, ns, fctx, NULL, propstat, NULL); + + buf_reset(&fctx->buf); + buf_printf(&fctx->buf, ANNOT_NS "<%s>%s", + (const char *) ns->href, name); + + if (fctx->mailbox && !fctx->record && + !(r = annotatemore_lookupmask(fctx->mailbox->name, buf_cstring(&fctx->buf), + httpd_userid, &attrib))) { + if (!buf_len(&attrib) && + !xmlStrcmp(name, BAD_CAST "displayname")) { + /* Special case empty displayname -- use last segment of path */ + buf_setcstr(&attrib, strrchr(fctx->mailbox->name, '.') + 1); + } + } + + if (r) return HTTP_SERVER_ERROR; + if (!buf_len(&attrib)) return HTTP_NOT_FOUND; + + node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + name, ns, NULL, 0); + xmlAddChild(node, xmlNewCDataBlock(fctx->root->doc, + BAD_CAST buf_cstring(&attrib), buf_len(&attrib))); + + return 0; +} + +/* Callback to write a property to annotation DB */ +int proppatch_todb(xmlNodePtr prop, unsigned set, + struct proppatch_ctx *pctx, + struct propstat propstat[], + void *rock) +{ + xmlChar *freeme = NULL; + annotate_state_t *astate = NULL; + struct buf value = BUF_INITIALIZER; + const char *userid = NULL; + int r; + + if (pctx->req_tgt->resource) + return proppatch_toresource(prop, set, pctx, propstat, NULL); + + buf_reset(&pctx->buf); + buf_printf(&pctx->buf, ANNOT_NS "<%s>%s", + (const char *) prop->ns->href, prop->name); + + if (set) { + if (rock) { + buf_init_ro_cstr(&value, (const char *)rock); + } + else { + freeme = xmlNodeGetContent(prop); + buf_init_ro_cstr(&value, (const char *)freeme); + } + } + + if (!mboxname_userownsmailbox(pctx->mailbox->name, httpd_userid)) + userid = httpd_userid; + + r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate); + if (!r) r = annotate_state_write(astate, buf_cstring(&pctx->buf), userid, &value); + + if (!r) { + xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK], + prop->name, prop->ns, NULL, 0); + } + else { + xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV], + &propstat[PROPSTAT_ERROR], prop->name, prop->ns, NULL, 0); + } + + if (freeme) xmlFree(freeme); + + return 0; +} + + +/* annotemore_findall callback for adding dead properties (allprop/propname) */ +static int allprop_cb(const char *mailbox __attribute__((unused)), + uint32_t uid __attribute__((unused)), + const char *entry, + const char *userid, const struct buf *attrib, + void *rock) +{ + struct allprop_rock *arock = (struct allprop_rock *) rock; + const struct prop_entry *pentry; + char *href, *name; + xmlNsPtr ns; + xmlNodePtr node; + + /* Make sure its a shared entry or the user's private one */ + if (*userid && strcmp(userid, arock->fctx->userid)) return 0; + + /* Split entry into namespace href and name ( <href>name ) */ + buf_setcstr(&arock->fctx->buf, entry + strlen(ANNOT_NS) + 1); + href = (char *) buf_cstring(&arock->fctx->buf); + if ((name = strchr(href, '>'))) *name++ = '\0'; + else if ((name = strchr(href, ':'))) *name++ = '\0'; + + /* Look for a match against live properties */ + for (pentry = arock->fctx->lprops; + pentry->name && + (strcmp(name, pentry->name) || + strcmp(href, known_namespaces[pentry->ns].href)); + pentry++); + + if (pentry->name && + (arock->fctx->mode == PROPFIND_ALL /* Skip all live properties */ + || (pentry->flags & PROP_ALLPROP))) /* Skip those already included */ + return 0; + + /* Look for an instance of this namespace in our response */ + ns = hash_lookup(href, arock->fctx->ns_table); + if (!ns) { + char prefix[5]; + snprintf(prefix, sizeof(prefix), "X%u", arock->fctx->prefix_count++); + ns = xmlNewNs(arock->fctx->root, BAD_CAST href, BAD_CAST prefix); + hash_insert(href, ns, arock->fctx->ns_table); + } + + /* Add the dead property to the response */ + node = xml_add_prop(HTTP_OK, arock->fctx->ns[NS_DAV], + &arock->propstat[PROPSTAT_OK], + BAD_CAST name, ns, NULL, 0); + + if (arock->fctx->mode == PROPFIND_ALL) { + xmlAddChild(node, xmlNewCDataBlock(arock->fctx->root->doc, + BAD_CAST attrib->s, attrib->len)); + } + + return 0; +} + + +static int prescreen_prop(const struct prop_entry *entry, + xmlNodePtr prop, + struct propfind_ctx *fctx) +{ + unsigned allowed = 1; + + if (fctx->req_tgt->resource && !(entry->flags & PROP_RESOURCE)) allowed = 0; + else if (entry->flags & PROP_PRESCREEN) { + void *rock = (entry->flags & PROP_NEEDPROP) ? prop : entry->rock; + + allowed = !entry->get(prop->name, NULL, fctx, NULL, NULL, rock); + } + + return allowed; +} + + +/* Parse the requested properties and create a linked list of fetch callbacks. + * The list gets reused for each href if Depth > 0 + */ +static int preload_proplist(xmlNodePtr proplist, struct propfind_ctx *fctx) +{ + int ret = 0; + xmlNodePtr prop; + const struct prop_entry *entry; + struct propfind_entry_list *tail = NULL; + + switch (fctx->mode) { + case PROPFIND_ALL: + case PROPFIND_NAME: + /* Add live properties for allprop/propname */ + for (entry = fctx->lprops; entry->name; entry++) { + if (entry->flags & PROP_ALLPROP) { + /* Pre-screen request based on prop flags */ + int allowed = prescreen_prop(entry, NULL, fctx); + + if (allowed || fctx->mode == PROP_ALLPROP) { + struct propfind_entry_list *nentry = + xzmalloc(sizeof(struct propfind_entry_list)); + + ensure_ns(fctx->ns, entry->ns, fctx->root, + known_namespaces[entry->ns].href, + known_namespaces[entry->ns].prefix); + + nentry->name = BAD_CAST entry->name; + nentry->ns = fctx->ns[entry->ns]; + if (allowed) { + nentry->flags = entry->flags; + nentry->get = entry->get; + nentry->rock = entry->rock; + } + + /* Append new entry to linked list */ + if (tail) { + tail->next = nentry; + tail = nentry; + } + else tail = fctx->elist = nentry; + } + } + } + /* Fall through and build hash table of namespaces */ + + case PROPFIND_EXPAND: + /* Add all namespaces attached to the response to our hash table */ + if (!fctx->ns_table->size) { + xmlNsPtr nsDef; + + construct_hash_table(fctx->ns_table, 10, 1); + + for (nsDef = fctx->root->nsDef; nsDef; nsDef = nsDef->next) { + hash_insert((const char *) nsDef->href, nsDef, fctx->ns_table); + } + } + } + + /* Iterate through requested properties */ + for (prop = proplist; !*fctx->ret && prop; prop = prop->next) { + if (prop->type == XML_ELEMENT_NODE) { + struct propfind_entry_list *nentry; + xmlChar *name; + xmlNsPtr ns = prop->ns; + + if (fctx->mode == PROPFIND_EXPAND) { + /* Get name/namespace from <property> */ + xmlChar *namespace; + + name = xmlGetProp(prop, BAD_CAST "name"); + namespace = xmlGetProp(prop, BAD_CAST "namespace"); + + if (namespace) { + const char *ns_href = (const char *) namespace; + unsigned i; + + /* Look for this namespace in our known array */ + for (i = 0; i < NUM_NAMESPACE; i++) { + if (!strcmp(ns_href, known_namespaces[i].href)) { + ensure_ns(fctx->ns, i, fctx->root, + known_namespaces[i].href, + known_namespaces[i].prefix); + ns = fctx->ns[i]; + break; + } + } + + if (i >= NUM_NAMESPACE) { + /* Look for this namespace in hash table */ + ns = hash_lookup(ns_href, fctx->ns_table); + if (!ns) { + char prefix[6]; + snprintf(prefix, sizeof(prefix), + "X%X", strhash(ns_href) & 0xffff); + ns = xmlNewNs(fctx->root, + BAD_CAST ns_href, BAD_CAST prefix); + hash_insert(ns_href, ns, fctx->ns_table); + } + } + + xmlFree(namespace); + } + } + else { + /* node IS the property */ + name = xmlStrdup(prop->name); + } + + /* Look for a match against our known properties */ + for (entry = fctx->lprops; + entry->name && + (strcmp((const char *) prop->name, entry->name) || + strcmp((const char *) prop->ns->href, + known_namespaces[entry->ns].href)); + entry++); + + /* Skip properties already included by allprop */ + if (fctx->mode == PROPFIND_ALL && (entry->flags & PROP_ALLPROP)) + continue; + + nentry = xzmalloc(sizeof(struct propfind_entry_list)); + nentry->name = name; + nentry->ns = ns; + if (entry->name) { + /* Found a match - Pre-screen request based on prop flags */ + if (prescreen_prop(entry, prop, fctx)) { + nentry->flags = entry->flags; + nentry->get = entry->get; + if ((entry->flags & PROP_NEEDPROP) || + ((entry->flags & PROP_EXPAND) && + fctx->mode == PROPFIND_EXPAND)) + nentry->rock = prop; + else + nentry->rock = entry->rock; + } + ret = *fctx->ret; + } + else { + /* No match, treat as a dead property. Need to look for both collections + * resources */ + nentry->flags = PROP_COLLECTION | PROP_RESOURCE; + nentry->get = propfind_fromdb; + } + + /* Append new entry to linked list */ + if (tail) { + tail->next = nentry; + tail = nentry; + } + else tail = fctx->elist = nentry; + } + } + + return ret; +} + + +/* Execute the given property patch instructions */ +static int do_proppatch(struct proppatch_ctx *pctx, xmlNodePtr instr) +{ + struct propstat propstat[NUM_PROPSTAT]; + int i; + + memset(propstat, 0, NUM_PROPSTAT * sizeof(struct propstat)); + + /* Iterate through propertyupdate children */ + for (; instr; instr = instr->next) { + if (instr->type == XML_ELEMENT_NODE) { + xmlNodePtr prop; + unsigned set = 0; + + if (!xmlStrcmp(instr->name, BAD_CAST "set")) set = 1; + else if ((pctx->meth == METH_PROPPATCH) && + !xmlStrcmp(instr->name, BAD_CAST "remove")) set = 0; + else { + syslog(LOG_INFO, "Unknown PROPPATCH instruction"); + pctx->err->desc = "Unknown PROPPATCH instruction"; + return HTTP_BAD_REQUEST; + } + + /* Find child element */ + for (prop = instr->children; + prop && prop->type != XML_ELEMENT_NODE; prop = prop->next); + if (!prop || xmlStrcmp(prop->name, BAD_CAST "prop")) { + pctx->err->desc = "Missing prop element"; + return HTTP_BAD_REQUEST; + } + + /* Iterate through requested properties */ + for (prop = prop->children; prop; prop = prop->next) { + if (prop->type == XML_ELEMENT_NODE) { + const struct prop_entry *entry; + + /* Look for a match against our known properties */ + for (entry = pctx->lprops; + entry->name && + (strcmp((const char *) prop->name, entry->name) || + strcmp((const char *) prop->ns->href, + known_namespaces[entry->ns].href)); + entry++); + + if (entry->name) { + if (!entry->put) { + /* Protected property */ + xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], + &propstat[PROPSTAT_FORBID], + prop->name, prop->ns, NULL, + DAV_PROT_PROP); + *pctx->ret = HTTP_FORBIDDEN; + } + else { + /* Write "live" property */ + entry->put(prop, set, pctx, propstat, entry->rock); + } + } + else { + /* Write "dead" property */ + proppatch_todb(prop, set, pctx, propstat, NULL); + } + } + } + } + } + + /* One or more of the properties failed */ + if (*pctx->ret && propstat[PROPSTAT_OK].root) { + /* 200 status must become 424 */ + propstat[PROPSTAT_FAILEDDEP].root = propstat[PROPSTAT_OK].root; + propstat[PROPSTAT_FAILEDDEP].status = HTTP_FAILED_DEP; + propstat[PROPSTAT_OK].root = NULL; + } + + /* Add status and optional error to the propstat elements + and then add them to the response element */ + for (i = 0; i < NUM_PROPSTAT; i++) { + struct propstat *stat = &propstat[i]; + + if (stat->root) { + xmlNewChild(stat->root, NULL, BAD_CAST "status", + BAD_CAST http_statusline(stat->status)); + if (stat->precond) { + struct error_t error = { NULL, stat->precond, NULL, 0 }; + xml_add_error(stat->root, &error, pctx->ns); + } + + xmlAddChild(pctx->root, stat->root); + } + } + + return 0; +} + + +/* Parse an XML body into a tree */ +int parse_xml_body(struct transaction_t *txn, xmlNodePtr *root) +{ + const char **hdr; + xmlParserCtxtPtr ctxt; + xmlDocPtr doc = NULL; + int r = 0; + + *root = NULL; + + /* Read body */ + txn->req_body.flags |= BODY_DECODE; + r = http_read_body(httpd_in, httpd_out, + txn->req_hdrs, &txn->req_body, &txn->error.desc); + if (r) { + txn->flags.conn = CONN_CLOSE; + return r; + } + + if (!buf_len(&txn->req_body.payload)) return 0; + + /* Check Content-Type */ + if (!(hdr = spool_getheader(txn->req_hdrs, "Content-Type")) || + (!is_mediatype("text/xml", hdr[0]) && + !is_mediatype("application/xml", hdr[0]))) { + txn->error.desc = "This method requires an XML body\r\n"; + return HTTP_BAD_MEDIATYPE; + } + + /* Parse the XML request */ + ctxt = xmlNewParserCtxt(); + if (ctxt) { + doc = xmlCtxtReadMemory(ctxt, buf_cstring(&txn->req_body.payload), + buf_len(&txn->req_body.payload), NULL, NULL, + XML_PARSE_NOWARNING); + xmlFreeParserCtxt(ctxt); + } + if (!doc) { + txn->error.desc = "Unable to parse XML body\r\n"; + return HTTP_BAD_REQUEST; + } + + /* Get the root element of the XML request */ + if (!(*root = xmlDocGetRootElement(doc))) { + txn->error.desc = "Missing root element in request\r\n"; + return HTTP_BAD_REQUEST; + } + + return 0; +} + + +/* Perform an ACL request + * + * preconditions: + * DAV:no-ace-conflict + * DAV:no-protected-ace-conflict + * DAV:no-inherited-ace-conflict + * DAV:limited-number-of-aces + * DAV:deny-before-grant + * DAV:grant-only + * DAV:no-invert + * DAV:no-abstract + * DAV:not-supported-privilege + * DAV:missing-required-principal + * DAV:recognized-principal + * DAV:allowed-principal + */ +int meth_acl(struct transaction_t *txn, void *params) +{ + struct meth_params *aparams = (struct meth_params *) params; + int ret = 0, r, rights; + xmlDocPtr indoc = NULL; + xmlNodePtr root, ace; + struct mailbox *mailbox = NULL; + mbentry_t *mbentry = NULL; + struct buf acl = BUF_INITIALIZER; + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Parse the path */ + if ((r = aparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) return r; + + /* Make sure method is allowed (only allowed on collections) */ + if (!(txn->req_tgt.allow & ALLOW_WRITECOL)) { + txn->error.desc = "ACLs can only be set on collections\r\n"; + syslog(LOG_DEBUG, "Tried to set ACL on non-collection"); + return HTTP_NOT_ALLOWED; + } + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + if (!mboxname_userownsmailbox(httpd_userid, txn->req_tgt.mboxname)) { + /* Check ACL for current user */ + rights = mbentry ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if (!(rights & DACL_ADMIN)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_ADMIN; + return HTTP_NO_PRIVS; + } + } + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + mboxlist_entry_free(&mbentry); + + /* Local Mailbox */ + + /* Open mailbox for writing */ + r = mailbox_open_iwl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Parse the ACL body */ + ret = parse_xml_body(txn, &root); + if (!ret && !root) { + txn->error.desc = "Missing request body\r\n"; + ret = HTTP_BAD_REQUEST; + } + if (ret) goto done; + + indoc = root->doc; + + /* Make sure its an DAV:acl element */ + if (xmlStrcmp(root->name, BAD_CAST "acl")) { + txn->error.desc = "Missing acl element in ACL request\r\n"; + ret = HTTP_BAD_REQUEST; + goto done; + } + + /* Parse the DAV:ace elements */ + for (ace = root->children; ace; ace = ace->next) { + if (ace->type == XML_ELEMENT_NODE) { + xmlNodePtr child = NULL, prin = NULL, privs = NULL; + const char *userid = NULL; + int deny = 0, rights = 0; + char rightstr[100]; + struct request_target_t tgt; + + for (child = ace->children; child; child = child->next) { + if (child->type == XML_ELEMENT_NODE) { + if (!xmlStrcmp(child->name, BAD_CAST "principal")) { + if (prin) { + txn->error.desc = "Multiple principals in ACE\r\n"; + ret = HTTP_BAD_REQUEST; + goto done; + } + + for (prin = child->children; prin && + prin->type != XML_ELEMENT_NODE; prin = prin->next); + } + else if (!xmlStrcmp(child->name, BAD_CAST "grant")) { + if (privs) { + txn->error.desc = "Multiple grant|deny in ACE\r\n"; + ret = HTTP_BAD_REQUEST; + goto done; + } + + for (privs = child->children; privs && + privs->type != XML_ELEMENT_NODE; privs = privs->next); + } + else if (!xmlStrcmp(child->name, BAD_CAST "deny")) { + if (privs) { + txn->error.desc = "Multiple grant|deny in ACE\r\n"; + ret = HTTP_BAD_REQUEST; + goto done; + } + + for (privs = child->children; privs && + privs->type != XML_ELEMENT_NODE; privs = privs->next); + deny = 1; + } + else if (!xmlStrcmp(child->name, BAD_CAST "invert")) { + /* DAV:no-invert */ + txn->error.precond = DAV_NO_INVERT; + ret = HTTP_FORBIDDEN; + goto done; + } + else { + txn->error.desc = "Unknown element in ACE\r\n"; + ret = HTTP_BAD_REQUEST; + goto done; + } + } + } + + if (!xmlStrcmp(prin->name, BAD_CAST "self")) { + userid = proxy_userid; + } + else if (!xmlStrcmp(prin->name, BAD_CAST "owner")) { + userid = mboxname_to_userid(mailbox->name); + } + else if (!xmlStrcmp(prin->name, BAD_CAST "authenticated")) { + userid = "anyone"; + } + else if (!xmlStrcmp(prin->name, BAD_CAST "href")) { + xmlChar *href = xmlNodeGetContent(prin); + xmlURIPtr uri; + const char *errstr = NULL; + size_t plen = strlen(namespace_principal.prefix); + + uri = parse_uri(METH_UNKNOWN, (const char *) href, 1, &errstr); + if (uri && + !strncmp(namespace_principal.prefix, uri->path, plen) && + uri->path[plen] == '/') { + memset(&tgt, 0, sizeof(struct request_target_t)); + tgt.namespace = URL_NS_PRINCIPAL; + /* XXX: there is no doubt that this leaks memory */ + r = prin_parse_path(uri->path, &tgt, &errstr); + if (!r && tgt.user) userid = tgt.user; + } + if (uri) xmlFreeURI(uri); + xmlFree(href); + } + + if (!userid) { + /* DAV:recognized-principal */ + txn->error.precond = DAV_RECOG_PRINC; + ret = HTTP_FORBIDDEN; + goto done; + } + + for (; privs; privs = privs->next) { + if (privs->type == XML_ELEMENT_NODE) { + xmlNodePtr priv = privs->children; + for (; priv->type != XML_ELEMENT_NODE; priv = priv->next); + + if (aparams->acl_ext && + aparams->acl_ext(txn, priv, &rights)) { + /* Extension (CalDAV) privileges */ + if (txn->error.precond) { + ret = HTTP_FORBIDDEN; + goto done; + } + } + else if (!xmlStrcmp(priv->ns->href, + BAD_CAST XML_NS_DAV)) { + /* WebDAV privileges */ + if (!xmlStrcmp(priv->name, + BAD_CAST "all")) { + if (deny) + rights |= ACL_FULL; /* wipe EVERYTHING */ + else + rights |= DACL_ALL; + } + else if (!xmlStrcmp(priv->name, + BAD_CAST "read")) + rights |= DACL_READ; + else if (!xmlStrcmp(priv->name, + BAD_CAST "write")) + rights |= DACL_WRITE; + else if (!xmlStrcmp(priv->name, + BAD_CAST "write-content")) + rights |= DACL_WRITECONT; + else if (!xmlStrcmp(priv->name, + BAD_CAST "write-properties")) + rights |= DACL_WRITEPROPS; + else if (!xmlStrcmp(priv->name, + BAD_CAST "bind")) + rights |= DACL_BIND; + else if (!xmlStrcmp(priv->name, + BAD_CAST "unbind")) + rights |= DACL_UNBIND; + else if (!xmlStrcmp(priv->name, + BAD_CAST "read-current-user-privilege-set") + || !xmlStrcmp(priv->name, + BAD_CAST "read-acl") + || !xmlStrcmp(priv->name, + BAD_CAST "write-acl") + || !xmlStrcmp(priv->name, + BAD_CAST "unlock")) { + /* DAV:no-abstract */ + txn->error.precond = DAV_NO_ABSTRACT; + ret = HTTP_FORBIDDEN; + goto done; + } + else { + /* DAV:not-supported-privilege */ + txn->error.precond = DAV_SUPP_PRIV; + ret = HTTP_FORBIDDEN; + goto done; + } + } + else if (!xmlStrcmp(priv->ns->href, + BAD_CAST XML_NS_CYRUS)) { + /* Cyrus-specific privileges */ + if (!xmlStrcmp(priv->name, + BAD_CAST "make-collection")) + rights |= DACL_MKCOL; + else if (!xmlStrcmp(priv->name, + BAD_CAST "remove-collection")) + rights |= DACL_RMCOL; + else if (!xmlStrcmp(priv->name, + BAD_CAST "add-resource")) + rights |= DACL_ADDRSRC; + else if (!xmlStrcmp(priv->name, + BAD_CAST "remove-resource")) + rights |= DACL_RMRSRC; + else if (!xmlStrcmp(priv->name, + BAD_CAST "admin")) + rights |= DACL_ADMIN; + else { + /* DAV:not-supported-privilege */ + txn->error.precond = DAV_SUPP_PRIV; + ret = HTTP_FORBIDDEN; + goto done; + } + } + else { + /* DAV:not-supported-privilege */ + txn->error.precond = DAV_SUPP_PRIV; + ret = HTTP_FORBIDDEN; + goto done; + } + } + } + + cyrus_acl_masktostr(rights, rightstr); + buf_printf(&acl, "%s%s\t%s\t", + deny ? "-" : "", userid, rightstr); + } + } + + if ((r = mboxlist_sync_setacls(txn->req_tgt.mboxname, buf_cstring(&acl)))) { + syslog(LOG_ERR, "mboxlist_sync_setacls(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + mailbox_set_acl(mailbox, buf_cstring(&acl), 0); + + response_header(HTTP_OK, txn); + + done: + buf_free(&acl); + if (indoc) xmlFreeDoc(indoc); + mailbox_close(&mailbox); + + return ret; +} + + +/* Perform a COPY/MOVE request + * + * preconditions: + * *DAV:need-privileges + */ +int meth_copy(struct transaction_t *txn, void *params) +{ + struct meth_params *cparams = (struct meth_params *) params; + int ret = HTTP_CREATED, r, precond, rights, overwrite = OVERWRITE_YES; + const char **hdr; + xmlURIPtr dest_uri; + static struct request_target_t dest_tgt; /* Parsed destination URL */ + struct backend *src_be = NULL, *dest_be = NULL; + struct mailbox *src_mbox = NULL, *dest_mbox = NULL; + mbentry_t *mbentry = NULL; + struct dav_data *ddata; + struct index_record src_rec; + const char *etag = NULL; + time_t lastmod = 0; + unsigned flags = 0; + void *src_davdb = NULL, *dest_davdb = NULL; + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Parse the source path */ + if ((r = cparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) return r; + + /* Make sure method is allowed (not allowed on collections yet) */ + if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED; + + /* Check for mandatory Destination header */ + if (!(hdr = spool_getheader(txn->req_hdrs, "Destination"))) { + txn->error.desc = "Missing Destination header\r\n"; + return HTTP_BAD_REQUEST; + } + + /* Parse destination URI */ + if (!(dest_uri = parse_uri(METH_UNKNOWN, hdr[0], 1, &txn->error.desc))) { + txn->error.desc = "Illegal Destination target URI"; + return HTTP_BAD_REQUEST; + } + + /* Make sure source and dest resources are NOT the same */ + if (!strcmp(txn->req_uri->path, dest_uri->path)) { + txn->error.desc = "Source and destination resources are the same\r\n"; + r = HTTP_FORBIDDEN; + } + + /* Parse the destination path */ + if (!r) { + r = cparams->parse_path(dest_uri->path, &dest_tgt, &txn->error.desc); + } + xmlFreeURI(dest_uri); + + if (r) return HTTP_FORBIDDEN; + + /* We don't yet handle COPY/MOVE on collections */ + if (!dest_tgt.resource) return HTTP_NOT_ALLOWED; + + /* Locate the source mailbox */ + if ((r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL))) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Check ACL for current user on source mailbox */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if (((rights & DACL_READ) != DACL_READ) || + ((txn->meth == METH_MOVE) && !(rights & DACL_RMRSRC))) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = + (rights & DACL_READ) != DACL_READ ? DACL_READ : DACL_RMRSRC; + mboxlist_entry_free(&mbentry); + return HTTP_NO_PRIVS; + } + + if (mbentry->server) { + /* Remote source mailbox */ + src_be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!src_be) return HTTP_UNAVAILABLE; + } + + mboxlist_entry_free(&mbentry); + + /* Locate the destination mailbox */ + r = http_mlookup(dest_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + dest_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Check ACL for current user on destination */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if (!(rights & DACL_ADDRSRC) || !(rights & DACL_WRITECONT)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = dest_tgt.path; + txn->error.rights = + !(rights & DACL_ADDRSRC) ? DACL_ADDRSRC : DACL_WRITECONT; + mboxlist_entry_free(&mbentry); + return HTTP_NO_PRIVS; + } + + if (mbentry->server) { + /* Remote destination mailbox */ + dest_be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!dest_be) return HTTP_UNAVAILABLE; + } + + mboxlist_entry_free(&mbentry); + + if (src_be) { + /* Remote source mailbox */ + /* XXX Currently only supports standard Murder */ + + if (!dest_be) return HTTP_NOT_ALLOWED; + + /* Replace cached Destination header with just the absolute path */ + hdr = spool_getheader(txn->req_hdrs, "Destination"); + strcpy((char *) hdr[0], dest_tgt.path); + + if (src_be == dest_be) { + /* Simply send the COPY to the backend */ + return http_pipe_req_resp(src_be, txn); + } + + /* This is the harder case: GET from source and PUT on destination */ + return http_proxy_copy(src_be, dest_be, txn); + } + + /* Local Mailbox */ + + /* Open dest mailbox for reading */ + r = mailbox_open_irl(dest_tgt.mboxname, &dest_mbox); + if (r) { + syslog(LOG_ERR, "mailbox_open_irl(%s) failed: %s", + dest_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Open the DAV DB corresponding to the dest mailbox */ + dest_davdb = cparams->davdb.open_db(dest_mbox); + + /* Find message UID for the dest resource, if exists */ + cparams->davdb.lookup_resource(dest_davdb, dest_tgt.mboxname, + dest_tgt.resource, 0, (void **) &ddata); + /* XXX Check errors */ + + /* Finished our initial read of dest mailbox */ + mailbox_unlock_index(dest_mbox, NULL); + + /* Check any preconditions on destination */ + if ((hdr = spool_getheader(txn->req_hdrs, "Overwrite")) && + !strcmp(hdr[0], "F")) { + + if (ddata->rowid) { + /* Don't overwrite the destination resource */ + ret = HTTP_PRECOND_FAILED; + goto done; + } + overwrite = OVERWRITE_NO; + } + + /* Open source mailbox for reading */ + r = mailbox_open_irl(txn->req_tgt.mboxname, &src_mbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Open the DAV DB corresponding to the src mailbox */ + src_davdb = cparams->davdb.open_db(src_mbox); + + /* Find message UID for the source resource */ + cparams->davdb.lookup_resource(src_davdb, txn->req_tgt.mboxname, + txn->req_tgt.resource, 0, (void **) &ddata); + if (!ddata->rowid) { + ret = HTTP_NOT_FOUND; + goto done; + } + + if (ddata->imap_uid) { + /* Mapped URL - Fetch index record for the resource */ + r = mailbox_find_index_record(src_mbox, ddata->imap_uid, &src_rec); + if (r) { + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + etag = message_guid_encode(&src_rec.guid); + lastmod = src_rec.internaldate; + } + else { + /* Unmapped URL (empty resource) */ + etag = NULL_ETAG; + lastmod = ddata->creationdate; + } + + /* Check any preconditions on source */ + precond = check_precond(txn, (void **) ddata, etag, lastmod); + + switch (precond) { + case HTTP_OK: + break; + + case HTTP_LOCKED: + txn->error.precond = DAV_NEED_LOCK_TOKEN; + txn->error.resource = txn->req_tgt.path; + + default: + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + if (get_preferences(txn) & PREFER_REP) flags |= PREFER_REP; + if ((txn->meth == METH_MOVE) && (dest_mbox == src_mbox)) + flags |= NO_DUP_CHECK; + + r = mailbox_lock_index(dest_mbox, LOCK_EXCLUSIVE); + if (r) { + syslog(LOG_ERR, "relock index(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Parse, validate, and store the resource */ + ret = cparams->copy(txn, src_mbox, &src_rec, dest_mbox, dest_tgt.resource, + dest_davdb, overwrite, flags); + + /* we're done, no need to keep this */ + mailbox_unlock_index(dest_mbox, NULL); + + /* For MOVE, we need to delete the source resource */ + if ((txn->meth == METH_MOVE) && + (ret == HTTP_CREATED || ret == HTTP_NO_CONTENT)) { + /* Lock source mailbox */ + mailbox_lock_index(src_mbox, LOCK_EXCLUSIVE); + + /* Find message UID for the source resource */ + cparams->davdb.lookup_resource(src_davdb, txn->req_tgt.mboxname, + txn->req_tgt.resource, 0, (void **) &ddata); + /* XXX Check errors */ + + /* Fetch index record for the source resource */ + if (ddata->imap_uid && + !mailbox_find_index_record(src_mbox, ddata->imap_uid, &src_rec)) { + + /* Expunge the source message */ + src_rec.system_flags |= FLAG_EXPUNGED; + if ((r = mailbox_rewrite_index_record(src_mbox, &src_rec))) { + syslog(LOG_ERR, "expunging src record (%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + } + } + + done: + if (ret == HTTP_CREATED) { + /* Tell client where to find the new resource */ + txn->location = dest_tgt.path; + } + else { + /* Don't confuse client by providing ETag of Destination resource */ + txn->resp_body.etag = NULL; + } + + if (dest_davdb) cparams->davdb.close_db(dest_davdb); + mailbox_close(&dest_mbox); + if (src_davdb) cparams->davdb.close_db(src_davdb); + mailbox_close(&src_mbox); + + return ret; +} + + +/* Perform a DELETE request */ +int meth_delete(struct transaction_t *txn, void *params) +{ + struct meth_params *dparams = (struct meth_params *) params; + int ret = HTTP_NO_CONTENT, r = 0, precond, rights; + struct mboxevent *mboxevent = NULL; + struct mailbox *mailbox = NULL; + mbentry_t *mbentry = NULL; + struct dav_data *ddata; + struct index_record record; + const char *etag = NULL; + time_t lastmod = 0; + void *davdb = NULL; + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Parse the path */ + r = dparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc); + if (r) return r; + + /* Make sure method is allowed */ + if (!(txn->req_tgt.allow & ALLOW_DELETE)) return HTTP_NOT_ALLOWED; + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Check ACL for current user */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if ((txn->req_tgt.resource && !(rights & DACL_RMRSRC)) || + !(rights & DACL_RMCOL)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + mboxlist_entry_free(&mbentry); + return HTTP_NO_PRIVS; + } + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + mboxlist_entry_free(&mbentry); + + /* Local Mailbox */ + + if (!txn->req_tgt.resource) { + /* DELETE collection */ + + /* Open mailbox for reading */ + r = mailbox_open_irl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + return HTTP_SERVER_ERROR; + } + + /* Open the DAV DB corresponding to the mailbox */ + davdb = dparams->davdb.open_db(mailbox); + + /* Do any special processing */ + if (dparams->delete) dparams->delete(txn, mailbox, NULL, NULL); + + mailbox_close(&mailbox); + + mboxevent = mboxevent_new(EVENT_MAILBOX_DELETE); + + if (mboxlist_delayed_delete_isenabled()) { + r = mboxlist_delayed_deletemailbox(txn->req_tgt.mboxname, + httpd_userisadmin || httpd_userisproxyadmin, + httpd_userid, httpd_authstate, mboxevent, + /*checkack*/1, /*force*/0); + } + else { + r = mboxlist_deletemailbox(txn->req_tgt.mboxname, + httpd_userisadmin || httpd_userisproxyadmin, + httpd_userid, httpd_authstate, mboxevent, + /*checkack*/1, /*localonly*/0, /*force*/0); + } + if (!r) dparams->davdb.delete_mbox(davdb, txn->req_tgt.mboxname, 0); + if (r == IMAP_PERMISSION_DENIED) ret = HTTP_FORBIDDEN; + else if (r == IMAP_MAILBOX_NONEXISTENT) ret = HTTP_NOT_FOUND; + else if (r) ret = HTTP_SERVER_ERROR; + + dparams->davdb.close_db(davdb); + + goto done; + } + + /* DELETE resource */ + + /* Open mailbox for writing */ + r = mailbox_open_iwl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Open the DAV DB corresponding to the mailbox */ + davdb = dparams->davdb.open_db(mailbox); + + /* Find message UID for the resource, if exists */ + dparams->davdb.lookup_resource(davdb, txn->req_tgt.mboxname, + txn->req_tgt.resource, 0, (void **) &ddata); + if (!ddata->rowid) { + ret = HTTP_NOT_FOUND; + goto done; + } + + memset(&record, 0, sizeof(struct index_record)); + if (ddata->imap_uid) { + /* Mapped URL - Fetch index record for the resource */ + r = mailbox_find_index_record(mailbox, ddata->imap_uid, &record); + if (r) { + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + etag = message_guid_encode(&record.guid); + lastmod = record.internaldate; + } + else { + /* Unmapped URL (empty resource) */ + etag = NULL_ETAG; + lastmod = ddata->creationdate; + } + + /* Check any preconditions */ + precond = dparams->check_precond(txn, (void *) ddata, etag, lastmod); + + switch (precond) { + case HTTP_OK: + break; + + case HTTP_LOCKED: + txn->error.precond = DAV_NEED_LOCK_TOKEN; + txn->error.resource = txn->req_tgt.path; + + default: + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + if (record.uid) { + /* Expunge the resource */ + record.system_flags |= FLAG_EXPUNGED; + + mboxevent = mboxevent_new(EVENT_MESSAGE_EXPUNGE); + + r = mailbox_rewrite_index_record(mailbox, &record); + + if (r) { + syslog(LOG_ERR, "expunging record (%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + mboxevent_extract_record(mboxevent, mailbox, &record); + mboxevent_extract_mailbox(mboxevent, mailbox); + mboxevent_set_numunseen(mboxevent, mailbox, -1); + } + + /* Do any special processing */ + if (dparams->delete) dparams->delete(txn, mailbox, &record, ddata); + + done: + if (davdb) dparams->davdb.close_db(davdb); + mailbox_close(&mailbox); + + if (!r) + mboxevent_notify(mboxevent); + mboxevent_free(&mboxevent); + + return ret; +} + + +/* Perform a GET/HEAD request on a DAV resource */ +int meth_get_dav(struct transaction_t *txn, void *params) +{ + struct meth_params *gparams = (struct meth_params *) params; + const char **hdr; + struct mime_type_t *mime; + int ret = 0, r, precond, rights; + const char *data = NULL; + unsigned long datalen, offset; + struct buf msg_buf = BUF_INITIALIZER; + struct resp_body_t *resp_body = &txn->resp_body; + struct mailbox *mailbox = NULL; + mbentry_t *mbentry = NULL; + struct dav_data *ddata; + struct index_record record; + const char *etag = NULL; + time_t lastmod = 0; + void *davdb = NULL; + char *freeme = NULL; + + /* Parse the path */ + ret = gparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc); + if (ret) return ret; + + /* We don't handle GET on a collection (yet) */ + if (!txn->req_tgt.resource) return HTTP_NO_CONTENT; + + /* Check requested MIME type: + 1st entry in gparams->mime_types array MUST be default MIME type */ + if ((hdr = spool_getheader(txn->req_hdrs, "Accept"))) + mime = get_accept_type(hdr, gparams->mime_types); + else mime = gparams->mime_types; + if (!mime) return HTTP_NOT_ACCEPTABLE; + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Check ACL for current user */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if (!(rights & DACL_READ)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_READ; + mboxlist_entry_free(&mbentry); + return HTTP_NO_PRIVS; + } + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + mboxlist_entry_free(&mbentry); + + /* Local Mailbox */ + + /* Open mailbox for reading */ + r = mailbox_open_irl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + goto done; + } + + /* Open the DAV DB corresponding to the mailbox */ + davdb = gparams->davdb.open_db(mailbox); + + /* Find message UID for the resource */ + gparams->davdb.lookup_resource(davdb, txn->req_tgt.mboxname, + txn->req_tgt.resource, 0, (void **) &ddata); + if (!ddata->rowid) { + ret = HTTP_NOT_FOUND; + goto done; + } + + memset(&record, 0, sizeof(struct index_record)); + if (ddata->imap_uid) { + /* Mapped URL - Fetch index record for the resource */ + r = mailbox_find_index_record(mailbox, ddata->imap_uid, &record); + if (r) { + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Resource length doesn't include RFC 5322 header */ + offset = record.header_size; + datalen = record.size - offset; + + txn->flags.ranges = 1; + etag = message_guid_encode(&record.guid); + lastmod = record.internaldate; + } + else { + /* Unmapped URL (empty resource) */ + offset = datalen = 0; + txn->flags.ranges = 0; + etag = NULL_ETAG; + lastmod = ddata->creationdate; + } + + /* Check any preconditions, including range request */ + precond = gparams->check_precond(txn, (void *) ddata, etag, lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_PARTIAL: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, Expires, and Cache-Control */ + resp_body->etag = etag; + resp_body->lastmod = lastmod; + resp_body->maxage = 3600; /* 1 hr */ + txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE; /* don't use stale data */ + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + if (record.uid) { + txn->flags.vary |= VARY_ACCEPT; + resp_body->type = mime->content_type; + + if (txn->meth == METH_GET) { + /* Load message containing the resource */ + r = mailbox_map_record(mailbox, &record, &msg_buf); + if (r) goto done; + + /* iCalendar data in response should not be transformed */ + txn->flags.cc |= CC_NOTRANSFORM; + + data = buf_base(&msg_buf) + offset; + + if (mime != gparams->mime_types) { + /* Not the storage format - convert into requested MIME type */ + void *obj = gparams->mime_types[0].from_string(data); + + data = freeme = mime->to_string(obj); + datalen = strlen(data); + gparams->mime_types[0].free(obj); + } + } + } + + write_body(precond, txn, data, datalen); + + buf_free(&msg_buf); + + done: + if (davdb) gparams->davdb.close_db(davdb); + if (r) { + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + } + free(freeme); + mailbox_close(&mailbox); + + return ret; +} + + +/* Perform a LOCK request + * + * preconditions: + * DAV:need-privileges + * DAV:no-conflicting-lock + * DAV:lock-token-submitted + */ +int meth_lock(struct transaction_t *txn, void *params) +{ + struct meth_params *lparams = (struct meth_params *) params; + int ret = HTTP_OK, r, precond, rights; + struct mailbox *mailbox = NULL; + mbentry_t *mbentry = NULL; + struct dav_data *ddata; + struct index_record oldrecord; + const char *etag; + time_t lastmod; + xmlDocPtr indoc = NULL, outdoc = NULL; + xmlNodePtr root = NULL; + xmlNsPtr ns[NUM_NAMESPACE]; + xmlChar *owner = NULL; + time_t now = time(NULL); + void *davdb = NULL; + + /* XXX We ignore Depth and Timeout header fields */ + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Parse the path */ + if ((r = lparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) return r; + + /* Make sure method is allowed (only allowed on resources) */ + if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED; + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Check ACL for current user */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if (!(rights & DACL_WRITECONT) || !(rights & DACL_ADDRSRC)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = + !(rights & DACL_WRITECONT) ? DACL_WRITECONT : DACL_ADDRSRC; + mboxlist_entry_free(&mbentry); + return HTTP_NO_PRIVS; + } + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + mboxlist_entry_free(&mbentry); + + /* Local Mailbox */ + + /* Open mailbox for reading */ + r = mailbox_open_irl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Open the DAV DB corresponding to the mailbox */ + davdb = lparams->davdb.open_db(mailbox); + + /* Find message UID for the resource, if exists */ + lparams->davdb.lookup_resource(davdb, txn->req_tgt.mboxname, + txn->req_tgt.resource, 1, (void *) &ddata); + + if (ddata->imap_uid) { + /* Locking existing resource */ + + /* Fetch index record for the resource */ + r = mailbox_find_index_record(mailbox, ddata->imap_uid, &oldrecord); + if (r) { + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + etag = message_guid_encode(&oldrecord.guid); + lastmod = oldrecord.internaldate; + } + else if (ddata->rowid) { + /* Unmapped URL (empty resource) */ + etag = NULL_ETAG; + lastmod = ddata->creationdate; + } + else { + /* New resource */ + etag = NULL; + lastmod = 0; + + ddata->creationdate = now; + ddata->mailbox = mailbox->name; + ddata->resource = txn->req_tgt.resource; + } + + /* Check any preconditions */ + precond = lparams->check_precond(txn, ddata, etag, lastmod); + + switch (precond) { + case HTTP_OK: + break; + + case HTTP_LOCKED: + if (strcmp(ddata->lock_ownerid, httpd_userid)) + txn->error.precond = DAV_LOCKED; + else + txn->error.precond = DAV_NEED_LOCK_TOKEN; + txn->error.resource = txn->req_tgt.path; + + default: + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + if (ddata->lock_expire <= now) { + /* Create new lock */ + xmlNodePtr node, sub; + unsigned owner_is_href = 0; + + /* Parse the required body */ + ret = parse_xml_body(txn, &root); + if (!ret && !root) { + txn->error.desc = "Missing request body"; + ret = HTTP_BAD_REQUEST; + } + if (ret) goto done; + + /* Check for correct root element */ + indoc = root->doc; + if (xmlStrcmp(root->name, BAD_CAST "lockinfo")) { + txn->error.desc = "Incorrect root element in XML request\r\n"; + ret = HTTP_BAD_MEDIATYPE; + goto done; + } + + /* Parse elements of lockinfo */ + for (node = root->children; node; node = node->next) { + if (node->type != XML_ELEMENT_NODE) continue; + + if (!xmlStrcmp(node->name, BAD_CAST "lockscope")) { + /* Find child element of lockscope */ + for (sub = node->children; + sub && sub->type != XML_ELEMENT_NODE; sub = sub->next); + /* Make sure its an exclusive element */ + if (!sub || xmlStrcmp(sub->name, BAD_CAST "exclusive")) { + txn->error.desc = "Only exclusive locks are supported"; + ret = HTTP_BAD_REQUEST; + goto done; + } + } + else if (!xmlStrcmp(node->name, BAD_CAST "locktype")) { + /* Find child element of locktype */ + for (sub = node->children; + sub && sub->type != XML_ELEMENT_NODE; sub = sub->next); + /* Make sure its a write element */ + if (!sub || xmlStrcmp(sub->name, BAD_CAST "write")) { + txn->error.desc = "Only write locks are supported"; + ret = HTTP_BAD_REQUEST; + goto done; + } + } + else if (!xmlStrcmp(node->name, BAD_CAST "owner")) { + /* Find child element of owner */ + for (sub = node->children; + sub && sub->type != XML_ELEMENT_NODE; sub = sub->next); + if (!sub) { + owner = xmlNodeGetContent(node); + } + /* Make sure its a href element */ + else if (xmlStrcmp(sub->name, BAD_CAST "href")) { + ret = HTTP_BAD_REQUEST; + goto done; + } + else { + owner_is_href = 1; + owner = xmlNodeGetContent(sub); + } + } + } + + ddata->lock_ownerid = httpd_userid; + if (owner) ddata->lock_owner = (const char *) owner; + + /* Construct lock-token */ + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, XML_NS_CYRUS "lock/%s-%x-%u", + mailbox->uniqueid, strhash(txn->req_tgt.resource), + owner_is_href); + + ddata->lock_token = buf_cstring(&txn->buf); + } + + /* Update lock expiration */ + ddata->lock_expire = now + 300; /* 5 min */ + + /* Start construction of our prop response */ + if (!(root = init_xml_response("prop", NS_DAV, root, ns))) { + ret = HTTP_SERVER_ERROR; + txn->error.desc = "Unable to create XML response\r\n"; + goto done; + } + + outdoc = root->doc; + root = xmlNewChild(root, NULL, BAD_CAST "lockdiscovery", NULL); + xml_add_lockdisc(root, txn->req_tgt.path, (struct dav_data *) ddata); + + lparams->davdb.write_resource(davdb, ddata, 1); + + txn->resp_body.lock = ddata->lock_token; + + if (!ddata->rowid) { + ret = HTTP_CREATED; + + /* Tell client about the new resource */ + txn->resp_body.etag = NULL_ETAG; + + /* Tell client where to find the new resource */ + txn->location = txn->req_tgt.path; + } + else ret = HTTP_OK; + + xml_response(ret, txn, outdoc); + ret = 0; + + done: + if (davdb) lparams->davdb.close_db(davdb); + mailbox_close(&mailbox); + if (outdoc) xmlFreeDoc(outdoc); + if (indoc) xmlFreeDoc(indoc); + if (owner) xmlFree(owner); + + return ret; +} + + +/* Perform a MKCOL/MKCALENDAR request */ +/* + * preconditions: + * DAV:resource-must-be-null + * DAV:need-privileges + * DAV:valid-resourcetype + * CALDAV:calendar-collection-location-ok + * CALDAV:valid-calendar-data (CALDAV:calendar-timezone) + */ +int meth_mkcol(struct transaction_t *txn, void *params) +{ + struct meth_params *mparams = (struct meth_params *) params; + int ret = 0, r = 0; + xmlDocPtr indoc = NULL, outdoc = NULL; + xmlNodePtr root = NULL, instr = NULL; + xmlNsPtr ns[NUM_NAMESPACE]; + char *partition = NULL; + struct proppatch_ctx pctx; + struct mailbox *mailbox = NULL; + + memset(&pctx, 0, sizeof(struct proppatch_ctx)); + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Parse the path */ + if ((r = mparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) { + txn->error.precond = CALDAV_LOCATION_OK; + return HTTP_FORBIDDEN; + } + + /* Make sure method is allowed (only allowed on home-set) */ + if (!(txn->req_tgt.allow & ALLOW_WRITECOL)) { + txn->error.precond = CALDAV_LOCATION_OK; + return HTTP_FORBIDDEN; + } + + /* Check if we are allowed to create the mailbox */ + r = mboxlist_createmailboxcheck(txn->req_tgt.mboxname, 0, NULL, + httpd_userisadmin || httpd_userisproxyadmin, + httpd_userid, httpd_authstate, + NULL, &partition, 0); + switch (r) { + case 0: + break; + + case IMAP_MAILBOX_EXISTS: + txn->error.precond = DAV_RSRC_EXISTS; + ret = HTTP_FORBIDDEN; + goto done; + + case IMAP_PERMISSION_DENIED: + ret = HTTP_NO_PRIVS; + goto done; + + default: + ret = HTTP_SERVER_ERROR; + txn->error.desc = error_message(r); + goto done; + } + + if (!config_partitiondir(partition)) { + /* Invalid partition, assume its a server (remote mailbox) */ + char *server = partition, *p; + struct backend *be; + + /* Trim remote partition */ + p = strchr(server, '!'); + if (p) *p++ = '\0'; + + be = proxy_findserver(server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + + if (!be) ret = HTTP_UNAVAILABLE; + else ret = http_pipe_req_resp(be, txn); + + goto done; + } + + /* Local Mailbox */ + + /* Parse the MKCOL/MKCALENDAR body, if exists */ + ret = parse_xml_body(txn, &root); + if (ret) goto done; + + if (root) { + /* Check for correct root element */ + indoc = root->doc; + + if (txn->meth == METH_MKCOL) + r = xmlStrcmp(root->name, BAD_CAST "mkcol"); + else + r = xmlStrcmp(root->name, BAD_CAST mparams->mkcol.xml_req); + if (r) { + txn->error.desc = "Incorrect root element in XML request\r\n"; + ret = HTTP_BAD_MEDIATYPE; + goto done; + } + + instr = root->children; + } + + /* Create the mailbox */ + r = mboxlist_createmailbox(txn->req_tgt.mboxname, mparams->mkcol.mbtype, + partition, + httpd_userisadmin || httpd_userisproxyadmin, + httpd_userid, httpd_authstate, + /*localonly*/0, /*forceuser*/0, + /*dbonly*/0, /*notify*/0, + &mailbox); + + if (instr && !r) { + /* Start construction of our mkcol/mkcalendar response */ + if (txn->meth == METH_MKCOL) + root = init_xml_response("mkcol-response", NS_DAV, root, ns); + else + root = init_xml_response(mparams->mkcol.xml_resp, + mparams->mkcol.xml_ns, root, ns); + if (!root) { + ret = HTTP_SERVER_ERROR; + txn->error.desc = "Unable to create XML response\r\n"; + goto done; + } + + outdoc = root->doc; + + /* Populate our proppatch context */ + pctx.req_tgt = &txn->req_tgt; + pctx.meth = txn->meth; + pctx.mailbox = mailbox; + pctx.lprops = mparams->lprops; + pctx.root = root; + pctx.ns = ns; + pctx.tid = NULL; + pctx.err = &txn->error; + pctx.ret = &r; + + /* Execute the property patch instructions */ + if (!r) ret = do_proppatch(&pctx, instr); + + if (ret || r) { + /* Something failed. Abort the txn and change the OK status */ + + if (!ret) { + /* Error response MUST be a multistatus */ + xmlNodeSetName(root, BAD_CAST "multistatus"); + xmlSetNs(root, ns[NS_DAV]); + + /* Output the XML response */ + xml_response(HTTP_MULTI_STATUS, txn, outdoc); + ret = 0; + } + + goto done; + } + } + + if (!r) ret = HTTP_CREATED; + else if (r == IMAP_PERMISSION_DENIED) ret = HTTP_NO_PRIVS; + else if (r == IMAP_MAILBOX_EXISTS) { + txn->error.precond = DAV_RSRC_EXISTS; + ret = HTTP_FORBIDDEN; + } + else if (r) { + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + } + + done: + buf_free(&pctx.buf); + mailbox_close(&mailbox); + + if (partition) free(partition); + if (outdoc) xmlFreeDoc(outdoc); + if (indoc) xmlFreeDoc(indoc); + + return ret; +} + + +/* dav_foreach() callback to find props on a resource */ +int propfind_by_resource(void *rock, void *data) +{ + struct propfind_ctx *fctx = (struct propfind_ctx *) rock; + struct dav_data *ddata = (struct dav_data *) data; + struct index_record record; + char *p; + size_t len; + int r, ret = 0; + + /* Append resource name to URL path */ + if (!fctx->req_tgt->resource) { + len = strlen(fctx->req_tgt->path); + p = fctx->req_tgt->path + len; + } + else { + p = fctx->req_tgt->resource; + len = p - fctx->req_tgt->path; + } + + if (p[-1] != '/') { + *p++ = '/'; + len++; + } + strlcpy(p, ddata->resource, MAX_MAILBOX_PATH - len); + fctx->req_tgt->resource = p; + fctx->req_tgt->reslen = strlen(p); + + fctx->data = data; + if (ddata->imap_uid && !fctx->record) { + /* Fetch index record for the resource */ + r = mailbox_find_index_record(fctx->mailbox, ddata->imap_uid, &record); + /* XXX Check errors */ + + fctx->record = r ? NULL : &record; + } + + if (!ddata->imap_uid || !fctx->record) { + /* Add response for missing target */ + ret = xml_add_response(fctx, HTTP_NOT_FOUND, 0); + } + else if (!fctx->filter || fctx->filter(fctx, data)) { + /* Add response for target */ + ret = xml_add_response(fctx, 0, 0); + } + + buf_free(&fctx->msg_buf); + fctx->record = NULL; + fctx->data = NULL; + + return ret; +} + + +/* mboxlist_findall() callback to find props on a collection */ +int propfind_by_collection(char *mboxname, int matchlen, + int maycreate __attribute__((unused)), + void *rock) +{ + struct propfind_ctx *fctx = (struct propfind_ctx *) rock; + mbentry_t *mbentry = NULL; + struct mailbox *mailbox = NULL; + char *p; + size_t len; + int r = 0, rights, root; + + /* If this function is called outside of mboxlist_findall() + with matchlen == 0, this is the root resource of the PROPFIND */ + root = !matchlen; + + /* Check ACL on mailbox for current user */ + if ((r = mboxlist_lookup(mboxname, &mbentry, NULL))) { + syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s", + mboxname, error_message(r)); + fctx->err->desc = error_message(r); + *fctx->ret = HTTP_SERVER_ERROR; + goto done; + } + + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if ((rights & fctx->reqd_privs) != fctx->reqd_privs) goto done; + + /* Open mailbox for reading */ + if ((r = mailbox_open_irl(mboxname, &mailbox))) { + syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s", + mboxname, error_message(r)); + fctx->err->desc = error_message(r); + *fctx->ret = HTTP_SERVER_ERROR; + goto done; + } + + fctx->mailbox = mailbox; + fctx->record = NULL; + + if (!fctx->req_tgt->resource) { + /* Append collection name to URL path */ + if (!fctx->req_tgt->collection) { + len = strlen(fctx->req_tgt->path); + p = fctx->req_tgt->path + len; + } + else { + p = fctx->req_tgt->collection; + len = p - fctx->req_tgt->path; + } + + if (p[-1] != '/') { + *p++ = '/'; + len++; + } + strlcpy(p, strrchr(mboxname, '.') + 1, MAX_MAILBOX_PATH - len); + strlcat(p, "/", MAX_MAILBOX_PATH - len - 1); + fctx->req_tgt->collection = p; + fctx->req_tgt->collen = strlen(p); + + /* If not filtering by calendar resource, and not excluding root, + add response for collection */ + if (!fctx->filter_crit && + (!root || (fctx->depth == 1) || !(fctx->prefer & PREFER_NOROOT)) && + (r = xml_add_response(fctx, 0, 0))) goto done; + } + + if (fctx->depth > 1) { + /* Resource(s) */ + + /* Open the DAV DB corresponding to the mailbox. + * + * Note we open the new one first before closing the old one, so we + * get refcounted retaining of the open database within a single user */ + sqlite3 *newdb = fctx->open_db(mailbox); + if (fctx->davdb) fctx->close_db(fctx->davdb); + fctx->davdb = newdb; + + if (fctx->req_tgt->resource) { + /* Add response for target resource */ + void *data; + + /* Find message UID for the resource */ + fctx->lookup_resource(fctx->davdb, + mboxname, fctx->req_tgt->resource, 0, &data); + /* XXX Check errors */ + + r = fctx->proc_by_resource(rock, data); + } + else { + /* Add responses for all contained resources */ + fctx->foreach_resource(fctx->davdb, mboxname, + fctx->proc_by_resource, rock); + + /* Started with NULL resource, end with NULL resource */ + fctx->req_tgt->resource = NULL; + fctx->req_tgt->reslen = 0; + } + } + + done: + mboxlist_entry_free(&mbentry); + if (mailbox) mailbox_close(&mailbox); + + return r; +} + + +/* Perform a PROPFIND request */ +EXPORTED int meth_propfind(struct transaction_t *txn, void *params) +{ + struct meth_params *fparams = (struct meth_params *) params; + int ret = 0, r; + const char **hdr; + unsigned depth; + xmlDocPtr indoc = NULL, outdoc = NULL; + xmlNodePtr root, cur = NULL, props = NULL; + xmlNsPtr ns[NUM_NAMESPACE]; + struct hash_table ns_table = { 0, NULL, NULL }; + struct propfind_ctx fctx; + struct propfind_entry_list *elist = NULL; + + memset(&fctx, 0, sizeof(struct propfind_ctx)); + + /* Parse the path */ + if (fparams->parse_path && + (r = fparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) return r; + + /* Make sure method is allowed */ + if (!(txn->req_tgt.allow & ALLOW_DAV)) return HTTP_NOT_ALLOWED; + + /* Check Depth */ + hdr = spool_getheader(txn->req_hdrs, "Depth"); + if (!hdr || !strcmp(hdr[0], "infinity")) { + depth = 2; + } + else if (hdr && ((sscanf(hdr[0], "%u", &depth) != 1) || (depth > 1))) { + txn->error.desc = "Illegal Depth value\r\n"; + return HTTP_BAD_REQUEST; + } + + if ((txn->req_tgt.namespace != URL_NS_PRINCIPAL) && txn->req_tgt.user) { + mbentry_t *mbentry = NULL; + int rights; + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Check ACL for current user */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if ((rights & DACL_READ) != DACL_READ) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_READ; + mboxlist_entry_free(&mbentry); + ret = HTTP_NO_PRIVS; + goto done; + } + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + mboxlist_entry_free(&mbentry); + + /* Local Mailbox */ + } + + /* Principal or Local Mailbox */ + + /* Normalize depth so that: + * 0 = home-set collection, 1+ = calendar collection, 2+ = calendar resource + */ + if (txn->req_tgt.collection) depth++; + if (txn->req_tgt.resource) depth++; + + /* Parse the PROPFIND body, if exists */ + ret = parse_xml_body(txn, &root); + if (ret) goto done; + + if (!root) { + /* Empty request */ + fctx.mode = PROPFIND_ALL; + } + else { + indoc = root->doc; + + /* Make sure its a propfind element */ + if (xmlStrcmp(root->name, BAD_CAST "propfind")) { + txn->error.desc = "Missing propfind element in PROFIND request\r\n"; + ret = HTTP_BAD_REQUEST; + goto done; + } + + /* Find child element of propfind */ + for (cur = root->children; + cur && cur->type != XML_ELEMENT_NODE; cur = cur->next); + + /* Add propfind type to our header cache */ + spool_cache_header(xstrdup(":type"), xstrdup((const char *) cur->name), + txn->req_hdrs); + + /* Make sure its a known element */ + if (!cur) { + ret = HTTP_BAD_REQUEST; + goto done; + } + else if (!xmlStrcmp(cur->name, BAD_CAST "allprop")) { + fctx.mode = PROPFIND_ALL; + } + else if (!xmlStrcmp(cur->name, BAD_CAST "propname")) { + fctx.mode = PROPFIND_NAME; + fctx.prefer = PREFER_MIN; /* Don't want 404 (Not Found) */ + } + else if (!xmlStrcmp(cur->name, BAD_CAST "prop")) { + fctx.mode = PROPFIND_PROP; + props = cur->children; + } + else { + ret = HTTP_BAD_REQUEST; + goto done; + } + + /* Check for extra elements */ + for (cur = cur->next; cur; cur = cur->next) { + if (cur->type == XML_ELEMENT_NODE) { + if ((fctx.mode == PROPFIND_ALL) && !props && + /* Check for 'include' element */ + !xmlStrcmp(cur->name, BAD_CAST "include")) { + props = cur->children; + } + else { + ret = HTTP_BAD_REQUEST; + goto done; + } + } + } + } + + /* Start construction of our multistatus response */ + if (!(root = init_xml_response("multistatus", NS_DAV, root, ns))) { + ret = HTTP_SERVER_ERROR; + txn->error.desc = "Unable to create XML response\r\n"; + goto done; + } + + outdoc = root->doc; + + /* Populate our propfind context */ + fctx.req_tgt = &txn->req_tgt; + fctx.depth = depth; + fctx.prefer |= get_preferences(txn); + fctx.req_hdrs = txn->req_hdrs; + fctx.userid = proxy_userid; + fctx.userisadmin = httpd_userisadmin; + fctx.authstate = httpd_authstate; + fctx.mailbox = NULL; + fctx.record = NULL; + fctx.reqd_privs = DACL_READ; + fctx.filter = NULL; + fctx.filter_crit = NULL; + fctx.open_db = fparams->davdb.open_db; + fctx.close_db = fparams->davdb.close_db; + fctx.lookup_resource = fparams->davdb.lookup_resource; + fctx.foreach_resource = fparams->davdb.foreach_resource; + fctx.proc_by_resource = &propfind_by_resource; + fctx.elist = NULL; + fctx.lprops = fparams->lprops; + fctx.root = root; + fctx.ns = ns; + fctx.ns_table = &ns_table; + fctx.err = &txn->error; + fctx.ret = &ret; + fctx.fetcheddata = 0; + + /* Parse the list of properties and build a list of callbacks */ + preload_proplist(props, &fctx); + + if (!txn->req_tgt.collection && + (!depth || !(fctx.prefer & PREFER_NOROOT))) { + /* Add response for principal or home-set collection */ + struct mailbox *mailbox = NULL; + + if (*txn->req_tgt.mboxname) { + /* Open mailbox for reading */ + if ((r = mailbox_open_irl(txn->req_tgt.mboxname, &mailbox))) { + syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + fctx.mailbox = mailbox; + } + + xml_add_response(&fctx, 0, 0); + + mailbox_close(&mailbox); + } + + if (depth > 0) { + /* Calendar collection(s) */ + + if (txn->req_tgt.collection) { + /* Add response for target calendar collection */ + propfind_by_collection(txn->req_tgt.mboxname, 0, 0, &fctx); + } + else { + /* Add responses for all contained calendar collections */ + strlcat(txn->req_tgt.mboxname, ".%", sizeof(txn->req_tgt.mboxname)); + r = mboxlist_findall(NULL, /* internal namespace */ + txn->req_tgt.mboxname, 1, httpd_userid, + httpd_authstate, propfind_by_collection, &fctx); + } + + if (fctx.davdb) fctx.close_db(fctx.davdb); + + ret = *fctx.ret; + } + + /* Output the XML response */ + if (!ret) { + /* iCalendar data in response should not be transformed */ + if (fctx.fetcheddata) txn->flags.cc |= CC_NOTRANSFORM; + + xml_response(HTTP_MULTI_STATUS, txn, outdoc); + } + + done: + /* Free the entry list */ + elist = fctx.elist; + while (elist) { + struct propfind_entry_list *freeme = elist; + elist = elist->next; + xmlFree(freeme->name); + free(freeme); + } + + buf_free(&fctx.buf); + + free_hash_table(&ns_table, NULL); + + if (outdoc) xmlFreeDoc(outdoc); + if (indoc) xmlFreeDoc(indoc); + + return ret; +} + + +/* Perform a PROPPATCH request + * + * preconditions: + * DAV:cannot-modify-protected-property + * CALDAV:valid-calendar-data (CALDAV:calendar-timezone) + */ +int meth_proppatch(struct transaction_t *txn, void *params) +{ + struct meth_params *pparams = (struct meth_params *) params; + int ret = 0, r = 0, rights; + xmlDocPtr indoc = NULL, outdoc = NULL; + xmlNodePtr root, instr, resp; + xmlNsPtr ns[NUM_NAMESPACE]; + struct mailbox *mailbox = NULL; + mbentry_t *mbentry = NULL; + struct proppatch_ctx pctx; + struct index_record record; + void *davdb = NULL; + + memset(&pctx, 0, sizeof(struct proppatch_ctx)); + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Parse the path */ + if ((r = pparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) return r; + + if (!txn->req_tgt.collection) { + txn->error.desc = "PROPPATCH requires a collection"; + return HTTP_NOT_ALLOWED; + } + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Check ACL for current user */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if (!(rights & DACL_WRITEPROPS)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_WRITEPROPS; + mboxlist_entry_free(&mbentry); + return HTTP_NO_PRIVS; + } + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + mboxlist_entry_free(&mbentry); + + /* Local Mailbox */ + + r = mailbox_open_iwl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "IOERROR: failed to open mailbox %s for proppatch", txn->req_tgt.mboxname); + return HTTP_SERVER_ERROR; + } + + /* Parse the PROPPATCH body */ + ret = parse_xml_body(txn, &root); + if (!ret && !root) { + txn->error.desc = "Missing request body\r\n"; + ret = HTTP_BAD_REQUEST; + } + if (ret) goto done; + + indoc = root->doc; + + /* Make sure its a propertyupdate element */ + if (xmlStrcmp(root->name, BAD_CAST "propertyupdate")) { + txn->error.desc = + "Missing propertyupdate element in PROPPATCH request\r\n"; + ret = HTTP_BAD_REQUEST; + goto done; + } + instr = root->children; + + /* Start construction of our multistatus response */ + if (!(root = init_xml_response("multistatus", NS_DAV, root, ns))) { + txn->error.desc = "Unable to create XML response\r\n"; + ret = HTTP_SERVER_ERROR; + goto done; + } + + outdoc = root->doc; + + /* Add a response tree to 'root' for the specified href */ + resp = xmlNewChild(root, NULL, BAD_CAST "response", NULL); + if (!resp) syslog(LOG_ERR, "new child response failed"); + xmlNewChild(resp, NULL, BAD_CAST "href", BAD_CAST txn->req_tgt.path); + + /* Populate our proppatch context */ + pctx.req_tgt = &txn->req_tgt; + pctx.meth = txn->meth; + pctx.mailbox = mailbox; + pctx.record = NULL; + pctx.lprops = pparams->lprops; + pctx.root = resp; + pctx.ns = ns; + pctx.tid = NULL; + pctx.err = &txn->error; + pctx.ret = &r; + + if (txn->req_tgt.resource) { + struct dav_data *ddata; + /* gotta find the resource */ + /* Open the DAV DB corresponding to the mailbox */ + davdb = pparams->davdb.open_db(mailbox); + + /* Find message UID for the resource */ + pparams->davdb.lookup_resource(davdb, txn->req_tgt.mboxname, + txn->req_tgt.resource, 0, (void **) &ddata); + if (!ddata->imap_uid) { + ret = HTTP_NOT_FOUND; + goto done; + } + + memset(&record, 0, sizeof(struct index_record)); + /* Mapped URL - Fetch index record for the resource */ + r = mailbox_find_index_record(mailbox, ddata->imap_uid, &record); + if (r) { + ret = HTTP_NOT_FOUND; + goto done; + } + + pctx.record = &record; + } + + /* Execute the property patch instructions */ + ret = do_proppatch(&pctx, instr); + + /* Output the XML response */ + if (!ret) { + if (get_preferences(txn) & PREFER_MIN) ret = HTTP_OK; + else xml_response(HTTP_MULTI_STATUS, txn, outdoc); + } + + done: + if (davdb) pparams->davdb.close_db(davdb); + mailbox_close(&mailbox); + buf_free(&pctx.buf); + + if (outdoc) xmlFreeDoc(outdoc); + if (indoc) xmlFreeDoc(indoc); + + return ret; +} + + +/* Perform a POST request */ +int meth_post(struct transaction_t *txn, void *params) +{ + struct meth_params *pparams = (struct meth_params *) params; + static unsigned post_count = 0; + struct strlist *action; + int r, ret; + size_t len; + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Parse the path */ + if ((r = pparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) return r; + + /* Make sure method is allowed (only allowed on certain collections) */ + if (!(txn->req_tgt.allow & ALLOW_POST)) return HTTP_NOT_ALLOWED; + + /* Do any special processing */ + if (pparams->post) { + ret = pparams->post(txn); + if (ret != HTTP_CONTINUE) return ret; + } + + action = hash_lookup("action", &txn->req_qparams); + if (!action || action->next || strcmp(action->s, "add-member")) + return HTTP_FORBIDDEN; + + /* POST add-member to regular collection */ + + /* Append a unique resource name to URL path and perform a PUT */ + len = strlen(txn->req_tgt.path); + txn->req_tgt.resource = txn->req_tgt.path + len; + txn->req_tgt.reslen = + snprintf(txn->req_tgt.resource, MAX_MAILBOX_PATH - len, + "%x-%d-%ld-%u.ics", + strhash(txn->req_tgt.path), getpid(), time(0), post_count++); + + /* Tell client where to find the new resource */ + txn->location = txn->req_tgt.path; + + ret = meth_put(txn, params); + + if (ret != HTTP_CREATED) txn->location = NULL; + + return ret; +} + + +/* Perform a PUT request + * + * preconditions: + * *DAV:supported-address-data + */ +int meth_put(struct transaction_t *txn, void *params) +{ + struct meth_params *pparams = (struct meth_params *) params; + int ret, r, precond, rights; + const char **hdr, *etag; + struct mime_type_t *mime = NULL; + struct mailbox *mailbox = NULL; + mbentry_t *mbentry = NULL; + struct dav_data *ddata; + struct index_record oldrecord; + quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_INITIALIZER; + time_t lastmod; + unsigned flags = 0; + void *davdb = NULL; + + if (txn->meth == METH_PUT) { + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Parse the path */ + if ((r = pparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) { + return HTTP_FORBIDDEN; + } + + /* Make sure method is allowed (only allowed on resources) */ + if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED; + } + + /* Make sure Content-Range isn't specified */ + if (spool_getheader(txn->req_hdrs, "Content-Range")) + return HTTP_BAD_REQUEST; + + /* Check Content-Type */ + if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) { + for (mime = pparams->mime_types; mime->content_type; mime++) { + if (is_mediatype(mime->content_type, hdr[0])) break; + } + } + if (!mime || !mime->content_type) { + txn->error.precond = pparams->put.supp_data_precond; + return HTTP_FORBIDDEN; + } + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Check ACL for current user */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if (!(rights & DACL_WRITECONT) || !(rights & DACL_ADDRSRC)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = + !(rights & DACL_WRITECONT) ? DACL_WRITECONT : DACL_ADDRSRC; + mboxlist_entry_free(&mbentry); + return HTTP_NO_PRIVS; + } + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + mboxlist_entry_free(&mbentry); + + /* Local Mailbox */ + + /* Open mailbox for reading */ + r = mailbox_open_irl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Open the DAV DB corresponding to the mailbox */ + davdb = pparams->davdb.open_db(mailbox); + + /* Find message UID for the resource, if exists */ + pparams->davdb.lookup_resource(davdb, txn->req_tgt.mboxname, + txn->req_tgt.resource, 0, (void *) &ddata); + /* XXX Check errors */ + + if (ddata->imap_uid) { + /* Overwriting existing resource */ + + /* Fetch index record for the resource */ + r = mailbox_find_index_record(mailbox, ddata->imap_uid, &oldrecord); + if (r) { + syslog(LOG_ERR, "mailbox_find_index_record(%s, %u) failed: %s", + txn->req_tgt.mboxname, ddata->imap_uid, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + etag = message_guid_encode(&oldrecord.guid); + lastmod = oldrecord.internaldate; + } + else if (ddata->rowid) { + /* Unmapped URL (empty resource) */ + etag = NULL_ETAG; + lastmod = ddata->creationdate; + } + else { + /* New resource */ + etag = NULL; + lastmod = 0; + } + + /* Finished our initial read */ + mailbox_unlock_index(mailbox, NULL); + + /* Check any preconditions */ + precond = pparams->check_precond(txn, ddata, etag, lastmod); + + switch (precond) { + case HTTP_OK: + break; + + case HTTP_LOCKED: + txn->error.precond = DAV_NEED_LOCK_TOKEN; + txn->error.resource = txn->req_tgt.path; + + default: + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + /* Read body */ + txn->req_body.flags |= BODY_DECODE; + ret = http_read_body(httpd_in, httpd_out, + txn->req_hdrs, &txn->req_body, &txn->error.desc); + if (ret) { + txn->flags.conn = CONN_CLOSE; + goto done; + } + + /* Make sure we have a body */ + qdiffs[QUOTA_STORAGE] = buf_len(&txn->req_body.payload); + if (!qdiffs[QUOTA_STORAGE]) { + txn->error.desc = "Missing request body\r\n"; + ret = HTTP_BAD_REQUEST; + goto done; + } + + /* Check if we can append a new message to mailbox */ + if ((r = append_check(txn->req_tgt.mboxname, + httpd_authstate, ACL_INSERT, qdiffs))) { + syslog(LOG_ERR, "append_check(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + if (get_preferences(txn) & PREFER_REP) flags |= PREFER_REP; + + r = mailbox_lock_index(mailbox, LOCK_EXCLUSIVE); + if (r) { + syslog(LOG_ERR, "relock index(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Parse, validate, and store the resource */ + ret = pparams->put.proc(txn, mime, mailbox, davdb, flags); + + done: + if (davdb) pparams->davdb.close_db(davdb); + mailbox_close(&mailbox); + + return ret; +} + + +/* Compare modseq in index maps -- used for sorting */ +static int map_modseq_cmp(const struct index_map *m1, + const struct index_map *m2) +{ + if (m1->modseq < m2->modseq) return -1; + if (m1->modseq > m2->modseq) return 1; + return 0; +} + + +/* DAV:sync-collection REPORT */ +int report_sync_col(struct transaction_t *txn, + xmlNodePtr inroot, struct propfind_ctx *fctx) +{ + int ret = 0, r, userflag, i; + struct mailbox *mailbox = NULL; + uint32_t uidvalidity = 0; + modseq_t syncmodseq = 0; + modseq_t basemodseq = 0; + modseq_t highestmodseq = 0; + modseq_t respmodseq = 0; + uint32_t limit = -1; + uint32_t recno; + uint32_t msgno; + uint32_t nresp = 0; + xmlNodePtr node; + struct index_state istate; + struct index_record record; + char tokenuri[MAX_MAILBOX_PATH+1]; + + /* XXX Handle Depth (cal-home-set at toplevel) */ + + istate.map = NULL; + + /* Open mailbox for reading */ + r = mailbox_open_irl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + fctx->mailbox = mailbox; + + highestmodseq = mailbox->i.highestmodseq; + if (mailbox_user_flag(mailbox, DFLAG_UNBIND, &userflag, 0)) userflag = -1; + + /* Parse children element of report */ + for (node = inroot->children; node; node = node->next) { + xmlNodePtr node2; + xmlChar *str = NULL; + if (node->type == XML_ELEMENT_NODE) { + if (!xmlStrcmp(node->name, BAD_CAST "sync-token") && + (str = xmlNodeListGetString(inroot->doc, node->children, 1))) { + /* Parse sync-token */ + r = sscanf((char *) str, SYNC_TOKEN_URL_SCHEME + "%u-" MODSEQ_FMT "-" MODSEQ_FMT "%1s", + &uidvalidity, &syncmodseq, &basemodseq, + tokenuri /* test for trailing junk */); + + /* Sanity check the token components */ + if (r < 2 || r > 3 || + (uidvalidity != mailbox->i.uidvalidity) || + (syncmodseq > highestmodseq)) { + fctx->err->desc = "Invalid sync-token"; + } + else if (r == 3) { + /* Previous partial read token */ + if (basemodseq > highestmodseq) { + fctx->err->desc = "Invalid sync-token"; + } + else if (basemodseq < mailbox->i.deletedmodseq) { + fctx->err->desc = "Stale sync-token"; + } + } + else { + /* Regular token */ + if (syncmodseq < mailbox->i.deletedmodseq) { + fctx->err->desc = "Stale sync-token"; + } + } + + if (fctx->err->desc) { + /* DAV:valid-sync-token */ + txn->error.precond = DAV_SYNC_TOKEN; + ret = HTTP_FORBIDDEN; + } + } + else if (!xmlStrcmp(node->name, BAD_CAST "sync-level") && + (str = xmlNodeListGetString(inroot->doc, node->children, 1))) { + if (!strcmp((char *) str, "infinity")) { + fctx->err->desc = + "This server DOES NOT support infinite depth requests"; + ret = HTTP_SERVER_ERROR; + } + else if ((sscanf((char *) str, "%u", &fctx->depth) != 1) || + (fctx->depth != 1)) { + fctx->err->desc = "Illegal sync-level"; + ret = HTTP_BAD_REQUEST; + } + } + else if (!xmlStrcmp(node->name, BAD_CAST "limit")) { + for (node2 = node->children; node2; node2 = node2->next) { + if ((node2->type == XML_ELEMENT_NODE) && + !xmlStrcmp(node2->name, BAD_CAST "nresults") && + (!(str = xmlNodeListGetString(inroot->doc, + node2->children, 1)) || + (sscanf((char *) str, "%u", &limit) != 1))) { + txn->error.precond = DAV_OVER_LIMIT; + ret = HTTP_FORBIDDEN; + } + } + } + + if (str) xmlFree(str); + if (ret) goto done; + } + } + + /* Check Depth */ + if (!fctx->depth) { + fctx->err->desc = "Illegal sync-level"; + ret = HTTP_BAD_REQUEST; + goto done; + } + + if (!syncmodseq) { + /* Initial sync - set basemodseq in case client limits results */ + basemodseq = highestmodseq; + } + + /* Construct array of records for sorting and/or fetching cached header */ + istate.mailbox = mailbox; + istate.map = xzmalloc(mailbox->i.num_records * + sizeof(struct index_map)); + + /* Find which resources we need to report */ + for (recno = 1; recno <= mailbox->i.num_records; recno++) { + /* XXX Corrupted record? Should we bail? */ + if (mailbox_read_index_record(mailbox, recno, &record)) + continue; + + /* Resource not added/removed since last sync */ + if (record.modseq <= syncmodseq) + continue; + + /* Resource replaced by a PUT, COPY, or MOVE - ignore it */ + if ((userflag >= 0) && + record.user_flags[userflag / 32] & (1 << (userflag & 31))) + continue; + + /* Initial sync - ignore unmapped resources */ + if (record.modseq <= basemodseq && (record.system_flags & FLAG_EXPUNGED)) + continue; + + /* copy data into map (just like index.c - XXX helper fn? */ + istate.map[nresp].recno = recno; + istate.map[nresp].uid = record.uid; + istate.map[nresp].modseq = record.modseq; + istate.map[nresp].system_flags = record.system_flags; + for (i = 0; i < MAX_USER_FLAGS/32; i++) + istate.map[nresp].user_flags[i] = record.user_flags[i]; + + nresp++; + } + + if (limit < nresp) { + /* Need to truncate the responses */ + struct index_map *map = istate.map; + + /* Sort the response records by modseq */ + qsort(map, nresp, sizeof(struct index_map), + (int (*)(const void *, const void *)) &map_modseq_cmp); + + /* Our last response MUST be the last record with its modseq */ + for (nresp = limit; + nresp && map[nresp-1].modseq == map[nresp].modseq; + nresp--); + + if (!nresp) { + /* DAV:number-of-matches-within-limits */ + fctx->err->desc = "Unable to truncate results"; + txn->error.precond = DAV_OVER_LIMIT; + ret = HTTP_NO_STORAGE; + goto done; + } + + /* respmodseq will be modseq of last record we return */ + respmodseq = map[nresp-1].modseq; + + /* Tell client we truncated the responses */ + xml_add_response(fctx, HTTP_NO_STORAGE, DAV_OVER_LIMIT); + } + else { + /* Full response - respmodseq will be highestmodseq of mailbox */ + respmodseq = highestmodseq; + } + + /* Report the resources within the client requested limit (if any) */ + for (msgno = 1; msgno <= nresp; msgno++) { + char *p, *resource = NULL; + struct dav_data ddata; + + if (mailbox_read_index_record(mailbox, istate.map[msgno-1].recno, &record)) + continue; + + /* Get resource filename from Content-Disposition header */ + if ((p = index_getheader(&istate, msgno, "Content-Disposition"))) { + resource = strstr(p, "filename=") + 9; + } + if (!resource) continue; /* No filename */ + + if (*resource == '\"') { + resource++; + if ((p = strchr(resource, '\"'))) *p = '\0'; + } + else if ((p = strchr(resource, ';'))) *p = '\0'; + + memset(&ddata, 0, sizeof(struct dav_data)); + ddata.resource = resource; + + if (record.system_flags & FLAG_EXPUNGED) { + /* report as NOT FOUND + IMAP UID of 0 will cause index record to be ignored + propfind_by_resource() will append our resource name */ + propfind_by_resource(fctx, &ddata); + } + else { + fctx->record = &record; + ddata.imap_uid = record.uid; + propfind_by_resource(fctx, &ddata); + } + } + + /* Add sync-token element */ + if (respmodseq < basemodseq) { + /* Client limited results of initial sync - include basemodseq */ + snprintf(tokenuri, MAX_MAILBOX_PATH, + SYNC_TOKEN_URL_SCHEME "%u-" MODSEQ_FMT "-" MODSEQ_FMT, + mailbox->i.uidvalidity, respmodseq, basemodseq); + } + else { + snprintf(tokenuri, MAX_MAILBOX_PATH, + SYNC_TOKEN_URL_SCHEME "%u-" MODSEQ_FMT, + mailbox->i.uidvalidity, respmodseq); + } + xmlNewChild(fctx->root, NULL, BAD_CAST "sync-token", BAD_CAST tokenuri); + + done: + if (istate.map) free(istate.map); + mailbox_close(&mailbox); + + return (ret ? ret : HTTP_MULTI_STATUS); +} + + +int expand_property(xmlNodePtr inroot, struct propfind_ctx *fctx, + const char *href, parse_path_t parse_path, + const struct prop_entry *lprops, + xmlNodePtr root, int depth) +{ + int ret = 0, r; + struct propfind_ctx prev_ctx; + struct request_target_t req_tgt; + + memcpy(&prev_ctx, fctx, sizeof(struct propfind_ctx)); + + fctx->mode = PROPFIND_EXPAND; + if (href) { + /* Parse the URL */ + memset(&req_tgt, 0, sizeof(struct request_target_t)); + parse_path(href, &req_tgt, &fctx->err->desc); + + fctx->req_tgt = &req_tgt; + } + fctx->lprops = lprops; + fctx->elist = NULL; + fctx->root = root; + fctx->depth = depth; + fctx->mailbox = NULL; + + ret = preload_proplist(inroot->children, fctx); + if (ret) goto done; + + if (!fctx->req_tgt->collection && !fctx->depth) { + /* Add response for principal or home-set collection */ + struct mailbox *mailbox = NULL; + + if (*fctx->req_tgt->mboxname) { + /* Open mailbox for reading */ + if ((r = mailbox_open_irl(fctx->req_tgt->mboxname, &mailbox))) { + syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s", + fctx->req_tgt->mboxname, error_message(r)); + fctx->err->desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + fctx->mailbox = mailbox; + } + + xml_add_response(fctx, 0, 0); + + mailbox_close(&mailbox); + } + + if (fctx->depth > 0) { + /* Calendar collection(s) */ + + if (fctx->req_tgt->collection) { + /* Add response for target calendar collection */ + propfind_by_collection(fctx->req_tgt->mboxname, 0, 0, fctx); + } + else { + /* Add responses for all contained calendar collections */ + strlcat(fctx->req_tgt->mboxname, ".%", + sizeof(fctx->req_tgt->mboxname)); + r = mboxlist_findall(NULL, /* internal namespace */ + fctx->req_tgt->mboxname, 1, httpd_userid, + httpd_authstate, propfind_by_collection, fctx); + } + + if (fctx->davdb) fctx->close_db(fctx->davdb); + + ret = *fctx->ret; + } + + done: + /* Free the entry list */ + while (fctx->elist) { + struct propfind_entry_list *freeme = fctx->elist; + fctx->elist = freeme->next; + xmlFree(freeme->name); + free(freeme); + } + + fctx->mailbox = prev_ctx.mailbox; + fctx->depth = prev_ctx.depth; + fctx->root = prev_ctx.root; + fctx->elist = prev_ctx.elist; + fctx->lprops = prev_ctx.lprops; + fctx->req_tgt = prev_ctx.req_tgt; + + if (root != fctx->root) { + /* Move any defined namespaces up to the previous parent */ + xmlNsPtr nsDef; + + if (fctx->root->nsDef) { + /* Find last nsDef in list */ + for (nsDef = fctx->root->nsDef; nsDef->next; nsDef = nsDef->next); + nsDef->next = root->nsDef; + } + else fctx->root->nsDef = root->nsDef; + root->nsDef = NULL; + } + + return ret; +} + + + +/* DAV:expand-property REPORT */ +int report_expand_prop(struct transaction_t *txn __attribute__((unused)), + xmlNodePtr inroot, struct propfind_ctx *fctx) +{ + int ret = expand_property(inroot, fctx, NULL, NULL, fctx->lprops, + fctx->root, fctx->depth); + + return (ret ? ret : HTTP_MULTI_STATUS); +} + + +struct search_crit { + struct strlist *props; + xmlChar *match; + struct search_crit *next; +}; + + +/* mboxlist_findall() callback to find user principals (has Inbox) */ +static int principal_search(char *mboxname, + int matchlen __attribute__((unused)), + int maycreate __attribute__((unused)), + void *rock) +{ + struct propfind_ctx *fctx = (struct propfind_ctx *) rock; + char userid[MAX_MAILBOX_NAME+1], *p; + struct search_crit *search_crit; + size_t len; + + if (!(p = mboxname_isusermailbox(mboxname, 1))) return 0; + + strlcpy(userid, p, MAX_MAILBOX_NAME+1); + mboxname_hiersep_toexternal(&httpd_namespace, userid, 0); + + for (search_crit = (struct search_crit *) fctx->filter_crit; + search_crit; search_crit = search_crit->next) { + struct strlist *prop; + + for (prop = search_crit->props; prop; prop = prop->next) { + if (!strcmp(prop->s, "displayname")) { + if (!xmlStrcasestr(BAD_CAST userid, + search_crit->match)) return 0; + } + else if (!strcmp(prop->s, "calendar-user-address-set")) { + char email[MAX_MAILBOX_NAME+1]; + + snprintf(email, MAX_MAILBOX_NAME, "%s@%s", + userid, config_servername); + if (!xmlStrcasestr(BAD_CAST email, + search_crit->match)) return 0; + } + else if (!strcmp(prop->s, "calendar-user-type")) { + if (!xmlStrcasestr(BAD_CAST "INDIVIDUAL", + search_crit->match)) return 0; + } + } + } + + + /* Append principal name to URL path */ + if (!fctx->req_tgt->user) { + len = strlen(namespace_principal.prefix); + p = fctx->req_tgt->path + len; + len += strlcpy(p, "/user/", MAX_MAILBOX_PATH - len); + p = fctx->req_tgt->path + len; + } + else { + p = fctx->req_tgt->user; + len = p - fctx->req_tgt->path; + } + strlcpy(p, userid, MAX_MAILBOX_PATH - len); + + fctx->req_tgt->user = p; + fctx->req_tgt->userlen = strlen(p); + + return xml_add_response(fctx, 0, 0); +} + + +static const struct prop_entry prin_search_props[] = { + + /* WebDAV (RFC 4918) properties */ + { "displayname", NS_DAV, 0, NULL, NULL, NULL }, + + /* CalDAV Scheduling (RFC 6638) properties */ + { "calendar-user-address-set", NS_CALDAV, 0, NULL, NULL, NULL }, + { "calendar-user-type", NS_CALDAV, 0, NULL, NULL, NULL }, + + { NULL, 0, 0, NULL, NULL, NULL } +}; + + +/* DAV:principal-property-search REPORT */ +static int report_prin_prop_search(struct transaction_t *txn, + xmlNodePtr inroot, + struct propfind_ctx *fctx) +{ + int ret = 0; + xmlNodePtr node; + struct search_crit *search_crit, *next; + unsigned apply_prin_set = 0; + + if (fctx->depth != 0) { + txn->error.desc = "Depth header field MUST have value zero (0)"; + return HTTP_BAD_REQUEST; + } + + /* Parse children element of report */ + fctx->filter_crit = NULL; + for (node = inroot->children; node; node = node->next) { + if (node->type == XML_ELEMENT_NODE) { + if (!xmlStrcmp(node->name, BAD_CAST "property-search")) { + xmlNodePtr search; + + search_crit = xzmalloc(sizeof(struct search_crit)); + search_crit->next = fctx->filter_crit; + fctx->filter_crit = search_crit; + + for (search = node->children; search; search = search->next) { + if (search->type == XML_ELEMENT_NODE) { + if (!xmlStrcmp(search->name, BAD_CAST "prop")) { + xmlNodePtr prop; + + for (prop = search->children; + prop; prop = prop->next) { + if (prop->type == XML_ELEMENT_NODE) { + const struct prop_entry *entry; + + for (entry = prin_search_props; + entry->name && + xmlStrcmp(prop->name, + BAD_CAST entry->name); + entry++); + + if (!entry->name) { + txn->error.desc = + "Unsupported XML search prop"; + ret = HTTP_BAD_REQUEST; + goto done; + } + else { + appendstrlist(&search_crit->props, + (char *) entry->name); + } + } + } + } + else if (!xmlStrcmp(search->name, BAD_CAST "match")) { + if (search_crit->match) { + txn->error.desc = + "Too many DAV:match XML elements"; + ret = HTTP_BAD_REQUEST; + goto done; + } + + search_crit->match = + xmlNodeListGetString(inroot->doc, + search->children, 1); + } + else { + txn->error.desc = "Unknown XML element"; + ret = HTTP_BAD_REQUEST; + goto done; + } + } + } + + if (!search_crit->props) { + txn->error.desc = "Missing DAV:prop XML element"; + ret = HTTP_BAD_REQUEST; + goto done; + } + if (!search_crit->match) { + txn->error.desc = "Missing DAV:match XML element"; + ret = HTTP_BAD_REQUEST; + goto done; + } + } + else if (!xmlStrcmp(node->name, BAD_CAST "prop")) { + /* Already parsed in meth_report() */ + } + else if (!xmlStrcmp(node->name, + BAD_CAST "apply-to-principal-collection-set")) { + apply_prin_set = 1; + } + else { + txn->error.desc = "Unknown XML element"; + ret = HTTP_BAD_REQUEST; + goto done; + } + } + } + + if (!fctx->filter_crit) { + txn->error.desc = "Missing DAV:property-search XML element"; + ret = HTTP_BAD_REQUEST; + goto done; + } + + /* Only search DAV:principal-collection-set */ + if (apply_prin_set || !fctx->req_tgt->user) { + /* XXX Do LDAP/SQL lookup of CN/email-address(es) here */ + + ret = mboxlist_findall(NULL, /* internal namespace */ + "user.%", 1, httpd_userid, + httpd_authstate, principal_search, fctx); + } + + done: + for (search_crit = fctx->filter_crit; search_crit; search_crit = next) { + next = search_crit->next; + + if (search_crit->match) xmlFree(search_crit->match); + freestrlist(search_crit->props); + free(search_crit); + } + + return (ret ? ret : HTTP_MULTI_STATUS); +} + + +/* DAV:principal-search-property-set REPORT */ +static int report_prin_search_prop_set(struct transaction_t *txn, + xmlNodePtr inroot, + struct propfind_ctx *fctx) +{ + xmlNodePtr node; + const struct prop_entry *entry; + + if (fctx->depth != 0) { + txn->error.desc = "Depth header field MUST have value zero (0)"; + return HTTP_BAD_REQUEST; + } + + /* Look for child elements in request */ + for (node = inroot->children; node; node = node->next) { + if (node->type == XML_ELEMENT_NODE) { + txn->error.desc = + "DAV:principal-search-property-set XML element MUST be empty"; + return HTTP_BAD_REQUEST; + } + } + + for (entry = prin_search_props; entry->name; entry++) { + node = xmlNewChild(fctx->root, NULL, + BAD_CAST "principal-search-property", NULL); + node = xmlNewChild(node, NULL, BAD_CAST "prop", NULL); + ensure_ns(fctx->ns, entry->ns, fctx->root, + known_namespaces[entry->ns].href, + known_namespaces[entry->ns].prefix); + xmlNewChild(node, fctx->ns[entry->ns], BAD_CAST entry->name, NULL); + } + + return HTTP_OK; +} + + +/* Perform a REPORT request */ +int meth_report(struct transaction_t *txn, void *params) +{ + struct meth_params *rparams = (struct meth_params *) params; + int ret = 0, r; + const char **hdr; + unsigned depth = 0; + xmlNodePtr inroot = NULL, outroot = NULL, cur, prop = NULL, props = NULL; + const struct report_type_t *report = NULL; + xmlNsPtr ns[NUM_NAMESPACE]; + struct hash_table ns_table = { 0, NULL, NULL }; + struct propfind_ctx fctx; + struct propfind_entry_list *elist = NULL; + + memset(&fctx, 0, sizeof(struct propfind_ctx)); + + /* Parse the path */ + if ((r = rparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) return r; + + /* Make sure method is allowed */ + if (!(txn->req_tgt.allow & ALLOW_DAV)) return HTTP_NOT_ALLOWED; + + /* Check Depth */ + if ((hdr = spool_getheader(txn->req_hdrs, "Depth"))) { + if (!strcmp(hdr[0], "infinity")) { + depth = 2; + } + else if ((sscanf(hdr[0], "%u", &depth) != 1) || (depth > 1)) { + txn->error.desc = "Illegal Depth value\r\n"; + return HTTP_BAD_REQUEST; + } + } + + /* Normalize depth so that: + * 0 = home-set collection, 1+ = calendar collection, 2+ = calendar resource + */ + if (txn->req_tgt.collection) depth++; + if (txn->req_tgt.resource) depth++; + + /* Parse the REPORT body */ + ret = parse_xml_body(txn, &inroot); + if (!ret && !inroot) { + txn->error.desc = "Missing request body\r\n"; + return HTTP_BAD_REQUEST; + } + if (ret) goto done; + + /* Add report type to our header cache */ + spool_cache_header(xstrdup(":type"), xstrdup((const char *) inroot->name), + txn->req_hdrs); + + /* Check the report type against our supported list */ + for (report = rparams->reports; report && report->name; report++) { + if (!xmlStrcmp(inroot->name, BAD_CAST report->name)) break; + } + if (!report || !report->name) { + syslog(LOG_WARNING, "REPORT %s", inroot->name); + /* DAV:supported-report */ + txn->error.precond = DAV_SUPP_REPORT; + ret = HTTP_FORBIDDEN; + goto done; + } + + if (report->flags & REPORT_NEED_MBOX) { + mbentry_t *mbentry = NULL; + int rights; + + /* Locate the mailbox */ + if ((r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL))) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: ret = HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: ret = HTTP_NOT_FOUND; + default: ret = HTTP_SERVER_ERROR; + } + goto done; + } + + /* Check ACL for current user */ + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if ((rights & report->reqd_privs) != report->reqd_privs) { + if (report->reqd_privs == DACL_READFB) ret = HTTP_NOT_FOUND; + else { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = report->reqd_privs; + ret = HTTP_NO_PRIVS; + } + mboxlist_entry_free(&mbentry); + goto done; + } + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) ret = HTTP_UNAVAILABLE; + else ret = http_pipe_req_resp(be, txn); + goto done; + } + + mboxlist_entry_free(&mbentry); + + /* Local Mailbox */ + } + + /* Principal or Local Mailbox */ + + /* Parse children element of report */ + for (cur = inroot->children; cur; cur = cur->next) { + if (cur->type == XML_ELEMENT_NODE) { + if (!xmlStrcmp(cur->name, BAD_CAST "allprop")) { + fctx.mode = PROPFIND_ALL; + prop = cur; + break; + } + else if (!xmlStrcmp(cur->name, BAD_CAST "propname")) { + fctx.mode = PROPFIND_NAME; + fctx.prefer = PREFER_MIN; /* Don't want 404 (Not Found) */ + prop = cur; + break; + } + else if (!xmlStrcmp(cur->name, BAD_CAST "prop")) { + fctx.mode = PROPFIND_PROP; + prop = cur; + props = cur->children; + break; + } + } + } + + if (!prop && (report->flags & REPORT_NEED_PROPS)) { + txn->error.desc = "Missing <prop> element in REPORT\r\n"; + ret = HTTP_BAD_REQUEST; + goto done; + } + + /* Start construction of our multistatus response */ + if (report->resp_root && + !(outroot = init_xml_response(report->resp_root, NS_DAV, inroot, ns))) { + txn->error.desc = "Unable to create XML response\r\n"; + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Populate our propfind context */ + fctx.req_tgt = &txn->req_tgt; + fctx.depth = depth; + fctx.prefer |= get_preferences(txn); + fctx.req_hdrs = txn->req_hdrs; + fctx.userid = proxy_userid; + fctx.userisadmin = httpd_userisadmin; + fctx.authstate = httpd_authstate; + fctx.mailbox = NULL; + fctx.record = NULL; + fctx.reqd_privs = report->reqd_privs; + fctx.elist = NULL; + fctx.lprops = rparams->lprops; + fctx.root = outroot; + fctx.ns = ns; + fctx.ns_table = &ns_table; + fctx.err = &txn->error; + fctx.ret = &ret; + fctx.fetcheddata = 0; + + /* Parse the list of properties and build a list of callbacks */ + if (fctx.mode) ret = preload_proplist(props, &fctx); + + /* Process the requested report */ + if (!ret) ret = (*report->proc)(txn, inroot, &fctx); + + /* Output the XML response */ + if (outroot) { + switch (ret) { + case HTTP_OK: + case HTTP_MULTI_STATUS: + /* iCalendar data in response should not be transformed */ + if (fctx.fetcheddata) txn->flags.cc |= CC_NOTRANSFORM; + + xml_response(ret, txn, outroot->doc); + + ret = 0; + break; + + default: + break; + } + } + + done: + /* Free the entry list */ + elist = fctx.elist; + while (elist) { + struct propfind_entry_list *freeme = elist; + elist = elist->next; + xmlFree(freeme->name); + free(freeme); + } + + buf_free(&fctx.buf); + + free_hash_table(&ns_table, NULL); + + if (inroot) xmlFreeDoc(inroot->doc); + if (outroot) xmlFreeDoc(outroot->doc); + + return ret; +} + + +/* Perform a UNLOCK request + * + * preconditions: + * DAV:need-privileges + * DAV:lock-token-matches-request-uri + */ +int meth_unlock(struct transaction_t *txn, void *params) +{ + struct meth_params *lparams = (struct meth_params *) params; + int ret = HTTP_NO_CONTENT, r, precond, rights; + const char **hdr, *token; + struct mailbox *mailbox = NULL; + mbentry_t *mbentry = NULL; + struct dav_data *ddata; + struct index_record record; + const char *etag; + time_t lastmod; + size_t len; + void *davdb = NULL; + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Parse the path */ + if ((r = lparams->parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) return r; + + /* Make sure method is allowed (only allowed on resources) */ + if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED; + + /* Check for mandatory Lock-Token header */ + if (!(hdr = spool_getheader(txn->req_hdrs, "Lock-Token"))) { + txn->error.desc = "Missing Lock-Token header"; + return HTTP_BAD_REQUEST; + } + token = hdr[0]; + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + mboxlist_entry_free(&mbentry); + + /* Local Mailbox */ + + /* Open mailbox for reading */ + r = mailbox_open_irl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + /* Open the DAV DB corresponding to the mailbox */ + davdb = lparams->davdb.open_db(mailbox); + + /* Find message UID for the resource, if exists */ + lparams->davdb.lookup_resource(davdb, txn->req_tgt.mboxname, + txn->req_tgt.resource, 1, (void **) &ddata); + if (!ddata->rowid) { + ret = HTTP_NOT_FOUND; + goto done; + } + + /* Check if resource is locked */ + if (ddata->lock_expire <= time(NULL)) { + /* DAV:lock-token-matches-request-uri */ + txn->error.precond = DAV_BAD_LOCK_TOKEN; + ret = HTTP_CONFLICT; + goto done; + } + + /* Check if current user owns the lock */ + if (strcmp(ddata->lock_ownerid, httpd_userid)) { + /* Check ACL for current user */ + if (!(rights & DACL_ADMIN)) { + /* DAV:need-privileges */ + txn->error.precond = DAV_NEED_PRIVS; + txn->error.resource = txn->req_tgt.path; + txn->error.rights = DACL_ADMIN; + ret = HTTP_NO_PRIVS; + goto done; + } + } + + /* Check if lock token matches */ + len = strlen(ddata->lock_token); + if (token[0] != '<' || strlen(token) != len+2 || token[len+1] != '>' || + strncmp(token+1, ddata->lock_token, len)) { + /* DAV:lock-token-matches-request-uri */ + txn->error.precond = DAV_BAD_LOCK_TOKEN; + ret = HTTP_CONFLICT; + goto done; + } + + if (ddata->imap_uid) { + /* Mapped URL - Fetch index record for the resource */ + r = mailbox_find_index_record(mailbox, ddata->imap_uid, &record); + if (r) { + txn->error.desc = error_message(r); + ret = HTTP_SERVER_ERROR; + goto done; + } + + etag = message_guid_encode(&record.guid); + lastmod = record.internaldate; + } + else { + /* Unmapped URL (empty resource) */ + etag = NULL_ETAG; + lastmod = ddata->creationdate; + } + + /* Check any preconditions */ + precond = lparams->check_precond(txn, ddata, etag, lastmod); + + if (precond != HTTP_OK) { + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + if (ddata->imap_uid) { + /* Mapped URL - Remove the lock */ + ddata->lock_token = NULL; + ddata->lock_owner = NULL; + ddata->lock_ownerid = NULL; + ddata->lock_expire = 0; + + lparams->davdb.write_resource(davdb, ddata, 1); + } + else { + /* Unmapped URL - Treat as lock-null and delete mapping entry */ + lparams->davdb.delete_resource(davdb, ddata->rowid, 1); + } + + done: + if (davdb) lparams->davdb.close_db(davdb); + mailbox_close(&mailbox); + + return ret; +}
View file
cyrus-imapd-2.5.tar.gz/imap/http_dav.h
Added
@@ -0,0 +1,624 @@ +/* http_dav.h -- Routines for dealing with DAV properties in httpd + * + * Copyright (c) 1994-2011 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 HTTP_DAV_H +#define HTTP_DAV_H + +#include <stdint.h> +#include <libical/ical.h> +#include <libxml/tree.h> + +#include "annotate.h" +#include "caldav_db.h" +#include "httpd.h" +#include "spool.h" +#include "quota.h" + +#define NULL_ETAG "da39a3ee5e6b4b0d3255bfef95601890afd80709" + /* SHA1("") */ + +#define DFLAG_UNBIND "DAV:unbind" + +#define ANNOT_NS "/vendor/cmu/cyrus-httpd/" + +#define SCHED_INBOX "Inbox/" +#define SCHED_OUTBOX "Outbox/" +#define SCHED_DEFAULT "Default/" + +/* XML namespace URIs */ +#define XML_NS_DAV "DAV:" +#define XML_NS_CALDAV "urn:ietf:params:xml:ns:caldav" +#define XML_NS_CARDDAV "urn:ietf:params:xml:ns:carddav" +#define XML_NS_ISCHED "urn:ietf:params:xml:ns:ischedule" +#define XML_NS_CS "http://calendarserver.org/ns/" +#define XML_NS_CYRUS "http://cyrusimap.org/ns/" + +/* Index into known namespace array */ +enum { + NS_DAV, + NS_CALDAV, + NS_CARDDAV, + NS_ISCHED, + NS_CS, + NS_CYRUS, +}; +#define NUM_NAMESPACE 6 + +/* Cyrus-specific privileges */ +#define DACL_MKCOL ACL_CREATE /* CY:make-collection */ +#define DACL_ADDRSRC ACL_POST /* CY:add-resource */ +#define DACL_RMCOL ACL_DELETEMBOX /* CY:remove-collection */ +#define DACL_RMRSRC (ACL_DELETEMSG|ACL_EXPUNGE) /* CY:remove-resource */ +#define DACL_ADMIN ACL_ADMIN /* CY:admin (aggregates + DAV:read-acl, write-acl, unlock) */ + +/* WebDAV (RFC 3744) privileges */ +#define DACL_READ (ACL_READ\ + |ACL_LOOKUP) /* DAV:read (aggregates + DAV:read-current-user-privilege-set + and CALDAV:read-free-busy) */ +#define DACL_WRITECONT ACL_INSERT /* DAV:write-content */ +#define DACL_WRITEPROPS ACL_WRITE /* DAV:write-properties */ +#define DACL_BIND (DACL_MKCOL\ + |DACL_ADDRSRC) /* DAV:bind */ +#define DACL_UNBIND (DACL_RMCOL\ + |DACL_RMRSRC) /* DAV:unbind */ +#define DACL_WRITE (DACL_WRITECONT\ + |DACL_WRITEPROPS\ + |DACL_BIND\ + |DACL_UNBIND) /* DAV:write */ +#define DACL_ALL (DACL_READ\ + |DACL_WRITE\ + |DACL_ADMIN) /* DAV:all */ + +/* CalDAV (RFC 4791) privileges */ +#define DACL_READFB ACL_USER9 /* CALDAV:read-free-busy + (implicit if user has DAV:read) */ + +/* CalDAV Scheduling (RFC 6638) privileges + + We use the same ACLs for both schedule-deliver* and schedule-send* because + functionality of Scheduling Inbox and Outbox are mutually exclusive. + We use ACL_USER9 for both read-free-busy and schedule-*-freebusy because + Scheduling Inbox and Outbox don't contribute to free-busy. +*/ +#define DACL_SCHEDFB ACL_USER9 /* For Scheduling Inbox: + CALDAV:schedule-query-freebusy + + For Scheduling Outbox: + CALDAV:schedule-send-freebusy */ +#define DACL_INVITE ACL_USER8 /* For Scheduling Inbox: + CALDAV:schedule-deliver-invite + + For Scheduling Outbox: + CALDAV:schedule-send-invite */ +#define DACL_REPLY ACL_USER7 /* For Scheduling Inbox: + CALDAV:schedule-deliver-reply + + For Scheduling Outbox: + CALDAV:schedule-send-reply */ +#define DACL_SCHED (DACL_SCHEDFB\ + |DACL_INVITE\ + |DACL_REPLY) /* For Scheduling Inbox: + CALDAV:schedule-deliver (aggregates + CALDAV:schedule-deliver-invite, + schedule-deliver-reply, + schedule-query-freebusy); + + For Scheduling Outbox: + CALDAV:schedule-send (aggregates + CALDAV:schedule-send-invite, + schedule-send-reply, + schedule-send-freebusy) */ + +/* Index into preconditions array */ +enum { + /* WebDAV (RFC 4918) preconditons */ + DAV_PROT_PROP = 1, + DAV_BAD_LOCK_TOKEN, + DAV_NEED_LOCK_TOKEN, + DAV_LOCKED, + + /* WebDAV Versioning (RFC 3253) preconditions */ + DAV_SUPP_REPORT, + DAV_RSRC_EXISTS, + + /* WebDAV ACL (RFC 3744) preconditions */ + DAV_NEED_PRIVS, + DAV_NO_INVERT, + DAV_NO_ABSTRACT, + DAV_SUPP_PRIV, + DAV_RECOG_PRINC, + + /* WebDAV Quota (RFC 4331) preconditions */ + DAV_OVER_QUOTA, + DAV_NO_DISK_SPACE, + + /* WebDAV Extended MKCOL (RFC 5689) preconditions */ + DAV_VALID_RESTYPE, + + /* WebDAV Sync (RFC 6578) preconditions */ + DAV_SYNC_TOKEN, + DAV_OVER_LIMIT, + + /* CalDAV (RFC 4791) preconditions */ + CALDAV_SUPP_DATA, + CALDAV_VALID_DATA, + CALDAV_VALID_OBJECT, + CALDAV_SUPP_COMP, + CALDAV_LOCATION_OK, + CALDAV_UID_CONFLICT, + CALDAV_SUPP_FILTER, + CALDAV_VALID_FILTER, + + /* RSCALE (draft-daboo-icalendar-rscale) preconditions */ + CALDAV_SUPP_RSCALE, + + /* TZ by Ref (draft-ietf-tzdist-caldav-timezone-ref) preconditions */ + CALDAV_VALID_TIMEZONE, + + /* CalDAV Scheduling (RFC 6638) preconditions */ + CALDAV_VALID_SCHED, + CALDAV_VALID_ORGANIZER, + CALDAV_UNIQUE_OBJECT, + CALDAV_SAME_ORGANIZER, + CALDAV_ALLOWED_ORG_CHANGE, + CALDAV_ALLOWED_ATT_CHANGE, + + /* iSchedule (draft-desruisseaux-ischedule) preconditions */ + ISCHED_UNSUPP_VERSION, + ISCHED_UNSUPP_DATA, + ISCHED_INVALID_DATA, + ISCHED_INVALID_SCHED, + ISCHED_ORIG_MISSING, + ISCHED_MULTIPLE_ORIG, + ISCHED_ORIG_INVALID, + ISCHED_ORIG_DENIED, + ISCHED_RECIP_MISSING, + ISCHED_RECIP_MISMATCH, + ISCHED_VERIFICATION_FAILED, + + /* CardDAV (RFC 6352) preconditions */ + CARDDAV_SUPP_DATA, + CARDDAV_VALID_DATA, + CARDDAV_UID_CONFLICT, + CARDDAV_LOCATION_OK, + CARDDAV_SUPP_FILTER +}; + +/* Preference bits */ +enum { + PREFER_MIN = (1<<0), + PREFER_REP = (1<<1), + PREFER_NOROOT = (1<<2) +}; + +#define NO_DUP_CHECK (1<<7) + + +typedef void *(*db_open_proc_t)(struct mailbox *mailbox); +typedef void (*db_close_proc_t)(void *davdb); + +/* Function to lookup DAV 'resource' in 'mailbox', with optional 'lock', + * placing the record in 'data' + */ +typedef int (*db_lookup_proc_t)(void *davdb, const char *mailbox, + const char *resource, int lock, void **data); + +/* Function to process each DAV resource in 'mailbox' with 'cb' */ +typedef int (*db_foreach_proc_t)(void *davdb, const char *mailbox, + int (*cb)(void *rock, void *data), void *rock); + +/* Context for fetching properties */ +struct propfind_entry_list; +struct prop_entry; +struct error_t; + +struct propfind_ctx { + struct request_target_t *req_tgt; /* parsed request target URL */ + unsigned mode; /* none, allprop, propname, prop */ + unsigned depth; /* 0 = root, 1 = calendar, 2 = resrc */ + unsigned prefer; /* bitmask of client preferences */ + hdrcache_t req_hdrs; /* Cached HTTP headers */ + const char *userid; /* userid client has logged in as */ + int userisadmin; /* is userid an admin */ + struct auth_state *authstate; /* authorization state for userid */ + void *davdb; /* DAV DB corresponding to collection */ + struct mailbox *mailbox; /* mailbox correspondng to collection */ + struct quota quota; /* quota info for collection */ + struct index_record *record; /* cyrus.index record for resource */ + void *data; /* DAV record for resource */ + struct buf msg_buf; /* mmap()'d resource file */ + unsigned long reqd_privs; /* privileges req'd on collections */ + int (*filter)(struct propfind_ctx *, + void *data); /* callback to filter resources */ + void *filter_crit; /* criteria to filter resources */ + db_open_proc_t open_db; /* open DAV DB for a given mailbox */ + db_close_proc_t close_db; /* close DAV DB for a given mailbox */ + db_lookup_proc_t lookup_resource; /* lookup a specific resource */ + db_foreach_proc_t foreach_resource; /* process all resources in a mailbox */ + int (*proc_by_resource)(void *rock, /* Callback to process a resource */ + void *data); + struct propfind_entry_list *elist; /* List of props to fetch w/callbacks */ + const struct prop_entry *lprops; /* Array of known "live" properties */ + xmlNodePtr root; /* root node to add to XML tree */ + xmlNsPtr *ns; /* Array of our known namespaces */ + struct hash_table *ns_table; /* Table of all ns attached to resp */ + unsigned prefix_count; /* Count of new ns added to resp */ + struct error_t *err; /* Error info to pass up to caller */ + int *ret; /* Return code to pass up to caller */ + int fetcheddata; /* Did we fetch iCalendar/vCard data? */ + struct buf buf; /* Working buffer */ +}; + + +/* Context for patching (writing) properties */ +struct proppatch_ctx { + struct request_target_t *req_tgt; /* parsed request target URL */ + unsigned meth; /* requested Method */ + struct mailbox *mailbox; /* mailbox related to the collection */ + struct index_record *record; /* record of the specific resource */ + const struct prop_entry *lprops; /* Array of known "live" properties */ + xmlNodePtr root; /* root node to add to XML tree */ + xmlNsPtr *ns; /* Array of our supported namespaces */ + struct txn *tid; /* Transaction ID for annot writes */ + struct error_t *err; /* Error info to pass up to caller */ + int *ret; /* Return code to pass up to caller */ + struct buf buf; /* Working buffer */ +}; + + +/* Structure for property status */ +struct propstat { + xmlNodePtr root; + long status; + unsigned precond; +}; + +/* Index into propstat array */ +enum { + PROPSTAT_OK = 0, + PROPSTAT_UNAUTH, + PROPSTAT_FORBID, + PROPSTAT_NOTFOUND, + PROPSTAT_CONFLICT, + PROPSTAT_FAILEDDEP, + PROPSTAT_ERROR, + PROPSTAT_OVERQUOTA +}; +#define NUM_PROPSTAT 8 + + +/* Context for "live" properties */ +struct prop_entry { + const char *name; /* Property name */ + unsigned ns; /* Property namespace */ + unsigned char flags; /* Flags for how/where props apply */ + int (*get)(const xmlChar *name, /* Callback to fetch property */ + xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat *propstat, void *rock); + int (*put)(xmlNodePtr prop, /* Callback to write property */ + unsigned set, struct proppatch_ctx *pctx, + struct propstat *propstat, void *rock); + void *rock; /* Add'l data to pass to callback */ +}; + +/* Bitmask of property flags */ +enum { + PROP_ALLPROP = (1<<0), /* Returned in <allprop> request */ + PROP_COLLECTION = (1<<1), /* Returned for collection */ + PROP_RESOURCE = (1<<2), /* Returned for resource */ + PROP_PRESCREEN = (1<<3), /* Prescreen property using callback */ + PROP_NEEDPROP = (1<<4), /* Pass property node into callback */ + PROP_EXPAND = (1<<5) /* Property is expandable (href) */ +}; + + +/* Function to check headers for preconditions */ +typedef int (*check_precond_t)(struct transaction_t *txn, const void *data, + const char *etag, time_t lastmod); + +/* Function to insert/update DAV resource in 'data', optionally commiting txn */ +typedef int (*db_write_proc_t)(void *davdb, void *data, int commit); + +/* Function to delete resource in 'rowid', optionally commiting txn */ +typedef int (*db_delete_proc_t)(void *davdb, unsigned rowid, int commit); + +/* Function to delete all entries in 'mailbox', optionally commiting txn */ +typedef int (*db_delmbox_proc_t)(void *davdb, const char *mailbox, int commit); + +struct davdb_params { + db_open_proc_t open_db; /* open DAV DB for a given mailbox */ + db_close_proc_t close_db; /* close DAV DB for a given mailbox */ + db_lookup_proc_t lookup_resource; /* lookup a specific resource */ + db_foreach_proc_t foreach_resource; /* process all resources in a mailbox */ + db_write_proc_t write_resource; /* write a specific resource */ + db_delete_proc_t delete_resource; /* delete a specific resource */ + db_delmbox_proc_t delete_mbox; /* delete all resources in mailbox */ +}; + +/* + * Process 'priv', augmenting 'rights' as necessary. + * Returns 1 if processing is complete. + * Returns 0 if processing should continue in meth_acl() + */ +typedef int (*acl_proc_t)(struct transaction_t *txn, xmlNodePtr priv, + int *rights); + +/* Function to process and COPY a resource */ +typedef int (*copy_proc_t)(struct transaction_t *txn, + struct mailbox *src_mbox, struct index_record *src_rec, + struct mailbox *dest_mbox, const char *dest_rsrc, + void *dest_davdb, + unsigned overwrite, unsigned flags); + +/* Function to do special processing for DELETE method (optional) */ +typedef int (*delete_proc_t)(struct transaction_t *txn, struct mailbox *mailbox, + struct index_record *record, void *data); + +/* Function to convert to/from MIME type */ +struct mime_type_t { + const char *content_type; + const char *version; + const char *file_ext; + const char *file_ext2; + char* (*to_string)(void *); + void* (*from_string)(const char *); + void (*free)(void *); + const char* (*begin_stream)(struct buf *); + void (*end_stream)(struct buf *); +}; + +/* meth_mkcol() parameters */ +struct mkcol_params { + unsigned mbtype; /* mbtype to use for created mailbox */ + const char *xml_req; /* toplevel XML request element */ + const char *xml_resp; /* toplevel XML response element */ + unsigned xml_ns; /* namespace of response element */ +}; + +/* + * Function to do special processing for POST method (optional). + * Returns HTTP_CONTINUE if processing should continue in meth_post(), + * otherwise processing is complete. + */ +typedef int (*post_proc_t)(struct transaction_t *txn); + +/* meth_put() parameters */ +typedef int (*put_proc_t)(struct transaction_t *txn, struct mime_type_t *mime, + struct mailbox *mailbox, void *davdb, unsigned flags); + +struct put_params { + unsigned supp_data_precond; /* precond code for unsupported data */ + put_proc_t proc; /* function to process & PUT a rsrc */ +}; + +/* meth_report() parameters */ +typedef int (*report_proc_t)(struct transaction_t *txn, xmlNodePtr inroot, + struct propfind_ctx *fctx); + +struct report_type_t { + const char *name; /* report name */ + unsigned ns; /* report namespace */ + const char *resp_root; /* name of XML root element in resp */ + report_proc_t proc; /* function to generate the report */ + unsigned long reqd_privs; /* privileges required to run report */ + unsigned flags; /* report-specific flags */ +}; + +/* Report flags */ +enum { + REPORT_NEED_MBOX = (1<<0), + REPORT_NEED_PROPS = (1<<1) +}; + +/* Overwrite flags */ +enum { + OVERWRITE_CHECK = -1, + OVERWRITE_NO, + OVERWRITE_YES +}; + +struct meth_params { + struct mime_type_t *mime_types; /* array of MIME types and conv funcs */ + parse_path_t parse_path; /* parse URI path & generate mboxname */ + check_precond_t check_precond; /* check headers for preconditions */ + struct davdb_params davdb; /* DAV DB access functions */ + acl_proc_t acl_ext; /* special ACL handling (extensions) */ + copy_proc_t copy; /* function to process & COPY a rsrc */ + delete_proc_t delete; /* special DELETE handling (optional) */ + struct mkcol_params mkcol; /* params for creating collection */ + post_proc_t post; /* special POST handling (optional) */ + struct put_params put; /* params for putting a resource */ + const struct prop_entry *lprops; /* array of "live" properties */ + const struct report_type_t *reports;/* array of reports & proc functions */ +}; + +int report_expand_prop(struct transaction_t *txn, xmlNodePtr inroot, + struct propfind_ctx *fctx); +int report_sync_col(struct transaction_t *txn, xmlNodePtr inroot, + struct propfind_ctx *fctx); + + +int parse_path(struct request_target_t *tgt, const char **errstr); +int target_to_mboxname(struct request_target_t *req_tgt, char *mboxname); +unsigned get_preferences(struct transaction_t *txn); +struct mime_type_t *get_accept_type(const char **hdr, struct mime_type_t *types); + +int parse_xml_body(struct transaction_t *txn, xmlNodePtr *root); + +/* Initialize an XML tree */ +xmlNodePtr init_xml_response(const char *resp, int ns, + xmlNodePtr req, xmlNsPtr *respNs); + +xmlNodePtr xml_add_href(xmlNodePtr parent, xmlNsPtr ns, const char *href); +xmlNodePtr xml_add_error(xmlNodePtr root, struct error_t *err, + xmlNsPtr *avail_ns); +xmlNodePtr xml_add_prop(long status, xmlNsPtr davns, + struct propstat *propstat, + const xmlChar *name, xmlNsPtr ns, + xmlChar *content, unsigned precond); +void xml_add_lockdisc(xmlNodePtr node, const char *path, struct dav_data *data); +int ensure_ns(xmlNsPtr *respNs, int ns, xmlNodePtr node, + const char *url, const char *prefix); + +int xml_add_response(struct propfind_ctx *fctx, long code, unsigned precond); +int propfind_by_resource(void *rock, void *data); +int propfind_by_collection(char *mboxname, int matchlen, + int maycreate, void *rock); +int expand_property(xmlNodePtr inroot, struct propfind_ctx *fctx, + const char *href, parse_path_t parse_path, + const struct prop_entry *lprops, + xmlNodePtr root, int depth); + +/* DAV method processing functions */ +int meth_acl(struct transaction_t *txn, void *params); +int meth_copy(struct transaction_t *txn, void *params); +int meth_delete(struct transaction_t *txn, void *params); +int meth_get_dav(struct transaction_t *txn, void *params); +int meth_lock(struct transaction_t *txn, void *params); +int meth_mkcol(struct transaction_t *txn, void *params); +int meth_propfind(struct transaction_t *txn, void *params); +int meth_proppatch(struct transaction_t *txn, void *params); +int meth_post(struct transaction_t *txn, void *params); +int meth_put(struct transaction_t *txn, void *params); +int meth_report(struct transaction_t *txn, void *params); +int meth_unlock(struct transaction_t *txn, void *params); + + +/* PROPFIND callbacks */ +int propfind_getdata(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, + struct propstat propstat[], xmlNodePtr prop, + struct mime_type_t *mime_types, int precond, + const char *data, unsigned long datalen); +int propfind_fromdb(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_fromhdr(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_creationdate(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_getlength(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_getetag(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_getlastmod(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_lockdisc(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_suplock(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +int propfind_reportset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +int propfind_owner(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_supprivset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_curprivset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_acl(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_aclrestrict(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_princolset(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +int propfind_quota(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +int propfind_curprin(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +int propfind_addmember(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +int propfind_sync_token(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +int propfind_calhome(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_schedinbox(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_schedoutbox(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_caluseraddr(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_calusertype(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); +int propfind_abookhome(const xmlChar *name, xmlNsPtr ns, + struct propfind_ctx *fctx, xmlNodePtr resp, + struct propstat propstat[], void *rock); + +/* PROPPATCH callbacks */ +int proppatch_todb(xmlNodePtr prop, unsigned set, struct proppatch_ctx *pctx, + struct propstat propstat[], void *rock); +int proppatch_restype(xmlNodePtr prop, unsigned set, struct proppatch_ctx *pctx, + struct propstat propstat[], void *rock); + +#endif /* HTTP_DAV_H */
View file
cyrus-imapd-2.5.tar.gz/imap/http_dblookup.c
Added
@@ -0,0 +1,156 @@ +/* http_dblookup.c -- Routines for dealing with HTTP based db lookups + * + * Copyright (c) 1994-2011 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 "carddav_db.h" +#include "http_err.h" +#include "http_dav.h" +#include <jansson.h> +#include "util.h" + +static int meth_get_db(struct transaction_t *txn, void *params); + +/* Namespace for DB lookups */ +struct namespace_t namespace_dblookup = { + URL_NS_DBLOOKUP, /*enabled*/1, "/dblookup", NULL, 0 /* auth */, + ALLOW_READ, + NULL, NULL, NULL, NULL, + { + { NULL, NULL }, /* ACL */ + { NULL, NULL }, /* COPY */ + { NULL, NULL }, /* DELETE */ + { &meth_get_db, NULL }, /* GET */ + { NULL, NULL }, /* HEAD */ + { NULL, NULL }, /* LOCK */ + { NULL, NULL }, /* MKCALENDAR */ + { NULL, NULL }, /* MKCOL */ + { NULL, NULL }, /* MOVE */ + { NULL, NULL }, /* OPTIONS */ + { NULL, NULL }, /* POST */ + { NULL, NULL }, /* PROPFIND */ + { NULL, NULL }, /* PROPPATCH */ + { NULL, NULL }, /* PUT */ + { NULL, NULL }, /* REPORT */ + { NULL, NULL }, /* TRACE */ + { NULL, NULL } /* UNLOCK */ + } +}; + +static int get_email(struct transaction_t *txn __attribute__((unused)), + const char *userid, const char *key) +{ + struct carddav_db *db = NULL; + int ret = HTTP_NOT_FOUND; + + /* XXX init just incase carddav not enabled? */ + db = carddav_open_userid(userid, /*flags*/0); + if (!db) goto done; + + if (carddav_getemail(db, key)) + ret = HTTP_NO_CONTENT; + +done: + if (db) carddav_close(db); + return ret; +} + +static int get_group(struct transaction_t *txn, const char *userid, const char *key) +{ + struct carddav_db *db = NULL; + strarray_t *array = NULL; + char *result = NULL; + json_t *json; + int ret = HTTP_NOT_FOUND; + int i; + + /* XXX init just incase carddav not enabled? */ + db = carddav_open_userid(userid, /*flags*/0); + if (!db) goto done; + + array = carddav_getgroup(db, key); + if (!array) goto done; + + json = json_array(); + for (i = 0; i < strarray_size(array); i++) { + json_array_append_new(json, json_string(strarray_nth(array, i))); + } + + result = json_dumps(json, JSON_PRESERVE_ORDER|JSON_COMPACT); + json_decref(json); + + txn->resp_body.type = "application/json"; + txn->resp_body.len = strlen(result); + + write_body(HTTP_OK, txn, result, txn->resp_body.len); + ret = 0; + +done: + free(result); + if (array) strarray_free(array); + if (db) carddav_close(db); + return ret; +} + +static int meth_get_db(struct transaction_t *txn, + void *params __attribute__((unused))) +{ + const char **userhdrs; + const char **keyhdrs; + + userhdrs = spool_getheader(txn->req_hdrs, "User"); + keyhdrs = spool_getheader(txn->req_hdrs, "Key"); + + if (!userhdrs) return HTTP_BAD_REQUEST; + if (!keyhdrs) return HTTP_BAD_REQUEST; + + if (userhdrs[1]) return HTTP_NOT_ALLOWED; + if (keyhdrs[1]) return HTTP_NOT_ALLOWED; + + if (!strcmp(txn->req_uri->path, "/dblookup/email")) + return get_email(txn, userhdrs[0], keyhdrs[0]); + + if (!strcmp(txn->req_uri->path, "/dblookup/group")) + return get_group(txn, userhdrs[0], keyhdrs[0]); + + return HTTP_NOT_FOUND; +}
View file
cyrus-imapd-2.5.tar.gz/imap/http_err.et
Added
@@ -0,0 +1,195 @@ +# http_err.et -- Error codes for the Cyrus HTTP server +# +# Copyright (c) 1994-2011 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. +# + +error_table http + +# HTTP/1.1 Status Codes (draft-ietf-httpbis-*) +# +# 1xx: Informational - Request received, continuing process +# 2xx: Successful - Request successfully received, understood, and accepted +# 3xx: Redirection - Further action must be taken to complete the request +# 4xx: Client Error - The request contains bad syntax or cannot be fulfilled +# 5xx: Server Error - The server is incapable of performing the request + +ec HTTP_CONTINUE, + "100 Continue" + +ec HTTP_SWITCH_PROT, + "101 Switching Protocols" + +ec HTTP_OK, + "200 OK" + +ec HTTP_CREATED, + "201 Created" + +ec HTTP_ACCEPTED, + "202 Accepted" + +ec HTTP_NO_CONTENT, + "204 No Content" + +ec HTTP_RESET_CONTENT, + "205 Reset Content" + +ec HTTP_PARTIAL, + "206 Partial Content" + +ec HTTP_MOVED, + "301 Moved Permanently" + +ec HTTP_SEE_OTHER, + "303 See Other" + +ec HTTP_NOT_MODIFIED, + "304 Not Modified" + +ec HTTP_TEMP_REDIRECT, + "307 Temporary Redirect" + +ec HTTP_BAD_REQUEST, + "400 Bad Request" + +ec HTTP_UNAUTHORIZED, + "401 Unauthorized" + +ec HTTP_FORBIDDEN, + "403 Forbidden" + +ec HTTP_NOT_FOUND, + "404 Not Found" + +ec HTTP_NOT_ALLOWED, + "405 Method Not Allowed" + +ec HTTP_NOT_ACCEPTABLE, + "406 Not Acceptable" + +ec HTTP_TIMEOUT, + "408 Request Timeout" + +ec HTTP_CONFLICT, + "409 Conflict" + +ec HTTP_GONE, + "410 Gone" + +ec HTTP_LENGTH_REQUIRED, + "411 Length Required" + +ec HTTP_PRECOND_FAILED, + "412 Precondition Failed" + +ec HTTP_TOO_LARGE, + "413 Payload Too Large" + +ec HTTP_TOO_LONG, + "414 URI Too Long" + +ec HTTP_BAD_MEDIATYPE, + "415 Unsupported Media Type" + +ec HTTP_UNSAT_RANGE, + "416 Range Not Satisfiable" + +ec HTTP_EXPECT_FAILED, + "417 Expectation Failed" + +ec HTTP_UPGRADE, + "426 Upgrade Required" + +ec HTTP_SERVER_ERROR, + "500 Internal Server Error" + +ec HTTP_NOT_IMPLEMENTED, + "501 Not Implemented" + +ec HTTP_BAD_GATEWAY, + "502 Bad Gateway" + +ec HTTP_UNAVAILABLE, + "503 Service Unavailable" + +ec HTTP_GATEWAY_TIMEOUT, + "504 Gateway Timeout" + +ec HTTP_BAD_VERSION, + "505 HTTP Version Not Supported" + + + +# WebDAV (RFC 2518/4918) + +ec HTTP_PROCESSING, + "102 Processing" + +ec HTTP_MULTI_STATUS, + "207 Multi-Status" + +ec HTTP_UNPROCESSABLE, + "422 Unprocessable Entity" + +ec HTTP_LOCKED, + "423 Locked" + +ec HTTP_FAILED_DEP, + "424 Failed Dependency" + +ec HTTP_NO_STORAGE, + "507 Insufficient Storage" + + + +# Permanent Redirect (RFC 7238) + +ec HTTP_PERM_REDIRECT, + "308 Permanent Redirect" + + + +# Additional HTTP Status Codes (RFC 6585) + +ec HTTP_PRECOND_REQUIRED, + "428 Precondition Required" + + +end
View file
cyrus-imapd-2.5.tar.gz/imap/http_ischedule.c
Added
@@ -0,0 +1,1137 @@ +/* http_ischedule.c -- Routines for handling iSchedule in httpd + * + * Copyright (c) 1994-2012 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 <errno.h> +#include <syslog.h> + +#include <libical/ical.h> + +#include "global.h" +#include "httpd.h" +#include "http_caldav_sched.h" +#include "http_dav.h" +#include "http_err.h" +#include "http_proxy.h" +#include "jcal.h" +#include "map.h" +#include "proxy.h" +#include "tok.h" +#include "util.h" +#include "xmalloc.h" +#include "xcal.h" +#include "xstrlcpy.h" +#include <sasl/saslutil.h> + +#define ISCHED_WELLKNOWN_URI "/.well-known/ischedule" + +#ifdef WITH_DKIM +#include <dkim.h> + +//#define TEST + +#define BASE64_LEN(inlen) ((((inlen) + 2) / 3) * 4) + +static DKIM_LIB *dkim_lib = NULL; +static struct buf privkey = BUF_INITIALIZER; +static struct buf tmpbuf = BUF_INITIALIZER; +static struct buf b64req = BUF_INITIALIZER; +#endif /* WITH_DKIM */ + +static void isched_init(struct buf *serverinfo); +static void isched_shutdown(void); + +static int meth_get_isched(struct transaction_t *txn, void *params); +static int meth_options_isched(struct transaction_t *txn, void *params); +static int meth_post_isched(struct transaction_t *txn, void *params); +static int dkim_auth(struct transaction_t *txn); +static int meth_get_domainkey(struct transaction_t *txn, void *params); +static time_t compile_time; + +static struct mime_type_t isched_mime_types[] = { + /* First item MUST be the default type and storage format */ + { "text/calendar; charset=utf-8", "2.0", "ics", "ifb", + (char* (*)(void *)) &icalcomponent_as_ical_string_r, + (void * (*)(const char*)) &icalparser_parse_string, + (void (*)(void *)) &icalcomponent_free, NULL, NULL + }, + { "application/calendar+xml; charset=utf-8", NULL, "xcs", "xfb", + (char* (*)(void *)) &icalcomponent_as_xcal_string, + (void * (*)(const char*)) &xcal_string_as_icalcomponent, + NULL, NULL, NULL + }, +#ifdef WITH_JSON + { "application/calendar+json; charset=utf-8", NULL, "jcs", "jfb", + (char* (*)(void *)) &icalcomponent_as_jcal_string, + (void * (*)(const char*)) &jcal_string_as_icalcomponent, + NULL, NULL, NULL, + }, +#endif + { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL } +}; + +struct namespace_t namespace_ischedule = { + URL_NS_ISCHEDULE, 0, "/ischedule", ISCHED_WELLKNOWN_URI, 0 /* auth */, + (ALLOW_READ | ALLOW_POST | ALLOW_ISCHEDULE), + isched_init, NULL, NULL, isched_shutdown, + { + { NULL, NULL }, /* ACL */ + { NULL, NULL }, /* COPY */ + { NULL, NULL }, /* DELETE */ + { &meth_get_isched, NULL }, /* GET */ + { &meth_get_isched, NULL }, /* HEAD */ + { NULL, NULL }, /* LOCK */ + { NULL, NULL }, /* MKCALENDAR */ + { NULL, NULL }, /* MKCOL */ + { NULL, NULL }, /* MOVE */ + { &meth_options_isched, NULL }, /* OPTIONS */ + { &meth_post_isched, NULL }, /* POST */ + { NULL, NULL }, /* PROPFIND */ + { NULL, NULL }, /* PROPPATCH */ + { NULL, NULL }, /* PUT */ + { NULL, NULL }, /* REPORT */ + { &meth_trace, NULL }, /* TRACE */ + { NULL, NULL } /* UNLOCK */ + } +}; + +struct namespace_t namespace_domainkey = { + URL_NS_DOMAINKEY, 0, "/domainkeys", "/.well-known/domainkey", 0 /* auth */, + ALLOW_READ, NULL, NULL, NULL, NULL, + { + { NULL, NULL }, /* ACL */ + { NULL, NULL }, /* COPY */ + { NULL, NULL }, /* DELETE */ + { &meth_get_domainkey, NULL }, /* GET */ + { &meth_get_domainkey, NULL }, /* HEAD */ + { NULL, NULL }, /* LOCK */ + { NULL, NULL }, /* MKCALENDAR */ + { NULL, NULL }, /* MKCOL */ + { NULL, NULL }, /* MOVE */ + { &meth_options, NULL }, /* OPTIONS */ + { NULL, NULL }, /* POST */ + { NULL, NULL }, /* PROPFIND */ + { NULL, NULL }, /* PROPPATCH */ + { NULL, NULL }, /* PUT */ + { NULL, NULL }, /* REPORT */ + { &meth_trace, NULL }, /* TRACE */ + { NULL, NULL } /* UNLOCK */ + } +}; + + +void isched_capa_hdr(struct transaction_t *txn, time_t *lastmod) +{ + struct stat sbuf; + time_t mtime; + + stat(config_filename, &sbuf); + mtime = MAX(compile_time, sbuf.st_mtime); + txn->resp_body.iserial = mtime + + (rscale_calendars ? rscale_calendars->num_elements : 0); + if (lastmod) *lastmod = mtime; +} + + +/* iSchedule Receiver Capabilities */ +static int meth_get_isched(struct transaction_t *txn, + void *params __attribute__((unused))) +{ + int precond; + struct strlist *action; + static struct message_guid prev_guid; + struct message_guid guid; + const char *etag; + static time_t lastmod = 0; + struct stat sbuf; + static xmlChar *buf = NULL; + static int bufsiz = 0; + + /* Initialize */ + if (!lastmod) message_guid_set_null(&prev_guid); + + /* Fill in iSchedule-Capabilities */ + isched_capa_hdr(txn, &lastmod); + + /* We don't handle GET on a anything other than ?action=capabilities */ + action = hash_lookup("action", &txn->req_qparams); + if (!action || action->next || strcmp(action->s, "capabilities")) { + txn->error.desc = "Invalid action"; + return HTTP_BAD_REQUEST; + } + + /* Generate ETag based on compile date/time of this source file, + the number of available RSCALEs and the config file size/mtime */ + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%ld-%d-%ld-%ld", (long) compile_time, + rscale_calendars ? rscale_calendars->num_elements : 0, + sbuf.st_mtime, sbuf.st_size); + + message_guid_generate(&guid, buf_cstring(&txn->buf), buf_len(&txn->buf)); + etag = message_guid_encode(&guid); + + /* Check any preconditions, including range request */ + txn->flags.ranges = 1; + precond = check_precond(txn, NULL, etag, lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_PARTIAL: + case HTTP_NOT_MODIFIED: + /* Fill in Etag, Last-Modified, and Expires */ + txn->resp_body.etag = etag; + txn->resp_body.lastmod = lastmod; + txn->resp_body.maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + return precond; + } + + if (!message_guid_equal(&prev_guid, &guid)) { + xmlNodePtr root, capa, node, comp, meth; + xmlNsPtr ns[NUM_NAMESPACE]; + struct mime_type_t *mime; + struct icaltimetype date; + icaltimezone *utc = icaltimezone_get_utc_timezone(); + int i, n, maxlen; + + /* Start construction of our query-result */ + if (!(root = init_xml_response("query-result", NS_ISCHED, NULL, ns))) { + txn->error.desc = "Unable to create XML response"; + return HTTP_SERVER_ERROR; + } + + capa = xmlNewChild(root, NULL, BAD_CAST "capabilities", NULL); + + buf_reset(&txn->buf); + buf_printf(&txn->buf, "%ld", txn->resp_body.iserial); + xmlNewChild(capa, NULL, BAD_CAST "serial-number", + BAD_CAST buf_cstring(&txn->buf)); + + node = xmlNewChild(capa, NULL, BAD_CAST "versions", NULL); + xmlNewChild(node, NULL, BAD_CAST "version", BAD_CAST "1.0"); + + node = xmlNewChild(capa, NULL, + BAD_CAST "scheduling-messages", NULL); + comp = xmlNewChild(node, NULL, BAD_CAST "component", NULL); + xmlNewProp(comp, BAD_CAST "name", BAD_CAST "VEVENT"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REQUEST"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REPLY"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "CANCEL"); + + comp = xmlNewChild(node, NULL, BAD_CAST "component", NULL); + xmlNewProp(comp, BAD_CAST "name", BAD_CAST "VTODO"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REQUEST"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REPLY"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "CANCEL"); + +#ifdef HAVE_VPOLL + comp = xmlNewChild(node, NULL, BAD_CAST "component", NULL); + xmlNewProp(comp, BAD_CAST "name", BAD_CAST "VPOLL"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "POLLSTATUS"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REQUEST"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REPLY"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "CANCEL"); +#endif /* HAVE_VPOLL */ + + comp = xmlNewChild(node, NULL, BAD_CAST "component", NULL); + xmlNewProp(comp, BAD_CAST "name", BAD_CAST "VFREEBUSY"); + meth = xmlNewChild(comp, NULL, BAD_CAST "method", NULL); + xmlNewProp(meth, BAD_CAST "name", BAD_CAST "REQUEST"); + + node = xmlNewChild(capa, NULL, + BAD_CAST "calendar-data-types", NULL); + for (mime = isched_mime_types; mime->content_type; mime++) { + xmlNodePtr type = xmlNewChild(node, NULL, + BAD_CAST "calendar-data-type", NULL); + + /* Trim any charset from content-type */ + buf_reset(&txn->buf); + buf_printf(&txn->buf, "%.*s", + (int) strcspn(mime->content_type, ";"), + mime->content_type); + + xmlNewProp(type, BAD_CAST "content-type", + BAD_CAST buf_cstring(&txn->buf)); + + if (mime->version) + xmlNewProp(type, BAD_CAST "version", BAD_CAST mime->version); + } + + node = xmlNewChild(capa, NULL, BAD_CAST "attachments", NULL); + xmlNewChild(node, NULL, BAD_CAST "inline", NULL); + + node = xmlNewChild(capa, NULL, BAD_CAST "rscales", NULL); + if (rscale_calendars) { + for (i = 0, n = rscale_calendars->num_elements; i < n; i++) { + const char **rscale = icalarray_element_at(rscale_calendars, i); + + xmlNewChild(node, NULL, BAD_CAST "rscale", BAD_CAST *rscale); + } + } + + maxlen = config_getint(IMAPOPT_MAXMESSAGESIZE); + if (!maxlen) maxlen = INT_MAX; + buf_reset(&txn->buf); + buf_printf(&txn->buf, "%d", maxlen); + xmlNewChild(capa, NULL, BAD_CAST "max-content-length", + BAD_CAST buf_cstring(&txn->buf)); + + date = icaltime_from_timet_with_zone(caldav_epoch, 0, utc); + xmlNewChild(capa, NULL, BAD_CAST "min-date-time", + BAD_CAST icaltime_as_ical_string(date)); + + date = icaltime_from_timet_with_zone(caldav_eternity, 0, utc); + xmlNewChild(capa, NULL, BAD_CAST "max-date-time", + BAD_CAST icaltime_as_ical_string(date)); + + /* XXX need to fill these with values */ + xmlNewChild(capa, NULL, BAD_CAST "max-recipients", NULL); + xmlNewChild(capa, NULL, BAD_CAST "administrator", NULL); + + /* Dump XML response tree into a text buffer */ + xmlDocDumpFormatMemoryEnc(root->doc, &buf, &bufsiz, "utf-8", 1); + xmlFreeDoc(root->doc); + + if (!buf) { + txn->error.desc = "Error dumping XML tree"; + return HTTP_SERVER_ERROR; + } + + message_guid_copy(&prev_guid, &guid); + } + + /* Output the XML response */ + txn->resp_body.type = "application/xml; charset=utf-8"; + write_body(precond, txn, (char *) buf, bufsiz); + + xmlFree(buf); + + return 0; +} + + +static int meth_options_isched(struct transaction_t *txn, void *params) +{ + /* Fill in iSchedule-Capabilities */ + isched_capa_hdr(txn, NULL); + + return meth_options(txn, params); +} + + +/* iSchedule Receiver */ +static int meth_post_isched(struct transaction_t *txn, + void *params __attribute__((unused))) +{ + int ret = 0, r, authd = 0; + const char **hdr, **recipients; + struct mime_type_t *mime = NULL; + icalcomponent *ical = NULL, *comp; + icalcomponent_kind kind = 0; + icalproperty_method meth = 0; + icalproperty *prop = NULL; + const char *uid = NULL; + + /* Fill in iSchedule-Capabilities */ + isched_capa_hdr(txn, NULL); + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Check iSchedule-Version */ + if (!(hdr = spool_getheader(txn->req_hdrs, "iSchedule-Version")) || + strcmp(hdr[0], "1.0")) { + txn->error.precond = ISCHED_UNSUPP_VERSION; + return HTTP_BAD_REQUEST; + } + + /* Check Content-Type */ + if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) { + for (mime = isched_mime_types; mime->content_type; mime++) { + if (is_mediatype(mime->content_type, hdr[0])) break; + } + } + if (!mime || !mime->content_type) { + txn->error.precond = ISCHED_UNSUPP_DATA; + return HTTP_BAD_REQUEST; + } + + /* Check Originator */ + if (!(hdr = spool_getheader(txn->req_hdrs, "Originator"))) { + txn->error.precond = ISCHED_ORIG_MISSING; + return HTTP_BAD_REQUEST; + } + else if (hdr[1]) { + /* Multiple Originators */ + txn->error.precond = ISCHED_MULTIPLE_ORIG; + return HTTP_BAD_REQUEST; + } + + /* Check Recipients */ + if (!(recipients = spool_getheader(txn->req_hdrs, "Recipient"))) { + txn->error.precond = ISCHED_RECIP_MISSING; + return HTTP_BAD_REQUEST; + } + + /* Read body */ + txn->req_body.flags |= BODY_DECODE; + r = http_read_body(httpd_in, httpd_out, + txn->req_hdrs, &txn->req_body, &txn->error.desc); + if (r) { + txn->flags.conn = CONN_CLOSE; + return r; + } + + /* Make sure we have a body */ + if (!buf_len(&txn->req_body.payload)) { + txn->error.desc = "Missing request body\r\n"; + return HTTP_BAD_REQUEST; + } + + /* Check authorization */ + if (httpd_userid && + config_mupdate_server && config_getstring(IMAPOPT_PROXYSERVERS)) { + /* Allow frontends to HTTP auth to backends and use iSchedule */ + authd = 1; + } + else if (!spool_getheader(txn->req_hdrs, "DKIM-Signature")) { + txn->error.desc = "No signature"; + } + else { + authd = dkim_auth(txn); + } + + if (!authd) { + ret = HTTP_FORBIDDEN; + txn->error.precond = ISCHED_VERIFICATION_FAILED; + goto done; + } + + /* Parse the iCal data for important properties */ + ical = mime->from_string(buf_cstring(&txn->req_body.payload)); + if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) { + txn->error.precond = ISCHED_INVALID_DATA; + return HTTP_BAD_REQUEST; + } + + icalrestriction_check(ical); + if ((txn->error.desc = get_icalcomponent_errstr(ical))) { + assert(!buf_len(&txn->buf)); + buf_setcstr(&txn->buf, txn->error.desc); + txn->error.desc = buf_cstring(&txn->buf); + txn->error.precond = ISCHED_INVALID_DATA; + return HTTP_BAD_REQUEST; + } + + meth = icalcomponent_get_method(ical); + comp = icalcomponent_get_first_real_component(ical); + if (comp) { + uid = icalcomponent_get_uid(comp); + kind = icalcomponent_isa(comp); + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + } + + /* Check method preconditions */ + if (!meth || !comp || !uid || !prop) { + txn->error.precond = ISCHED_INVALID_SCHED; + ret = HTTP_BAD_REQUEST; + goto done; + } + + switch (kind) { + case ICAL_VFREEBUSY_COMPONENT: + if (meth == ICAL_METHOD_REQUEST) + ret = sched_busytime_query(txn, mime, ical); + else goto invalid_meth; + break; + + case ICAL_VEVENT_COMPONENT: + case ICAL_VTODO_COMPONENT: + case ICAL_VPOLL_COMPONENT: + switch (meth) { + case ICAL_METHOD_POLLSTATUS: + if (kind != ICAL_VPOLL_COMPONENT) goto invalid_meth; + + case ICAL_METHOD_REQUEST: + case ICAL_METHOD_REPLY: + case ICAL_METHOD_CANCEL: { + struct sched_data sched_data = + { 1, meth == ICAL_METHOD_REPLY, + ical, NULL, 0, ICAL_SCHEDULEFORCESEND_NONE, NULL }; + xmlNodePtr root = NULL; + xmlNsPtr ns[NUM_NAMESPACE]; + struct auth_state *authstate; + int i; + + /* Start construction of our schedule-response */ + if (!(root = init_xml_response("schedule-response", + NS_ISCHED, NULL, ns))) { + ret = HTTP_SERVER_ERROR; + txn->error.desc = "Unable to create XML response\r\n"; + goto done; + } + + authstate = auth_newstate("anonymous"); + + /* Process each recipient */ + for (i = 0; recipients[i]; i++) { + tok_t tok = + TOK_INITIALIZER(recipients[i], ",", TOK_TRIMLEFT|TOK_TRIMRIGHT); + char *recipient; + + while ((recipient = tok_next(&tok))) { + /* Is recipient remote or local? */ + struct sched_param sparam; + int r = caladdress_lookup(recipient, &sparam); + + /* Don't allow scheduling with remote users via iSchedule */ + if (sparam.flags & SCHEDTYPE_REMOTE) r = HTTP_FORBIDDEN; + + if (r) sched_data.status = REQSTAT_NOUSER; + else sched_deliver(recipient, &sched_data, authstate); + + xml_add_schedresponse(root, NULL, BAD_CAST recipient, + BAD_CAST sched_data.status); + } + tok_fini(&tok); + } + + xml_response(HTTP_OK, txn, root->doc); + + auth_freestate(authstate); + } + break; + + default: + invalid_meth: + txn->error.desc = "Unsupported iTIP method"; + txn->error.precond = ISCHED_INVALID_SCHED; + ret = HTTP_BAD_REQUEST; + goto done; + } + break; + + default: + txn->error.desc = "Unsupported iCalendar component"; + txn->error.precond = ISCHED_INVALID_SCHED; + ret = HTTP_BAD_REQUEST; + } + + done: + if (ical) icalcomponent_free(ical); + + return ret; +} + + +int isched_send(struct sched_param *sparam, const char *recipient, + icalcomponent *ical, xmlNodePtr *xml) +{ + int r = 0; + struct backend *be; + static unsigned send_count = 0; + static struct buf hdrs = BUF_INITIALIZER; + const char *body, *uri, *originator; + size_t bodylen; + icalcomponent *comp; + icalcomponent_kind kind; + icalproperty *prop; + unsigned code; + struct transaction_t txn; + + *xml = NULL; + memset(&txn, 0, sizeof(struct transaction_t)); + + if (sparam->flags & SCHEDTYPE_REMOTE) uri = ISCHED_WELLKNOWN_URI; + else uri = namespace_ischedule.prefix; + + /* Open connection to iSchedule receiver. + Use header buffer to construct remote server[:port][/tls] */ + buf_setcstr(&txn.buf, sparam->server); + if (sparam->port) buf_printf(&txn.buf, ":%u", sparam->port); + if (sparam->flags & SCHEDTYPE_SSL) buf_appendcstr(&txn.buf, "/tls"); + if (sparam->flags & SCHEDTYPE_REMOTE) buf_appendcstr(&txn.buf, "/noauth"); + be = proxy_findserver(buf_cstring(&txn.buf), &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + if (!be) return HTTP_UNAVAILABLE; + + /* Create iSchedule request body */ + body = icalcomponent_as_ical_string(ical); + bodylen = strlen(body); + + /* Create iSchedule request header. + * XXX Make sure that we don't use multiple headers of the same name + * or add WSP around commas in signed headers + * to obey ischedule-relaxed canonicalization. + */ + buf_reset(&hdrs); + buf_printf(&hdrs, "Host: %s", sparam->server); + if (sparam->port) buf_printf(&hdrs, ":%u", sparam->port); + buf_printf(&hdrs, "\r\n"); + buf_printf(&hdrs, "Cache-Control: no-cache, no-transform\r\n"); + if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) { + buf_printf(&hdrs, "User-Agent: %s\r\n", buf_cstring(&serverinfo)); + } + buf_printf(&hdrs, "iSchedule-Version: 1.0\r\n"); + buf_printf(&hdrs, "iSchedule-Message-ID: <cmu-ischedule-%u-%ld-%u@%s>\r\n", + getpid(), time(NULL), send_count++, config_servername); + buf_printf(&hdrs, "Content-Type: text/calendar; charset=utf-8"); + + comp = icalcomponent_get_first_real_component(ical); + kind = icalcomponent_isa(comp); + buf_printf(&hdrs, "; method=REQUEST; component=%s\r\n", + icalcomponent_kind_to_string(kind)); + + buf_printf(&hdrs, "Content-Length: %u\r\n", (unsigned) bodylen); + + /* Determine Originator based on method and component */ + if (icalcomponent_get_method(ical) == ICAL_METHOD_REPLY) { + if (icalcomponent_isa(comp) == ICAL_VPOLL_COMPONENT) { + prop = icalcomponent_get_first_property(comp, ICAL_VOTER_PROPERTY); + originator = icalproperty_get_voter(prop); + } + else { + prop = + icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY); + originator = icalproperty_get_attendee(prop); + } + } + else { + prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY); + originator = icalproperty_get_organizer(prop); + } + buf_printf(&hdrs, "Originator: %s\r\n", originator); + + if (recipient) { + /* Single recipient */ + buf_printf(&hdrs, "Recipient: %s\r\n", recipient); + } + else { + /* VFREEBUSY REQUEST - use ATTENDEES as Recipients */ + char sep = ' '; + + buf_printf(&hdrs, "Recipient:"); + for (prop = icalcomponent_get_first_property(comp, + ICAL_ATTENDEE_PROPERTY); + prop; + prop = icalcomponent_get_next_property(comp, + ICAL_ATTENDEE_PROPERTY)) { + buf_printf(&hdrs, "%c%s", sep, icalproperty_get_attendee(prop)); + sep = ','; + } + buf_printf(&hdrs, "\r\n"); + } + + buf_printf(&hdrs, "\r\n"); + + redirect: + /* Send request line */ + prot_printf(be->out, "POST %s %s\r\n", uri, HTTP_VERSION); + + if (sparam->flags & SCHEDTYPE_REMOTE) { +#ifdef WITH_DKIM + DKIM *dkim = NULL; + DKIM_STAT stat; + unsigned char *sig = NULL; + size_t siglen; + const char *selector = config_getstring(IMAPOPT_ISCHEDULE_DKIM_SELECTOR); + const char *domain = config_getstring(IMAPOPT_ISCHEDULE_DKIM_DOMAIN); + + /* Create iSchedule/DKIM signature */ + if (dkim_lib && + (dkim = dkim_sign(dkim_lib, NULL /* id */, NULL, + (dkim_sigkey_t) buf_cstring(&privkey), + (const u_char *) selector, + (const u_char *) domain, + /* Requires modified version of OpenDKIM + until we get OpenDOSETA */ + DKIM_CANON_ISCHEDULE, DKIM_CANON_SIMPLE, + DKIM_SIGN_RSASHA256, -1 /* entire body */, + &stat))) { + + /* Suppress folding of DKIM header */ +// stat = dkim_set_margin(dkim, 0); + + /* Add our query method list */ + stat = dkim_add_querymethod(dkim, "private-exchange", NULL); + stat = dkim_add_querymethod(dkim, "http", "well-known"); +// stat = dkim_add_querymethod(dkim, "dns", "txt"); + + /* Process the headers and body */ + stat = dkim_chunk(dkim, + (u_char *) buf_cstring(&hdrs), buf_len(&hdrs)); + stat = dkim_chunk(dkim, (u_char *) body, bodylen); + stat = dkim_chunk(dkim, NULL, 0); + stat = dkim_eom(dkim, NULL); + + /* Generate the signature */ + stat = dkim_getsighdr_d(dkim, strlen(DKIM_SIGNHEADER) + 2, + &sig, &siglen); + + /* Prepend a DKIM-Signature header */ + prot_printf(be->out, "%s: %s\r\n", DKIM_SIGNHEADER, sig); + + dkim_free(dkim); + } +#else + syslog(LOG_WARNING, "DKIM-Signature required, but DKIM isn't supported"); +#endif /* WITH_DKIM */ + } + + /* Send request headers and body */ + prot_putbuf(be->out, &hdrs); + prot_write(be->out, body, bodylen); + + /* Read response (req_hdr and req_body are actually the response) */ + txn.req_body.flags = BODY_DECODE; + r = http_read_response(be, METH_POST, &code, NULL, + &txn.req_hdrs, &txn.req_body, &txn.error.desc); + if (!r) { + switch (code) { + case 200: /* Successful */ + r = parse_xml_body(&txn, xml); + break; + + case 301: + case 302: + case 307: + case 308: /* Redirection */ + uri = spool_getheader(txn.req_hdrs, "Location")[0]; + if (txn.req_body.flags & BODY_CLOSE) { + proxy_downserver(be); + be = proxy_findserver(buf_cstring(&txn.buf), &http_protocol, + NULL, &backend_cached, + NULL, NULL, httpd_in); + if (!be) { + r = HTTP_UNAVAILABLE; + break; + } + } + goto redirect; + + default: + r = HTTP_UNAVAILABLE; + } + } + + if (txn.req_hdrs) spool_free_hdrcache(txn.req_hdrs); + buf_free(&txn.req_body.payload); + buf_free(&txn.buf); + + return r; +} + + +#ifdef WITH_DKIM +static DKIM_CBSTAT isched_get_key(DKIM *dkim, DKIM_SIGINFO *sig, + u_char *buf, size_t buflen) +{ + DKIM_CBSTAT stat = DKIM_CBSTAT_NOTFOUND; + const char *domain, *selector, *query; + tok_t tok; + char *type, *opts; + + assert(dkim != NULL); + assert(sig != NULL); + + domain = (const char *) dkim_sig_getdomain(sig); + selector = (const char *) dkim_sig_getselector(sig); + if (!domain || !selector) return DKIM_CBSTAT_ERROR; + + query = (const char *) dkim_sig_gettagvalue(sig, 0, (u_char *) "q"); + if (!query) query = "dns/txt"; /* implicit default */ + + /* Parse the q= tag */ + tok_init(&tok, query, ":", 0); + while ((type = tok_next(&tok))) { + /* Split type/options */ + if ((opts = strchr(type, '/'))) *opts++ = '\0'; + +#ifdef IOPTEST /* CalConnect ioptest */ + if (1) { +#else + if (!strcmp(type, "private-exchange")) { +#endif + const char *prefix = config_getstring(IMAPOPT_HTTPDOCROOT); + struct buf path = BUF_INITIALIZER; + FILE *f; + + if (!prefix) continue; + + buf_setcstr(&path, prefix); + buf_printf(&path, "%s/%s/%s", + namespace_domainkey.prefix, domain, selector); + + if (!(f = fopen(buf_cstring(&path), "r"))) { + syslog(LOG_NOTICE, "%s: fopen(): %s", + buf_cstring(&path), strerror(errno)); + } + buf_free(&path); + if (!f) continue; + + memset(buf, '\0', buflen); + fgets((char *) buf, buflen, f); + fclose(f); + + if (buf[0] != '\0') { + stat = DKIM_CBSTAT_CONTINUE; + break; + } + } + else if (!strcmp(type, "http") && !strcmp(opts, "well-known")) { + } + else if (!strcmp(type, "dns") && !strcmp(opts, "txt")) { + stat = DKIM_CBSTAT_DEFAULT; + break; + } + } + + tok_fini(&tok); + + return stat; +} + + +static void dkim_cachehdr(const char *name, const char *contents, void *rock) +{ + struct buf *hdrfield = &tmpbuf; + static const char *lastname = NULL; + int dup_hdr = name && lastname && !strcmp(name, lastname); + + /* Ignore private headers in our cache */ + if (name && name[0] == ':') return; + + /* Combine header fields of the same name. + * Our hash table will always feed us duplicate headers consecutively. + */ + if (lastname && !dup_hdr) { + dkim_header((DKIM *) rock, + (u_char *) buf_cstring(hdrfield), buf_len(hdrfield)); + } + + lastname = name; + + if (name) { + tok_t tok; + char *token, sep = ':'; + + if (!dup_hdr) buf_setcstr(hdrfield, name); + else sep = ','; + + /* Trim leading/trailing WSP around comma-separated values */ + tok_init(&tok, contents, ",", TOK_TRIMLEFT|TOK_TRIMRIGHT|TOK_EMPTY); + while ((token = tok_next(&tok))) { + buf_printf(hdrfield, "%c%s", sep, token); + sep = ','; + } + tok_fini(&tok); + } +} + +static int dkim_auth(struct transaction_t *txn) +{ + int authd = 0; + DKIM *dkim = NULL; + DKIM_STAT stat; + + if (!dkim_lib) return 0; + + dkim = dkim_verify(dkim_lib, NULL /* id */, NULL, &stat); + if (!dkim) return 0; + +#ifdef TEST + { + /* XXX Hack for local testing */ + dkim_query_t qtype = DKIM_QUERY_FILE; + struct buf keyfile = BUF_INITIALIZER; + + stat = dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_QUERYMETHOD, + &qtype, sizeof(qtype)); + + buf_printf(&keyfile, "%s/dkim.public", config_dir); + stat = dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_QUERYINFO, + (void *) buf_cstring(&keyfile), + buf_len(&keyfile)); + } +#endif + + /* Process the cached headers and body */ + spool_enum_hdrcache(txn->req_hdrs, &dkim_cachehdr, dkim); + dkim_cachehdr(NULL, NULL, dkim); /* Force canon of last header */ + stat = dkim_eoh(dkim); + if (stat == DKIM_STAT_OK) { + stat = dkim_body(dkim, (u_char *) buf_cstring(&txn->req_body.payload), + buf_len(&txn->req_body.payload)); + stat = dkim_eom(dkim, NULL); + } + + if (stat == DKIM_STAT_OK) authd = 1; + else if (stat == DKIM_STAT_CBREJECT) { + txn->error.desc = + "Unable to verify: HTTP request-line mismatch"; + } + else { + DKIM_SIGINFO *sig = dkim_getsignature(dkim); + + if (sig) { + const char *sigerr; + + if (dkim_sig_getbh(sig) == DKIM_SIGBH_MISMATCH) + sigerr = "body hash mismatch"; + else { + DKIM_SIGERROR err = dkim_sig_geterror(sig); + + sigerr = dkim_sig_geterrorstr(err); + } + + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%s: %s", + dkim_getresultstr(stat), sigerr); + txn->error.desc = buf_cstring(&txn->buf); + } + else txn->error.desc = dkim_getresultstr(stat); + } + + dkim_free(dkim); + + return authd; +} +#else +static int dkim_auth(struct transaction_t *txn __attribute__((unused))) +{ + syslog(LOG_WARNING, "DKIM-Signature provided, but DKIM isn't supported"); + + return 0; +} +#endif /* WITH_DKIM */ + + +/* Perform a GET/HEAD request for a domainkey */ +static int meth_get_domainkey(struct transaction_t *txn, + void *params __attribute__((unused))) +{ + int ret = 0, r, fd = -1, precond; + const char *path; + static struct buf pathbuf = BUF_INITIALIZER; + struct stat sbuf; + const char *msg_base = NULL; + unsigned long msg_size = 0; + struct resp_body_t *resp_body = &txn->resp_body; + + /* See if file exists and get Content-Length & Last-Modified time */ + buf_setcstr(&pathbuf, config_dir); + buf_appendcstr(&pathbuf, txn->req_uri->path); + path = buf_cstring(&pathbuf); + r = stat(path, &sbuf); + if (r || !S_ISREG(sbuf.st_mode)) return HTTP_NOT_FOUND; + + /* Generate Etag */ + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%ld-%ld", (long) sbuf.st_mtime, (long) sbuf.st_size); + + /* Check any preconditions, including range request */ + txn->flags.ranges = 1; + precond = check_precond(txn, NULL, buf_cstring(&txn->buf), sbuf.st_mtime); + + switch (precond) { + case HTTP_OK: + case HTTP_PARTIAL: + case HTTP_NOT_MODIFIED: + /* Fill in Content-Type, ETag, Last-Modified, and Expires */ + resp_body->type = "text/plain"; + resp_body->etag = buf_cstring(&txn->buf); + resp_body->lastmod = sbuf.st_mtime; + resp_body->maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE; + if (httpd_userid) txn->flags.cc |= CC_PUBLIC; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + resp_body->type = NULL; + return precond; + } + + if (txn->meth == METH_GET) { + /* Open and mmap the file */ + if ((fd = open(path, O_RDONLY)) == -1) return HTTP_SERVER_ERROR; + map_refresh(fd, 1, &msg_base, &msg_size, sbuf.st_size, path, NULL); + } + + write_body(precond, txn, msg_base, sbuf.st_size); + + if (fd != -1) { + map_free(&msg_base, &msg_size); + close(fd); + } + + return ret; +} + + +static void isched_init(struct buf *serverinfo) +{ + if (!(config_httpmodules & IMAP_ENUM_HTTPMODULES_CALDAV) || + !config_getenum(IMAPOPT_CALDAV_ALLOWSCHEDULING)) { + /* Need CALDAV and CALDAV_SCHED in order to have ISCHEDULE */ + return; + } + + compile_time = calc_compile_time(__TIME__, __DATE__); + + if (config_mupdate_server && config_getstring(IMAPOPT_PROXYSERVERS)) { + /* If backend server, we require iSchedule (w/o DKIM) */ + namespace_ischedule.enabled = -1; + buf_len(serverinfo); // squash compiler warning when #undef WITH_DKIM + } +#ifdef WITH_DKIM + else { + namespace_ischedule.enabled = + config_httpmodules & IMAP_ENUM_HTTPMODULES_ISCHEDULE; + } + + /* Add OpenDKIM version to serverinfo string */ + uint32_t ver = dkim_libversion(); + buf_printf(serverinfo, " OpenDKIM/%u.%u.%u", + (ver >> 24) & 0xff, (ver >> 16) & 0xff, (ver >> 8) & 0xff); + if (ver & 0xff) buf_printf(serverinfo, ".%u", ver & 0xff); + + if (namespace_ischedule.enabled) { + int fd; + unsigned flags = ( DKIM_LIBFLAGS_BADSIGHANDLES | DKIM_LIBFLAGS_CACHE | +// DKIM_LIBFLAGS_KEEPFILES | DKIM_LIBFLAGS_TMPFILES | + DKIM_LIBFLAGS_VERIFYONE ); + uint64_t ttl = 3600; /* 1 hour */ + const char *requiredhdrs[] = { "Content-Type", "iSchedule-Version", + "Originator", "Recipient", NULL }; + const char *signhdrs[] = { "iSchedule-Message-ID", "User-Agent", NULL }; + const char *skiphdrs[] = { "Cache-Control", "Connection", + "Content-Length", "Host", "Keep-Alive", + "Proxy-Authenticate", "Proxy-Authorization", + "TE", "Trailer", "Transfer-Encoding", + "Upgrade", "Via", NULL }; + const char *senderhdrs[] = { "Originator", NULL }; + const char *keyfile = config_getstring(IMAPOPT_ISCHEDULE_DKIM_KEY_FILE); + unsigned need_dkim = + namespace_ischedule.enabled == IMAP_ENUM_HTTPMODULES_ISCHEDULE; + + /* Initialize DKIM library */ + if (!(dkim_lib = dkim_init(NULL, NULL))) { + syslog(LOG_ERR, "unable to initialize libopendkim"); + namespace_ischedule.enabled = !need_dkim; + return; + } + + /* Install our callback for doing key lookups */ + dkim_set_key_lookup(dkim_lib, isched_get_key); + + /* Setup iSchedule DKIM options */ +#ifdef TEST + flags |= ( DKIM_LIBFLAGS_SIGNLEN | DKIM_LIBFLAGS_ZTAGS ); +#endif + dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_FLAGS, + &flags, sizeof(flags)); + dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_SIGNATURETTL, + &ttl, sizeof(ttl)); + dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_REQUIREDHDRS, + requiredhdrs, sizeof(const char **)); + dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_MUSTBESIGNED, + requiredhdrs, sizeof(const char **)); + dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_SIGNHDRS, + signhdrs, sizeof(const char **)); + dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_SKIPHDRS, + skiphdrs, sizeof(const char **)); + dkim_options(dkim_lib, DKIM_OP_SETOPT, DKIM_OPTS_SENDERHDRS, + senderhdrs, sizeof(const char **)); + + /* Fetch DKIM private key for signing */ + if ((fd = open(keyfile, O_RDONLY)) != -1) { + const char *base = NULL; + unsigned long len = 0; + + map_refresh(fd, 1, &base, &len, MAP_UNKNOWN_LEN, keyfile, NULL); + buf_setmap(&privkey, base, len); + map_free(&base, &len); + close(fd); + } + else { + syslog(LOG_ERR, "unable to open private key file %s", keyfile); + namespace_ischedule.enabled = !need_dkim; + } + + namespace_domainkey.enabled = + config_httpmodules & IMAP_ENUM_HTTPMODULES_DOMAINKEY; + } +#endif /* WITH_DKIM */ +} + + +static void isched_shutdown(void) +{ +#ifdef WITH_DKIM + buf_free(&privkey); + buf_free(&tmpbuf); + buf_free(&b64req); + if (dkim_lib) dkim_close(dkim_lib); +#endif +}
View file
cyrus-imapd-2.5.tar.gz/imap/http_proxy.c
Added
@@ -0,0 +1,1171 @@ +/* http_proxy.c - HTTP proxy support functions + * + * Copyright (c) 1994-2011 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 <assert.h> +#include <ctype.h> +#include <syslog.h> +#include <sasl/sasl.h> +#include <sasl/saslutil.h> + +#include "httpd.h" +#include "http_err.h" +#include "http_proxy.h" +#include "imap_err.h" +#include "iptostring.h" +#include "mupdate-client.h" +#include "prot.h" +#include "proxy.h" +#include "spool.h" +#include "tls.h" +#include "tok.h" +#include "version.h" +#include "xmalloc.h" +#include "xstrlcat.h" +#include "xstrlcpy.h" + +#include <libxml/uri.h> + +static int login(struct backend *s, const char *userid, + sasl_callback_t *cb, const char **status, + int noauth); +static int ping(struct backend *s, const char *userid); +static int logout(struct backend *s __attribute__((unused))); + + +HIDDEN struct protocol_t http_protocol = +{ "http", "HTTP", TYPE_SPEC, + { .spec = { &login, &ping, &logout } } +}; + + +EXPORTED const char *digest_recv_success(hdrcache_t hdrs) +{ + const char **hdr = spool_getheader(hdrs, "Authentication-Info"); + + return (hdr ? hdr[0]: NULL); +} + + +static const char *callback_getdata(sasl_conn_t *conn, + sasl_callback_t *callbacks, + unsigned long callbackid) +{ + sasl_callback_t *cb; + const char *result = NULL; + + for (cb = callbacks; cb->id != SASL_CB_LIST_END; cb++) { + if (cb->id == callbackid) { + switch (cb->id) { + case SASL_CB_USER: + case SASL_CB_AUTHNAME: { + sasl_getsimple_t *simple_cb = (sasl_getsimple_t *) cb->proc; + simple_cb(cb->context, cb->id, &result, NULL); + break; + } + + case SASL_CB_PASS: { + sasl_secret_t *pass; + sasl_getsecret_t *pass_cb = (sasl_getsecret_t *) cb->proc; + pass_cb(conn, cb->context, cb->id, &pass); + result = (const char *) pass->data; + break; + } + } + } + } + + return result; +} + + +#define BASE64_BUF_SIZE 21848 /* per RFC 2222bis: ((16K / 3) + 1) * 4 */ + +static int login(struct backend *s, const char *userid, + sasl_callback_t *cb, const char **status, + int noauth __attribute__((unused))) +{ + int r = 0; + socklen_t addrsize; + struct sockaddr_storage saddr_l, saddr_r; + char remoteip[60], localip[60]; + static struct buf buf = BUF_INITIALIZER; + sasl_security_properties_t secprops = + { 0, 0xFF, PROT_BUFSIZE, 0, NULL, NULL }; /* default secprops */ + const char *mech_conf, *pass, *clientout = NULL; + struct auth_scheme_t *scheme = NULL; + unsigned need_tls = 0, tls_done = 0, clientoutlen; + hdrcache_t hdrs = NULL; + + if (status) *status = NULL; + + /* set the IP addresses */ + addrsize = sizeof(struct sockaddr_storage); + if (getpeername(s->sock, (struct sockaddr *) &saddr_r, &addrsize) || + iptostring((struct sockaddr *) &saddr_r, addrsize, remoteip, 60)) { + if (status) *status = "Failed to get remote IP address"; + return SASL_FAIL; + } + + addrsize = sizeof(struct sockaddr_storage); + if (getsockname(s->sock, (struct sockaddr *) &saddr_l, &addrsize) || + iptostring((struct sockaddr *) &saddr_l, addrsize, localip, 60)) { + if (status) *status = "Failed to get local IP address"; + return SASL_FAIL; + } + + /* Create callbacks, if necessary */ + if (!cb) { + buf_setmap(&buf, s->hostname, strcspn(s->hostname, ".")); + buf_appendcstr(&buf, "_password"); + pass = config_getoverflowstring(buf_cstring(&buf), NULL); + if (!pass) pass = config_getstring(IMAPOPT_PROXY_PASSWORD); + cb = mysasl_callbacks(NULL, /* userid */ + config_getstring(IMAPOPT_PROXY_AUTHNAME), + config_getstring(IMAPOPT_PROXY_REALM), + pass); + s->sasl_cb = cb; + } + + /* Create SASL context */ + r = sasl_client_new(s->prot->sasl_service, s->hostname, + localip, remoteip, cb, SASL_USAGE_FLAGS, &s->saslconn); + if (r != SASL_OK) goto done; + + r = sasl_setprop(s->saslconn, SASL_SEC_PROPS, &secprops); + if (r != SASL_OK) goto done; + + /* Get SASL mechanism list. We can force a particular + mechanism using a <shorthost>_mechs option */ + buf_setmap(&buf, s->hostname, strcspn(s->hostname, ".")); + buf_appendcstr(&buf, "_mechs"); + if (!(mech_conf = config_getoverflowstring(buf_cstring(&buf), NULL))) { + mech_conf = config_getstring(IMAPOPT_FORCE_SASL_CLIENT_MECH); + } + + do { + unsigned code; + const char **hdr, *errstr, *serverin; + char base64[BASE64_BUF_SIZE+1]; + unsigned int serverinlen; + struct body_t resp_body; +#ifdef SASL_HTTP_REQUEST + sasl_http_request_t httpreq = { "OPTIONS", /* Method */ + "*", /* URI */ + (u_char *) "", /* Empty body */ + 0, /* Zero-length body */ + 0 }; /* Persistent cxn? */ +#endif + + /* Base64 encode any client response, if necessary */ + if (clientout && scheme && (scheme->flags & AUTH_BASE64)) { + r = sasl_encode64(clientout, clientoutlen, + base64, BASE64_BUF_SIZE, &clientoutlen); + if (r != SASL_OK) break; + + clientout = base64; + } + + /* Send Authorization and/or Upgrade request to server */ + prot_puts(s->out, "OPTIONS * HTTP/1.1\r\n"); + prot_printf(s->out, "Host: %s\r\n", s->hostname); + prot_printf(s->out, "User-Agent: %s\r\n", buf_cstring(&serverinfo)); + if (scheme) { + prot_printf(s->out, "Authorization: %s %s\r\n", + scheme->name, clientout ? clientout : ""); + prot_printf(s->out, "Authorize-As: %s\r\n", + userid ? userid : "anonymous"); + } + else { + prot_printf(s->out, "Upgrade: %s\r\n", TLS_VERSION); + if (need_tls) { + prot_puts(s->out, "Connection: Upgrade\r\n"); + need_tls = 0; + } + prot_puts(s->out, "Authorization: \r\n"); + } + prot_puts(s->out, "\r\n"); + prot_flush(s->out); + + serverin = clientout = NULL; + serverinlen = clientoutlen = 0; + + /* Read response(s) from backend until final response or error */ + do { + resp_body.flags = BODY_DISCARD; + r = http_read_response(s, METH_OPTIONS, &code, NULL, + &hdrs, &resp_body, &errstr); + if (r) { + if (status) *status = errstr; + break; + } + + if (code == 101) { /* Switching Protocols */ + if (tls_done) { + r = HTTP_BAD_GATEWAY; + if (status) *status = "TLS already active"; + break; + } + else if (backend_starttls(s, NULL, NULL, NULL)) { + r = HTTP_SERVER_ERROR; + if (status) *status = "Unable to start TLS"; + break; + } + else tls_done = 1; + } + } while (code < 200); + + switch (code) { + default: /* Failure */ + if (!r) { + r = HTTP_BAD_GATEWAY; + if (status) { + buf_reset(&buf); + buf_printf(&buf, + "Unexpected status code from backend: %u", code); + *status = buf_cstring(&buf); + } + } + break; + + case 426: /* Upgrade Required */ + if (tls_done) { + r = HTTP_BAD_GATEWAY; + if (status) *status = "TLS already active"; + } + else need_tls = 1; + break; + + case 200: /* OK */ + if (scheme->recv_success && + (serverin = scheme->recv_success(hdrs))) { + serverinlen = strlen(serverin); + } + /* Fall through and process any success data */ + + case 401: /* Unauthorized */ + if (!serverin) { + int i = 0; + + hdr = spool_getheader(hdrs, "WWW-Authenticate"); + + if (!scheme) { + unsigned avail_auth_schemes = 0; + const char *mech = NULL; + size_t len; + + /* Compare authentication schemes offered in + * WWW-Authenticate header(s) to what we support */ + buf_reset(&buf); + for (i = 0; hdr && hdr[i]; i++) { + len = strcspn(hdr[i], " "); + + for (scheme = auth_schemes; scheme->name; scheme++) { + if (!strncmp(scheme->name, hdr[i], len) && + !((scheme->flags & AUTH_NEED_PERSIST) && + (resp_body.flags & BODY_CLOSE))) { + /* Tag the scheme as available */ + avail_auth_schemes |= (1 << scheme->idx); + + /* Add SASL-based schemes to SASL mech list */ + if (scheme->saslmech) { + if (buf_len(&buf)) buf_putc(&buf, ' '); + buf_appendcstr(&buf, scheme->saslmech); + } + break; + } + } + } + + /* If we have a mech_conf, use it */ + if (mech_conf && buf_len(&buf)) { + char *conf = xstrdup(mech_conf); + char *newmechlist = + intersect_mechlists(conf, + (char *) buf_cstring(&buf)); + + if (newmechlist) { + buf_setcstr(&buf, newmechlist); + free(newmechlist); + } + else { + syslog(LOG_DEBUG, "%s did not offer %s", + s->hostname, mech_conf); + buf_reset(&buf); + } + free(conf); + } + +#ifdef SASL_HTTP_REQUEST + /* Set HTTP request as specified above (REQUIRED) */ + httpreq.non_persist = (resp_body.flags & BODY_CLOSE); + sasl_setprop(s->saslconn, SASL_HTTP_REQUEST, &httpreq); +#endif + + /* Try to start SASL exchange using available mechs */ + r = sasl_client_start(s->saslconn, buf_cstring(&buf), + NULL, /* no prompts */ + NULL, NULL, /* no initial resp */ + &mech); + + if (mech) { + /* Find auth scheme associated with chosen SASL mech */ + for (scheme = auth_schemes; scheme->name; scheme++) { + if (scheme->saslmech && + !strcmp(scheme->saslmech, mech)) break; + } + } + else { + /* No matching SASL mechs - try Basic */ + scheme = &auth_schemes[AUTH_BASIC]; + if (!(avail_auth_schemes & (1 << scheme->idx))) { + need_tls = !tls_done; + break; /* case 401 */ + } + } + + /* Find the associated WWW-Authenticate header */ + for (i = 0; hdr && hdr[i]; i++) { + len = strcspn(hdr[i], " "); + if (!strncmp(scheme->name, hdr[i], len)) break; + } + } + + /* Get server challenge, if any */ + if (hdr) { + const char *p = strchr(hdr[i], ' '); + serverin = p ? ++p : ""; + serverinlen = strlen(serverin); + } + } + + if (serverin) { + /* Perform the next step in the auth exchange */ + + if (scheme->idx == AUTH_BASIC) { + /* Don't care about "realm" in server challenge */ + const char *authid = + callback_getdata(s->saslconn, cb, SASL_CB_AUTHNAME); + pass = callback_getdata(s->saslconn, cb, SASL_CB_PASS); + + buf_reset(&buf); + buf_printf(&buf, "%s:%s", authid, pass); + clientout = buf_cstring(&buf); + clientoutlen = buf_len(&buf); + } + else { + /* Base64 decode any server challenge, if necessary */ + if (serverin && (scheme->flags & AUTH_BASE64)) { + r = sasl_decode64(serverin, serverinlen, + base64, BASE64_BUF_SIZE, &serverinlen); + if (r != SASL_OK) break; /* case 401 */ + + serverin = base64; + } + + /* SASL mech (Digest, Negotiate, NTLM) */ + r = sasl_client_step(s->saslconn, serverin, serverinlen, + NULL, /* no prompts */ + &clientout, &clientoutlen); + } + } + break; /* case 401 */ + } + + } while (need_tls || clientout); + + done: + if (hdrs) spool_free_hdrcache(hdrs); + + if (r && status && !*status) *status = sasl_errstring(r, NULL, NULL); + + return r; +} + + +static int ping(struct backend *s, const char *userid) +{ + unsigned code = 0; + const char *errstr; + hdrcache_t resp_hdrs = NULL; + struct body_t resp_body; + + /* Send Authorization request to server */ + prot_puts(s->out, "OPTIONS * HTTP/1.1\r\n"); + prot_printf(s->out, "Host: %s\r\n", s->hostname); + prot_printf(s->out, "User-Agent: %s\r\n", buf_cstring(&serverinfo)); + prot_printf(s->out, "Authorize-As: %s\r\n", userid ? userid : "anonymous"); + prot_puts(s->out, "\r\n"); + prot_flush(s->out); + + /* Read response(s) from backend until final response or error */ + do { + resp_body.flags = BODY_DISCARD; + if (http_read_response(s, METH_OPTIONS, &code, NULL, + &resp_hdrs, &resp_body, &errstr)) { + break; + } + } while (code < 200); + + if (resp_hdrs) spool_free_hdrcache(resp_hdrs); + + return (code != 200); +} + + +static int logout(struct backend *s __attribute__((unused))) +{ + /* Nothing to send, client just closes connection */ + return 0; +} + + +/* proxy mboxlist_lookup; on misses, it asks the listener for this + * machine to make a roundtrip to the master mailbox server to make + * sure it's up to date + */ +EXPORTED int http_mlookup(const char *name, mbentry_t **mbentryp, void *tid) +{ + mbentry_t *mbentry = NULL; + int r; + + r = mboxlist_lookup(name, &mbentry, tid); + if (r == IMAP_MAILBOX_NONEXISTENT && config_mupdate_server) { + kick_mupdate(); + r = mboxlist_lookup(name, &mbentry, tid); + } + if (r) return r; + if (mbentry->mbtype & MBTYPE_RESERVE) { + r = IMAP_MAILBOX_RESERVED; + goto done; + } + if (mbentry->mbtype & MBTYPE_MOVING) { + r = IMAP_MAILBOX_MOVED; + goto done; + } + if (mbentry->mbtype & MBTYPE_DELETED) { + r = IMAP_MAILBOX_NONEXISTENT; + goto done; + } + +done: + if (!r && mbentryp) *mbentryp = mbentry; + else mboxlist_entry_free(&mbentry); + return r; +} + + +/* Fetch protocol and host used for request from headers */ +EXPORTED void http_proto_host(hdrcache_t req_hdrs, const char **proto, const char **host) +{ + const char **fwd; + + if (config_mupdate_server && config_getstring(IMAPOPT_PROXYSERVERS) && + (fwd = spool_getheader(req_hdrs, "Forwarded"))) { + /* Proxied request - parse last Forwarded header for proto and host */ + /* XXX This is destructive of the header but we don't care + * and more importantly, we need the tokens available after tok_fini() + */ + tok_t tok; + char *token; + + while (fwd[1]) ++fwd; /* Skip to last Forwarded header */ + + tok_initm(&tok, (char *) fwd[0], ";", 0); + while ((token = tok_next(&tok))) { + if (proto && !strncmp(token, "proto=", 6)) *proto = token+6; + else if (host && !strncmp(token, "host=", 5)) *host = token+5; + } + tok_fini(&tok); + } + else { + /* Use our protocol and host */ + if (proto) *proto = https ? "https" : "http"; + if (host) *host = *spool_getheader(req_hdrs, "Host"); + } +} + +/* Construct and write Via header to protstream. */ +static void write_forwarding_hdrs(struct protstream *pout, hdrcache_t hdrs, + const char *version, const char *proto) +{ + const char **via = spool_getheader(hdrs, "Via"); + const char **fwd = spool_getheader(hdrs, "Forwarded"); + + /* Add any existing Via headers */ + for (; via && *via; via++) prot_printf(pout, "Via: %s\r\n", *via); + + /* Create our own Via header */ + prot_printf(pout, "Via: %s %s", version+5, config_servername); + if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) { + prot_printf(pout, " (Cyrus/%s)", cyrus_version()); + } + prot_puts(pout, "\r\n"); + + /* Add any existing Forwarded headers */ + for (; fwd && *fwd; fwd++) prot_printf(pout, "Forwarded: %s\r\n", *fwd); + + /* Create our own Forwarded header */ + if (proto) { + char localip[60], remoteip[60], *p; + socklen_t salen = sizeof(httpd_remoteaddr); + const char **host = spool_getheader(hdrs, "Host"); + + prot_printf(pout, "Forwarded: proto=%s", proto); + if (host) prot_printf(pout, ";host=%s", *host); + if (!iptostring((struct sockaddr *)&httpd_remoteaddr, salen, + remoteip, 60)) { + if ((p = strrchr(remoteip, ';'))) *p = '\0'; + prot_printf(pout, ";for=%s", remoteip); + } + if (!iptostring((struct sockaddr *)&httpd_localaddr, salen, + localip, 60)) { + if ((p = strrchr(localip, ';'))) *p = '\0'; + prot_printf(pout, ";by=%s", localip); + } + prot_puts(pout, "\r\n"); + } +} + + +/* Write end-to-end header (ignoring hop-by-hop) from cache to protstream. */ +static void write_cachehdr(const char *name, const char *contents, void *rock) +{ + struct protstream *pout = (struct protstream *) rock; + const char **hdr, *hop_by_hop[] = + { "authorization", "connection", "content-length", "expect", + "forwarded", "host", "keep-alive", "strict-transport-security", + "te", "trailer", "transfer-encoding", "upgrade", "via", NULL }; + + /* Ignore private headers in our cache */ + if (name[0] == ':') return; + + for (hdr = hop_by_hop; *hdr && strcmp(name, *hdr); hdr++); + + if (!*hdr) { + if (!strcmp(name, "max-forwards")) { + /* Decrement Max-Forwards before forwarding */ + unsigned long max = strtoul(contents, NULL, 10); + + prot_printf(pout, "Max-Forwards: %lu\r\n", max-1); + } + else { + prot_printf(pout, "%c%s: %s\r\n", toupper(*name), name+1, contents); + } + } +} + + +/* Send a cached response to the client */ +static void send_response(const char *statline, hdrcache_t hdrs, + struct buf *body, struct txn_flags_t *flags) +{ + unsigned long len; + + /* Stop method processing alarm */ + alarm(0); + + /* + * - Use cached Status Line + * - Add/append-to Via: header + * - Add our own hop-by-hop headers + * - Use all cached end-to-end headers + */ + prot_puts(httpd_out, statline); + write_forwarding_hdrs(httpd_out, hdrs, HTTP_VERSION, NULL); + if (flags->conn) { + /* Construct Connection header */ + const char *conn_tokens[] = + { "close", "Upgrade", "Keep-Alive", NULL }; + + if (flags->conn & CONN_KEEPALIVE) { + prot_printf(httpd_out, "Keep-Alive: timeout=%d\r\n", httpd_timeout); + } + + comma_list_hdr("Connection", conn_tokens, flags->conn); + } + if (httpd_tls_done) { + prot_puts(httpd_out, "Strict-Transport-Security: max-age=600\r\n"); + } + + spool_enum_hdrcache(hdrs, &write_cachehdr, httpd_out); + + if (!body || !(len = buf_len(body))) { + /* Empty body -- use payload headers from response, if any */ + const char **hdr; + + if (!flags->ver1_0 && + (hdr = spool_getheader(hdrs, "Transfer-Encoding"))) { + prot_printf(httpd_out, "Transfer-Encoding: %s\r\n", hdr[0]); + if ((hdr = spool_getheader(hdrs, "Trailer"))) { + prot_printf(httpd_out, "Trailer: %s\r\n", hdr[0]); + } + } + else if ((hdr = spool_getheader(hdrs, "Content-Length"))) { + prot_printf(httpd_out, "Content-Length: %s\r\n", hdr[0]); + } + + prot_puts(httpd_out, "\r\n"); + } + else { + /* Body is buffered, so send using "identity" TE */ + prot_printf(httpd_out, "Content-Length: %lu\r\n\r\n", len); + prot_putbuf(httpd_out, body); + } +} + + +/* Proxy (pipe) a chunk of body data to a client/server. */ +static unsigned pipe_chunk(struct protstream *pin, struct protstream *pout, + unsigned len) +{ + char buf[PROT_BUFSIZE]; + unsigned n = 0; + + /* Read 'len' octets */ + for (; len; len -= n) { + n = prot_read(pin, buf, MIN(len, PROT_BUFSIZE)); + if (!n) break; + + prot_write(pout, buf, n); + } + + return n; +} + + +/* Proxy (pipe) a response body to a client/server. */ +static int pipe_resp_body(struct protstream *pin, struct protstream *pout, + hdrcache_t resp_hdrs, struct body_t *resp_body, + int ver1_0, const char **errstr) +{ + char buf[PROT_BUFSIZE]; + + if (resp_body->framing == FRAMING_UNKNOWN) { + /* Get message framing */ + int r = http_parse_framing(resp_hdrs, resp_body, errstr); + if (r) return r; + } + + /* Read and pipe the body */ + switch (resp_body->framing) { + case FRAMING_LENGTH: + /* Read 'len' octets */ + if (resp_body->len && !pipe_chunk(pin, pout, resp_body->len)) { + syslog(LOG_ERR, "prot_read() error"); + *errstr = "Unable to read body data"; + return HTTP_BAD_GATEWAY; + } + break; + + case FRAMING_CHUNKED: { + unsigned chunk; + char *c; + + /* Read chunks until last-chunk (zero chunk-size) */ + do { + /* Read chunk-size */ + prot_NONBLOCK(pin); + c = prot_fgets(buf, PROT_BUFSIZE, pin); + prot_BLOCK(pin); + if (!c) { + prot_flush(pout); + c = prot_fgets(buf, PROT_BUFSIZE, pin); + } + if (!c || sscanf(buf, "%x", &chunk) != 1) { + *errstr = "Unable to read chunk size"; + return HTTP_BAD_GATEWAY; + + /* XXX Do we need to parse chunk-ext? */ + } + else if (chunk > resp_body->max - resp_body->len) + return HTTP_TOO_LARGE; + else if (!ver1_0) prot_puts(pout, buf); + + if (chunk) { + /* Read 'chunk' octets */ + if (!pipe_chunk(pin, pout, chunk)) { + syslog(LOG_ERR, "prot_read() error"); + *errstr = "Unable to read chunk data"; + return HTTP_BAD_GATEWAY; + } + } + else { + /* Read any trailing headers */ + for (*c = prot_ungetc(prot_getc(pin), pin); + *c != '\r' && *c != '\n'; + *c = prot_ungetc(prot_getc(pin), pin)) { + if (!prot_fgets(buf, sizeof(buf), pin)) { + *errstr = "Error reading trailer"; + return HTTP_BAD_GATEWAY; + } + else if (!ver1_0) prot_puts(pout, buf); + } + } + + + /* Read CRLF terminating the chunk/trailer */ + if (!prot_fgets(buf, sizeof(buf), pin)) { + *errstr = "Missing CRLF following chunk/trailer"; + return HTTP_BAD_GATEWAY; + } + else if (!ver1_0) prot_puts(pout, buf); + + } while (chunk); + + break; + } + + case FRAMING_CLOSE: + /* Read until EOF */ + if (pipe_chunk(pin, pout, UINT_MAX) || !pin->eof) + return HTTP_BAD_GATEWAY; + + break; + + default: + /* XXX Should never get here */ + *errstr = "Unknown length of body data"; + return HTTP_BAD_GATEWAY; + } + + return 0; +} + + + +/* Proxy (pipe) a client-request/server-response to/from a backend. */ +EXPORTED int http_pipe_req_resp(struct backend *be, struct transaction_t *txn) +{ + int r = 0, sent_body = 0; + xmlChar *uri; + unsigned code; + const char **hdr, *statline; + hdrcache_t resp_hdrs = NULL; + struct body_t resp_body; + + /* + * Send client request to backend: + * + * - Piece the Request Line back together + * - Add/append-to Via: header + * - Add Expect:100-continue header (for synchonicity) + * - Use all cached end-to-end headers from client + * - Body will be sent using "chunked" TE, since we might not have it yet + */ + uri = xmlURIEscapeStr(BAD_CAST txn->req_uri->path, BAD_CAST "/"); + prot_printf(be->out, "%s %s", txn->req_line.meth, uri); + free(uri); + if (URI_QUERY(txn->req_uri)) { + prot_printf(be->out, "?%s", URI_QUERY(txn->req_uri)); + } + prot_printf(be->out, " %s\r\n", HTTP_VERSION); + prot_printf(be->out, "Host: %s\r\n", be->hostname); + write_forwarding_hdrs(be->out, txn->req_hdrs, txn->req_line.ver, + https ? "https" : "http"); + spool_enum_hdrcache(txn->req_hdrs, &write_cachehdr, be->out); + if ((hdr = spool_getheader(txn->req_hdrs, "TE"))) { + for (; *hdr; hdr++) prot_printf(be->out, "TE: %s\r\n", *hdr); + } + if (http_methods[txn->meth].flags & METH_NOBODY) + prot_puts(be->out, "Content-Length: 0\r\n"); + else if (spool_getheader(txn->req_hdrs, "Transfer-Encoding") || + spool_getheader(txn->req_hdrs, "Content-Length")) { + prot_puts(be->out, "Expect: 100-continue\r\n"); + prot_puts(be->out, "Transfer-Encoding: chunked\r\n"); + } + prot_puts(be->out, "\r\n"); + prot_flush(be->out); + + /* Read response(s) from backend until final response or error */ + memset(&resp_body, 0, sizeof(struct body_t)); + + do { + r = http_read_response(be, txn->meth, &code, &statline, + &resp_hdrs, NULL, &txn->error.desc); + if (r) break; + + if (code == 100) { /* Continue */ + if (!sent_body++) { + unsigned len; + + /* Read body from client */ + r = http_read_body(httpd_in, httpd_out, txn->req_hdrs, + &txn->req_body, &txn->error.desc); + if (r) { + /* Couldn't get the body and can't finish request */ + txn->flags.conn = CONN_CLOSE; + break; + } + + /* Send single-chunk body to backend to complete the request */ + if ((len = buf_len(&txn->req_body.payload))) { + prot_printf(be->out, "%x\r\n", len); + prot_putbuf(be->out, &txn->req_body.payload); + prot_puts(be->out, "\r\n"); + } + prot_puts(be->out, "0\r\n\r\n"); + prot_flush(be->out); + } + else { + prot_puts(httpd_out, statline); + spool_enum_hdrcache(resp_hdrs, &write_cachehdr, httpd_out); + prot_puts(httpd_out, "\r\n"); + prot_flush(httpd_out); + } + } + } while (code < 200); + + if (r) proxy_downserver(be); + else if (code == 401) { + /* Don't pipe a 401 response (discard body). + Frontend should send its own 401 since it will process auth */ + resp_body.flags |= BODY_DISCARD; + http_read_body(be->in, httpd_out, + resp_hdrs, &resp_body, &txn->error.desc); + + r = HTTP_UNAUTHORIZED; + } + else { + /* Send response to client */ + send_response(statline, resp_hdrs, NULL, &txn->flags); + + /* Not expecting a body for 204/304 response or any HEAD response */ + switch (code) { + case 204: /* No Content */ + case 304: /* Not Modified */ + break; + + default: + if (txn->meth == METH_HEAD) break; + + if (pipe_resp_body(be->in, httpd_out, resp_hdrs, &resp_body, + txn->flags.ver1_0, &txn->error.desc)) { + /* Couldn't pipe the body and can't finish response */ + txn->flags.conn = CONN_CLOSE; + } + } + } + + if (resp_body.flags & BODY_CLOSE) proxy_downserver(be); + + if (resp_hdrs) spool_free_hdrcache(resp_hdrs); + + return r; +} + + +/* + * Proxy a COPY/MOVE client-request when the source and destination are + * on different backends. This is handled as a GET from the source and + * PUT on the destination, while obeying any Overwrite header. + * + * For a MOVE request, we also LOCK, DELETE, and possibly UNLOCK the source. + * + * XXX This function buffers the response bodies of the LOCK & GET requests. + * The response body of the PUT request is piped to the client. + */ +EXPORTED int http_proxy_copy(struct backend *src_be, struct backend *dest_be, + struct transaction_t *txn) +{ + int r = 0, sent_body; + unsigned code; + char *lock = NULL; + const char **hdr, *statline; + hdrcache_t resp_hdrs = NULL; + struct body_t resp_body; + +#define write_hdr(pout, name, hdrs) \ + if ((hdr = spool_getheader(hdrs, name))) \ + for (; *hdr; hdr++) prot_printf(pout, "%s: %s\r\n", name, *hdr) + + + resp_body.payload = txn->resp_body.payload; + + if (txn->meth == METH_MOVE) { + /* + * Send a LOCK request to source backend: + * + * - Use any relevant conditional headers specified by client + */ + prot_printf(src_be->out, "LOCK %s %s\r\n" + "Host: %s\r\n" + "User-Agent: %s\r\n", + txn->req_tgt.path, HTTP_VERSION, + src_be->hostname, buf_cstring(&serverinfo)); + write_hdr(src_be->out, "If", txn->req_hdrs); + write_hdr(src_be->out, "If-Match", txn->req_hdrs); + write_hdr(src_be->out, "If-Unmodified-Since", txn->req_hdrs); + write_hdr(src_be->out, "If-Schedule-Tag-Match", txn->req_hdrs); + + assert(!buf_len(&txn->buf)); + buf_printf_markup(&txn->buf, 0, + "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"); + buf_printf_markup(&txn->buf, 0, "<D:lockinfo xmlns:D='DAV:'>"); + buf_printf_markup(&txn->buf, 1, + "<D:lockscope><D:exclusive/></D:lockscope>"); + buf_printf_markup(&txn->buf, 1, + "<D:locktype><D:write/></D:locktype>"); + buf_printf_markup(&txn->buf, 1, "<D:owner>%s</D:owner>", httpd_userid); + buf_printf_markup(&txn->buf, 0, "</D:lockinfo>"); + + prot_printf(src_be->out, + "Content-Type: application/xml; charset=utf-8\r\n" + "Content-Length: %u\r\n\r\n%s", + (unsigned)buf_len(&txn->buf), buf_cstring(&txn->buf)); + buf_reset(&txn->buf); + + prot_flush(src_be->out); + + /* Read response(s) from source backend until final response or error */ + resp_body.flags = 0; + + do { + r = http_read_response(src_be, METH_LOCK, &code, &statline, + &resp_hdrs, &resp_body, &txn->error.desc); + if (r) { + proxy_downserver(src_be); + goto done; + } + } while (code < 200); + + /* Get lock token */ + if ((hdr = spool_getheader(resp_hdrs, "Lock-Token"))) + lock = xstrdup(*hdr); + + switch (code) { + case 200: + /* Success, continue */ + break; + + case 201: + /* Created empty resource, treat as 404 (Not Found) */ + r = HTTP_NOT_FOUND; + goto delete; + + case 409: + /* Failed to create resource, treat as 404 (Not Found) */ + r = HTTP_NOT_FOUND; + goto done; + + default: + /* Send failure response to client */ + send_response(statline, resp_hdrs, &resp_body.payload, &txn->flags); + goto done; + } + } + + + /* + * Send a GET request to source backend to fetch body: + * + * - Use any relevant conditional headers specified by client + * (if not already sent in LOCK request) + */ + prot_printf(src_be->out, "GET %s %s\r\n" + "Host: %s\r\n" + "User-Agent: %s\r\n", + txn->req_tgt.path, HTTP_VERSION, + src_be->hostname, buf_cstring(&serverinfo)); + if (txn->meth != METH_MOVE) { + write_hdr(src_be->out, "If", txn->req_hdrs); + write_hdr(src_be->out, "If-Match", txn->req_hdrs); + write_hdr(src_be->out, "If-Unmodified-Since", txn->req_hdrs); + write_hdr(src_be->out, "If-Schedule-Tag-Match", txn->req_hdrs); + } + prot_puts(src_be->out, "\r\n"); + prot_flush(src_be->out); + + /* Read response(s) from source backend until final response or error */ + resp_body.flags = 0; + + do { + r = http_read_response(src_be, METH_GET, &code, &statline, + &resp_hdrs, &resp_body, &txn->error.desc); + if (r || (resp_body.flags & BODY_CLOSE)) { + proxy_downserver(src_be); + goto done; + } + } while (code < 200); + + if (code != 200) { + /* Send failure response to client */ + send_response(statline, resp_hdrs, &resp_body.payload, &txn->flags); + goto done; + } + + + /* + * Send a synchonizing PUT request to dest backend: + * + * - Add Expect:100-continue header (for synchonicity) + * - Obey Overwrite by adding If-None-Match header + * - Use any TE, Prefer, Accept* headers specified by client + * - Use Content-Type, -Encoding, -Language headers from GET response + * - Body is buffered, so send using "identity" TE + */ + prot_printf(dest_be->out, "PUT %s %s\r\n" + "Host: %s\r\n" + "User-Agent: %s\r\n" + "Expect: 100-continue\r\n", + *spool_getheader(txn->req_hdrs, "Destination"), HTTP_VERSION, + dest_be->hostname, buf_cstring(&serverinfo)); + hdr = spool_getheader(txn->req_hdrs, "Overwrite"); + if (hdr && !strcmp(*hdr, "F")) + prot_puts(dest_be->out, "If-None-Match: *\r\n"); + write_hdr(dest_be->out, "TE", txn->req_hdrs); + write_hdr(dest_be->out, "Prefer", txn->req_hdrs); + write_hdr(dest_be->out, "Accept", txn->req_hdrs); + write_hdr(dest_be->out, "Accept-Charset", txn->req_hdrs); + write_hdr(dest_be->out, "Accept-Encoding", txn->req_hdrs); + write_hdr(dest_be->out, "Accept-Language", txn->req_hdrs); + write_hdr(dest_be->out, "Content-Type", resp_hdrs); + write_hdr(dest_be->out, "Content-Encoding", resp_hdrs); + write_hdr(dest_be->out, "Content-Language", resp_hdrs); + prot_printf(dest_be->out, "Content-Length: %u\r\n\r\n", + (unsigned)buf_len(&resp_body.payload)); + prot_flush(dest_be->out); + + /* Read response(s) from dest backend until final response or error */ + sent_body = 0; + + do { + r = http_read_response(dest_be, METH_PUT, &code, &statline, + &resp_hdrs, NULL, &txn->error.desc); + if (r) { + proxy_downserver(dest_be); + goto done; + } + + if ((code == 100) /* Continue */ && !sent_body++) { + /* Send body to dest backend to complete the PUT */ + prot_putbuf(dest_be->out, &resp_body.payload); + prot_flush(dest_be->out); + } + } while (code < 200); + + /* Send response to client */ + send_response(statline, resp_hdrs, NULL, &txn->flags); + if (code != 204) { + resp_body.framing = FRAMING_UNKNOWN; + if (pipe_resp_body(dest_be->in, httpd_out, resp_hdrs, &resp_body, + 0, &txn->error.desc)) { + /* Couldn't pipe the body and can't finish response */ + txn->flags.conn = CONN_CLOSE; + proxy_downserver(dest_be); + goto done; + } + } + + + delete: + if ((txn->meth == METH_MOVE) && (code < 300)) { + /* + * Send a DELETE request to source backend: + * + * - Add If header with lock token + */ + prot_printf(src_be->out, "DELETE %s %s\r\n" + "Host: %s\r\n" + "User-Agent: %s\r\n", + txn->req_tgt.path, HTTP_VERSION, + src_be->hostname, buf_cstring(&serverinfo)); + if (lock) prot_printf(src_be->out, "If: (%s)\r\n", lock); + prot_puts(src_be->out, "\r\n"); + prot_flush(src_be->out); + + /* Read response(s) from source backend until final resp or error */ + resp_body.flags = BODY_DISCARD; + + do { + if (http_read_response(src_be, METH_DELETE, &code, NULL, + &resp_hdrs, &resp_body, &txn->error.desc) + || (resp_body.flags & BODY_CLOSE)) { + proxy_downserver(src_be); + break; + } + } while (code < 200); + + if (code < 300 && lock) { + free(lock); + lock = NULL; + } + } + + + done: + if (lock) { + /* + * Something failed - Send an UNLOCK request to source backend: + */ + prot_printf(src_be->out, "UNLOCK %s %s\r\n" + "Host: %s\r\n" + "User-Agent: %s\r\n" + "Lock-Token: %s\r\n\r\n", + txn->req_tgt.path, HTTP_VERSION, + src_be->hostname, buf_cstring(&serverinfo), lock); + prot_flush(src_be->out); + + /* Read response(s) from source backend until final resp or error */ + resp_body.flags = BODY_DISCARD; + + do { + if (http_read_response(src_be, METH_UNLOCK, &code, NULL, + &resp_hdrs, &resp_body, &txn->error.desc)) { + proxy_downserver(src_be); + break; + } + } while (code < 200); + + free(lock); + } + + txn->resp_body.payload = resp_body.payload; + if (resp_hdrs) spool_free_hdrcache(resp_hdrs); + + return r; +}
View file
cyrus-imapd-2.5.tar.gz/imap/http_proxy.h
Added
@@ -0,0 +1,59 @@ +/* http_proxy.h - HTTP proxy support functions + * + * Copyright (c) 1994-2011 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 _HTTP_PROXY_H +#define _HTTP_PROXY_H + +#include "backend.h" + + +extern struct protocol_t http_protocol; + +extern int http_mlookup(const char *name, mbentry_t **mbentryp, void *tid); +extern void http_proto_host(hdrcache_t req_hdrs, + const char **proto, const char **host); +extern int http_pipe_req_resp(struct backend *be, struct transaction_t *txn); +extern int http_proxy_copy(struct backend *src_be, struct backend *dest_be, + struct transaction_t *txn); + +#endif /* _HTTP_PROXY_H */
View file
cyrus-imapd-2.5.tar.gz/imap/http_rss.c
Added
@@ -0,0 +1,1327 @@ +/* http_rss.c -- Routines for handling RSS feeds of mailboxes in httpd + * + * Copyright (c) 1994-2011 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 "acl.h" +#include "annotate.h" +#include "charset.h" +#include "global.h" +#include "httpd.h" +#include "http_err.h" +#include "http_proxy.h" +#include "imap_err.h" +#include "mailbox.h" +#include "map.h" +#include "mboxlist.h" +#include "message.h" +#include "parseaddr.h" +#include "proxy.h" +#include "times.h" +#include "seen.h" +#include "tok.h" +#include "util.h" +#include "version.h" +#include "wildmat.h" +#include "xmalloc.h" +#include "xstrlcpy.h" + +#define XML_NS_ATOM "http://www.w3.org/2005/Atom" +#define GUID_URL_SCHEME "data:," +#define MAX_SECTION_LEN 128 +#define FEEDLIST_VAR "%RSS_FEEDLIST%" + +static const char def_template[] = + HTML_DOCTYPE + "<html>\n<head>\n<title>Cyrus RSS Feeds</title>\n</head>\n" + "<body>\n<h2>Cyrus RSS Feeds</h2>\n" + FEEDLIST_VAR + "</body>\n</html>\n"; + +static time_t compile_time; +static void rss_init(struct buf *serverinfo); +static int meth_get(struct transaction_t *txn, void *params); +static int rss_parse_path(const char *path, + struct request_target_t *tgt, const char **errstr); +static int is_feed(const char *mbox); +static int list_feeds(struct transaction_t *txn); +static int fetch_message(struct transaction_t *txn, struct mailbox *mailbox, + unsigned recno, uint32_t uid, + struct index_record *record, struct body **body, + struct buf *msg_buf); +static int list_messages(struct transaction_t *txn, struct mailbox *mailbox); +static void display_message(struct transaction_t *txn, + const char *mboxname, const struct index_record *record, + struct body *body, const struct buf *msg_buf); +static void fetch_part(struct transaction_t *txn, struct body *body, + const char *findsection, const char *cursection, + struct buf *msg_buf); + + +/* Namespace for RSS feeds of mailboxes */ +struct namespace_t namespace_rss = { + URL_NS_RSS, 0, "/rss", NULL, 1 /* auth */, ALLOW_READ, + rss_init, NULL, NULL, NULL, + { + { NULL, NULL }, /* ACL */ + { 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, &rss_parse_path }, /* OPTIONS */ + { NULL, NULL }, /* POST */ + { NULL, NULL }, /* PROPFIND */ + { NULL, NULL }, /* PROPPATCH */ + { NULL, NULL }, /* PUT */ + { NULL, NULL }, /* REPORT */ + { &meth_trace, &rss_parse_path }, /* TRACE */ + { NULL, NULL } /* UNLOCK */ + } +}; + + +static void rss_init(struct buf *serverinfo __attribute__((unused))) +{ + namespace_rss.enabled = config_httpmodules & IMAP_ENUM_HTTPMODULES_RSS; + + if (!namespace_rss.enabled) return; + + compile_time = calc_compile_time(__TIME__, __DATE__); +} + +/* Perform a GET/HEAD request */ +static int meth_get(struct transaction_t *txn, + void *params __attribute__((unused))) +{ + int ret = 0, r, rights; + struct strlist *param; + char *section = NULL; + uint32_t uid = 0; + struct mailbox *mailbox = NULL; + mbentry_t *mbentry = NULL; + + /* Construct mailbox name corresponding to request target URI */ + if ((r = rss_parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) { + txn->error.desc = error_message(r); + return HTTP_NOT_FOUND; + } + + /* If no mailboxname, list all available feeds */ + if (!*txn->req_tgt.mboxname) return list_feeds(txn); + + /* Make sure its a mailbox that we are treating as an RSS feed */ + if (!is_feed(txn->req_tgt.mboxname)) return HTTP_NOT_FOUND; + + /* Locate the mailbox */ + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Check ACL for current user */ + rights = mbentry ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0; + if (!(rights & ACL_READ)) return HTTP_NO_PRIVS; + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + mboxlist_entry_free(&mbentry); + + /* Local Mailbox */ + + /* Open mailbox for reading */ + r = mailbox_open_irl(txn->req_tgt.mboxname, &mailbox); + if (r) { + syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + /* Check query params, if any */ + param = hash_lookup("uid", &txn->req_qparams); + if (param) { + uid = strtoul(param->s, NULL, 10); + if (!uid) uid = -1; + + param = hash_lookup("section", &txn->req_qparams); + if (param) section = param->s; + } + + /* If no UID specified, list messages as an RSS feed */ + if (!uid) ret = list_messages(txn, mailbox); + else if (uid > mailbox->i.last_uid) { + txn->error.desc = "Message does not exist\r\n"; + ret = HTTP_NOT_FOUND; + } + else { + struct index_record record; + struct buf msg_buf = BUF_INITIALIZER; + struct body *body = NULL; + + /* Fetch the message */ + if (!(ret = fetch_message(txn, mailbox, 0, uid, + &record, &body, &msg_buf))) { + int precond; + const char *etag = NULL; + time_t lastmod = 0; + struct resp_body_t *resp_body = &txn->resp_body; + + /* Check any preconditions */ + if (section && !strcmp(section, "0")) { + /* Entire raw message */ + txn->flags.ranges = 1; + } + + etag = message_guid_encode(&record.guid); + lastmod = record.internaldate; + precond = check_precond(txn, NULL, etag, lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_PARTIAL: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, and Expires */ + resp_body->etag = etag; + resp_body->lastmod = lastmod; + resp_body->maxage = 31536000; /* 1 year */ + txn->flags.cc |= CC_MAXAGE; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + if (!section) { + /* Return entire message formatted as text/html */ + display_message(txn, mailbox->name, &record, body, &msg_buf); + } + else if (!strcmp(section, "0")) { + /* Return entire message as text/plain */ + resp_body->type = "text/plain"; + write_body(precond, txn, buf_base(&msg_buf), buf_len(&msg_buf)); + } + else { + /* Fetch, decode, and return the specified MIME message part */ + fetch_part(txn, body, section, "1", &msg_buf); + } + + done: + buf_free(&msg_buf); + + if (body) { + message_free_body(body); + free(body); + } + } + } + + mailbox_close(&mailbox); + + return ret; + +} + + +/* Create a mailbox name from the request URL */ +static int rss_parse_path(const char *path, + struct request_target_t *tgt, + const char **errstr __attribute__((unused))) +{ + const char *start, *end; + size_t len; + + /* Clip off RSS prefix */ + start = path + strlen(namespace_rss.prefix); + if (*start == '/') start++; + end = start + strlen(start); + + if ((end > start) && (end[-1] == '/')) end--; + + len = end - start; + if (len > MAX_MAILBOX_BUFFER) return IMAP_MAILBOX_BADNAME; + + strncpy(tgt->mboxname, start, len); + tgt->mboxname[len] = '\0'; + + mboxname_hiersep_tointernal(&httpd_namespace, tgt->mboxname, len); + + return 0; +} + + +/* + * Checks to make sure that the given mailbox is actually something + * that we're treating as an RSS feed. Returns 1 if yes, 0 if no. + */ +static int is_feed(const char *mbox) +{ + static struct wildmat *feeds = NULL; + struct wildmat *wild; + + if (!feeds) { + feeds = split_wildmats((char *) config_getstring(IMAPOPT_RSS_FEEDS), + NULL); + } + + /* check mailbox against the 'rss_feeds' wildmat */ + wild = feeds; + while (wild->pat && wildmat(mbox, wild->pat) != 1) wild++; + + /* if we don't have a match, or its a negative match, don't use it */ + if (!wild->pat || wild->not) return 0; + + /* otherwise, its usable */ + return 1; +} + + +/* + * mboxlist_findall() callback function to list RSS feeds as a tree + */ +struct node { + char name[MAX_MAILBOX_BUFFER]; + size_t len; + struct node *parent; + struct node *child; +}; + +struct list_rock { + struct transaction_t *txn; + struct node *last; +}; + +static int list_cb(char *name, int matchlen, int maycreate, void *rock) +{ + struct list_rock *lrock = (struct list_rock *) rock; + struct node *last = lrock->last; + struct buf *buf = &lrock->txn->resp_body.payload; + + if (name) { + mbentry_t *mbentry = NULL; + int r; + + /* Don't list mailboxes that we don't treat as RSS feeds */ + if (!is_feed(name)) return 0; + + /* Don't list deleted mailboxes */ + if (mboxname_isdeletedmailbox(name, NULL)) return 0; + + /* Lookup the mailbox and make sure its readable */ + r = http_mlookup(name, &mbentry, NULL); + if (r || !mbentry->acl || !(cyrus_acl_myrights(httpd_authstate, mbentry->acl) & ACL_READ)) { + mboxlist_entry_free(&mbentry); + return 0; + } + mboxlist_entry_free(&mbentry); + } + + if (name && + !strncmp(name, last->name, last->len) && + (!last->len || (name[last->len] == '.'))) { + /* Found closest ancestor of 'name' */ + struct node *node; + size_t len = matchlen; + char shortname[MAX_MAILBOX_NAME+1], path[MAX_MAILBOX_PATH+1]; + char *cp, *href = NULL; + + /* Send a body chunk once in a while */ + if (buf_len(buf) > PROT_BUFSIZE) { + write_body(0, lrock->txn, buf_cstring(buf), buf_len(buf)); + buf_reset(buf); + } + + if (last->child) { + /* Reuse our sibling */ + buf_printf(buf, "</li>\n"); + node = last->child; + } + else { + /* Create first child */ + buf_printf(buf, "\n<ul%s>\n", + last->parent ? "" : " id='feed'"); /* needed by CSS */ + node = xmalloc(sizeof(struct node)); + } + + /* See if we have a missing ancestor in the tree */ + if ((cp = strchr(&name[last->len+1], '.'))) len = cp - name; + else href = path; + + /* Populate new/updated node */ + strncpy(node->name, name, len); + node->name[len] = '\0'; + node->len = len; + node->parent = last; + node->child = NULL; + lrock->last = last->child = node; + + /* Get last segment of mailbox name */ + if ((cp = strrchr(node->name, '.'))) cp++; + else cp = node->name; + + /* Translate short mailbox name to external form */ + strlcpy(shortname, cp, sizeof(shortname)); + mboxname_hiersep_toexternal(&httpd_namespace, shortname, 0); + + if (href) { + /* Add selectable feed with link */ + snprintf(path, sizeof(path), ".rss.%s", node->name); + mboxname_hiersep_toexternal(&httpd_namespace, href, 0); + buf_printf(buf, "<li><a href=\"%s\">%s</a>", + href, shortname); + } + else { + /* Add missing ancestor and recurse down the tree */ + buf_printf(buf, "<li>%s", shortname); + + list_cb(name, matchlen, maycreate, rock); + } + } + else { + /* Remove child */ + if (last->child) { + buf_printf(buf, "</li>\n</ul>\n"); + free(last->child); + last->child = NULL; + } + + if (last->parent) { + /* Recurse back up the tree */ + lrock->last = last->parent; + list_cb(name, matchlen, maycreate, rock); + } + } + + return 0; +} + + +/* Create a HTML document listing all RSS feeds available to the user */ +static int list_feeds(struct transaction_t *txn) +{ + const char *template_file = config_getstring(IMAPOPT_RSS_FEEDLIST_TEMPLATE); + const char *var = NULL, *template = NULL, *prefix, *suffix; + unsigned long template_len = 0, prefix_len, suffix_len; + size_t varlen = strlen(FEEDLIST_VAR); + int fd = -1; + struct message_guid guid; + time_t lastmod; + char mboxlist[MAX_MAILBOX_PATH+1]; + struct stat sbuf; + int ret = 0, precond; + struct buf *body = &txn->resp_body.payload; + struct list_rock lrock; + struct node root = { "", 0, NULL, NULL }; + + if (template_file) { + /* See if template exists and contains feedlist variable */ + if (!stat(template_file, &sbuf) && S_ISREG(sbuf.st_mode) && + (size_t) sbuf.st_size >= varlen && + (fd = open(template_file, O_RDONLY)) != -1) { + const char *p; + unsigned long len; + + map_refresh(fd, 1, &template, &template_len, sbuf.st_size, + template_file, NULL); + + for (p = template, len = template_len; + len >= varlen && strncmp(p, FEEDLIST_VAR, varlen); p++, len--); + if (len >= varlen) { + var = p; + lastmod = sbuf.st_mtime; + } + else { + map_free(&template, &template_len); + close(fd); + fd = -1; + } + } + } + + if (!var) { + /* No usable template specified, use our default */ + template = def_template; + template_len = strlen(def_template); + var = strstr(template, FEEDLIST_VAR); + lastmod = compile_time; + } + + prefix = template; + prefix_len = var - template; + suffix = template + prefix_len + varlen; + suffix_len = template_len - (prefix_len + varlen); + + /* Begin to generate ETag */ + message_guid_generate(&guid, template, template_len); + buf_setcstr(&txn->buf, message_guid_encode(&guid)); + + /* stat() mailboxes.db for Last-Modified and ETag */ + snprintf(mboxlist, MAX_MAILBOX_PATH, "%s%s", config_dir, FNAME_MBOXLIST); + stat(mboxlist, &sbuf); + lastmod = MAX(lastmod, sbuf.st_mtime); + buf_printf(&txn->buf, "-%ld-%ld", sbuf.st_mtime, sbuf.st_size); + + /* stat() imapd.conf for Last-Modified and ETag */ + stat(config_filename, &sbuf); + lastmod = MAX(lastmod, sbuf.st_mtime); + buf_printf(&txn->buf, "-%ld-%ld", sbuf.st_mtime, sbuf.st_size); + + /* Check any preconditions */ + precond = check_precond(txn, NULL, buf_cstring(&txn->buf), lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, and Expires */ + txn->resp_body.etag = buf_cstring(&txn->buf); + txn->resp_body.lastmod = lastmod; + txn->resp_body.maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + ret = precond; + goto done; + } + + /* Setup for chunked response */ + txn->flags.te |= TE_CHUNKED; + txn->resp_body.type = "text/html; charset=utf-8"; + + /* Short-circuit for HEAD request */ + if (txn->meth == METH_HEAD) { + response_header(HTTP_OK, txn); + goto done; + } + + /* Send beginning of template */ + write_body(HTTP_OK, txn, prefix, prefix_len); + + /* Generate tree view of feeds */ + buf_reset(body); + lrock.txn = txn; + lrock.last = &root; + mboxlist_findall(NULL, "*", httpd_userisadmin, NULL, httpd_authstate, + list_cb, &lrock); + + /* Close out the tree */ + list_cb(NULL, 0, 0, &lrock); + if (buf_len(body)) write_body(0, txn, buf_cstring(body), buf_len(body)); + + /* Send rest of template */ + if (suffix_len) write_body(0, txn, suffix, suffix_len); + + /* End of output */ + write_body(0, txn, NULL, 0); + + done: + if (fd != -1) { + map_free(&template, &template_len); + close(fd); + } + + return ret; +} + + +/* Fetch the index record & bodystructure, and mmap the message */ +static int fetch_message(struct transaction_t *txn, struct mailbox *mailbox, + unsigned recno, uint32_t uid, + struct index_record *record, struct body **body, + struct buf *msg_buf) +{ + int r; + + buf_reset(msg_buf); + + /* Fetch index record for the message */ + if (uid) r = mailbox_find_index_record(mailbox, uid, record); + else r = mailbox_read_index_record(mailbox, recno, record); + if ((r == CYRUSDB_NOTFOUND) || + (record->system_flags & (FLAG_DELETED|FLAG_EXPUNGED))) { + txn->error.desc = "Message has been removed\r\n"; + + /* Fill in Expires */ + txn->resp_body.maxage = 31536000; /* 1 year */ + txn->flags.cc |= CC_MAXAGE; + return HTTP_GONE; + } + else if (r) { + syslog(LOG_ERR, "find index record failed"); + txn->error.desc = error_message(r); + return HTTP_SERVER_ERROR; + } + + /* Fetch cache record for the message */ + if ((r = mailbox_cacherecord(mailbox, record))) { + syslog(LOG_ERR, "read cache failed"); + txn->error.desc = error_message(r); + return HTTP_SERVER_ERROR; + } + + /* Read message bodystructure */ + message_read_bodystructure(record, body); + + /* Map the message into memory */ + mailbox_map_record(mailbox, record, msg_buf); + + return 0; +} + + +static void buf_escapestr(struct buf *buf, const char *str, unsigned max, + unsigned replace, unsigned level) +{ + const char *c; + unsigned buflen = buf_len(buf), len = 0; + + if (!replace && config_httpprettytelemetry) + buf_printf(buf, "%*s", level * MARKUP_INDENT, ""); + + for (c = str; c && *c && (!max || len < max); c++, len++) { + /* Translate CR to HTML <br> tag */ + if (*c == '\r') buf_appendcstr(buf, "<br>"); + else if (*c == '\n' && !config_httpprettytelemetry) continue; + + /* Translate XML/HTML specials */ + else if (*c == '"') buf_appendcstr(buf, """); +// else if (*c == '\'') buf_appendcstr(buf, "'"); + else if (*c == '&') buf_appendcstr(buf, "&"); + else if (*c == '<') buf_appendcstr(buf, "<"); + else if (*c == '>') buf_appendcstr(buf, ">"); + + /* Handle multi-byte UTF-8 sequences */ + else if ((*c & 0xc0) == 0xc0) { + /* Code points larger than 127 are represented by + * multi-byte sequences, composed of a leading byte and + * one or more continuation bytes. The leading byte has + * two or more high-order 1s followed by a 0, while + * continuation bytes all have '10' in the high-order + * position. The number of high-order 1s in the leading + * byte of a multi-byte sequence indicates the number of + * bytes in the sequence. + */ + unsigned char lead = *c; + + do buf_putc(buf, *c); + while (((lead <<= 1) & 0x80) && c++); + } + + /* Check for non-printable chars */ + else if (!(isspace(*c) || isprint(*c))) { + if (replace) { + /* Replace entire string with a warning */ + buf_truncate(buf, buflen); + buf_printf_markup(buf, level++, "<blockquote>"); + buf_printf_markup(buf, level, "<i><b>NOTE:</b> " + "This message contains characters " + "that can not be displayed in RSS</i>"); + buf_printf_markup(buf, --level, "</blockquote>"); + return; + } + else { + /* Translate non-printable chars to X */ + buf_putc(buf, 'X'); + } + } + + else buf_putc(buf, *c); + } + + if (!replace && config_httpprettytelemetry) buf_appendcstr(buf, "\n"); +} + + +/* List messages as an RSS feed */ +static int list_messages(struct transaction_t *txn, struct mailbox *mailbox) +{ + const char *proto = NULL, *host = NULL; + uint32_t url_len, recno, recentuid = 0; + int max_age, max_items, max_len, nitems, precond; + time_t age_mark = 0, lastmod; + char datestr[80]; + static char etag[33]; + struct buf *url = &txn->buf; + struct buf *buf = &txn->resp_body.payload; + unsigned level = 0; + char mboxname[MAX_MAILBOX_NAME+1]; + struct buf attrib = BUF_INITIALIZER; + + /* Check any preconditions */ + lastmod = mailbox->i.last_appenddate; + sprintf(etag, "%u-%u-%u", + mailbox->i.uidvalidity, mailbox->i.last_uid, mailbox->i.exists); + precond = check_precond(txn, NULL, etag, lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, and Expires */ + txn->resp_body.etag = etag; + txn->resp_body.lastmod = lastmod; + txn->resp_body.maxage = 3600; /* 1 hr */ + txn->flags.cc |= CC_MAXAGE; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + return precond; + } + + /* Setup for chunked response */ + txn->flags.te |= TE_CHUNKED; + txn->resp_body.type = "application/atom+xml; charset=utf-8"; + + /* Short-circuit for HEAD request */ + if (txn->meth == METH_HEAD) { + response_header(HTTP_OK, txn); + return 0; + } + + /* Get maximum age of items to display */ + max_age = config_getint(IMAPOPT_RSS_MAXAGE); + if (max_age > 0) age_mark = time(0) - (max_age * 60 * 60 * 24); + + /* Get number of items to display */ + max_items = config_getint(IMAPOPT_RSS_MAXITEMS); + if (max_items < 0) max_items = 0; + + /* Get length of description to display */ + max_len = config_getint(IMAPOPT_RSS_MAXSYNOPSIS); + if (max_len < 0) max_len = 0; + +#if 0 + /* Obtain recentuid */ + if (mailbox_internal_seen(mailbox, httpd_userid)) { + recentuid = mailbox->i.recentuid; + } + else if (httpd_userid) { + struct seen *seendb = NULL; + struct seendata sd; + + r = seen_open(httpd_userid, SEEN_CREATE, &seendb); + if (!r) r = seen_read(seendb, mailbox->uniqueid, &sd); + seen_close(&seendb); + + /* handle no seen DB gracefully */ + if (r) { + recentuid = mailbox->i.last_uid; + syslog(LOG_ERR, "Could not open seen state for %s (%s)", + httpd_userid, error_message(r)); + } + else { + recentuid = sd.lastuid; + free(sd.seenuids); + } + } + else { + recentuid = mailbox->i.last_uid; /* nothing is recent! */ + } +#endif + + /* Translate mailbox name to external form */ + strlcpy(mboxname, mailbox->name, sizeof(mboxname)); + mboxname_hiersep_toexternal(&httpd_namespace, mboxname, 0); + + /* Construct base URL */ + http_proto_host(txn->req_hdrs, &proto, &host); + assert(!buf_len(url)); + buf_printf(url, "%s://%s%s", proto, host, txn->req_uri->path); + url_len = buf_len(url); + + /* Start XML */ + buf_reset(buf); + buf_printf_markup(buf, level, XML_DECLARATION); + + /* Set up the Atom <feed> response for the mailbox */ + buf_printf_markup(buf, level++, + "<feed xmlns=\"" XML_NS_ATOM "\">"); + + /* <title> - required */ + buf_printf_markup(buf, level, "<title>%s</title>", mboxname); + + /* <id> - required */ + buf_printf_markup(buf, level, "<id>%s%s</id>", + GUID_URL_SCHEME, mailbox->uniqueid); + + /* <updated> - required */ + time_to_rfc3339(lastmod, datestr, sizeof(datestr)); + buf_printf_markup(buf, level, "<updated>%s</updated>", datestr); + + /* <author> - required (use 'Anonymous' as default <name>) */ + buf_printf_markup(buf, level++, "<author>"); + buf_printf_markup(buf, level, "<name>Anonymous</name>"); + buf_printf_markup(buf, --level, "</author>"); + + /* <subtitle> - optional */ + annotatemore_lookup(mailbox->name, "/comment", NULL, &attrib); + if (age_mark) { + time_to_rfc822(age_mark, datestr, sizeof(datestr)); + buf_printf_markup(buf, level, + "<subtitle>%s [posts since %s]</subtitle>", + buf_cstring(&attrib), datestr); + } + else { + buf_printf_markup(buf, level, + "<subtitle>%s [%u most recent posts]</subtitle>", + buf_cstring(&attrib), + max_items ? (unsigned) max_items : mailbox->i.exists); + } + + /* <link> - optional */ + buf_printf_markup(buf, level, + "<link rel=\"self\" type=\"application/atom+xml\"" + " href=\"%s\"/>", buf_cstring(url)); + + /* <generator> - optional */ + if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) { + buf_printf_markup(buf, level, "<generator>Cyrus HTTP %s</generator>", + cyrus_version()); + } + + write_body(HTTP_OK, txn, buf_cstring(buf), buf_len(buf)); + buf_reset(buf); + + /* Add an <entry> for each message */ + for (recno = mailbox->i.num_records, nitems = 0; + recno >= 1 && (!max_items || nitems < max_items); recno--) { + struct index_record record; + struct buf msg_buf = BUF_INITIALIZER; + struct body *body = NULL; + char *subj; + struct address *addr = NULL; + const char *content_types[] = { "text", NULL }; + struct message_content content; + struct bodypart **parts; + + /* Send a body chunk once in a while */ + if (buf_len(buf) > PROT_BUFSIZE) { + write_body(0, txn, buf_cstring(buf), buf_len(buf)); + buf_reset(buf); + } + + /* Fetch the message */ + if (fetch_message(txn, mailbox, recno, 0, + &record, &body, &msg_buf)) { + continue; + } + + /* XXX Are we going to do anything with \Recent? */ + if (record.uid <= recentuid) { + syslog(LOG_DEBUG, "recno %u not recent (%u/%u)", + recno, record.uid, recentuid); + continue; + } + + /* Make sure the message is new enough */ + if (record.gmtime < age_mark) continue; + + /* Feeding this message, increment counter */ + nitems++; + + buf_printf_markup(buf, level++, "<entry>"); + + /* <title> - required */ + subj = charset_parse_mimeheader(body->subject); + buf_printf_markup(buf, level++, "<title type=\"html\">"); + buf_escapestr(buf, subj && *subj ? subj : "[Untitled]", 0, 0, level); + buf_printf_markup(buf, --level, "</title>"); + free(subj); + + /* <id> - required */ + buf_printf_markup(buf, level, "<id>%s%s</id>", + GUID_URL_SCHEME, message_guid_encode(&record.guid)); + + /* <updated> - required */ + time_to_rfc3339(record.gmtime, datestr, sizeof(datestr)); + buf_printf_markup(buf, level, "<updated>%s</updated>", datestr); + + /* <published> - optional */ + buf_printf_markup(buf, level, "<published>%s</published>", datestr); + + /* <link> - optional */ + buf_truncate(url, url_len); + buf_printf(url, "?uid=%u", record.uid); + buf_printf_markup(buf, level, "<link rel=\"alternate\"" + " type=\"text/html\" href=\"%s\"/>", + buf_cstring(url)); + + /* <author> - optional */ + addr = body->from; + if (!addr) addr = body->sender; + if (addr && *addr->mailbox) { + buf_printf_markup(buf, level++, "<author>"); + + /* <name> - required */ + if (addr->name) { + char *name = charset_parse_mimeheader(addr->name); + buf_printf_markup(buf, level++, "<name>"); + buf_escapestr(buf, name, 0, 0, level); + buf_printf_markup(buf, --level, "</name>"); + free(name); + } + else { + buf_printf_markup(buf, level, "<name>%s@%s</name>", + addr->mailbox, addr->domain); + } + + /* <email> - optional */ + buf_printf_markup(buf, level, "<email>%s@%s</email>", + addr->mailbox, addr->domain); + + buf_printf_markup(buf, --level, "</author>"); + } + + /* <summary> - optional (find and use the first text/ part) */ + content.base = buf_base(&msg_buf); + content.len = buf_len(&msg_buf); + content.body = body; + message_fetch_part(&content, content_types, &parts); + + if (parts && *parts) { + buf_printf_markup(buf, level++, "<summary type=\"html\">"); + buf_printf_markup(buf, level++, "<![CDATA["); + buf_escapestr(buf, parts[0]->decoded_body, max_len, 1, level); + buf_printf_markup(buf, --level, "]]>"); + buf_printf_markup(buf, --level, "</summary>"); + } + + buf_printf_markup(buf, --level, "</entry>"); + + /* free the results */ + if (parts) { + struct bodypart **p; + + for (p = parts; *p; p++) free(*p); + free(parts); + } + + if (body) { + message_free_body(body); + free(body); + } + + buf_free(&msg_buf); + } + + /* End of Atom <feed> */ + buf_printf_markup(buf, --level, "</feed>"); + write_body(0, txn, buf_cstring(buf), buf_len(buf)); + + /* End of output */ + write_body(0, txn, NULL, 0); + + return 0; +} + + +static void display_address(struct buf *buf, struct address *addr, + const char *sep, unsigned level) +{ + if (config_httpprettytelemetry) + buf_printf(buf, "%*s", level * MARKUP_INDENT, ""); + + buf_printf(buf, "%s", sep); + if (addr->name) buf_printf(buf, "\"%s\" ", addr->name); + buf_printf(buf, "<a href=\"mailto:%s@%s\"><%s@%s></a>", + addr->mailbox, addr->domain, addr->mailbox, addr->domain); + + if (config_httpprettytelemetry) buf_appendcstr(buf, "\n"); +} + + +static void display_part(struct transaction_t *txn, + struct body *body, const struct index_record *record, + const char *cursection, const struct buf *msg_buf, + unsigned level) +{ + struct buf *buf = &txn->resp_body.payload; + char nextsection[MAX_SECTION_LEN+1]; + + if (!strcmp(body->type, "MULTIPART")) { + int i; + + if (!strcmp(body->subtype, "ALTERNATIVE") && + !strcmp(body->subpart[0].type, "TEXT")) { + /* Display a multpart/ or text/html subpart, + otherwise display first subpart */ + for (i = body->numparts; --i;) { + if (!strcmp(body->subpart[i].type, "MULTIPART") || + !strcmp(body->subpart[i].subtype, "HTML")) break; + } + snprintf(nextsection, sizeof(nextsection), "%s%s%d", + cursection, *cursection ? "." : "", i+1); + display_part(txn, &body->subpart[i], + record, nextsection, msg_buf, level); + } + else { + /* Display all subparts */ + for (i = 0; i < body->numparts; i++) { + snprintf(nextsection, sizeof(nextsection), "%s%s%d", + cursection, *cursection ? "." : "", i+1); + display_part(txn, &body->subpart[i], + record, nextsection, msg_buf, level); + } + } + } + else if (!strcmp(body->type, "MESSAGE") && + !strcmp(body->subtype, "RFC822")) { + struct body *subpart = body->subpart; + struct address *addr; + char *sep; + + /* Display enclosed message header as a shaded table */ + buf_printf_markup(buf, level++, + "<table width=\"100%%\" bgcolor=\"#CCCCCC\">"); + /* Subject header field */ + if (subpart->subject) { + char *subj; + + subj = charset_parse_mimeheader(subpart->subject); + buf_printf_markup(buf, level++, "<tr>"); + buf_printf_markup(buf, level, + "<td align=right valign=top><b>Subject: </b></td>"); + buf_printf_markup(buf, level, "<td>%s</td>", subj); + buf_printf_markup(buf, --level, "</tr>"); + free(subj); + } + /* From header field */ + if (subpart->from && *subpart->from->mailbox) { + buf_printf_markup(buf, level++, "<tr>"); + buf_printf_markup(buf, level, + "<td align=right><b>From: </b></td>"); + buf_printf_markup(buf, level++, "<td>"); + display_address(buf, subpart->from, "", level); + buf_printf_markup(buf, --level, "</td>"); + buf_printf_markup(buf, --level, "</tr>"); + } + /* Sender header field (if different than From */ + if (subpart->sender && *subpart->sender->mailbox && + (!subpart->from || + strcmp(subpart->sender->mailbox, subpart->from->mailbox) || + strcmp(subpart->sender->domain, subpart->from->domain))) { + buf_printf_markup(buf, level++, "<tr>"); + buf_printf_markup(buf, level, + "<td align=right><b>Sender: </b></td>"); + buf_printf_markup(buf, level++, "<td>"); + display_address(buf, subpart->sender, "", level); + buf_printf_markup(buf, --level, "</td>"); + buf_printf_markup(buf, --level, "</tr>"); + } + /* Reply-To header field (if different than From */ + if (subpart->reply_to && *subpart->reply_to->mailbox && + (!subpart->from || + strcmp(subpart->reply_to->mailbox, subpart->from->mailbox) || + strcmp(subpart->reply_to->domain, subpart->from->domain))) { + buf_printf_markup(buf, level++, "<tr>"); + buf_printf_markup(buf, level, + "<td align=right><b>Reply-To: </b></td>"); + buf_printf_markup(buf, level++, "<td>"); + display_address(buf, subpart->reply_to, "", level); + buf_printf_markup(buf, --level, "</td>"); + buf_printf_markup(buf, --level, "</tr>"); + } + /* Date header field */ + buf_printf_markup(buf, level++, "<tr>"); + buf_printf_markup(buf, level, + "<td align=right><b>Date: </b></td>"); + buf_printf_markup(buf, level, + "<td width=\"100%%\">%s</td>", subpart->date); + buf_printf_markup(buf, --level, "</tr>"); + /* To header field (possibly multiple addresses) */ + if (subpart->to) { + buf_printf_markup(buf, level++, "<tr>"); + buf_printf_markup(buf, level, + "<td align=right valign=top><b>To: </b></td>"); + buf_printf_markup(buf, level++, "<td>"); + for (sep = "", addr = subpart->to; addr; addr = addr->next) { + display_address(buf, addr, sep, level); + sep = ", "; + } + buf_printf_markup(buf, --level, "</td>"); + buf_printf_markup(buf, --level, "</tr>"); + } + /* Cc header field (possibly multiple addresses) */ + if (subpart->cc) { + buf_printf_markup(buf, level++, "<tr>"); + buf_printf_markup(buf, level, + "<td align=right valign=top><b>Cc: </b></td>"); + buf_printf_markup(buf, level++, "<td>"); + for (sep = "", addr = subpart->cc; addr; addr = addr->next) { + display_address(buf, addr, sep, level); + sep = ", "; + } + buf_printf_markup(buf, --level, "</td>"); + buf_printf_markup(buf, --level, "</tr>"); + } + buf_printf_markup(buf, --level, "</table>"); +// buf_printf_markup(buf, level, "<br>"); + + /* Display subpart */ + snprintf(nextsection, sizeof(nextsection), "%s%s%d", + cursection, *cursection ? "." : "", 1); + display_part(txn, subpart, record, nextsection, msg_buf, level); + } + else { + /* Leaf part - display something */ + + if (!strcmp(body->type, "TEXT")) { + /* Display text part */ + int ishtml = !strcmp(body->subtype, "HTML"); + int charset = body->charset_cte >> 16; + int encoding = body->charset_cte & 0xff; + + if (charset < 0) charset = 0; /* unknown, try ASCII */ + body->decoded_body = + charset_to_utf8(buf_base(msg_buf) + body->content_offset, + body->content_size, charset, encoding); + if (!ishtml) buf_printf_markup(buf, level, "<pre>"); + write_body(0, txn, buf_cstring(buf), buf_len(buf)); + buf_reset(buf); + + write_body(0, txn, body->decoded_body, strlen(body->decoded_body)); + if (!ishtml) buf_printf_markup(buf, level, "</pre>"); + } + else { + int is_image = !strcmp(body->type, "IMAGE"); + struct param *param = body->params; + const char *file_attr = "NAME"; + + /* Anything else is shown as an attachment. + * Show images inline, using name/description as alternative text. + */ + /* Look for a filename in parameters */ + if (body->disposition) { + if (!strcmp(body->disposition, "ATTACHMENT")) is_image = 0; + param = body->disposition_params; + file_attr = "FILENAME"; + } + for (; param && strcmp(param->attribute, file_attr); + param = param->next); + + buf_printf_markup(buf, level++, "<div align=center>"); + + /* Create link */ + buf_printf_markup(buf, level++, + "<a href=\"%s?uid=%u;section=%s\" type=\"%s/%s\">", + txn->req_tgt.path, record->uid, cursection, + body->type, body->subtype); + + if (config_httpprettytelemetry) + buf_printf(buf, "%*s", level * MARKUP_INDENT, ""); + + /* Add image */ + if (is_image) { + buf_printf(buf, "<img src=\"%s?uid=%u;section=%s\" alt=\"", + txn->req_tgt.path, record->uid, cursection); + } + + /* Create text for link or alternative text for image */ + if (param) buf_printf(buf, "%s", param->value); + else { + buf_printf(buf, "[%s/%s %lu bytes]", + body->type, body->subtype, body->content_size); + } + + if (is_image) buf_printf(buf, "\">"); + + if (config_httpprettytelemetry) buf_appendcstr(buf, "\n"); + + buf_printf_markup(buf, --level, "</a>"); + buf_printf_markup(buf, --level, "</div>"); + } + + buf_printf_markup(buf, level, "<hr>"); + } +} + + +/* Return entire message formatted as text/html */ +static void display_message(struct transaction_t *txn, + const char *mboxname, const struct index_record *record, + struct body *body, const struct buf *msg_buf) +{ + struct body toplevel; + struct buf *buf = &txn->resp_body.payload; + unsigned level = 0; + + /* Setup for chunked response */ + txn->flags.te |= TE_CHUNKED; + txn->resp_body.type = "text/html; charset=utf-8"; + + /* Short-circuit for HEAD request */ + if (txn->meth == METH_HEAD) { + response_header(HTTP_OK, txn); + return; + } + + /* Start HTML */ + buf_reset(buf); + buf_printf_markup(buf, level, HTML_DOCTYPE); + buf_printf_markup(buf, level++, "<html>"); + buf_printf_markup(buf, level++, "<head>"); + buf_printf_markup(buf, level, "<title>%s:%u</title>", + mboxname, record->uid); + buf_printf_markup(buf, --level, "</head>"); + buf_printf_markup(buf, level++, "<body>"); + + /* Create link to message source */ + buf_printf_markup(buf, level++, "<div align=center>"); + buf_printf_markup(buf, level, + "<a href=\"%s?uid=%u;section=0\" type=\"plain/text\">" + "[View message source]</a>", + txn->req_tgt.path, record->uid); + buf_printf_markup(buf, --level, "</div>"); + buf_printf_markup(buf, level, "<hr>"); + + write_body(HTTP_OK, txn, buf_cstring(buf), buf_len(buf)); + buf_reset(buf); + + /* Encapsulate our body in a message/rfc822 to display toplevel hdrs */ + memset(&toplevel, 0, sizeof(struct body)); + toplevel.type = "MESSAGE"; + toplevel.subtype = "RFC822"; + toplevel.subpart = body; + + display_part(txn, &toplevel, record, "", msg_buf, level); + + /* End of HTML */ + buf_printf_markup(buf, --level, "</body>"); + buf_printf_markup(buf, --level, "</html>"); + + write_body(0, txn, buf_cstring(buf), buf_len(buf)); + + /* End of output */ + write_body(0, txn, NULL, 0); +} + + +/* Fetch, decode, and return the specified MIME message part */ +static void fetch_part(struct transaction_t *txn, struct body *body, + const char *findsection, const char *cursection, + struct buf *msg_buf) +{ + char nextsection[MAX_SECTION_LEN+1]; + + if (!strcmp(body->type, "MULTIPART")) { + int i; + + /* Recurse through all subparts */ + for (i = 0; i < body->numparts; i++) { + snprintf(nextsection, sizeof(nextsection), "%s%s%d", + cursection, *cursection ? "." : "", i+1); + fetch_part(txn, &body->subpart[i], + findsection, nextsection, msg_buf); + } + } + else if (!strcmp(body->type, "MESSAGE") && + !strcmp(body->subtype, "RFC822")) { + /* Recurse into supbart */ + snprintf(nextsection, sizeof(nextsection), "%s%s%d", + cursection, *cursection ? "." : "", 1); + fetch_part(txn, body->subpart, findsection, nextsection, msg_buf); + } + else if (!strcmp(findsection, cursection)) { + int encoding = body->charset_cte & 0xff; + const char *outbuf; + size_t outsize; + + outbuf = charset_decode_mimebody(buf_base(msg_buf) + body->content_offset, + body->content_size, encoding, + &body->decoded_body, &outsize); + + if (!outbuf) { + txn->error.desc = "Unknown MIME encoding\r\n"; + error_response(HTTP_SERVER_ERROR, txn); + return; + + } + + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%s/%s", body->type, body->subtype); + txn->resp_body.type = buf_cstring(&txn->buf); + + write_body(HTTP_OK, txn, outbuf, outsize); + } +}
View file
cyrus-imapd-2.5.tar.gz/imap/http_timezone.c
Added
@@ -0,0 +1,1466 @@ +/* http_timezone.c -- Routines for handling timezone service requests in httpd + * + * Copyright (c) 1994-2014 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. + * + */ + +/* + * TODO: + * - Implement localized names and "lang" parameter + */ + +#include <config.h> + +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif +#include <ctype.h> +#include <string.h> +#include <syslog.h> +#include <assert.h> + +#include "global.h" +#include "hash.h" +#include "httpd.h" +#include "http_dav.h" +#include "http_err.h" +#include "http_proxy.h" +#include "jcal.h" +#include "map.h" +#include "tok.h" +#include "strhash.h" +#include "tz_err.h" +#include "util.h" +#include "version.h" +#include "xcal.h" +#include "xstrlcpy.h" +#include "zoneinfo_db.h" + + +#define TIMEZONE_WELLKNOWN_URI "/.well-known/timezone" + +static time_t compile_time; +static void timezone_init(struct buf *serverinfo); +static void timezone_shutdown(void); +static int meth_get(struct transaction_t *txn, void *params); +static int action_capa(struct transaction_t *txn); +static int action_list(struct transaction_t *txn); +static int action_get(struct transaction_t *txn); +static int action_expand(struct transaction_t *txn); +static int json_response(int code, struct transaction_t *txn, json_t *root, + char **resp); +static int json_error_response(struct transaction_t *txn, long tz_code, + struct strlist *param, icaltimetype *time); + +struct observance { + const char *name; + icaltimetype onset; + int offset_from; + int offset_to; + int is_daylight; +}; + +static const struct action_t { + const char *name; + int (*proc)(struct transaction_t *txn); +} actions[] = { + { "capabilities", &action_capa }, + { "list", &action_list }, + { "get", &action_get }, + { "expand", &action_expand }, + { "find", &action_list }, + { NULL, NULL} +}; + + +static struct mime_type_t tz_mime_types[] = { + /* First item MUST be the default type and storage format */ + { "text/calendar; charset=utf-8", "2.0", "ics", "ifb", + (char* (*)(void *)) &icalcomponent_as_ical_string_r, + NULL, NULL, NULL, NULL + }, + { "application/calendar+xml; charset=utf-8", NULL, "xcs", "xfb", + (char* (*)(void *)) &icalcomponent_as_xcal_string, + NULL, NULL, NULL, NULL + }, + { "application/calendar+json; charset=utf-8", NULL, "jcs", "jfb", + (char* (*)(void *)) &icalcomponent_as_jcal_string, + NULL, NULL, NULL, NULL + }, + { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL } +}; + + +/* Namespace for TIMEZONE feeds of mailboxes */ +struct namespace_t namespace_timezone = { + URL_NS_TIMEZONE, 0, "/timezone", TIMEZONE_WELLKNOWN_URI, 0 /* auth */, ALLOW_READ, + timezone_init, NULL, NULL, timezone_shutdown, + { + { NULL, NULL }, /* ACL */ + { 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, NULL }, /* OPTIONS */ + { NULL, NULL }, /* POST */ + { NULL, NULL }, /* PROPFIND */ + { NULL, NULL }, /* PROPPATCH */ + { NULL, NULL }, /* PUT */ + { NULL, NULL }, /* REPORT */ + { &meth_trace, NULL }, /* TRACE */ + { NULL, NULL } /* UNLOCK */ + } +}; + + +static void timezone_init(struct buf *serverinfo __attribute__((unused))) +{ + namespace_timezone.enabled = + config_httpmodules & IMAP_ENUM_HTTPMODULES_TIMEZONE; + + if (!namespace_timezone.enabled) return; + + /* Open zoneinfo db */ + if (zoneinfo_open(NULL)) { + namespace_timezone.enabled = 0; + return; + } + + compile_time = calc_compile_time(__TIME__, __DATE__); + + initialize_tz_error_table(); +} + + +static void timezone_shutdown(void) +{ + zoneinfo_close(NULL); +} + + +/* Perform a GET/HEAD request */ +static int meth_get(struct transaction_t *txn, + void *params __attribute__((unused))) +{ + int ret; + struct strlist *action; + const struct action_t *ap = NULL; + + action = hash_lookup("action", &txn->req_qparams); + if (action && !action->next /* mandatory, once only */) { + for (ap = actions; ap->name && strcmp(action->s, ap->name); ap++); + } + + if (!ap || !ap->name) + ret = json_error_response(txn, TZ_INVALID_ACTION, action, NULL); + else + ret = ap->proc(txn); + + return ret; +} + + +/* Perform a capabilities action */ +static int action_capa(struct transaction_t *txn) +{ + int precond; + struct message_guid guid; + const char *etag; + static time_t lastmod = 0; + static char *resp = NULL; + json_t *root = NULL; + + /* Generate ETag based on compile date/time of this source file. + * Extend this to include config file size/mtime if we add run-time options. + */ + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%ld", (long) compile_time); + message_guid_generate(&guid, buf_cstring(&txn->buf), buf_len(&txn->buf)); + etag = message_guid_encode(&guid); + + /* Check any preconditions, including range request */ + txn->flags.ranges = 1; + precond = check_precond(txn, NULL, etag, compile_time); + + switch (precond) { + case HTTP_OK: + case HTTP_PARTIAL: + case HTTP_NOT_MODIFIED: + /* Fill in Etag, Last-Modified, Expires */ + txn->resp_body.etag = etag; + txn->resp_body.lastmod = compile_time; + txn->resp_body.maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + return precond; + } + + if (txn->resp_body.lastmod > lastmod) { + struct zoneinfo info; + int r; + + /* Get info record from the database */ + if ((r = zoneinfo_lookup_info(&info))) return HTTP_SERVER_ERROR; + + /* Construct our response */ + root = json_pack("{s:i" /* version */ + " s:{" /* info */ + " s:s" /* primary-source */ + " s:{s:b s:b}" /* truncated */ + " s:s" /* provider-details */ + " s:[]" /* contacts */ + " }" + " s:[" /* actions */ + " {s:s s:[]}" /* capabilities */ + " {s:s s:[" /* list */ +// " {s:s s:b s:b}" /* lang */ + " {s:s s:b s:b}" /* tzid */ + " {s:s s:b s:b}" /* changedsince */ + " ]}" + " {s:s s:[" /* get */ +// " {s:s s:b s:b}" /* lang */ + " {s:s s:b s:b}" /* tzid */ + " {s:s s:b s:b s:[s s s]}"/* format */ + " {s:s s:b s:b}" /* truncate */ + " ]}" + " {s:s s:[" /* expand */ +// " {s:s s:b s:b}" /* lang */ + " {s:s s:b s:b}" /* tzid */ + " {s:s s:b s:b}" /* changedsince */ + " {s:s s:b s:b}" /* start */ + " {s:s s:b s:b}" /* end */ + " ]}" + " {s:s s:[" /* find */ +// " {s:s s:b s:b}" /* lang */ + " {s:s s:b s:b}" /* pattern */ + " ]}" + " ]}", + "version", 1, + "info", "primary-source", info.data->s, + "truncated", "any", 1, "untruncated", 1, + "provider-details", "", "contacts", + "actions", + "name", "capabilities", "parameters", + + "name", "list", "parameters", +// "name", "lang", "required", 0, "multi", 1, + "name", "tzid", "required", 0, "multi", 1, + "name", "changedsince", "required", 0, "multi", 0, + + "name", "get", "parameters", +// "name", "lang", "required", 0, "multi", 1, + "name", "tzid", "required", 1, "multi", 0, + "name", "format", "required", 0, "multi", 0, + "values", "text/calendar", "application/calendar+xml", + "application/calendar+json", + "name", "truncate", "required", 0, "multi", 0, + + "name", "expand", "parameters", +// "name", "lang", "required", 0, "multi", 1, + "name", "tzid", "required", 1, "multi", 0, + "name", "changedsince", "required", 0, "multi", 0, + "name", "start", "required", 1, "multi", 0, + "name", "end", "required", 0, "multi", 0, + + "name", "find", "parameters", +// "name", "lang", "required", 0, "multi", 1, + "name", "pattern", "required", 1, "multi", 0); + freestrlist(info.data); + + if (!root) { + txn->error.desc = "Unable to create JSON response"; + return HTTP_SERVER_ERROR; + } + + /* Update lastmod */ + lastmod = txn->resp_body.lastmod; + } + + /* Output the JSON object */ + return json_response(precond, txn, root, &resp); +} + +struct list_rock { + json_t *tzarray; + struct hash_table *tztable; +}; + +static int list_cb(const char *tzid, int tzidlen, + struct zoneinfo *zi, void *rock) +{ + struct list_rock *lrock = (struct list_rock *) rock; + char tzidbuf[200], lastmod[21]; + json_t *tz; + + if (lrock->tztable) { + if (hash_lookup(tzid, lrock->tztable)) return 0; + hash_insert(tzid, (void *) 0xDEADBEEF, lrock->tztable); + } + + strlcpy(tzidbuf, tzid, tzidlen+1); + rfc3339date_gen(lastmod, sizeof(lastmod), zi->dtstamp); + + tz = json_pack("{s:s s:s}", "tzid", tzidbuf, "last-modified", lastmod); + json_array_append_new(lrock->tzarray, tz); + + if (zi->data) { + struct strlist *sl; + json_t *aliases = json_array(); + + json_object_set_new(tz, "aliases", aliases); + + for (sl = zi->data; sl; sl = sl->next) + json_array_append_new(aliases, json_string(sl->s)); + } + + return 0; +} + + +/* Perform a list action */ +static int action_list(struct transaction_t *txn) +{ + int r, precond, tzid_only = 1; + struct strlist *param, *name = NULL; + icaltimetype changedsince = icaltime_null_time(); + struct resp_body_t *resp_body = &txn->resp_body; + struct zoneinfo info; + time_t lastmod; + json_t *root = NULL; + + /* Sanity check the parameters */ + param = hash_lookup("action", &txn->req_qparams); + if (!strcmp("find", param->s)) { + name = hash_lookup("pattern", &txn->req_qparams); + if (!name || name->next /* mandatory, once only */ + || !name->s || !*name->s /* not empty */ + || !strcspn(name->s, "*")) { /* not (*)+ */ + return json_error_response(txn, TZ_INVALID_PATTERN, name, NULL); + } + tzid_only = 0; + } + else { + param = hash_lookup("changedsince", &txn->req_qparams); + if (param) { + changedsince = icaltime_from_string(param->s); + if (param->next || !changedsince.is_utc) { /* once only, UTC */ + return json_error_response(txn, TZ_INVALID_CHANGEDSINCE, + param, &changedsince); + } + } + + name = hash_lookup("tzid", &txn->req_qparams); + if (name) { + if (changedsince.is_utc) { + return json_error_response(txn, TZ_INVALID_TZID, + param, &changedsince); + } + else { + /* Check for tzid=*, and revert to empty list */ + struct strlist *sl; + + for (sl = name; sl && strcmp(sl->s, "*"); sl = sl->next); + if (sl) name = NULL; + } + } + } + + /* Get info record from the database */ + if ((r = zoneinfo_lookup_info(&info))) return HTTP_SERVER_ERROR; + + /* Generate ETag & Last-Modified from info record */ + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%u-%ld", strhash(info.data->s), info.dtstamp); + lastmod = info.dtstamp; + freestrlist(info.data); + + /* Check any preconditions, including range request */ + txn->flags.ranges = 1; + precond = check_precond(txn, NULL, buf_cstring(&txn->buf), lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_PARTIAL: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, and Expires */ + resp_body->etag = buf_cstring(&txn->buf); + resp_body->lastmod = lastmod; + resp_body->maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE; + if (httpd_userid) txn->flags.cc |= CC_PUBLIC; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + resp_body->type = NULL; + return precond; + } + + + if (txn->meth != METH_HEAD) { + struct list_rock lrock = { NULL, NULL }; + struct hash_table tzids; + char dtstamp[21]; + + /* Start constructing our response */ + rfc3339date_gen(dtstamp, sizeof(dtstamp), lastmod); + root = json_pack("{s:s s:[]}", "dtstamp", dtstamp, "timezones"); + if (!root) { + txn->error.desc = "Unable to create JSON response"; + return HTTP_SERVER_ERROR; + } + + lrock.tzarray = json_object_get(root, "timezones"); + if (!tzid_only) { + construct_hash_table(&tzids, 500, 1); + lrock.tztable = &tzids; + } + + /* Add timezones to array */ + do { + zoneinfo_find(name ? name->s : NULL, tzid_only, + icaltime_as_timet(changedsince), &list_cb, &lrock); + } while (name && (name = name->next)); + + if (!tzid_only) free_hash_table(&tzids, NULL); + } + + /* Output the JSON object */ + return json_response(precond, txn, root, NULL); +} + + +static void check_tombstone(struct observance *tombstone, + struct observance *obs) +{ + if (icaltime_compare(obs->onset, tombstone->onset) > 0) { + /* onset is closer to cutoff than existing tombstone */ + tombstone->name = icalmemory_tmp_copy(obs->name); + tombstone->offset_from = tombstone->offset_to = obs->offset_to; + tombstone->is_daylight = obs->is_daylight; + tombstone->onset = obs->onset; + } +} + +struct rdate { + icalproperty *prop; + struct icaldatetimeperiodtype date; +}; + +static int rdate_compare(const void *rdate1, const void *rdate2) +{ + return icaltime_compare(((struct rdate *) rdate1)->date.time, + ((struct rdate *) rdate2)->date.time); +} + +static const struct observance *truncate_vtimezone(icalcomponent *vtz, + icaltimetype start, + icaltimetype end, + icalarray *obsarray) +{ + icalcomponent *comp, *nextc, *tomb_std = NULL, *tomb_day = NULL; + icalproperty *prop, *proleptic_prop = NULL; + static struct observance tombstone; + unsigned need_tomb = !icaltime_is_null_time(start); + + /* See if we have a proleptic tzname in VTIMEZONE */ + for (prop = icalcomponent_get_first_property(vtz, ICAL_X_PROPERTY); + prop; + prop = icalcomponent_get_next_property(vtz, ICAL_X_PROPERTY)) { + if (!strcmp("X-PROLEPTIC-TZNAME", icalproperty_get_x_name(prop))) { + proleptic_prop = prop; + break; + } + } + + memset(&tombstone, 0, sizeof(struct observance)); + tombstone.name = icalmemory_tmp_copy(proleptic_prop ? + icalproperty_get_x(proleptic_prop) : + "LMT"); + + /* Process each VTMEZONE STANDARD/DAYLIGHT subcomponent */ + for (comp = icalcomponent_get_first_component(vtz, ICAL_ANY_COMPONENT); + comp; comp = nextc) { + icalproperty *dtstart_prop = NULL, *rrule_prop = NULL; + icalarray *rdate_array = icalarray_new(sizeof(struct rdate), 10); + icaltimetype dtstart; + struct observance obs; + unsigned n, trunc_dtstart = 0; + int r; + + nextc = icalcomponent_get_next_component(vtz, ICAL_ANY_COMPONENT); + + memset(&obs, 0, sizeof(struct observance)); + obs.offset_from = obs.offset_to = INT_MAX; + obs.is_daylight = (icalcomponent_isa(comp) == ICAL_XDAYLIGHT_COMPONENT); + + /* Grab the properties that we require to expand recurrences */ + for (prop = icalcomponent_get_first_property(comp, ICAL_ANY_PROPERTY); + prop; + prop = icalcomponent_get_next_property(comp, ICAL_ANY_PROPERTY)) { + + switch (icalproperty_isa(prop)) { + case ICAL_TZNAME_PROPERTY: + obs.name = icalproperty_get_tzname(prop); + break; + + case ICAL_DTSTART_PROPERTY: + dtstart_prop = prop; + obs.onset = dtstart = icalproperty_get_dtstart(prop); + break; + + case ICAL_TZOFFSETFROM_PROPERTY: + obs.offset_from = icalproperty_get_tzoffsetfrom(prop); + break; + + case ICAL_TZOFFSETTO_PROPERTY: + obs.offset_to = icalproperty_get_tzoffsetto(prop); + break; + + case ICAL_RRULE_PROPERTY: + rrule_prop = prop; + break; + + case ICAL_RDATE_PROPERTY: { + struct rdate rdate = { prop, icalproperty_get_rdate(prop) }; + + icalarray_append(rdate_array, &rdate); + break; + } + + default: + /* ignore all other properties */ + break; + } + } + + /* We MUST have DTSTART, TZNAME, TZOFFSETFROM, and TZOFFSETTO */ + if (!dtstart_prop || !obs.name || + obs.offset_from == INT_MAX || obs.offset_to == INT_MAX) { + icalarray_free(rdate_array); + continue; + } + + /* Adjust DTSTART observance to UTC */ + icaltime_adjust(&obs.onset, 0, 0, 0, -obs.offset_from); + obs.onset.is_utc = 1; + + /* Check DTSTART vs window close */ + if (!icaltime_is_null_time(end) && + icaltime_compare(obs.onset, end) >= 0) { + /* All observances occur on/after window close - remove component */ + icalcomponent_remove_component(vtz, comp); + icalcomponent_free(comp); + + /* Nothing else to do */ + icalarray_free(rdate_array); + continue; + } + + /* Check DTSTART vs window open */ + r = icaltime_compare(obs.onset, start); + if (r < 0) { + /* DTSTART is prior to our window open - check it vs tombstone */ + if (need_tomb) check_tombstone(&tombstone, &obs); + + /* Adjust it */ + trunc_dtstart = 1; + } + else { + /* DTSTART is on/after our window open */ + if (r == 0) need_tomb = 0; + + if (obsarray && !rrule_prop) { + /* Add the DTSTART observance to our array */ + icalarray_append(obsarray, &obs); + } + } + + if (rrule_prop) { + struct icalrecurrencetype rrule = + icalproperty_get_rrule(rrule_prop); + icalrecur_iterator *ritr = NULL; + unsigned infinite = icaltime_is_null_time(rrule.until); + unsigned trunc_until = 0; + + /* Check RRULE duration */ + if (!infinite && icaltime_compare(rrule.until, start) < 0) { + /* RRULE ends prior to our window open - + check UNTIL vs tombstone */ + obs.onset = rrule.until; + if (need_tomb) check_tombstone(&tombstone, &obs); + + /* Remove RRULE */ + icalcomponent_remove_property(comp, rrule_prop); + icalproperty_free(rrule_prop); + } + else { + /* RRULE ends on/after our window open */ + if (!icaltime_is_null_time(end) && + (infinite || icaltime_compare(rrule.until, end) > 0)) { + /* RRULE ends after our window close - need to adjust it */ + trunc_until = 1; + } + + if (!infinite) { + /* Adjust UNTIL to local time (for iterator) */ + icaltime_adjust(&rrule.until, 0, 0, 0, obs.offset_from); + rrule.until.is_utc = 0; + } + + if (trunc_dtstart) { + /* Bump RRULE start to 1 year prior to our window open */ + dtstart.year = start.year - 1; + } + + ritr = icalrecur_iterator_new(rrule, dtstart); + } + + /* Process any RRULE observances within our window */ + if (ritr) { + icaltimetype recur, prev_onset; + + /* Mark original DTSTART (UTC) */ + dtstart = obs.onset; + + while (!icaltime_is_null_time(obs.onset = recur = + icalrecur_iterator_next(ritr))) { + unsigned ydiff; + + /* Adjust observance to UTC */ + icaltime_adjust(&obs.onset, 0, 0, 0, -obs.offset_from); + obs.onset.is_utc = 1; + + if (trunc_until && icaltime_compare(obs.onset, end) > 0) { + /* Observance is on/after window close */ + + /* Check if DSTART is within 1yr of prev onset */ + ydiff = prev_onset.year - dtstart.year; + if (ydiff <= 1) { + /* Remove RRULE */ + icalcomponent_remove_property(comp, rrule_prop); + icalproperty_free(rrule_prop); + + if (ydiff) { + /* Add previous onset as RDATE */ + struct icaldatetimeperiodtype rdate = { + prev_onset, + icalperiodtype_null_period() + }; + prop = icalproperty_new_rdate(rdate); + icalcomponent_add_property(comp, prop); + } + } + else { + /* Set UNTIL to previous onset */ + rrule.until = prev_onset; + icalproperty_set_rrule(rrule_prop, rrule); + } + + /* We're done */ + break; + } + + /* Check observance vs our window open */ + r = icaltime_compare(obs.onset, start); + if (r < 0) { + /* Observance is prior to our window open - + check it vs tombstone */ + if (need_tomb) check_tombstone(&tombstone, &obs); + } + else { + /* Observance is on/after our window open */ + if (r == 0) need_tomb = 0; + + if (trunc_dtstart) { + /* Make this observance the new DTSTART */ + icalproperty_set_dtstart(dtstart_prop, recur); + dtstart = obs.onset; + trunc_dtstart = 0; + + /* Check if new DSTART is within 1yr of UNTIL */ + ydiff = rrule.until.year - recur.year; + if (!trunc_until && ydiff <= 1) { + /* Remove RRULE */ + icalcomponent_remove_property(comp, rrule_prop); + icalproperty_free(rrule_prop); + + if (ydiff) { + /* Add UNTIL as RDATE */ + struct icaldatetimeperiodtype rdate = { + rrule.until, + icalperiodtype_null_period() + }; + prop = icalproperty_new_rdate(rdate); + icalcomponent_add_property(comp, prop); + } + } + } + + if (obsarray) { + /* Add the observance to our array */ + icalarray_append(obsarray, &obs); + } + else if (!trunc_until) { + /* We're done */ + break; + } + } + prev_onset = obs.onset; + } + icalrecur_iterator_free(ritr); + } + } + + /* Sort the RDATEs by onset */ + icalarray_sort(rdate_array, &rdate_compare); + + /* Check RDATEs */ + for (n = 0; n < rdate_array->num_elements; n++) { + struct rdate *rdate = icalarray_element_at(rdate_array, n); + + if (n == 0 && icaltime_compare(rdate->date.time, dtstart) == 0) { + /* RDATE is same as DTSTART - remove it */ + icalcomponent_remove_property(comp, rdate->prop); + icalproperty_free(rdate->prop); + continue; + } + + obs.onset = rdate->date.time; + + /* Adjust observance to UTC */ + icaltime_adjust(&obs.onset, 0, 0, 0, -obs.offset_from); + obs.onset.is_utc = 1; + + if (!icaltime_is_null_time(end) && + icaltime_compare(obs.onset, end) >= 0) { + /* RDATE is after our window close - remove it */ + icalcomponent_remove_property(comp, rdate->prop); + icalproperty_free(rdate->prop); + continue; + } + + r = icaltime_compare(obs.onset, start); + if (r < 0) { + /* RDATE is prior to window open - check it vs tombstone */ + if (need_tomb) check_tombstone(&tombstone, &obs); + + /* Remove it */ + icalcomponent_remove_property(comp, rdate->prop); + icalproperty_free(rdate->prop); + } + else { + /* RDATE is on/after our window open */ + if (r == 0) need_tomb = 0; + + if (trunc_dtstart) { + /* Make this RDATE the new DTSTART */ + icalproperty_set_dtstart(dtstart_prop, + rdate->date.time); + trunc_dtstart = 0; + + icalcomponent_remove_property(comp, rdate->prop); + icalproperty_free(rdate->prop); + } + + if (obsarray) { + /* Add the observance to our array */ + icalarray_append(obsarray, &obs); + } + } + } + icalarray_free(rdate_array); + + /* Final check */ + if (trunc_dtstart) { + /* All observances in comp occur prior to window open, remove it + unless we haven't saved a tombstone comp of this type yet */ + if (icalcomponent_isa(comp) == ICAL_XDAYLIGHT_COMPONENT) { + if (!tomb_day) { + tomb_day = comp; + comp = NULL; + } + } + else if (!tomb_std) { + tomb_std = comp; + comp = NULL; + } + + if (comp) { + icalcomponent_remove_component(vtz, comp); + icalcomponent_free(comp); + } + } + } + + if (need_tomb && !icaltime_is_null_time(tombstone.onset)) { + /* Need to add tombstone component/observance starting at window open + as long as its not prior to start of TZ data */ + icalcomponent *tomb; + icalproperty *prop, *nextp; + + if (obsarray) { + /* Add the tombstone to our array */ + tombstone.onset = start; + icalarray_append(obsarray, &tombstone); + } + + /* Determine which tombstone component we need */ + if (tombstone.is_daylight) { + tomb = tomb_day; + tomb_day = NULL; + } + else { + tomb = tomb_std; + tomb_std = NULL; + } + + /* Set property values on our tombstone */ + for (prop = icalcomponent_get_first_property(tomb, ICAL_ANY_PROPERTY); + prop; prop = nextp) { + + nextp = icalcomponent_get_next_property(tomb, ICAL_ANY_PROPERTY); + + switch (icalproperty_isa(prop)) { + case ICAL_TZNAME_PROPERTY: + icalproperty_set_tzname(prop, tombstone.name); + break; + case ICAL_TZOFFSETFROM_PROPERTY: + icalproperty_set_tzoffsetfrom(prop, tombstone.offset_from); + break; + case ICAL_TZOFFSETTO_PROPERTY: + icalproperty_set_tzoffsetto(prop, tombstone.offset_to); + break; + case ICAL_DTSTART_PROPERTY: + /* Adjust window open to local time */ + icaltime_adjust(&start, 0, 0, 0, tombstone.offset_from); + start.is_utc = 0; + + icalproperty_set_dtstart(prop, start); + break; + default: + icalcomponent_remove_property(tomb, prop); + icalproperty_free(prop); + break; + } + } + + /* Remove X-PROLEPTIC-TZNAME as it no longer applies */ + if (proleptic_prop) { + icalcomponent_remove_property(vtz, proleptic_prop); + icalproperty_free(proleptic_prop); + } + } + + /* Remove any unused tombstone components */ + if (tomb_std) { + icalcomponent_remove_component(vtz, tomb_std); + icalcomponent_free(tomb_std); + } + if (tomb_day) { + icalcomponent_remove_component(vtz, tomb_day); + icalcomponent_free(tomb_day); + } + + return &tombstone; +} + +#ifndef HAVE_TZDIST_PROPS +static icalproperty *icalproperty_new_tzidaliasof(const char *v) +{ + icalproperty *prop = icalproperty_new_x(v); + icalproperty_set_x_name(prop, "TZID-ALIAS-OF"); + return prop; +} + +static icalproperty *icalproperty_new_tzuntil(struct icaltimetype v) +{ + icalproperty *prop = icalproperty_new_x(icaltime_as_ical_string(v)); + icalproperty_set_x_name(prop, "TZUNTIL"); + return prop; +} +#endif /* HAVE_TZDIST_PROPS */ + +/* Perform a get action */ +static int action_get(struct transaction_t *txn) +{ + int r, precond; + struct strlist *param; + const char *tzid, *truncate = NULL; + struct zoneinfo zi; + time_t lastmod; + icaltimetype start = icaltime_null_time(), end = icaltime_null_time(); + char *data = NULL; + unsigned long datalen = 0; + struct resp_body_t *resp_body = &txn->resp_body; + struct mime_type_t *mime = NULL; + + /* Sanity check the parameters */ + param = hash_lookup("tzid", &txn->req_qparams); + if (!param || param->next) { /* mandatory, once only */ + return json_error_response(txn, TZ_INVALID_TZID, param, NULL); + } + if (strchr(param->s, '.')) { /* paranoia */ + return json_error_response(txn, TZ_NOT_FOUND, NULL, NULL); + } + tzid = param->s; + + /* Check/find requested MIME type */ + param = hash_lookup("format", &txn->req_qparams); + if (param && !param->next /* optional, once only */) { + for (mime = tz_mime_types; mime->content_type; mime++) { + if (is_mediatype(param->s, mime->content_type)) break; + } + } + else mime = tz_mime_types; + + if (!mime || !mime->content_type) { + return json_error_response(txn, TZ_INVALID_FORMAT, param, NULL); + } + + /* Check for any truncation */ + param = hash_lookup("truncate", &txn->req_qparams); + if (param) { + tok_t tok; + char *token; + + truncate = param->s; + + if (param->next) { /* once only */ + return json_error_response(txn, TZ_INVALID_TRUNCATE, param, NULL); + } + + tok_init(&tok, truncate, ",", TOK_TRIMLEFT|TOK_TRIMRIGHT|TOK_EMPTY); + token = tok_next(&tok); + if (!token || + (strcmp(token, "*") && + !icaltime_is_utc((start = icaltime_from_string(token))))) { + return json_error_response(txn, TZ_INVALID_TRUNCATE, param, &start); + } + token = tok_next(&tok); + if (!token || + (strcmp(token, "*") && + (!icaltime_is_utc((end = icaltime_from_string(token))) || + icaltime_compare(end, start) <= 0))) { + return json_error_response(txn, TZ_INVALID_TRUNCATE, param, &end); + } + if (tok_next(&tok)) { + return json_error_response(txn, TZ_INVALID_TRUNCATE, param, NULL); + } + tok_fini(&tok); + } + + /* Get info record from the database */ + if ((r = zoneinfo_lookup(tzid, &zi))) { + return (r == CYRUSDB_NOTFOUND ? + json_error_response(txn, TZ_NOT_FOUND, NULL, NULL) + : HTTP_SERVER_ERROR); + } + + /* Generate ETag & Last-Modified from info record */ + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%u-%ld", strhash(tzid), zi.dtstamp); + lastmod = zi.dtstamp; + freestrlist(zi.data); + + /* Check any preconditions, including range request */ + txn->flags.ranges = 1; + precond = check_precond(txn, NULL, buf_cstring(&txn->buf), lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_PARTIAL: + case HTTP_NOT_MODIFIED: + /* Fill in Content-Type, ETag, Last-Modified, and Expires */ + resp_body->type = mime->content_type; + resp_body->etag = buf_cstring(&txn->buf); + resp_body->lastmod = lastmod; + resp_body->maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE; + if (httpd_userid) txn->flags.cc |= CC_PUBLIC; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + resp_body->type = NULL; + return precond; + } + + + if (txn->meth != METH_HEAD) { + static struct buf pathbuf = BUF_INITIALIZER; + const char *path, *proto, *host, *msg_base = NULL; + unsigned long msg_size = 0; + icalcomponent *ical, *vtz; + icalproperty *prop; + int fd; + + /* Open, mmap, and parse the file */ + buf_reset(&pathbuf); + buf_printf(&pathbuf, "%s%s/%s.ics", + config_dir, FNAME_ZONEINFODIR, tzid); + path = buf_cstring(&pathbuf); + if ((fd = open(path, O_RDONLY)) == -1) return HTTP_SERVER_ERROR; + + map_refresh(fd, 1, &msg_base, &msg_size, MAP_UNKNOWN_LEN, path, NULL); + if (!msg_base) return HTTP_SERVER_ERROR; + + ical = icalparser_parse_string(msg_base); + map_free(&msg_base, &msg_size); + close(fd); + + vtz = icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT); + prop = icalcomponent_get_first_property(vtz, ICAL_TZID_PROPERTY); + + if (zi.type == ZI_LINK) { + /* Add TZID-ALIAS-OF */ + const char *aliasof = icalproperty_get_tzid(prop); + icalproperty *atzid = icalproperty_new_tzidaliasof(aliasof); + + icalcomponent_add_property(vtz, atzid); + + /* Substitute TZID alias */ + icalproperty_set_tzid(prop, tzid); + } + + /* Start constructing TZURL */ + buf_reset(&pathbuf); + http_proto_host(txn->req_hdrs, &proto, &host); + buf_printf(&pathbuf, "%s://%s%s?action=get&tzid=%s", + proto, host, namespace_timezone.prefix, tzid); + if (mime != tz_mime_types) { + buf_printf(&pathbuf, "&format=%.*s", + (int) strcspn(mime->content_type, ";"), + mime->content_type); + } + if (truncate) { + if (!icaltime_is_null_time(end)) { + /* Add TZUNTIL to VTIMEZONE */ + icalproperty *tzuntil = icalproperty_new_tzuntil(end); + icalcomponent_add_property(vtz, tzuntil); + } + + /* Add truncation parameter to TZURL */ + buf_printf(&pathbuf, "&truncate=%s", truncate); + + /* Truncate the VTIMEZONE */ + truncate_vtimezone(vtz, start, end, NULL); + } + + /* Set TZURL property */ + prop = icalproperty_new_tzurl(buf_cstring(&pathbuf)); + icalcomponent_add_property(vtz, prop); + + /* Convert to requested MIME type */ + data = mime->to_string(ical); + datalen = strlen(data); + + /* Set Content-Disposition filename */ + buf_reset(&pathbuf); + buf_printf(&pathbuf, "%s.%s", tzid, mime->file_ext); + resp_body->fname = buf_cstring(&pathbuf); + + icalcomponent_free(ical); + } + + write_body(precond, txn, data, datalen); + + if (data) free(data); + + return 0; +} + + +static int observance_compare(const void *obs1, const void *obs2) +{ + return icaltime_compare(((struct observance *) obs1)->onset, + ((struct observance *) obs2)->onset); +} + + +static const char *dow[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; +static const char *mon[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + +#define CTIME_FMT "%s %s %2d %02d:%02d:%02d %4d" +#define CTIME_ARGS(tt) \ + dow[icaltime_day_of_week(tt)-1], mon[tt.month-1], \ + tt.day, tt.hour, tt.minute, tt.second, tt.year + + +/* Perform an expand action */ +static int action_expand(struct transaction_t *txn) +{ + int r, precond, zdump = 0; + struct strlist *param; + const char *tzid; + struct zoneinfo zi; + time_t lastmod; + icaltimetype start, end, changedsince = icaltime_null_time(); + struct resp_body_t *resp_body = &txn->resp_body; + json_t *root = NULL; + + /* Sanity check the parameters */ + param = hash_lookup("tzid", &txn->req_qparams); + if (!param || param->next) { /* mandatory, once only */ + return json_error_response(txn, TZ_INVALID_TZID, param, NULL); + } + if (strchr(param->s, '.')) { /* paranoia */ + return json_error_response(txn, TZ_NOT_FOUND, NULL, NULL); + } + tzid = param->s; + + param = hash_lookup("changedsince", &txn->req_qparams); + if (param) { + changedsince = icaltime_from_string(param->s); + if (param->next || !changedsince.is_utc) { /* once only, UTC */ + return json_error_response(txn, TZ_INVALID_CHANGEDSINCE, + param, &changedsince); + } + } + + param = hash_lookup("start", &txn->req_qparams); + if (!param || param->next) /* mandatory, once only */ + return json_error_response(txn, TZ_INVALID_START, param, NULL); + + start = icaltime_from_string(param->s); + if (!start.is_utc) /* MUST be UTC */ + return json_error_response(txn, TZ_INVALID_START, param, &start); + + param = hash_lookup("end", &txn->req_qparams); + if (param) { + end = icaltime_from_string(param->s); + if (param->next || !end.is_utc /* once only, UTC */ + || icaltime_compare(end, start) <= 0) { /* end MUST be > start */ + return json_error_response(txn, TZ_INVALID_END, param, &end); + } + } + else { + /* Default to start + 10 years */ + end = start; + end.year += 10; + } + + /* Check requested format (debugging only) */ + param = hash_lookup("format", &txn->req_qparams); + if (param) { + if (param->next || strcmp(param->s, "zdump")) /* optional, once only */ + return json_error_response(txn, TZ_INVALID_FORMAT, param, NULL); + + /* Mimic zdump(8) -V output for comparision: + + For each zonename, print the times both one second before and + exactly at each detected time discontinuity, the time at one day + less than the highest possible time value, and the time at the + highest possible time value. Each line is followed by isdst=D + where D is positive, zero, or negative depending on whether the + given time is daylight saving time, standard time, or an unknown + time type, respectively. Each line is also followed by gmtoff=N + if the given local time is known to be N seconds east of Green‐ + wich. + */ + zdump = 1; + } + + /* Get info record from the database */ + if ((r = zoneinfo_lookup(tzid, &zi))) { + return (r == CYRUSDB_NOTFOUND ? + json_error_response(txn, TZ_NOT_FOUND, NULL, NULL) + : HTTP_SERVER_ERROR); + } + + /* Generate ETag & Last-Modified from info record */ + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%u-%ld", strhash(tzid), zi.dtstamp); + lastmod = zi.dtstamp; + freestrlist(zi.data); + + /* Check any preconditions, including range request */ + txn->flags.ranges = 1; + if (lastmod <= icaltime_as_timet(changedsince)) precond = HTTP_NOT_MODIFIED; + else precond = check_precond(txn, NULL, buf_cstring(&txn->buf), lastmod); + + switch (precond) { + case HTTP_OK: + case HTTP_PARTIAL: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, and Expires */ + resp_body->etag = buf_cstring(&txn->buf); + resp_body->lastmod = lastmod; + resp_body->maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE; + if (httpd_userid) txn->flags.cc |= CC_PUBLIC; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + resp_body->type = NULL; + return precond; + } + + + if (txn->meth != METH_HEAD) { + static struct buf pathbuf = BUF_INITIALIZER; + const char *path, *msg_base = NULL; + unsigned long msg_size = 0; + icalcomponent *ical, *vtz; + const struct observance *proleptic; + char dtstamp[21]; + icalarray *obsarray; + json_t *jobsarray; + unsigned n; + int fd; + + /* Open, mmap, and parse the file */ + buf_reset(&pathbuf); + buf_printf(&pathbuf, "%s%s/%s.ics", + config_dir, FNAME_ZONEINFODIR, tzid); + path = buf_cstring(&pathbuf); + if ((fd = open(path, O_RDONLY)) == -1) return HTTP_SERVER_ERROR; + + map_refresh(fd, 1, &msg_base, &msg_size, MAP_UNKNOWN_LEN, path, NULL); + if (!msg_base) return HTTP_SERVER_ERROR; + + ical = icalparser_parse_string(msg_base); + map_free(&msg_base, &msg_size); + close(fd); + + /* Start constructing our response */ + if (zdump) { + txn->resp_body.type = "text/plain; charset=us-ascii"; + } + else { + rfc3339date_gen(dtstamp, sizeof(dtstamp), lastmod); + root = json_pack("{s:s s:s s:[]}", + "dtstamp", dtstamp, "tzid", tzid, "observances"); + if (!root) { + txn->error.desc = "Unable to create JSON response"; + return HTTP_SERVER_ERROR; + } + } + + + /* Create an array of observances */ + obsarray = icalarray_new(sizeof(struct observance), 20); + vtz = icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT); + proleptic = truncate_vtimezone(vtz, start, end, obsarray); + + /* Sort the observances by onset */ + icalarray_sort(obsarray, &observance_compare); + + if (zdump) { + struct buf *body = &txn->resp_body.payload; + struct icaldurationtype off = icaldurationtype_null_duration(); + const char *prev_name = proleptic->name; + int prev_isdst = proleptic->is_daylight; + + for (n = 0; n < obsarray->num_elements; n++) { + struct observance *obs = icalarray_element_at(obsarray, n); + struct icaltimetype local, ut; + + /* Skip any no-ops as zdump doesn't output them */ + if (obs->offset_from == obs->offset_to + && prev_isdst == obs->is_daylight + && !strcmp(prev_name, obs->name)) continue; + + /* UT and local time 1 second before onset */ + off.seconds = -1; + ut = icaltime_add(obs->onset, off); + + off.seconds = obs->offset_from; + local = icaltime_add(ut, off); + + buf_printf(body, + "%s " CTIME_FMT " UT = " CTIME_FMT " %s" + " isdst=%d gmtoff=%d\n", + tzid, CTIME_ARGS(ut), CTIME_ARGS(local), + prev_name, prev_isdst, obs->offset_from); + + /* UT and local time at onset */ + icaltime_adjust(&ut, 0, 0, 0, 1); + + off.seconds = obs->offset_to; + local = icaltime_add(ut, off); + + buf_printf(body, + "%s " CTIME_FMT " UT = " CTIME_FMT " %s" + " isdst=%d gmtoff=%d\n", + tzid, CTIME_ARGS(ut), CTIME_ARGS(local), + obs->name, obs->is_daylight, obs->offset_to); + + prev_name = obs->name; + prev_isdst = obs->is_daylight; + } + } + else { + /* Add observances to JSON array */ + jobsarray = json_object_get(root, "observances"); + for (n = 0; n < obsarray->num_elements; n++) { + struct observance *obs = icalarray_element_at(obsarray, n); + + json_array_append_new(jobsarray, + json_pack( + "{s:s s:s s:i s:i}", + "name", obs->name, + "onset", + icaltime_as_iso_string(obs->onset), + "utc-offset-from", obs->offset_from, + "utc-offset-to", obs->offset_to)); + } + } + icalarray_free(obsarray); + + icalcomponent_free(ical); + } + + if (zdump) { + struct buf *body = &txn->resp_body.payload; + + write_body(precond, txn, buf_cstring(body), buf_len(body)); + + return 0; + } + else { + /* Output the JSON object */ + return json_response(precond, txn, root, NULL); + } +} + + +static int json_response(int code, struct transaction_t *txn, json_t *root, + char **resp) +{ + size_t flags = JSON_PRESERVE_ORDER; + char *buf = NULL; + + if (root) { + /* 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; + } + else if (resp) { + if (*resp) free(*resp); + *resp = buf; + } + } + else if (resp) buf = *resp; + + /* Output the JSON object */ + txn->resp_body.type = "application/json; charset=utf-8"; + write_body(code, txn, buf, buf ? strlen(buf) : 0); + + if (!resp && buf) free(buf); + + return 0; +} + + +/* Array of parameter names - MUST be kept in sync with tz_err.et */ +static const char *param_names[] = { + "action", + "tzid", + "pattern", + "format", + "start", + "end", + "changedsince", + "truncate", + "tzid" +}; + +static int json_error_response(struct transaction_t *txn, long tz_code, + struct strlist *param, icaltimetype *time) +{ + long http_code = HTTP_BAD_REQUEST; + const char *param_name, *fmt = NULL; + char desc[100]; + json_t *root; + + param_name = param_names[tz_code - tz_err_base]; + + if (!param) fmt = "missing %s parameter"; + else if (param->next) fmt = "multiple %s parameters"; + else if (!param->s || !param->s[0]) fmt = "missing %s value"; + else if (!time) fmt = "unknown %s value"; + else if (!time->is_utc) fmt = "invalid %s UTC value"; + + switch (tz_code) { + case TZ_INVALID_TZID: + if (!fmt) fmt = "tzid used with changedsince"; + break; + + case TZ_INVALID_END: + case TZ_INVALID_TRUNCATE: + if (!fmt) fmt = "end <= start"; + break; + + case TZ_NOT_FOUND: + http_code = HTTP_NOT_FOUND; + fmt = "time zone not found"; + break; + } + + snprintf(desc, sizeof(desc), fmt ? fmt : "unknown error", param_name); + + root = json_pack("{s:s s:s}", "error", error_message(tz_code), + "description", desc); + if (!root) { + txn->error.desc = "Unable to create JSON response"; + return HTTP_SERVER_ERROR; + } + + return json_response(http_code, txn, root, NULL); +}
View file
cyrus-imapd-2.5.tar.gz/imap/httpd.c
Added
@@ -0,0 +1,3820 @@ +/* httpd.c -- HTTP/WebDAV/CalDAV server protocol parsing + * + * Copyright (c) 1994-2011 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 <stdio.h> +#include <errno.h> +#include <string.h> +#include <fcntl.h> +#include <signal.h> +#include <sys/types.h> +#include <sys/param.h> +#include <syslog.h> +#include <netdb.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <ctype.h> +#include "prot.h" + +#include <sasl/sasl.h> +#include <sasl/saslutil.h> + +#include "httpd.h" +#include "http_proxy.h" + +#include "assert.h" +#include "util.h" +#include "iptostring.h" +#include "global.h" +#include "tls.h" +#include "map.h" + +#include "exitcodes.h" +#include "imapd.h" +#include "imap_err.h" +#include "http_err.h" +#include "proc.h" +#include "version.h" +#include "xstrlcpy.h" +#include "xstrlcat.h" +#include "sync_log.h" +#include "telemetry.h" +#include "backend.h" +#include "proxy.h" +#include "userdeny.h" +#include "message.h" +#include "idle.h" +#include "times.h" +#include "tok.h" +#include "wildmat.h" +#include "md5.h" + +#ifdef WITH_DAV +#include "http_dav.h" +#endif + +#include <libxml/tree.h> +#include <libxml/HTMLtree.h> +#include <libxml/uri.h> + +#ifdef HAVE_ZLIB +#include <zlib.h> +#endif /* HAVE_ZLIB */ + + +static const char tls_message[] = + HTML_DOCTYPE + "<html>\n<head>\n<title>TLS Required</title>\n</head>\n" \ + "<body>\n<h2>TLS is required to use Basic authentication</h2>\n" \ + "Use <a href=\"%s\">%s</a> instead.\n" \ + "</body>\n</html>\n"; + +extern int optind; +extern char *optarg; +extern int opterr; + + +#ifdef HAVE_SSL +static SSL *tls_conn; +#endif /* HAVE_SSL */ + +sasl_conn_t *httpd_saslconn; /* the sasl connection context */ + +static struct wildmat *allow_cors = NULL; +int httpd_timeout, httpd_keepalive; +char *httpd_userid = NULL, *proxy_userid = NULL; +struct auth_state *httpd_authstate = 0; +int httpd_userisadmin = 0; +int httpd_userisproxyadmin = 0; +struct sockaddr_storage httpd_localaddr, httpd_remoteaddr; +int httpd_haveaddr = 0; +char httpd_clienthost[NI_MAXHOST*2+1] = "[local]"; +struct protstream *httpd_out = NULL; +struct protstream *httpd_in = NULL; +struct protgroup *protin = NULL; +static int httpd_logfd = -1; + +static sasl_ssf_t extprops_ssf = 0; +int https = 0; +int httpd_tls_done = 0; +int httpd_tls_required = 0; +unsigned avail_auth_schemes = 0; /* bitmask of available auth schemes */ +unsigned long config_httpmodules; +int config_httpprettytelemetry; + +static time_t compile_time; +struct buf serverinfo = BUF_INITIALIZER; + +static void digest_send_success(const char *name __attribute__((unused)), + const char *data) +{ + prot_printf(httpd_out, "Authentication-Info: %s\r\n", data); +} + +/* List of HTTP auth schemes that we support */ +struct auth_scheme_t auth_schemes[] = { + { AUTH_BASIC, "Basic", NULL, AUTH_SERVER_FIRST | AUTH_BASE64, NULL, NULL }, + { AUTH_DIGEST, "Digest", HTTP_DIGEST_MECH, AUTH_NEED_REQUEST|AUTH_SERVER_FIRST, + &digest_send_success, digest_recv_success }, + { AUTH_SPNEGO, "Negotiate", "GSS-SPNEGO", AUTH_BASE64, NULL, NULL }, + { AUTH_NTLM, "NTLM", "NTLM", AUTH_NEED_PERSIST | AUTH_BASE64, NULL, NULL }, + { -1, NULL, NULL, 0, NULL, NULL } +}; + + +/* the sasl proxy policy context */ +static struct proxy_context httpd_proxyctx = { + 0, 1, &httpd_authstate, &httpd_userisadmin, &httpd_userisproxyadmin +}; + +/* signal to config.c */ +const int config_need_data = CONFIG_NEED_PARTITION_DATA; + +/* current namespace */ +HIDDEN struct namespace httpd_namespace; + +/* PROXY STUFF */ +/* we want a list of our outgoing connections here and which one we're + currently piping */ + +/* the current server most commands go to */ +struct backend *backend_current = NULL; + +/* our cached connections */ +struct backend **backend_cached = NULL; + +/* end PROXY stuff */ + +static void starttls(int https); +void usage(void); +void shut_down(int code) __attribute__ ((noreturn)); + +/* Enable the resetting of a sasl_conn_t */ +static int reset_saslconn(sasl_conn_t **conn); + +static void cmdloop(void); +static int parse_expect(struct transaction_t *txn); +static void parse_connection(struct transaction_t *txn); +static int parse_ranges(const char *hdr, unsigned long len, + struct range **ranges); +static int proxy_authz(const char **authzid, struct transaction_t *txn); +static void auth_success(struct transaction_t *txn); +static int http_auth(const char *creds, struct transaction_t *txn); +static void keep_alive(int sig); + +static int meth_get(struct transaction_t *txn, void *params); +static int meth_propfind_root(struct transaction_t *txn, void *params); + + +static struct { + char *ipremoteport; + char *iplocalport; + sasl_ssf_t ssf; + char *authid; +} saslprops = {NULL,NULL,0,NULL}; + +static struct sasl_callback mysasl_cb[] = { + { SASL_CB_GETOPT, (mysasl_cb_ft *) &mysasl_config, NULL }, + { SASL_CB_PROXY_POLICY, (mysasl_cb_ft *) &mysasl_proxy_policy, (void*) &httpd_proxyctx }, + { SASL_CB_CANON_USER, (mysasl_cb_ft *) &mysasl_canon_user, NULL }, + { SASL_CB_LIST_END, NULL, NULL } +}; + +/* Array of HTTP methods known by our server. */ +const struct known_meth_t http_methods[] = { + { "ACL", 0 }, + { "COPY", METH_NOBODY }, + { "DELETE", METH_NOBODY }, + { "GET", METH_NOBODY }, + { "HEAD", METH_NOBODY }, + { "LOCK", 0 }, + { "MKCALENDAR", 0 }, + { "MKCOL", 0 }, + { "MOVE", METH_NOBODY }, + { "OPTIONS", METH_NOBODY }, + { "POST", 0 }, + { "PROPFIND", 0 }, + { "PROPPATCH", 0 }, + { "PUT", 0 }, + { "REPORT", 0 }, + { "TRACE", METH_NOBODY }, + { "UNLOCK", METH_NOBODY }, + { NULL, 0 } +}; + +/* Namespace to fetch static content from filesystem */ +struct namespace_t namespace_default = { + URL_NS_DEFAULT, 1, "", NULL, 0 /* no auth */, ALLOW_READ, + NULL, NULL, NULL, NULL, + { + { NULL, NULL }, /* ACL */ + { 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, NULL }, /* OPTIONS */ + { NULL, NULL }, /* POST */ + { &meth_propfind_root, NULL }, /* PROPFIND */ + { NULL, NULL }, /* PROPPATCH */ + { NULL, NULL }, /* PUT */ + { NULL, NULL }, /* REPORT */ + { &meth_trace, NULL }, /* TRACE */ + { NULL, NULL }, /* UNLOCK */ + } +}; + +/* Array of different namespaces and features supported by the server */ +struct namespace_t *namespaces[] = { +#ifdef WITH_DAV +#ifdef WITH_JSON + &namespace_timezone, /* MUST be before namespace_calendar!! */ +#endif /* WITH_JSON */ + &namespace_principal, + &namespace_calendar, + &namespace_addressbook, + &namespace_ischedule, + &namespace_domainkey, +#endif /* WITH_DAV */ + &namespace_rss, + &namespace_dblookup, + &namespace_default, /* MUST be present and be last!! */ + NULL, +}; + + +static void httpd_reset(void) +{ + int i; + int bytes_in = 0; + int bytes_out = 0; + + /* Do any namespace specific cleanup */ + for (i = 0; namespaces[i]; i++) { + if (namespaces[i]->enabled && namespaces[i]->reset) + namespaces[i]->reset(); + } + + proc_cleanup(); + + /* close backend connections */ + i = 0; + while (backend_cached && backend_cached[i]) { + proxy_downserver(backend_cached[i]); + free(backend_cached[i]->context); + free(backend_cached[i]); + i++; + } + if (backend_cached) free(backend_cached); + backend_cached = NULL; + backend_current = NULL; + + if (httpd_in) { + prot_NONBLOCK(httpd_in); + prot_fill(httpd_in); + bytes_in = prot_bytes_in(httpd_in); + prot_free(httpd_in); + } + + if (httpd_out) { + prot_flush(httpd_out); + bytes_out = prot_bytes_out(httpd_out); + prot_free(httpd_out); + } + + if (config_auditlog) { + syslog(LOG_NOTICE, + "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", + session_id(), bytes_in, bytes_out); + } + + httpd_in = httpd_out = NULL; + + if (protin) protgroup_reset(protin); + +#ifdef HAVE_SSL + if (tls_conn) { + tls_reset_servertls(&tls_conn); + tls_conn = NULL; + } +#endif + + cyrus_reset_stdio(); + + strcpy(httpd_clienthost, "[local]"); + if (httpd_logfd != -1) { + close(httpd_logfd); + httpd_logfd = -1; + } + if (httpd_userid != NULL) { + free(httpd_userid); + httpd_userid = NULL; + } + if (proxy_userid != NULL) { + free(proxy_userid); + proxy_userid = NULL; + } + if (httpd_authstate) { + auth_freestate(httpd_authstate); + httpd_authstate = NULL; + } + if (httpd_saslconn) { + sasl_dispose(&httpd_saslconn); + httpd_saslconn = NULL; + } + httpd_tls_done = 0; + + if(saslprops.iplocalport) { + free(saslprops.iplocalport); + saslprops.iplocalport = NULL; + } + if(saslprops.ipremoteport) { + free(saslprops.ipremoteport); + saslprops.ipremoteport = NULL; + } + if(saslprops.authid) { + free(saslprops.authid); + saslprops.authid = NULL; + } + saslprops.ssf = 0; +} + +/* + * run once when process is forked; + * MUST NOT exit directly; must return with non-zero error code + */ +int service_init(int argc __attribute__((unused)), + char **argv __attribute__((unused)), + char **envp __attribute__((unused))) +{ + int r, opt, i, allow_trace = config_getswitch(IMAPOPT_HTTPALLOWTRACE); + + LIBXML_TEST_VERSION + + initialize_http_error_table(); + + if (geteuid() == 0) fatal("must run as the Cyrus user", EC_USAGE); + setproctitle_init(argc, argv, envp); + + /* set signal handlers */ + signals_set_shutdown(&shut_down); + signal(SIGPIPE, SIG_IGN); + + /* load the SASL plugins */ + global_sasl_init(1, 1, mysasl_cb); + + /* open the mboxlist, we'll need it for real work */ + mboxlist_init(0); + mboxlist_open(NULL); + + /* open the quota db, we'll need it for expunge */ + quotadb_init(0); + quotadb_open(NULL); + + /* open the user deny db */ + denydb_init(0); + denydb_open(/*create*/0); + + /* open annotations.db, we'll need it for collection properties */ + annotatemore_open(); + + /* setup for sending IMAP IDLE notifications */ + idle_enabled(); + + /* Set namespace */ + if ((r = mboxname_init_namespace(&httpd_namespace, 1)) != 0) { + syslog(LOG_ERR, "%s", error_message(r)); + fatal(error_message(r), EC_CONFIG); + } + /* External names are in URIs (UNIX sep) */ + httpd_namespace.hier_sep = '/'; + + /* open the mboxevent system */ + mboxevent_init(); + + mboxevent_setnamespace(&httpd_namespace); + + while ((opt = getopt(argc, argv, "sp:")) != EOF) { + switch(opt) { + case 's': /* https (do TLS right away) */ + https = 1; + if (!tls_enabled()) { + syslog(LOG_ERR, "https: required OpenSSL options not present"); + fatal("https: required OpenSSL options not present", + EC_CONFIG); + } + break; + + case 'p': /* external protection */ + extprops_ssf = atoi(optarg); + break; + + default: + usage(); + } + } + + /* Create a protgroup for input from the client and selected backend */ + protin = protgroup_new(2); + + config_httpprettytelemetry = config_getswitch(IMAPOPT_HTTPPRETTYTELEMETRY); + + if (config_getstring(IMAPOPT_HTTPALLOWCORS)) { + allow_cors = + split_wildmats((char *) config_getstring(IMAPOPT_HTTPALLOWCORS), + NULL); + } + + /* Construct serverinfo string */ + buf_printf(&serverinfo, "Cyrus/%s%s Cyrus-SASL/%u.%u.%u", + cyrus_version(), config_mupdate_server ? " (Murder)" : "", + SASL_VERSION_MAJOR, SASL_VERSION_MINOR, SASL_VERSION_STEP); +#ifdef HAVE_SSL + buf_printf(&serverinfo, " OpenSSL/%s", SHLIB_VERSION_NUMBER); +#endif +#ifdef HAVE_ZLIB + buf_printf(&serverinfo, " Zlib/%s", ZLIB_VERSION); +#endif + buf_printf(&serverinfo, " LibXML%s", LIBXML_DOTTED_VERSION); + + /* Do any namespace specific initialization */ + config_httpmodules = config_getbitfield(IMAPOPT_HTTPMODULES); + for (i = 0; namespaces[i]; i++) { + if (allow_trace) namespaces[i]->allow |= ALLOW_TRACE; + if (namespaces[i]->init) namespaces[i]->init(&serverinfo); + } + + compile_time = calc_compile_time(__TIME__, __DATE__); + + return 0; +} + + +/* + * run for each accepted connection + */ +int service_main(int argc __attribute__((unused)), + char **argv __attribute__((unused)), + char **envp __attribute__((unused))) +{ + socklen_t salen; + char hbuf[NI_MAXHOST]; + char localip[60], remoteip[60]; + int niflags; + sasl_security_properties_t *secprops=NULL; + const char *mechlist, *mech; + int mechcount = 0; + size_t mechlen; + struct auth_scheme_t *scheme; + + session_new_id(); + + signals_poll(); + + sync_log_init(); + + httpd_in = prot_new(0, 0); + httpd_out = prot_new(1, 1); + protgroup_insert(protin, httpd_in); + + /* Find out name of client host */ + salen = sizeof(httpd_remoteaddr); + if (getpeername(0, (struct sockaddr *)&httpd_remoteaddr, &salen) == 0 && + (httpd_remoteaddr.ss_family == AF_INET || + httpd_remoteaddr.ss_family == AF_INET6)) { + if (getnameinfo((struct sockaddr *)&httpd_remoteaddr, salen, + hbuf, sizeof(hbuf), NULL, 0, NI_NAMEREQD) == 0) { + strncpy(httpd_clienthost, hbuf, sizeof(hbuf)); + strlcat(httpd_clienthost, " ", sizeof(httpd_clienthost)); + } else { + httpd_clienthost[0] = '\0'; + } + niflags = NI_NUMERICHOST; +#ifdef NI_WITHSCOPEID + if (((struct sockaddr *)&httpd_remoteaddr)->sa_family == AF_INET6) + niflags |= NI_WITHSCOPEID; +#endif + if (getnameinfo((struct sockaddr *)&httpd_remoteaddr, salen, hbuf, + sizeof(hbuf), NULL, 0, niflags) != 0) + strlcpy(hbuf, "unknown", sizeof(hbuf)); + strlcat(httpd_clienthost, "[", sizeof(httpd_clienthost)); + strlcat(httpd_clienthost, hbuf, sizeof(httpd_clienthost)); + strlcat(httpd_clienthost, "]", sizeof(httpd_clienthost)); + salen = sizeof(httpd_localaddr); + if (getsockname(0, (struct sockaddr *)&httpd_localaddr, &salen) == 0) { + httpd_haveaddr = 1; + } + + /* Create pre-authentication telemetry log based on client IP */ + httpd_logfd = telemetry_log(hbuf, httpd_in, httpd_out, 0); + } + + /* other params should be filled in */ + if (sasl_server_new("HTTP", config_servername, NULL, NULL, NULL, NULL, + SASL_USAGE_FLAGS, &httpd_saslconn) != SASL_OK) + fatal("SASL failed initializing: sasl_server_new()",EC_TEMPFAIL); + + /* will always return something valid */ + secprops = mysasl_secprops(0); + + /* no HTTP clients seem to use "auth-int" */ + secprops->max_ssf = 0; /* "auth" only */ + secprops->maxbufsize = 0; /* don't need maxbuf */ + if (sasl_setprop(httpd_saslconn, SASL_SEC_PROPS, secprops) != SASL_OK) + fatal("Failed to set SASL property", EC_TEMPFAIL); + if (sasl_setprop(httpd_saslconn, SASL_SSF_EXTERNAL, &extprops_ssf) != SASL_OK) + fatal("Failed to set SASL property", EC_TEMPFAIL); + + if(iptostring((struct sockaddr *)&httpd_localaddr, + salen, localip, 60) == 0) { + sasl_setprop(httpd_saslconn, SASL_IPLOCALPORT, localip); + saslprops.iplocalport = xstrdup(localip); + } + + if(iptostring((struct sockaddr *)&httpd_remoteaddr, + salen, remoteip, 60) == 0) { + sasl_setprop(httpd_saslconn, SASL_IPREMOTEPORT, remoteip); + saslprops.ipremoteport = xstrdup(remoteip); + } + + /* See which auth schemes are available to us */ + if ((extprops_ssf >= 2) || config_getswitch(IMAPOPT_ALLOWPLAINTEXT)) { + avail_auth_schemes |= (1 << AUTH_BASIC); + } + sasl_listmech(httpd_saslconn, NULL, NULL, " ", NULL, + &mechlist, NULL, &mechcount); + for (mech = mechlist; mechcount--; mech += ++mechlen) { + mechlen = strcspn(mech, " \0"); + for (scheme = auth_schemes; scheme->name; scheme++) { + if (scheme->saslmech && !strncmp(mech, scheme->saslmech, mechlen)) { + avail_auth_schemes |= (1 << scheme->idx); + break; + } + } + } + httpd_tls_required = !avail_auth_schemes; + + proc_register("httpd", httpd_clienthost, NULL, NULL, NULL); + + /* Set inactivity timer */ + httpd_timeout = config_getint(IMAPOPT_HTTPTIMEOUT); + if (httpd_timeout < 0) httpd_timeout = 0; + httpd_timeout *= 60; + prot_settimeout(httpd_in, httpd_timeout); + prot_setflushonread(httpd_in, httpd_out); + + /* we were connected on https port so we should do + TLS negotiation immediatly */ + if (https == 1) starttls(1); + + /* Setup the signal handler for keepalive heartbeat */ + httpd_keepalive = config_getint(IMAPOPT_HTTPKEEPALIVE); + if (httpd_keepalive < 0) httpd_keepalive = 0; + if (httpd_keepalive) { + struct sigaction action; + + sigemptyset(&action.sa_mask); + action.sa_flags = 0; +#ifdef SA_RESTART + action.sa_flags |= SA_RESTART; +#endif + action.sa_handler = keep_alive; + if (sigaction(SIGALRM, &action, NULL) < 0) { + syslog(LOG_ERR, "unable to install signal handler for %d: %m", SIGALRM); + httpd_keepalive = 0; + } + } + + cmdloop(); + + /* Closing connection */ + + /* cleanup */ + signal(SIGALRM, SIG_IGN); + httpd_reset(); + + return 0; +} + + +/* Called by service API to shut down the service */ +void service_abort(int error) +{ + shut_down(error); +} + + +void usage(void) +{ + prot_printf(httpd_out, "%s: usage: httpd [-C <alt_config>] [-s]\r\n", + error_message(HTTP_SERVER_ERROR)); + prot_flush(httpd_out); + exit(EC_USAGE); +} + + +/* + * Cleanly shut down and exit + */ +void shut_down(int code) +{ + int i; + int bytes_in = 0; + int bytes_out = 0; + + in_shutdown = 1; + + if (allow_cors) free_wildmats(allow_cors); + + /* Do any namespace specific cleanup */ + for (i = 0; namespaces[i]; i++) { + if (namespaces[i]->enabled && namespaces[i]->shutdown) + namespaces[i]->shutdown(); + } + + xmlCleanupParser(); + + proc_cleanup(); + + /* close backend connections */ + i = 0; + while (backend_cached && backend_cached[i]) { + proxy_downserver(backend_cached[i]); + free(backend_cached[i]->context); + free(backend_cached[i]); + i++; + } + if (backend_cached) free(backend_cached); + + sync_log_done(); + + mboxlist_close(); + mboxlist_done(); + + quotadb_close(); + quotadb_done(); + + denydb_close(); + denydb_done(); + + annotatemore_close(); + + if (httpd_in) { + prot_NONBLOCK(httpd_in); + prot_fill(httpd_in); + bytes_in = prot_bytes_in(httpd_in); + prot_free(httpd_in); + } + + if (httpd_out) { + prot_flush(httpd_out); + bytes_out = prot_bytes_out(httpd_out); + prot_free(httpd_out); + } + + if (protin) protgroup_free(protin); + + if (config_auditlog) + syslog(LOG_NOTICE, + "auditlog: traffic sessionid=<%s> bytes_in=<%d> bytes_out=<%d>", + session_id(), bytes_in, bytes_out); + +#ifdef HAVE_SSL + tls_shutdown_serverengine(); +#endif + + cyrus_done(); + + exit(code); +} + + +void fatal(const char* s, int code) +{ + static int recurse_code = 0; + + if (recurse_code) { + /* We were called recursively. Just give up */ + proc_cleanup(); + exit(recurse_code); + } + recurse_code = code; + if (httpd_out) { + prot_printf(httpd_out, + "HTTP/1.1 %s\r\n" + "Content-Type: text/plain\r\n" + "Connection: close\r\n\r\n" + "Fatal error: %s\r\n", + error_message(HTTP_SERVER_ERROR), s); + prot_flush(httpd_out); + } + syslog(LOG_ERR, "Fatal error: %s", s); + shut_down(code); +} + + + + +#ifdef HAVE_SSL +static void starttls(int https) +{ + int result; + int *layerp; + sasl_ssf_t ssf; + char *auth_id; + + /* SASL and openssl have different ideas about whether ssf is signed */ + layerp = (int *) &ssf; + + result=tls_init_serverengine("http", + 5, /* depth to verify */ + !https, /* can client auth? */ + !https); /* TLS only? */ + + if (result == -1) { + syslog(LOG_ERR, "[httpd] error initializing TLS"); + fatal("tls_init() failed",EC_TEMPFAIL); + } + + if (!https) { + /* tell client to start TLS upgrade (RFC 2817) */ + response_header(HTTP_SWITCH_PROT, NULL); + } + + result=tls_start_servertls(0, /* read */ + 1, /* write */ + https ? 180 : httpd_timeout, + layerp, + &auth_id, + &tls_conn); + + /* if error */ + if (result == -1) { + syslog(LOG_NOTICE, "https failed: %s", httpd_clienthost); + fatal("tls_start_servertls() failed", EC_TEMPFAIL); + } + + /* tell SASL about the negotiated layer */ + result = sasl_setprop(httpd_saslconn, SASL_SSF_EXTERNAL, &ssf); + if (result != SASL_OK) { + fatal("sasl_setprop() failed: starttls()", EC_TEMPFAIL); + } + saslprops.ssf = ssf; + + result = sasl_setprop(httpd_saslconn, SASL_AUTH_EXTERNAL, auth_id); + if (result != SASL_OK) { + fatal("sasl_setprop() failed: starttls()", EC_TEMPFAIL); + } + if (saslprops.authid) { + free(saslprops.authid); + saslprops.authid = NULL; + } + if (auth_id) saslprops.authid = xstrdup(auth_id); + + /* tell the prot layer about our new layers */ + prot_settls(httpd_in, tls_conn); + prot_settls(httpd_out, tls_conn); + + httpd_tls_done = 1; + httpd_tls_required = 0; + + avail_auth_schemes |= (1 << AUTH_BASIC); +} +#else +static void starttls(int https __attribute__((unused))) +{ + fatal("starttls() called, but no OpenSSL", EC_SOFTWARE); +} +#endif /* HAVE_SSL */ + + +/* Reset the given sasl_conn_t to a sane state */ +static int reset_saslconn(sasl_conn_t **conn) +{ + int ret; + sasl_security_properties_t *secprops = NULL; + + sasl_dispose(conn); + /* do initialization typical of service_main */ + ret = sasl_server_new("HTTP", config_servername, NULL, NULL, NULL, NULL, + SASL_USAGE_FLAGS, conn); + if(ret != SASL_OK) return ret; + + if(saslprops.ipremoteport) + ret = sasl_setprop(*conn, SASL_IPREMOTEPORT, + saslprops.ipremoteport); + if(ret != SASL_OK) return ret; + + if(saslprops.iplocalport) + ret = sasl_setprop(*conn, SASL_IPLOCALPORT, + saslprops.iplocalport); + if(ret != SASL_OK) return ret; + secprops = mysasl_secprops(0); + + /* no HTTP clients seem to use "auth-int" */ + secprops->max_ssf = 0; /* "auth" only */ + secprops->maxbufsize = 0; /* don't need maxbuf */ + ret = sasl_setprop(*conn, SASL_SEC_PROPS, secprops); + if(ret != SASL_OK) return ret; + /* end of service_main initialization excepting SSF */ + + /* If we have TLS/SSL info, set it */ + if(saslprops.ssf) { + ret = sasl_setprop(*conn, SASL_SSF_EXTERNAL, &saslprops.ssf); + } else { + ret = sasl_setprop(*conn, SASL_SSF_EXTERNAL, &extprops_ssf); + } + + if(ret != SASL_OK) return ret; + + if(saslprops.authid) { + ret = sasl_setprop(*conn, SASL_AUTH_EXTERNAL, saslprops.authid); + if(ret != SASL_OK) return ret; + } + /* End TLS/SSL Info */ + + return SASL_OK; +} + + +/* + * Top-level command loop parsing + */ +static void cmdloop(void) +{ + int gzip_enabled = 0; + struct transaction_t txn; + + /* Start with an empty (clean) transaction */ + memset(&txn, 0, sizeof(struct transaction_t)); + + /* Pre-allocate our working buffer */ + buf_ensure(&txn.buf, 1024); + +#ifdef HAVE_ZLIB + /* Always use gzip format because IE incorrectly uses raw deflate */ + if (config_getswitch(IMAPOPT_HTTPALLOWCOMPRESS) && + deflateInit2(&txn.zstrm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + 16+MAX_WBITS, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY) == Z_OK) { + gzip_enabled = 1; + } +#endif + + for (;;) { + int ret, empty, r, i, c; + char *p; + tok_t tok; + const char **hdr, *query; + const struct namespace_t *namespace; + const struct method_t *meth_t; + struct request_line_t *req_line = &txn.req_line; + + /* Reset txn state */ + txn.meth = METH_UNKNOWN; + memset(&txn.flags, 0, sizeof(struct txn_flags_t)); + txn.flags.conn = 0; + txn.flags.vary = VARY_AE; + memset(req_line, 0, sizeof(struct request_line_t)); + memset(&txn.req_tgt, 0, sizeof(struct request_target_t)); + construct_hash_table(&txn.req_qparams, 10, 1); + txn.req_uri = NULL; + txn.auth_chal.param = NULL; + txn.req_hdrs = NULL; + txn.req_body.flags = 0; + buf_reset(&txn.req_body.payload); + txn.location = NULL; + memset(&txn.error, 0, sizeof(struct error_t)); + memset(&txn.resp_body, 0, /* Don't zero the response payload buffer */ + sizeof(struct resp_body_t) - sizeof(struct buf)); + buf_reset(&txn.resp_body.payload); + buf_reset(&txn.buf); + ret = empty = 0; + + /* Create header cache */ + if (!(txn.req_hdrs = spool_new_hdrcache())) { + txn.error.desc = "Unable to create header cache"; + ret = HTTP_SERVER_ERROR; + } + + req_line: + do { + /* Flush any buffered output */ + prot_flush(httpd_out); + if (backend_current) prot_flush(backend_current->out); + + /* Check for shutdown file */ + if (shutdown_file(txn.buf.s, txn.buf.alloc) || + (httpd_userid && + userdeny(httpd_userid, config_ident, txn.buf.s, txn.buf.alloc))) { + txn.error.desc = txn.buf.s; + ret = HTTP_UNAVAILABLE; + break; + } + + signals_poll(); + + } while (!proxy_check_input(protin, httpd_in, httpd_out, + backend_current ? backend_current->in : NULL, + NULL, 0)); + if (ret) { + txn.flags.conn = CONN_CLOSE; + error_response(ret, &txn); + protgroup_free(protin); + shut_down(0); + } + + + /* Read request-line */ + syslog(LOG_DEBUG, "read & parse request-line"); + if (!prot_fgets(req_line->buf, MAX_REQ_LINE+1, httpd_in)) { + txn.error.desc = prot_error(httpd_in); + if (txn.error.desc && strcmp(txn.error.desc, PROT_EOF_STRING)) { + /* client timed out */ + syslog(LOG_WARNING, "%s, closing connection", txn.error.desc); + ret = HTTP_TIMEOUT; + } + else { + /* client closed connection */ + } + + txn.flags.conn = CONN_CLOSE; + goto done; + } + + /* Trim CRLF from request-line */ + p = req_line->buf + strlen(req_line->buf); + if (p[-1] == '\n') *--p = '\0'; + if (p[-1] == '\r') *--p = '\0'; + + /* Ignore 1 empty line before request-line per HTTPbis Part 1 Sec 3.5 */ + if (!empty++ && !*req_line->buf) goto req_line; + + /* Parse request-line = method SP request-target SP HTTP-version CRLF */ + tok_initm(&tok, req_line->buf, " ", 0); + if (!(req_line->meth = tok_next(&tok))) { + ret = HTTP_BAD_REQUEST; + txn.error.desc = "Missing method in request-line"; + } + else if (!(req_line->uri = tok_next(&tok))) { + ret = HTTP_BAD_REQUEST; + txn.error.desc = "Missing request-target in request-line"; + } + else if ((size_t) (p - req_line->buf) > MAX_REQ_LINE - 2) { + /* request-line overran the size of our buffer */ + ret = HTTP_TOO_LONG; + buf_printf(&txn.buf, + "Length of request-line MUST be less than %u octets", + MAX_REQ_LINE); + txn.error.desc = buf_cstring(&txn.buf); + } + else if (!(req_line->ver = tok_next(&tok))) { + ret = HTTP_BAD_REQUEST; + txn.error.desc = "Missing HTTP-version in request-line"; + } + else if (tok_next(&tok)) { + ret = HTTP_BAD_REQUEST; + txn.error.desc = "Unexpected extra argument(s) in request-line"; + } + + /* Check HTTP-Version - MUST be HTTP/1.x */ + else if (strlen(req_line->ver) != HTTP_VERSION_LEN + || strncmp(req_line->ver, HTTP_VERSION, HTTP_VERSION_LEN-1) + || !isdigit(req_line->ver[HTTP_VERSION_LEN-1])) { + ret = HTTP_BAD_VERSION; + buf_printf(&txn.buf, + "This server only speaks %.*sx", + HTTP_VERSION_LEN-1, HTTP_VERSION); + txn.error.desc = buf_cstring(&txn.buf); + } + else if (req_line->ver[HTTP_VERSION_LEN-1] == '0') { + /* HTTP/1.0 connection */ + txn.flags.ver1_0 = 1; + } + tok_fini(&tok); + + if (ret) { + txn.flags.conn = CONN_CLOSE; + goto done; + } + + /* Read and parse headers */ + syslog(LOG_DEBUG, "read & parse headers"); + if ((r = spool_fill_hdrcache(httpd_in, NULL, txn.req_hdrs, NULL))) { + ret = HTTP_BAD_REQUEST; + txn.error.desc = error_message(r); + } + else if ((txn.error.desc = prot_error(httpd_in)) && + strcmp(txn.error.desc, PROT_EOF_STRING)) { + /* client timed out */ + syslog(LOG_WARNING, "%s, closing connection", txn.error.desc); + ret = HTTP_TIMEOUT; + } + + /* Read CRLF separating headers and body */ + else if ((c = prot_getc(httpd_in)) != '\r' || + (c = prot_getc(httpd_in)) != '\n') { + ret = HTTP_BAD_REQUEST; + txn.error.desc = error_message(IMAP_MESSAGE_NOBLANKLINE); + } + + if (ret) { + txn.flags.conn = CONN_CLOSE; + goto done; + } + + /* Check for Connection options */ + parse_connection(&txn); + if (txn.flags.conn & CONN_UPGRADE) { + starttls(0); + txn.flags.conn &= ~CONN_UPGRADE; + } + + /* Check for HTTP method override */ + if (!strcmp(req_line->meth, "POST") && + (hdr = spool_getheader(txn.req_hdrs, "X-HTTP-Method-Override"))) { + txn.flags.override = 1; + req_line->meth = (char *) hdr[0]; + } + + /* Check Method against our list of known methods */ + for (txn.meth = 0; (txn.meth < METH_UNKNOWN) && + strcmp(http_methods[txn.meth].name, req_line->meth); + txn.meth++); + + if (txn.meth == METH_UNKNOWN) ret = HTTP_NOT_IMPLEMENTED; + + /* Parse request-target URI */ + else if (!(txn.req_uri = parse_uri(txn.meth, req_line->uri, 1, + &txn.error.desc))) { + ret = HTTP_BAD_REQUEST; + } + + /* Check message framing */ + else if ((r = http_parse_framing(txn.req_hdrs, &txn.req_body, + &txn.error.desc))) { + ret = r; + } + + /* Check for Expectations */ + else if ((r = parse_expect(&txn))) { + ret = r; + } + + /* Check for mandatory Host header (HTTP/1.1+ only) */ + else if ((hdr = spool_getheader(txn.req_hdrs, "Host")) && hdr[1]) { + ret = HTTP_BAD_REQUEST; + txn.error.desc = "Too many Host headers"; + } + else if (!hdr) { + if (txn.flags.ver1_0) { + /* HTTP/1.0 - create a Host header from URI */ + if (txn.req_uri->server) { + buf_setcstr(&txn.buf, txn.req_uri->server); + if (txn.req_uri->port) + buf_printf(&txn.buf, ":%d", txn.req_uri->port); + } + else buf_setcstr(&txn.buf, config_servername); + + spool_cache_header(xstrdup("Host"), + xstrdup(buf_cstring(&txn.buf)), + txn.req_hdrs); + buf_reset(&txn.buf); + } + else { + ret = HTTP_BAD_REQUEST; + txn.error.desc = "Missing Host header"; + } + } + + if (ret) goto done; + + query = URI_QUERY(txn.req_uri); + + /* Find the namespace of the requested resource */ + for (i = 0; namespaces[i]; i++) { + const char *path = txn.req_uri->path; + size_t len; + + /* Skip disabled namespaces */ + if (!namespaces[i]->enabled) continue; + + /* Handle any /.well-known/ bootstrapping */ + if (namespaces[i]->well_known) { + len = strlen(namespaces[i]->well_known); + if (!strncmp(path, namespaces[i]->well_known, len) && + (!path[len] || path[len] == '/')) { + + hdr = spool_getheader(txn.req_hdrs, "Host"); + buf_reset(&txn.buf); + buf_printf(&txn.buf, "%s://%s", + https? "https" : "http", hdr[0]); + buf_appendcstr(&txn.buf, namespaces[i]->prefix); + buf_appendcstr(&txn.buf, path + len); + if (query) buf_printf(&txn.buf, "?%s", query); + txn.location = buf_cstring(&txn.buf); + + ret = HTTP_MOVED; + goto done; + } + } + + /* See if the prefix matches - terminated with NUL or '/' */ + len = strlen(namespaces[i]->prefix); + if (!strncmp(path, namespaces[i]->prefix, len) && + (!path[len] || (path[len] == '/') || !strcmp(path, "*"))) { + break; + } + } + if ((namespace = namespaces[i])) { + txn.req_tgt.namespace = namespace->id; + txn.req_tgt.allow = namespace->allow; + + /* Check if method is supported in this namespace */ + meth_t = &namespace->methods[txn.meth]; + if (!meth_t->proc) ret = HTTP_NOT_ALLOWED; + + /* Check if method expects a body */ + else if ((http_methods[txn.meth].flags & METH_NOBODY) && + (txn.req_body.framing != FRAMING_LENGTH || + /* XXX Will break if client sends just a last-chunk */ + txn.req_body.len)) { + ret = HTTP_BAD_MEDIATYPE; + } + } else { + /* XXX Should never get here */ + ret = HTTP_SERVER_ERROR; + } + + if (ret) goto done; + + /* Perform authentication, if necessary */ + if ((hdr = spool_getheader(txn.req_hdrs, "Authorization"))) { + if (httpd_userid) { + /* Reauth - reinitialize */ + syslog(LOG_DEBUG, "reauth - reinit"); + reset_saslconn(&httpd_saslconn); + txn.auth_chal.scheme = NULL; + } + + /* Check the auth credentials */ + r = http_auth(hdr[0], &txn); + if ((r < 0) || !txn.auth_chal.scheme) { + /* Auth failed - reinitialize */ + syslog(LOG_DEBUG, "auth failed - reinit"); + reset_saslconn(&httpd_saslconn); + txn.auth_chal.scheme = NULL; + ret = HTTP_UNAUTHORIZED; + } + } + else if (!httpd_userid && txn.auth_chal.scheme) { + /* Started auth exchange, but client didn't engage - reinit */ + syslog(LOG_DEBUG, "client didn't complete auth - reinit"); + reset_saslconn(&httpd_saslconn); + txn.auth_chal.scheme = NULL; + } + + /* Perform proxy authorization, if necessary */ + else if (saslprops.authid && + (hdr = spool_getheader(txn.req_hdrs, "Authorize-As")) && + *hdr[0]) { + const char *authzid = hdr[0]; + + r = proxy_authz(&authzid, &txn); + if (r) { + /* Proxy authz failed - reinitialize */ + syslog(LOG_DEBUG, "proxy authz failed - reinit"); + reset_saslconn(&httpd_saslconn); + txn.auth_chal.scheme = NULL; + ret = HTTP_UNAUTHORIZED; + } + else { + httpd_userid = xstrdup(authzid); + auth_success(&txn); + } + } + + /* Request authentication, if necessary */ + switch (txn.meth) { + case METH_GET: + case METH_HEAD: + case METH_OPTIONS: + /* Let method processing function decide if auth is needed */ + break; + + default: + if (!httpd_userid && namespace->need_auth) { + /* Authentication required */ + ret = HTTP_UNAUTHORIZED; + } + } + + if (ret) goto need_auth; + + /* Check if this is a Cross-Origin Resource Sharing request */ + if (allow_cors && (hdr = spool_getheader(txn.req_hdrs, "Origin"))) { + const char *err = NULL; + xmlURIPtr uri = parse_uri(METH_UNKNOWN, hdr[0], 0, &err); + + if (uri && uri->scheme && uri->server) { + int o_https = !strcasecmp(uri->scheme, "https"); + + if ((https == o_https) && + !strcasecmp(uri->server, + *spool_getheader(txn.req_hdrs, "Host"))) { + txn.flags.cors = CORS_SIMPLE; + } + else { + struct wildmat *wild; + + /* Create URI w/o path or default port */ + assert(!buf_len(&txn.buf)); + buf_printf(&txn.buf, "%s://%s", + lcase(uri->scheme), lcase(uri->server)); + if (uri->port && + ((o_https && uri->port != 443) || + (!o_https && uri->port != 80))) { + buf_printf(&txn.buf, ":%d", uri->port); + } + + /* Check Origin against the 'httpallowcors' wildmat */ + for (wild = allow_cors; wild->pat; wild++) { + if (wildmat(buf_cstring(&txn.buf), wild->pat)) { + /* If we have a non-negative match, allow request */ + if (!wild->not) txn.flags.cors = CORS_SIMPLE; + break; + } + } + buf_reset(&txn.buf); + } + } + xmlFreeURI(uri); + } + + /* Check if we should compress response body */ + if (gzip_enabled) { + /* XXX Do we want to support deflate even though M$ + doesn't implement it correctly (raw deflate vs. zlib)? */ + + if (!txn.flags.ver1_0 && + (hdr = spool_getheader(txn.req_hdrs, "TE"))) { + struct accept *e, *enc = parse_accept(hdr); + + for (e = enc; e && e->token; e++) { + if (e->qual > 0.0 && + (!strcasecmp(e->token, "gzip") || + !strcasecmp(e->token, "x-gzip"))) { + txn.flags.te = TE_GZIP; + } + free(e->token); + } + if (enc) free(enc); + } + else if ((hdr = spool_getheader(txn.req_hdrs, "Accept-Encoding"))) { + struct accept *e, *enc = parse_accept(hdr); + + for (e = enc; e && e->token; e++) { + if (e->qual > 0.0 && + (!strcasecmp(e->token, "gzip") || + !strcasecmp(e->token, "x-gzip"))) { + txn.resp_body.enc = CE_GZIP; + } + free(e->token); + } + if (enc) free(enc); + } + } + + /* Parse any query parameters */ + if (query) { + /* Parse the query string and add param/value pairs to hash table */ + tok_t tok; + char *param; + + assert(!buf_len(&txn.buf)); /* Unescape buffer */ + + tok_init(&tok, (char *) query, ";&=", + TOK_TRIMLEFT|TOK_TRIMRIGHT|TOK_EMPTY); + while ((param = tok_next(&tok))) { + struct strlist *vals; + char *value = tok_next(&tok); + size_t len; + + if (!value) value = ""; + len = strlen(value); + buf_ensure(&txn.buf, len); + + vals = hash_lookup(param, &txn.req_qparams); + appendstrlist(&vals, + xmlURIUnescapeString(value, len, txn.buf.s)); + hash_insert(param, vals, &txn.req_qparams); + } + tok_fini(&tok); + + buf_reset(&txn.buf); + } + + /* Start method processing alarm (HTTP/1.1+ only) */ + if (!txn.flags.ver1_0) alarm(httpd_keepalive); + + /* Process the requested method */ + ret = (*meth_t->proc)(&txn, meth_t->params); + + need_auth: + if (ret == HTTP_UNAUTHORIZED) { + /* User must authenticate */ + + if (httpd_tls_required) { + /* We only support TLS+Basic, so tell client to use TLS */ + ret = 0; + + /* Check which response is required */ + if ((hdr = spool_getheader(txn.req_hdrs, "Upgrade")) && + !strncmp(hdr[0], TLS_VERSION, strcspn(hdr[0], " ,"))) { + /* Client (Murder proxy) supports RFC 2817 (TLS upgrade) */ + + response_header(HTTP_UPGRADE, &txn); + } + else { + /* All other clients use RFC 2818 (HTTPS) */ + const char *path = txn.req_uri->path; + struct buf *html = &txn.resp_body.payload; + + /* Create https URL */ + hdr = spool_getheader(txn.req_hdrs, "Host"); + buf_printf(&txn.buf, "https://%s", hdr[0]); + if (strcmp(path, "*")) { + buf_appendcstr(&txn.buf, path); + if (query) buf_printf(&txn.buf, "?%s", query); + } + + txn.location = buf_cstring(&txn.buf); + + /* Create HTML body */ + buf_reset(html); + buf_printf(html, tls_message, + buf_cstring(&txn.buf), buf_cstring(&txn.buf)); + + /* Output our HTML response */ + txn.resp_body.type = "text/html; charset=utf-8"; + write_body(HTTP_MOVED, &txn, + buf_cstring(html), buf_len(html)); + } + } + else { + /* Tell client to authenticate */ + if (r == SASL_CONTINUE) + txn.error.desc = "Continue authentication exchange"; + else if (r) txn.error.desc = "Authentication failed"; + else txn.error.desc = + "Must authenticate to access the specified target"; + } + } + + done: + /* Handle errors (success responses handled by method functions) */ + if (ret) error_response(ret, &txn); + + /* Read and discard any unread request body */ + if (!(txn.flags.conn & CONN_CLOSE)) { + txn.req_body.flags |= BODY_DISCARD; + if (http_read_body(httpd_in, httpd_out, + txn.req_hdrs, &txn.req_body, &txn.error.desc)) { + txn.flags.conn = CONN_CLOSE; + } + } + + /* Memory cleanup */ + if (txn.req_uri) xmlFreeURI(txn.req_uri); + if (txn.req_hdrs) spool_free_hdrcache(txn.req_hdrs); + free_hash_table(&txn.req_qparams, (void (*)(void *)) &freestrlist); + + if (txn.flags.conn & CONN_CLOSE) { + buf_free(&txn.buf); + buf_free(&txn.req_body.payload); + buf_free(&txn.resp_body.payload); +#ifdef HAVE_ZLIB + deflateEnd(&txn.zstrm); + buf_free(&txn.zbuf); +#endif + return; + } + + continue; + } +} + +/**************************** Parsing Routines ******************************/ + +/* Parse URI, returning the path */ +EXPORTED xmlURIPtr parse_uri(unsigned meth, const char *uri, unsigned path_reqd, + const char **errstr) +{ + xmlURIPtr p_uri; /* parsed URI */ + + /* Parse entire URI */ + if ((p_uri = xmlParseURI(uri)) == NULL) { + *errstr = "Illegal request target URI"; + goto bad_request; + } + + if (p_uri->scheme) { + /* Check sanity of scheme */ + + if (strcasecmp(p_uri->scheme, "http") && + strcasecmp(p_uri->scheme, "https")) { + *errstr = "Unsupported URI scheme"; + goto bad_request; + } + } + + /* Check sanity of path */ + if (path_reqd && (!p_uri->path || !*p_uri->path)) { + *errstr = "Empty path in target URI"; + goto bad_request; + } + else if (p_uri->path) { + if ((p_uri->path[0] != '/') && + (strcmp(p_uri->path, "*") || (meth != METH_OPTIONS))) { + /* No special URLs except for "OPTIONS * HTTP/1.1" */ + *errstr = "Illegal request target URI"; + goto bad_request; + } + else if (strstr(p_uri->path, "/..")) { + /* Don't allow access up directory tree */ + *errstr = "Illegal request target URI"; + goto bad_request; + } + else if (strlen(p_uri->path) > MAX_MAILBOX_PATH) { + *errstr = "Request target URI too long"; + goto bad_request; + } + } + + return p_uri; + + bad_request: + if (p_uri) xmlFreeURI(p_uri); + return NULL; +} + + +/* Calculate compile time of a file for use as Last-Modified and/or ETag */ +EXPORTED time_t calc_compile_time(const char *time, const char *date) +{ + struct tm tm; + char month[4]; + const char *monthname[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + + memset(&tm, 0, sizeof(struct tm)); + tm.tm_isdst = -1; + sscanf(time, "%02d:%02d:%02d", &tm.tm_hour, &tm.tm_min, &tm.tm_sec); + sscanf(date, "%s %2d %4d", month, &tm.tm_mday, &tm.tm_year); + tm.tm_year -= 1900; + for (tm.tm_mon = 0; tm.tm_mon < 12; tm.tm_mon++) { + if (!strcmp(month, monthname[tm.tm_mon])) break; + } + + return mktime(&tm); +} + +/* Parse Expect header(s) for interesting expectations */ +static int parse_expect(struct transaction_t *txn) +{ + const char **exp = spool_getheader(txn->req_hdrs, "Expect"); + int i, ret = 0; + + /* Expect not supported by HTTP/1.0 clients */ + if (exp && txn->flags.ver1_0) return HTTP_EXPECT_FAILED; + + /* Look for interesting expectations. Unknown == error */ + for (i = 0; !ret && exp && exp[i]; i++) { + tok_t tok = TOK_INITIALIZER(exp[i], ",", TOK_TRIMLEFT|TOK_TRIMRIGHT); + char *token; + + while (!ret && (token = tok_next(&tok))) { + /* Check if this is a non-persistent connection */ + if (!strcasecmp(token, "100-continue")) { + syslog(LOG_DEBUG, "Expect: 100-continue"); + txn->req_body.flags |= BODY_CONTINUE; + } + else { + txn->error.desc = "Unsupported Expectation"; + ret = HTTP_EXPECT_FAILED; + } + } + + tok_fini(&tok); + } + + return ret; +} + + +/* Parse Connection header(s) for interesting options */ +static void parse_connection(struct transaction_t *txn) +{ + const char **conn = spool_getheader(txn->req_hdrs, "Connection"); + int i; + + /* Look for interesting connection tokens */ + for (i = 0; conn && conn[i]; i++) { + tok_t tok = TOK_INITIALIZER(conn[i], ",", TOK_TRIMLEFT|TOK_TRIMRIGHT); + char *token; + + while ((token = tok_next(&tok))) { + if (httpd_timeout) { + /* Check if this is a non-persistent connection */ + if (!strcasecmp(token, "close")) { + txn->flags.conn |= CONN_CLOSE; + continue; + } + + /* Check if this is a persistent connection */ + else if (!strcasecmp(token, "keep-alive")) { + txn->flags.conn |= CONN_KEEPALIVE; + continue; + } + } + + /* Check if we need to upgrade to TLS */ + if (!httpd_tls_done && tls_enabled() && + !strcasecmp(token, "Upgrade")) { + const char **upgrd; + + if ((upgrd = spool_getheader(txn->req_hdrs, "Upgrade")) && + !strncmp(upgrd[0], TLS_VERSION, strcspn(upgrd[0], " ,"))) { + syslog(LOG_DEBUG, "client requested TLS"); + txn->flags.conn |= CONN_UPGRADE; + } + } + } + + tok_fini(&tok); + } + + if (!httpd_timeout) txn->flags.conn |= CONN_CLOSE; + else if (txn->flags.conn & CONN_CLOSE) { + /* close overrides keep-alive */ + txn->flags.conn &= ~CONN_KEEPALIVE; + } + else if (txn->flags.ver1_0 && !(txn->flags.conn & CONN_KEEPALIVE)) { + /* HTTP/1.0 - non-persistent connection unless keep-alive */ + txn->flags.conn |= CONN_CLOSE; + } +} + + +/* Compare accept quality values so that they sort in descending order */ +static int compare_accept(const struct accept *a1, const struct accept *a2) +{ + if (a2->qual < a1->qual) return -1; + if (a2->qual > a1->qual) return 1; + return 0; +} + +struct accept *parse_accept(const char **hdr) +{ + int i, n = 0, alloc = 0; + struct accept *ret = NULL; +#define GROW_ACCEPT 10; + + for (i = 0; hdr && hdr[i]; i++) { + tok_t tok = TOK_INITIALIZER(hdr[i], ";,", TOK_TRIMLEFT|TOK_TRIMRIGHT); + char *token; + + while ((token = tok_next(&tok))) { + if (!strncmp(token, "q=", 2)) { + if (!ret) break; + ret[n-1].qual = strtof(token+2, NULL); + } + else { + if (n + 1 >= alloc) { + alloc += GROW_ACCEPT; + ret = xrealloc(ret, alloc * sizeof(struct accept)); + } + ret[n].token = xstrdup(token); + ret[n].qual = 1.0; + ret[++n].token = NULL; + } + } + tok_fini(&tok); + } + + qsort(ret, n, sizeof(struct accept), + (int (*)(const void *, const void *)) &compare_accept); + + return ret; +} + + +/**************************** Response Routines *****************************/ + + +/* Create RFC3339 date ('buf' must be at least 21 characters) */ +EXPORTED char *rfc3339date_gen(char *buf, size_t len, time_t t) +{ + struct tm *tm = gmtime(&t); + + snprintf(buf, len, "%4d-%02d-%02dT%02d:%02d:%02dZ", + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec); + + return buf; +} + + +/* Create HTTP-date ('buf' must be at least 30 characters) */ +EXPORTED char *httpdate_gen(char *buf, size_t len, time_t t) +{ + static char *month[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + static char *wday[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + + struct tm *tm = gmtime(&t); + + snprintf(buf, len, "%3s, %02d %3s %4d %02d:%02d:%02d GMT", + wday[tm->tm_wday], + tm->tm_mday, month[tm->tm_mon], tm->tm_year + 1900, + tm->tm_hour, tm->tm_min, tm->tm_sec); + + return buf; +} + + +/* Create an HTTP Status-Line given response code */ +EXPORTED const char *http_statusline(long code) +{ + static struct buf statline = BUF_INITIALIZER; + static unsigned tail = 0; + + if (!tail) { + buf_setcstr(&statline, HTTP_VERSION); + buf_putc(&statline, ' '); + tail = buf_len(&statline); + } + + buf_truncate(&statline, tail); + buf_appendcstr(&statline, error_message(code)); + return buf_cstring(&statline); +} + + +/* Output an HTTP response header. + * 'code' specifies the HTTP Status-Code and Reason-Phrase. + * 'txn' contains the transaction context + */ + +#define WWW_Authenticate(name, param) \ + prot_printf(httpd_out, "WWW-Authenticate: %s", name); \ + if (param) prot_printf(httpd_out, " %s", param); \ + prot_puts(httpd_out, "\r\n") + +#define Access_Control_Expose(hdr) \ + prot_puts(httpd_out, "Access-Control-Expose-Headers: " hdr "\r\n") + +EXPORTED void comma_list_hdr(const char *hdr, const char *vals[], unsigned flags, ...) +{ + const char *sep = " "; + va_list args; + int i; + + va_start(args, flags); + prot_printf(httpd_out, "%s:", hdr); + for (i = 0; vals[i]; i++) { + if (flags & (1 << i)) { + prot_puts(httpd_out, sep); + prot_vprintf(httpd_out, vals[i], args); + sep = ", "; + } + else { + /* discard any unused args */ + vsnprintf(NULL, 0, vals[i], args); + } + } + prot_puts(httpd_out, "\r\n"); + va_end(args); +} + +EXPORTED void allow_hdr(const char *hdr, unsigned allow) +{ + const char *meths[] = { + "OPTIONS, GET, HEAD", "POST", "PUT", "DELETE", "TRACE", NULL + }; + + comma_list_hdr(hdr, meths, allow); + + if (allow & ALLOW_DAV) { + prot_printf(httpd_out, "%s: PROPFIND, REPORT", hdr); + if (allow & ALLOW_WRITE) { + prot_puts(httpd_out, ", COPY, MOVE, LOCK, UNLOCK"); + } + if (allow & ALLOW_WRITECOL) { + prot_puts(httpd_out, ", PROPPATCH, MKCOL, ACL"); + if (allow & ALLOW_CAL) { + prot_printf(httpd_out, "\r\n%s: MKCALENDAR", hdr); + } + } + prot_puts(httpd_out, "\r\n"); + } +} + +#define MD5_BASE64_LEN 25 /* ((MD5_DIGEST_LENGTH / 3) + 1) * 4 */ + +EXPORTED void Content_MD5(const unsigned char *md5) +{ + char base64[MD5_BASE64_LEN+1]; + + sasl_encode64((char *) md5, MD5_DIGEST_LENGTH, + base64, MD5_BASE64_LEN, NULL); + prot_printf(httpd_out, "Content-MD5: %s\r\n", base64); +} + + +EXPORTED void response_header(long code, struct transaction_t *txn) +{ + time_t now; + char datestr[30]; + unsigned keepalive; + const char **hdr; + struct auth_challenge_t *auth_chal; + struct resp_body_t *resp_body; + static struct buf log = BUF_INITIALIZER; + + /* Stop method processing alarm */ + keepalive = alarm(0); + + + /* Status-Line */ + prot_printf(httpd_out, "%s\r\n", http_statusline(code)); + + + /* Connection Management */ + switch (code) { + case HTTP_SWITCH_PROT: + keepalive = 0; /* No alarm during TLS negotiation */ + + prot_printf(httpd_out, "Upgrade: %s\r\n", TLS_VERSION); + prot_puts(httpd_out, "Connection: Upgrade\r\n"); + + /* Fall through as provisional response */ + + case HTTP_CONTINUE: + case HTTP_PROCESSING: + /* Provisional response - nothing else needed */ + + /* CRLF terminating the header block */ + prot_puts(httpd_out, "\r\n"); + + /* Force the response to the client immediately */ + prot_flush(httpd_out); + + /* Reset method processing alarm */ + alarm(keepalive); + + return; + + case HTTP_UPGRADE: + txn->flags.conn |= CONN_UPGRADE; + prot_printf(httpd_out, "Upgrade: %s\r\n", TLS_VERSION); + + /* Fall through as final response */ + + default: + /* Final response */ + if (txn->flags.conn) { + /* Construct Connection header */ + const char *conn_tokens[] = + { "close", "Upgrade", "Keep-Alive", NULL }; + + if (txn->flags.conn & CONN_KEEPALIVE) { + prot_printf(httpd_out, "Keep-Alive: timeout=%d\r\n", + httpd_timeout); + } + + comma_list_hdr("Connection", conn_tokens, txn->flags.conn); + } + + auth_chal = &txn->auth_chal; + resp_body = &txn->resp_body; + } + + + /* Control Data */ + now = time(0); + httpdate_gen(datestr, sizeof(datestr), now); + prot_printf(httpd_out, "Date: %s\r\n", datestr); + + if (httpd_tls_done) { + prot_puts(httpd_out, "Strict-Transport-Security: max-age=600\r\n"); + } + if (txn->location) { + prot_printf(httpd_out, "Location: %s\r\n", txn->location); + } + if (txn->flags.cc) { + /* Construct Cache-Control header */ + const char *cc_dirs[] = + { "must-revalidate", "no-cache", "no-store", "no-transform", + "public", "private", "max-age=%d", NULL }; + + comma_list_hdr("Cache-Control", cc_dirs, txn->flags.cc, + resp_body->maxage); + + if (txn->flags.cc & CC_MAXAGE) { + httpdate_gen(datestr, sizeof(datestr), now + resp_body->maxage); + prot_printf(httpd_out, "Expires: %s\r\n", datestr); + } + } + if (txn->flags.cors) { + /* Construct Cross-Origin Resource Sharing headers */ + prot_printf(httpd_out, "Access-Control-Allow-Origin: %s\r\n", + *spool_getheader(txn->req_hdrs, "Origin")); + prot_puts(httpd_out, "Access-Control-Allow-Credentials: true\r\n"); + + if (txn->flags.cors == CORS_PREFLIGHT) { + allow_hdr("Access-Control-Allow-Methods", txn->req_tgt.allow); + + for (hdr = spool_getheader(txn->req_hdrs, + "Access-Control-Request-Headers"); + hdr && *hdr; hdr++) { + prot_printf(httpd_out, + "Access-Control-Allow-Headers: %s\r\n", *hdr); + } + prot_puts(httpd_out, "Access-Control-Max-Age: 3600\r\n"); + } + } + if (txn->flags.vary) { + /* Construct Vary header */ + const char *vary_hdrs[] = + { "Accept", "Accept-Encoding", "Brief", "Prefer", NULL }; + + comma_list_hdr("Vary", vary_hdrs, txn->flags.vary); + } + + + /* Response Context */ + if (txn->flags.mime) { + prot_puts(httpd_out, "MIME-Version: 1.0\r\n"); + } + if (txn->req_tgt.allow & ALLOW_ISCHEDULE) { + prot_puts(httpd_out, "iSchedule-Version: 1.0\r\n"); + if (resp_body->iserial) { + prot_printf(httpd_out, "iSchedule-Capabilities: %ld\r\n", + resp_body->iserial); + } + } + if (resp_body->prefs) { + /* Construct Preference-Applied header */ + const char *prefs[] = + { "return=minimal", "return=representation", "depth-noroot", NULL }; + + comma_list_hdr("Preference-Applied", prefs, resp_body->prefs); + if (txn->flags.cors) Access_Control_Expose("Preference-Applied"); + } + + switch (code) { + case HTTP_OK: + switch (txn->meth) { + case METH_GET: + case METH_HEAD: + /* Construct Accept-Ranges header for GET and HEAD responses */ + prot_printf(httpd_out, "Accept-Ranges: %s\r\n", + txn->flags.ranges ? "bytes" : "none"); + break; + + case METH_OPTIONS: + if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) { + prot_printf(httpd_out, "Server: %s\r\n", + buf_cstring(&serverinfo)); + } + + if (txn->req_tgt.allow & ALLOW_DAV) { + /* Construct DAV header(s) based on namespace of request URL */ + prot_printf(httpd_out, "DAV: 1,%s 3, access-control%s\r\n", + (txn->req_tgt.allow & ALLOW_WRITE) ? " 2," : "", + (txn->req_tgt.allow & ALLOW_WRITECOL) ? + ", extended-mkcol" : ""); + if (txn->req_tgt.allow & ALLOW_CAL) { + prot_printf(httpd_out, "DAV: calendar-access%s%s%s\r\n", + (txn->req_tgt.allow & ALLOW_CAL_AVAIL) ? + ", calendar-availability" : "", + (txn->req_tgt.allow & ALLOW_CAL_SCHED) ? + ", calendar-auto-schedule" : "", + (txn->req_tgt.allow & ALLOW_CAL_NOTZ) ? + ", calendar-no-timezone" : ""); + + /* Backwards compatibility with older Apple VAV clients */ + if ((txn->req_tgt.allow & + (ALLOW_CAL_AVAIL | ALLOW_CAL_SCHED)) == + (ALLOW_CAL_AVAIL | ALLOW_CAL_SCHED)) { + if ((hdr = spool_getheader(txn->req_hdrs, "User-Agent")) + && strstr(hdr[0], "CalendarAgent/")) { + prot_printf(httpd_out, + "DAV: inbox-availability\r\n"); + } + } + } + if (txn->req_tgt.allow & ALLOW_CARD) { + prot_puts(httpd_out, "DAV: addressbook\r\n"); + } + } + + if (txn->flags.cors == CORS_PREFLIGHT) { + /* Access-Control-Allow-Methods supersedes Allow */ + break; + } + else goto allow; + } + goto authorized; + + case HTTP_NOT_ALLOWED: + allow: + /* Construct Allow header(s) for OPTIONS and 405 response */ + allow_hdr("Allow", txn->req_tgt.allow); + goto authorized; + + case HTTP_BAD_MEDIATYPE: + if (txn->req_body.te == TE_UNKNOWN) { + /* Construct Allow-Encoding header for 415 response */ +#ifdef HAVE_ZLIB + prot_puts(httpd_out, "Allow-Encoding: gzip, deflate\r\n"); +#else + prot_puts(httpd_out, "Allow-Encoding: identity\r\n"); +#endif + } + goto authorized; + + case HTTP_UNAUTHORIZED: + /* Authentication Challenges */ + if (!auth_chal->scheme) { + /* Require authentication by advertising all possible schemes */ + struct auth_scheme_t *scheme; + + for (scheme = auth_schemes; scheme->name; scheme++) { + /* Only advertise what is available and + can work with the type of connection */ + if ((avail_auth_schemes & (1 << scheme->idx)) && + !((txn->flags.conn & CONN_CLOSE) && + (scheme->flags & AUTH_NEED_PERSIST))) { + auth_chal->param = NULL; + + if (scheme->flags & AUTH_SERVER_FIRST) { + /* Generate the initial challenge */ + http_auth(scheme->name, txn); + + if (!auth_chal->param) continue; /* If fail, skip it */ + } + WWW_Authenticate(scheme->name, auth_chal->param); + } + } + } + else { + /* Continue with current authentication exchange */ + WWW_Authenticate(auth_chal->scheme->name, auth_chal->param); + } + break; + + default: + authorized: + /* Authentication completed/unnecessary */ + if (auth_chal->param) { + /* Authentication completed with success data */ + if (auth_chal->scheme->send_success) { + /* Special handling of success data for this scheme */ + auth_chal->scheme->send_success(auth_chal->scheme->name, + auth_chal->param); + } + else { + /* Default handling of success data */ + WWW_Authenticate(auth_chal->scheme->name, auth_chal->param); + } + } + } + + + /* Validators */ + if (resp_body->lock) { + prot_printf(httpd_out, "Lock-Token: <%s>\r\n", resp_body->lock); + if (txn->flags.cors) Access_Control_Expose("Lock-Token"); + } + if (resp_body->stag) { + prot_printf(httpd_out, "Schedule-Tag: \"%s\"\r\n", resp_body->stag); + if (txn->flags.cors) Access_Control_Expose("Schedule-Tag"); + } + if (resp_body->etag) { + prot_printf(httpd_out, "ETag: %s\"%s\"\r\n", + resp_body->enc ? "W/" : "", resp_body->etag); + if (txn->flags.cors) Access_Control_Expose("ETag"); + } + if (resp_body->lastmod) { + /* Last-Modified MUST NOT be in the future */ + resp_body->lastmod = MIN(resp_body->lastmod, now); + httpdate_gen(datestr, sizeof(datestr), resp_body->lastmod); + prot_printf(httpd_out, "Last-Modified: %s\r\n", datestr); + } + + + /* Representation Metadata */ + if (resp_body->type) { + prot_printf(httpd_out, "Content-Type: %s\r\n", resp_body->type); + + if (resp_body->fname) { + prot_printf(httpd_out, + "Content-Disposition: inline; filename=\"%s\"\r\n", + resp_body->fname); + } + if (txn->resp_body.enc) { + /* Construct Content-Encoding header */ + const char *ce[] = + { "deflate", "gzip", NULL }; + + comma_list_hdr("Content-Encoding", ce, txn->resp_body.enc); + } + if (resp_body->lang) { + prot_printf(httpd_out, "Content-Language: %s\r\n", resp_body->lang); + } + if (resp_body->loc) { + prot_printf(httpd_out, "Content-Location: %s\r\n", resp_body->loc); + if (txn->flags.cors) Access_Control_Expose("Content-Location"); + } + if (resp_body->md5) { + Content_MD5(resp_body->md5); + } + } + + + /* Payload */ + switch (code) { + case HTTP_NO_CONTENT: + case HTTP_NOT_MODIFIED: + /* MUST NOT include a body */ + break; + + case HTTP_UNSAT_RANGE: + prot_printf(httpd_out, "Content-Range: bytes */%lu\r\n", + resp_body->len); + resp_body->len = 0; /* No content */ + + /* Fall through and specify framing */ + + case HTTP_PARTIAL: + if (resp_body->range) { + prot_printf(httpd_out, "Content-Range: bytes %lu-%lu/%lu\r\n", + resp_body->range->first, resp_body->range->last, + resp_body->len); + + /* Set actual content length of range */ + resp_body->len = resp_body->range->last - + resp_body->range->first + 1; + + free(resp_body->range); + } + + /* Fall through and specify framing */ + + default: + if (txn->flags.te) { + /* HTTP/1.1+ only - we use close-delimiting for HTTP/1.0 */ + if (!txn->flags.ver1_0) { + /* Construct Transfer-Encoding header */ + const char *te[] = + { "deflate", "gzip", "chunked", NULL }; + + comma_list_hdr("Transfer-Encoding", te, txn->flags.te); + + if (txn->flags.trailer) { + /* Construct Trailer header */ + const char *trailer_hdrs[] = + { "Content-MD5", NULL }; + + comma_list_hdr("Trailer", trailer_hdrs, txn->flags.trailer); + } + } + } + else if (resp_body->len || txn->meth != METH_HEAD) { + prot_printf(httpd_out, "Content-Length: %lu\r\n", resp_body->len); + } + } + + + /* CRLF terminating the header block */ + prot_puts(httpd_out, "\r\n"); + + + /* Log the client request and our response */ + buf_reset(&log); + /* Add client data */ + buf_printf(&log, "%s", httpd_clienthost); + if (proxy_userid) buf_printf(&log, " as \"%s\"", proxy_userid); + if (txn->req_hdrs && + (hdr = spool_getheader(txn->req_hdrs, "User-Agent"))) { + buf_printf(&log, " with \"%s\"", hdr[0]); + if ((hdr = spool_getheader(txn->req_hdrs, "X-Client"))) + buf_printf(&log, " by \"%s\"", hdr[0]); + else if ((hdr = spool_getheader(txn->req_hdrs, "X-Requested-With"))) + buf_printf(&log, " by \"%s\"", hdr[0]); + } + /* Add request-line */ + buf_appendcstr(&log, "; \""); + if (txn->req_line.meth) { + buf_printf(&log, "%s", + txn->flags.override ? "POST" : txn->req_line.meth); + if (txn->req_line.uri) { + buf_printf(&log, " %s", txn->req_line.uri); + if (txn->req_line.ver) { + buf_printf(&log, " %s", txn->req_line.ver); + if (code != HTTP_TOO_LONG) { + char *p = txn->req_line.ver + strlen(txn->req_line.ver) + 1; + if (*p) buf_printf(&log, " %s", p); + } + } + } + } + buf_appendcstr(&log, "\""); + if (txn->req_hdrs) { + /* Add any request modifying headers */ + const char *sep = " ("; + + if (txn->flags.override) { + buf_printf(&log, "%smethod-override=%s", sep, txn->req_line.meth); + sep = "; "; + } + if ((hdr = spool_getheader(txn->req_hdrs, "Origin"))) { + buf_printf(&log, "%sorigin=%s", sep, hdr[0]); + sep = "; "; + } + if ((hdr = spool_getheader(txn->req_hdrs, "Referer"))) { + buf_printf(&log, "%sreferer=%s", sep, hdr[0]); + sep = "; "; + } + if ((hdr = spool_getheader(txn->req_hdrs, "Destination"))) { + buf_printf(&log, "%sdestination=%s", sep, hdr[0]); + sep = "; "; + } + if ((hdr = spool_getheader(txn->req_hdrs, ":type"))) { + buf_printf(&log, "%stype=%s", sep, hdr[0]); + sep = "; "; + } + if ((hdr = spool_getheader(txn->req_hdrs, "Depth"))) { + buf_printf(&log, "%sdepth=%s", sep, hdr[0]); + sep = "; "; + } + if (*sep == ';') buf_appendcstr(&log, ")"); + } + buf_printf(&log, " => \"%s\"", error_message(code)); + /* Add any auxiliary response data */ + if (txn->location) { + buf_printf(&log, " (location=%s)", txn->location); + } + else if (txn->flags.cors) { + buf_appendcstr(&log, " (allow-origin)"); + } + else if (txn->error.desc) { + buf_printf(&log, " (error=%s)", txn->error.desc); + } + syslog(LOG_INFO, "%s", buf_cstring(&log)); +} + + +static void keep_alive(int sig) +{ + if (sig == SIGALRM) { + response_header(HTTP_CONTINUE, NULL); + alarm(httpd_keepalive); + } +} + + +/* + * Output an HTTP response with multipart body data. + * + * An initial call with 'code' != 0 will output a response header + * and the preamble. + * All subsequent calls should have 'code' = 0 to output just a body part. + * A final call with 'len' = 0 ends the multipart body. + */ +EXPORTED void write_multipart_body(long code, struct transaction_t *txn, + const char *buf, unsigned len) +{ + static char boundary[100]; + struct buf *body = &txn->resp_body.payload; + + if (code) { + const char *preamble = + "This is a message with multiple parts in MIME format.\r\n"; + + txn->flags.mime = 1; + + /* Create multipart boundary */ + snprintf(boundary, sizeof(boundary), "%s-%ld-%ld-%ld", + *spool_getheader(txn->req_hdrs, "Host"), + (long) getpid(), (long) time(0), (long) rand()); + + /* Create Content-Type w/ boundary */ + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%s; boundary=\"%s\"", + txn->resp_body.type, boundary); + txn->resp_body.type = buf_cstring(&txn->buf); + + /* Setup for chunked response and begin multipart */ + txn->flags.te |= TE_CHUNKED; + if (!buf) { + buf = preamble; + len = strlen(preamble); + } + write_body(code, txn, buf, len); + } + else if (len) { + /* Output delimiter and MIME part-headers */ + buf_reset(body); + buf_printf(body, "\r\n--%s\r\n", boundary); + buf_printf(body, "Content-Type: %s\r\n", txn->resp_body.type); + if (txn->resp_body.range) { + buf_printf(body, "Content-Range: bytes %lu-%lu/%lu\r\n", + txn->resp_body.range->first, + txn->resp_body.range->last, + txn->resp_body.len); + } + buf_printf(body, "Content-Length: %d\r\n\r\n", len); + write_body(0, txn, buf_cstring(body), buf_len(body)); + + /* Output body-part data */ + write_body(0, txn, buf, len); + } + else { + const char *epilogue = "\r\nEnd of MIME multipart body.\r\n"; + + /* Output close-delimiter and epilogue */ + buf_reset(body); + buf_printf(body, "\r\n--%s--\r\n%s", boundary, epilogue); + write_body(0, txn, buf_cstring(body), buf_len(body)); + + /* End of output */ + write_body(0, txn, NULL, 0); + } +} + + +/* Output multipart/byteranges */ +static void multipart_byteranges(struct transaction_t *txn, + const char *msg_base) +{ + /* Save Content-Range and Content-Type pointers */ + struct range *range = txn->resp_body.range; + const char *type = txn->resp_body.type; + + /* Start multipart response */ + txn->resp_body.range = NULL; + txn->resp_body.type = "multipart/byteranges"; + write_multipart_body(HTTP_PARTIAL, txn, NULL, 0); + + txn->resp_body.type = type; + while (range) { + unsigned long offset = range->first; + unsigned long datalen = range->last - range->first + 1; + struct range *next = range->next; + + /* Output range as body part */ + txn->resp_body.range = range; + write_multipart_body(0, txn, msg_base + offset, datalen); + + /* Cleanup */ + free(range); + range = next; + } + + /* End of multipart body */ + write_multipart_body(0, txn, NULL, 0); +} + + +/* + * Output an HTTP response with body data, compressed as necessary. + * + * For chunked body data, an initial call with 'code' != 0 will output + * a response header and the first body chunk. + * All subsequent calls should have 'code' = 0 to output just the body chunk. + * A final call with 'len' = 0 ends the chunked body. + * + * NOTE: HTTP/1.0 clients can't handle chunked encoding, + * so we use bare chunks and close the connection when done. + */ +EXPORTED void write_body(long code, struct transaction_t *txn, + const char *buf, unsigned len) +{ + unsigned is_dynamic = code ? (txn->flags.te & TE_CHUNKED) : 1; + unsigned outlen = len, offset = 0; + int do_md5 = config_getswitch(IMAPOPT_HTTPCONTENTMD5); + static MD5_CTX ctx; + static unsigned char md5[MD5_DIGEST_LENGTH]; + + if (!is_dynamic && len < GZIP_MIN_LEN) { + /* Don't compress small static content */ + txn->resp_body.enc = CE_IDENTITY; + txn->flags.te = TE_NONE; + } + + /* Compress data */ + if (txn->resp_body.enc || txn->flags.te & ~TE_CHUNKED) { +#ifdef HAVE_ZLIB + /* Only flush for static content or on last (zero-length) chunk */ + unsigned flush = (is_dynamic && len) ? Z_NO_FLUSH : Z_FINISH; + + if (code) deflateReset(&txn->zstrm); + + txn->zstrm.next_in = (Bytef *) buf; + txn->zstrm.avail_in = len; + buf_reset(&txn->zbuf); + + do { + buf_ensure(&txn->zbuf, + deflateBound(&txn->zstrm, txn->zstrm.avail_in)); + + txn->zstrm.next_out = (Bytef *) txn->zbuf.s + txn->zbuf.len; + txn->zstrm.avail_out = txn->zbuf.alloc - txn->zbuf.len; + + deflate(&txn->zstrm, flush); + txn->zbuf.len = txn->zbuf.alloc - txn->zstrm.avail_out; + + } while (!txn->zstrm.avail_out); + + buf = txn->zbuf.s; + outlen = txn->zbuf.len; +#else + /* XXX should never get here */ + fatal("Compression requested, but no zlib", EC_SOFTWARE); +#endif /* HAVE_ZLIB */ + } + + if (code) { + /* Initial call - prepare response header based on CE, TE and version */ + if (do_md5) MD5Init(&ctx); + + if (txn->flags.te & ~TE_CHUNKED) { + /* Transfer-Encoded content MUST be chunked */ + txn->flags.te |= TE_CHUNKED; + + if (!is_dynamic) { + /* Handle static content as last chunk */ + len = 0; + } + } + + if (!(txn->flags.te & TE_CHUNKED)) { + /* Full/partial body (no encoding). + * + * In all cases, 'resp_body.len' is used to specify complete-length + * In the case of a 206 or 416 response, Content-Length will be + * set accordingly in response_header(). + */ + txn->resp_body.len = outlen; + + if (code == HTTP_PARTIAL) { + /* check_precond() tells us that this is a range request */ + code = parse_ranges(*spool_getheader(txn->req_hdrs, "Range"), + outlen, &txn->resp_body.range); + + switch (code) { + case HTTP_OK: + /* Full body (unknown range-unit) */ + break; + + case HTTP_PARTIAL: + /* One or more range request(s) */ + txn->resp_body.len = outlen; + + if (txn->resp_body.range->next) { + /* Multiple ranges */ + multipart_byteranges(txn, buf); + return; + } + else { + /* Single range - set data parameters accordingly */ + offset += txn->resp_body.range->first; + outlen = txn->resp_body.range->last - + txn->resp_body.range->first + 1; + } + break; + + case HTTP_UNSAT_RANGE: + /* No valid ranges */ + outlen = 0; + break; + } + } + + if (outlen && do_md5) { + MD5Update(&ctx, buf+offset, outlen); + MD5Final(md5, &ctx); + txn->resp_body.md5 = md5; + } + } + else if (txn->flags.ver1_0) { + /* HTTP/1.0 doesn't support chunked - close-delimit the body */ + txn->flags.conn = CONN_CLOSE; + } + else if (do_md5) txn->flags.trailer = TRAILER_CMD5; + + response_header(code, txn); + + /* MUST NOT send a body for 1xx/204/304 response or any HEAD response */ + switch (code) { + case HTTP_CONTINUE: + case HTTP_SWITCH_PROT: + case HTTP_PROCESSING: + case HTTP_NO_CONTENT: + case HTTP_NOT_MODIFIED: + return; + + default: + if (txn->meth == METH_HEAD) return; + } + } + + /* Output data */ + if ((txn->flags.te & TE_CHUNKED) && !txn->flags.ver1_0) { + /* HTTP/1.1 chunk */ + if (outlen) { + prot_printf(httpd_out, "%x\r\n", outlen); + prot_write(httpd_out, buf, outlen); + prot_puts(httpd_out, "\r\n"); + + if (do_md5) MD5Update(&ctx, buf, outlen); + } + if (!len) { + /* Terminate the HTTP/1.1 body with a zero-length chunk */ + prot_puts(httpd_out, "0\r\n"); + + /* Trailer */ + if (do_md5) { + MD5Final(md5, &ctx); + Content_MD5(md5); + } + + prot_puts(httpd_out, "\r\n"); + } + } + else { + /* Full body or HTTP/1.0 close-delimited body */ + prot_write(httpd_out, buf + offset, outlen); + } +} + + +/* Output an HTTP response with application/xml body */ +EXPORTED void xml_response(long code, struct transaction_t *txn, xmlDocPtr xml) +{ + xmlChar *buf; + int bufsiz; + + switch (code) { + case HTTP_OK: + case HTTP_CREATED: + case HTTP_NO_CONTENT: + case HTTP_MULTI_STATUS: + break; + + default: + /* Neither Brief nor Prefer affect error response bodies */ + txn->flags.vary &= ~(VARY_BRIEF | VARY_PREFER); + txn->resp_body.prefs = 0; + } + + /* Dump XML response tree into a text buffer */ + xmlDocDumpFormatMemoryEnc(xml, &buf, &bufsiz, "utf-8", + config_httpprettytelemetry); + + if (buf) { + /* Output the XML response */ + txn->resp_body.type = "application/xml; charset=utf-8"; + + write_body(code, txn, (char *) buf, bufsiz); + + /* Cleanup */ + xmlFree(buf); + } + else { + txn->error.precond = 0; + txn->error.desc = "Error dumping XML tree\r\n"; + error_response(HTTP_SERVER_ERROR, txn); + } +} + +EXPORTED void buf_printf_markup(struct buf *buf, unsigned level, const char *fmt, ...) +{ + va_list args; + const char *eol = "\n"; + + if (!config_httpprettytelemetry) { + level = 0; + eol = ""; + } + + va_start(args, fmt); + + buf_printf(buf, "%*s", level * MARKUP_INDENT, ""); + buf_vprintf(buf, fmt, args); + buf_appendcstr(buf, eol); + + va_end(args); +} + + +/* Output an HTTP error response with optional XML or HTML body */ +EXPORTED void error_response(long code, struct transaction_t *txn) +{ + struct buf *html = &txn->resp_body.payload; + + /* Neither Brief nor Prefer affect error response bodies */ + txn->flags.vary &= ~(VARY_BRIEF | VARY_PREFER); + txn->resp_body.prefs = 0; + +#ifdef WITH_DAV + if (code != HTTP_UNAUTHORIZED && txn->error.precond) { + xmlNodePtr root = xml_add_error(NULL, &txn->error, NULL); + + if (root) { + xml_response(code, txn, root->doc); + xmlFreeDoc(root->doc); + return; + } + } +#endif + + if (!txn->error.desc) { + switch (code) { + /* 4xx codes */ + case HTTP_BAD_REQUEST: + txn->error.desc = + "The request was not understood by this server."; + break; + + case HTTP_NOT_FOUND: + txn->error.desc = + "The requested URL was not found on this server."; + break; + + case HTTP_NOT_ALLOWED: + txn->error.desc = + "The requested method is not allowed for the URL."; + break; + + case HTTP_GONE: + txn->error.desc = + "The requested URL has been removed from this server."; + break; + + /* 5xx codes */ + case HTTP_SERVER_ERROR: + txn->error.desc = + "The server encountered an internal error."; + break; + + case HTTP_NOT_IMPLEMENTED: + txn->error.desc = + "The requested method is not implemented by this server."; + break; + + case HTTP_UNAVAILABLE: + txn->error.desc = + "The server is unable to process the request at this time."; + break; + } + } + + buf_reset(html); + if (txn->error.desc) { + const char **hdr, *host = ""; + char *port = NULL; + unsigned level = 0; + + if (txn->req_hdrs && + (hdr = spool_getheader(txn->req_hdrs, "Host")) && + hdr[0] && *hdr[0]) { + host = (char *) hdr[0]; + if ((port = strchr(host, ':'))) *port++ = '\0'; + } + else if (config_serverinfo != IMAP_ENUM_SERVERINFO_OFF) { + host = config_servername; + } + if (!port) port = strchr(saslprops.iplocalport, ';')+1; + + buf_printf_markup(html, level, HTML_DOCTYPE); + buf_printf_markup(html, level++, "<html>"); + buf_printf_markup(html, level++, "<head>"); + buf_printf_markup(html, level, "<title>%s</title>", + error_message(code)); + buf_printf_markup(html, --level, "</head>"); + buf_printf_markup(html, level++, "<body>"); + buf_printf_markup(html, level, "<h1>%s</h1>", error_message(code)+4); + buf_printf_markup(html, level, "<p>%s</p>", txn->error.desc); + buf_printf_markup(html, level, "<hr>"); + buf_printf_markup(html, level, + "<address>%s Server at %s Port %s</address>", + buf_cstring(&serverinfo), host, port); + buf_printf_markup(html, --level, "</body>"); + buf_printf_markup(html, --level, "</html>"); + + txn->resp_body.type = "text/html; charset=utf-8"; + } + + write_body(code, txn, buf_cstring(html), buf_len(html)); +} + + +static int proxy_authz(const char **authzid, struct transaction_t *txn) +{ + static char authzbuf[MAX_MAILBOX_BUFFER]; + unsigned authzlen; + int status; + + syslog(LOG_DEBUG, "proxy_auth: authzid='%s'", *authzid); + + /* Free userid & authstate previously allocated for auth'd user */ + if (httpd_userid) { + free(httpd_userid); + httpd_userid = NULL; + } + if (httpd_authstate) { + auth_freestate(httpd_authstate); + httpd_authstate = NULL; + } + + if (!(config_mupdate_server && config_getstring(IMAPOPT_PROXYSERVERS))) { + /* Not a backend in a Murder - proxy authz is not allowed */ + syslog(LOG_NOTICE, "badlogin: %s %s %s %s", + httpd_clienthost, txn->auth_chal.scheme->name, saslprops.authid, + "proxy authz attempted on non-Murder backend"); + return SASL_NOAUTHZ; + } + + /* Canonify the authzid */ + status = mysasl_canon_user(httpd_saslconn, NULL, + *authzid, strlen(*authzid), + SASL_CU_AUTHZID, NULL, + authzbuf, sizeof(authzbuf), &authzlen); + if (status) { + syslog(LOG_NOTICE, "badlogin: %s %s %s invalid user", + httpd_clienthost, txn->auth_chal.scheme->name, + beautify_string(*authzid)); + return status; + } + + /* See if auth'd user is allowed to proxy */ + status = mysasl_proxy_policy(httpd_saslconn, &httpd_proxyctx, + authzbuf, authzlen, + saslprops.authid, strlen(saslprops.authid), + NULL, 0, NULL); + + if (status) { + syslog(LOG_NOTICE, "badlogin: %s %s %s %s", + httpd_clienthost, txn->auth_chal.scheme->name, saslprops.authid, + sasl_errdetail(httpd_saslconn)); + return status; + } + + *authzid = authzbuf; + + return status; +} + + +/* Write cached header (redacting authorization credentials) to buffer. */ +static void log_cachehdr(const char *name, const char *contents, void *rock) +{ + struct buf *buf = (struct buf *) rock; + + /* Ignore private headers in our cache */ + if (name[0] == ':') return; + + buf_printf(buf, "%c%s: ", toupper(name[0]), name+1); + if (!strcmp(name, "authorization")) { + /* Replace authorization credentials with an ellipsis */ + const char *creds = strchr(contents, ' ') + 1; + buf_printf(buf, "%.*s%-*s\r\n", (int) (creds - contents), contents, + (int) strlen(creds), "..."); + } + else buf_printf(buf, "%s\r\n", contents); +} + + +static void auth_success(struct transaction_t *txn) +{ + struct auth_scheme_t *scheme = txn->auth_chal.scheme; + int i; + + proc_register("httpd", httpd_clienthost, httpd_userid, NULL, NULL); + + syslog(LOG_NOTICE, "login: %s %s %s%s %s SESSIONID=<%s>", + httpd_clienthost, httpd_userid, scheme->name, + httpd_tls_done ? "+TLS" : "", "User logged in", + session_id()); + + + /* Recreate telemetry log entry for request (w/ credentials redacted) */ + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "<%ld<", time(NULL)); /* timestamp */ + buf_printf(&txn->buf, "%s %s %s\r\n", /* request-line*/ + txn->req_line.meth, txn->req_line.uri, txn->req_line.ver); + spool_enum_hdrcache(txn->req_hdrs, /* header fields */ + &log_cachehdr, &txn->buf); + buf_appendcstr(&txn->buf, "\r\n"); /* CRLF */ + buf_append(&txn->buf, &txn->req_body.payload); /* message body */ + buf_appendmap(&txn->buf, /* buffered input */ + (const char *) httpd_in->ptr, httpd_in->cnt); + + if (httpd_logfd != -1) { + /* Rewind log to current request and truncate it */ + off_t end = lseek(httpd_logfd, 0, SEEK_END); + + ftruncate(httpd_logfd, end - buf_len(&txn->buf)); + } + + if (!proxy_userid || strcmp(proxy_userid, httpd_userid)) { + /* Close existing telemetry log */ + close(httpd_logfd); + + prot_setlog(httpd_in, PROT_NO_FD); + prot_setlog(httpd_out, PROT_NO_FD); + + /* Create telemetry log based on new userid */ + httpd_logfd = telemetry_log(httpd_userid, httpd_in, httpd_out, 0); + } + + if (httpd_logfd != -1) { + /* Log credential-redacted request */ + write(httpd_logfd, buf_cstring(&txn->buf), buf_len(&txn->buf)); + } + + buf_reset(&txn->buf); + + /* Make a copy of the external userid for use in proxying */ + if (proxy_userid) free(proxy_userid); + proxy_userid = xstrdup(httpd_userid); + + /* Translate any separators in userid */ + mboxname_hiersep_tointernal(&httpd_namespace, httpd_userid, + config_virtdomains ? + strcspn(httpd_userid, "@") : 0); + + /* Do any namespace specific post-auth processing */ + for (i = 0; namespaces[i]; i++) { + if (namespaces[i]->enabled && namespaces[i]->auth) + namespaces[i]->auth(httpd_userid); + } +} + + +/* Perform HTTP Authentication based on the given credentials ('creds'). + * Returns the selected auth scheme and any server challenge in 'chal'. + * May be called multiple times if auth scheme requires multiple steps. + * SASL status between steps is maintained in 'status'. + */ +#define BASE64_BUF_SIZE 21848 /* per RFC 4422: ((16K / 3) + 1) * 4 */ + +static int http_auth(const char *creds, struct transaction_t *txn) +{ + struct auth_challenge_t *chal = &txn->auth_chal; + static int status = SASL_OK; + int slen; + const char *clientin = NULL, *realm = NULL, *user, **authzid; + unsigned int clientinlen = 0; + struct auth_scheme_t *scheme; + static char base64[BASE64_BUF_SIZE+1]; + const void *canon_user; + + /* Split credentials into auth scheme and response */ + slen = strcspn(creds, " \0"); + if ((clientin = strchr(creds, ' '))) clientinlen = strlen(++clientin); + + syslog(LOG_DEBUG, + "http_auth: status=%d scheme='%s' creds='%.*s%s'", + status, chal->scheme ? chal->scheme->name : "", + slen, creds, clientin ? " <response>" : ""); + + /* Free userid & authstate previously allocated for auth'd user */ + if (httpd_userid) { + free(httpd_userid); + httpd_userid = NULL; + } + if (httpd_authstate) { + auth_freestate(httpd_authstate); + httpd_authstate = NULL; + } + chal->param = NULL; + + if (chal->scheme) { + /* Use current scheme, if possible */ + scheme = chal->scheme; + + if (strncasecmp(scheme->name, creds, slen)) { + /* Changing auth scheme -> reset state */ + syslog(LOG_DEBUG, "http_auth: changing scheme"); + reset_saslconn(&httpd_saslconn); + chal->scheme = NULL; + status = SASL_OK; + } + } + + if (!chal->scheme) { + /* Find the client-specified auth scheme */ + syslog(LOG_DEBUG, "http_auth: find client scheme"); + for (scheme = auth_schemes; scheme->name; scheme++) { + if (slen && !strncasecmp(scheme->name, creds, slen)) { + /* Found a supported scheme, see if its available */ + if (!(avail_auth_schemes & (1 << scheme->idx))) scheme = NULL; + break; + } + } + if (!scheme || !scheme->name) { + /* Didn't find a matching scheme that is available */ + syslog(LOG_DEBUG, "Unknown auth scheme '%.*s'", slen, creds); + return SASL_NOMECH; + } + /* We found it! */ + syslog(LOG_DEBUG, "http_auth: found matching scheme: %s", scheme->name); + chal->scheme = scheme; + status = SASL_OK; + } + + /* Base64 decode any client response, if necesary */ + if (clientin && (scheme->flags & AUTH_BASE64)) { + int r = sasl_decode64(clientin, clientinlen, + base64, BASE64_BUF_SIZE, &clientinlen); + if (r != SASL_OK) { + syslog(LOG_ERR, "Base64 decode failed: %s", + sasl_errstring(r, NULL, NULL)); + return r; + } + clientin = base64; + } + + /* Get realm - based on namespace of URL */ + switch (txn->req_tgt.namespace) { + case URL_NS_DEFAULT: + case URL_NS_PRINCIPAL: + realm = config_getstring(IMAPOPT_DAV_REALM); + break; + + case URL_NS_CALENDAR: + realm = config_getstring(IMAPOPT_CALDAV_REALM); + break; + + case URL_NS_ADDRESSBOOK: + realm = config_getstring(IMAPOPT_CARDDAV_REALM); + break; + + case URL_NS_RSS: + realm = config_getstring(IMAPOPT_RSS_REALM); + break; + } + if (!realm) realm = config_servername; + +#ifdef SASL_HTTP_REQUEST + /* Setup SASL HTTP request, if necessary */ + if (scheme->flags & AUTH_NEED_REQUEST) { + sasl_http_request_t sasl_http_req; + + sasl_http_req.method = txn->req_line.meth; + sasl_http_req.uri = txn->req_line.uri; + sasl_http_req.entity = NULL; + sasl_http_req.elen = 0; + sasl_http_req.non_persist = txn->flags.conn & CONN_CLOSE; + sasl_setprop(httpd_saslconn, SASL_HTTP_REQUEST, &sasl_http_req); + } +#endif /* SASL_HTTP_REQUEST */ + + if (scheme->idx == AUTH_BASIC) { + /* Basic (plaintext) authentication */ + char *pass; + + if (!clientin) { + /* Create initial challenge (base64 buffer is static) */ + snprintf(base64, BASE64_BUF_SIZE, "realm=\"%s\"", realm); + chal->param = base64; + chal->scheme = NULL; /* make sure we don't reset the SASL ctx */ + return status; + } + + /* Split credentials into <user> ':' <pass>. + * We are working with base64 buffer, so we can modify it. + */ + user = base64; + pass = strchr(base64, ':'); + if (!pass) { + syslog(LOG_ERR, "Basic auth: Missing password"); + return SASL_BADPARAM; + } + *pass++ = '\0'; + + /* Verify the password */ + status = sasl_checkpass(httpd_saslconn, user, strlen(user), + pass, strlen(pass)); + memset(pass, 0, strlen(pass)); /* erase plaintext password */ + + if (status) { + syslog(LOG_NOTICE, "badlogin: %s Basic %s %s", + httpd_clienthost, user, sasl_errdetail(httpd_saslconn)); + + /* Don't allow user probing */ + if (status == SASL_NOUSER) status = SASL_BADAUTH; + return status; + } + + /* Successful authentication - fall through */ + } + else { + /* SASL-based authentication (Digest, Negotiate, NTLM) */ + const char *serverout = NULL; + unsigned int serveroutlen = 0; + + if (status == SASL_CONTINUE) { + /* Continue current authentication exchange */ + syslog(LOG_DEBUG, "http_auth: continue %s", scheme->saslmech); + status = sasl_server_step(httpd_saslconn, clientin, clientinlen, + &serverout, &serveroutlen); + } + else { + /* Start new authentication exchange */ + syslog(LOG_DEBUG, "http_auth: start %s", scheme->saslmech); + status = sasl_server_start(httpd_saslconn, scheme->saslmech, + clientin, clientinlen, + &serverout, &serveroutlen); + } + + /* Failure - probably bad client response */ + if ((status != SASL_OK) && (status != SASL_CONTINUE)) { + syslog(LOG_ERR, "SASL failed: %s", + sasl_errstring(status, NULL, NULL)); + return status; + } + + /* Base64 encode any server challenge, if necesary */ + if (serverout && (scheme->flags & AUTH_BASE64)) { + int r = sasl_encode64(serverout, serveroutlen, + base64, BASE64_BUF_SIZE, NULL); + if (r != SASL_OK) { + syslog(LOG_ERR, "Base64 encode failed: %s", + sasl_errstring(r, NULL, NULL)); + return r; + } + serverout = base64; + } + + chal->param = serverout; + + if (status == SASL_CONTINUE) { + /* Need another step to complete authentication */ + return status; + } + + /* Successful authentication + * + * HTTP doesn't support security layers, + * so don't attach SASL context to prot layer. + */ + } + + /* Get the userid from SASL - already canonicalized */ + status = sasl_getprop(httpd_saslconn, SASL_USERNAME, &canon_user); + if (status != SASL_OK) { + syslog(LOG_ERR, "weird SASL error %d getting SASL_USERNAME", status); + return status; + } + user = (const char *) canon_user; + + if (saslprops.authid) free(saslprops.authid); + saslprops.authid = xstrdup(user); + + authzid = spool_getheader(txn->req_hdrs, "Authorize-As"); + if (authzid && *authzid[0]) { + /* Trying to proxy as another user */ + user = authzid[0]; + + status = proxy_authz(&user, txn); + if (status) return status; + } + + httpd_userid = xstrdup(user); + + auth_success(txn); + + return status; +} + + +/************************* Method Execution Routines ************************/ + + +/* Compare an etag in a header to a resource etag. + * Returns 0 if a match, non-zero otherwise. + */ +EXPORTED int etagcmp(const char *hdr, const char *etag) +{ + size_t len; + + if (!etag) return -1; /* no representation */ + if (!strcmp(hdr, "*")) return 0; /* any representation */ + + len = strlen(etag); + if (!strncmp(hdr, "W/", 2)) hdr+=2; /* skip weak prefix */ + if (*hdr++ != '\"') return 1; /* match/skip open DQUOTE */ + if (strlen(hdr) != len+1) return 1; /* make sure lengths match */ + if (hdr[len] != '\"') return 1; /* match close DQUOTE */ + + return strncmp(hdr, etag, len); +} + + +/* Compare a resource etag to a comma-separated list and/or multiple headers + * looking for a match. Returns 1 if a match is found, 0 otherwise. + */ +static unsigned etag_match(const char *hdr[], const char *etag) +{ + unsigned i, match = 0; + tok_t tok; + char *token; + + for (i = 0; !match && hdr[i]; i++) { + tok_init(&tok, hdr[i], ",", TOK_TRIMLEFT|TOK_TRIMRIGHT); + while (!match && (token = tok_next(&tok))) { + if (!etagcmp(token, etag)) match = 1; + } + tok_fini(&tok); + } + + return match; +} + + +/* Evaluate If header. Note that we can't short-circuit any of the tests + because we need to check for a lock-token anywhere in the header */ +static int eval_if(const char *hdr, const char *etag, const char *lock_token, + unsigned *locked) +{ + unsigned ret = 0; + tok_t tok_l; + char *list; + + /* Process each list, ORing the results */ + tok_init(&tok_l, hdr, ")", TOK_TRIMLEFT|TOK_TRIMRIGHT); + while ((list = tok_next(&tok_l))) { + unsigned ret_l = 1; + tok_t tok_c; + char *cond; + + /* XXX Need to handle Resource-Tag for Tagged-list (COPY/MOVE dest) */ + + /* Process each condition, ANDing the results */ + tok_initm(&tok_c, list+1, "]>", TOK_TRIMLEFT|TOK_TRIMRIGHT); + while ((cond = tok_next(&tok_c))) { + unsigned r, not = 0; + + if (!strncmp(cond, "Not", 3)) { + not = 1; + cond += 3; + while (*cond == ' ') cond++; + } + if (*cond == '[') { + /* ETag */ + r = !etagcmp(cond+1, etag); + } + else { + /* State Token */ + if (!lock_token) r = 0; + else { + r = !strcmp(cond+1, lock_token); + if (r) { + /* Correct lock-token has been provided */ + *locked = 0; + } + } + } + + ret_l &= (not ? !r : r); + } + + tok_fini(&tok_c); + + ret |= ret_l; + } + + tok_fini(&tok_l); + + return (ret || locked); +} + + +static int parse_ranges(const char *hdr, unsigned long len, + struct range **ranges) +{ + int ret = HTTP_UNSAT_RANGE; + struct range *new, *tail = *ranges = NULL; + tok_t tok; + char *token; + + if (!len) return HTTP_OK; /* need to know length of representation */ + + /* we only handle byte-unit */ + if (!hdr || strncmp(hdr, "bytes=", 6)) return HTTP_OK; + + tok_init(&tok, hdr+6, ",", TOK_TRIMLEFT|TOK_TRIMRIGHT); + while ((token = tok_next(&tok))) { + /* default to entire representation */ + unsigned long first = 0; + unsigned long last = len - 1; + char *p, *endp; + + if (!(p = strchr(token, '-'))) continue; /* bad byte-range-set */ + + if (p == token) { + /* suffix-byte-range-spec */ + unsigned long suffix = strtoul(++p, &endp, 10); + + if (endp == p || *endp) continue; /* bad suffix-length */ + if (!suffix) continue; /* unsatisfiable suffix-length */ + + /* don't start before byte zero */ + if (suffix < len) first = len - suffix; + } + else { + /* byte-range-spec */ + first = strtoul(token, &endp, 10); + if (endp != p) continue; /* bad first-byte-pos */ + if (first >= len) continue; /* unsatisfiable first-byte-pos */ + + if (*++p) { + /* last-byte-pos */ + last = strtoul(p, &endp, 10); + if (*endp || last < first) continue; /* bad last-byte-pos */ + + /* don't go past end of representation */ + if (last >= len) last = len - 1; + } + } + + ret = HTTP_PARTIAL; + + /* Coalesce overlapping ranges, or those with a gap < 80 bytes */ + if (tail && + first >= tail->first && (long) (first - tail->last) < 80) { + tail->last = MAX(last, tail->last); + continue; + } + + /* Create a new range and append it to linked list */ + new = xzmalloc(sizeof(struct range)); + new->first = first; + new->last = last; + + if (tail) tail->next = new; + else *ranges = new; + tail = new; + } + + tok_fini(&tok); + + return ret; +} + + +/* Check headers for any preconditions. + * + * Interaction is complex and is documented in RFC 4918 and + * Section 5 of HTTPbis, Part 4. + */ +EXPORTED int check_precond(struct transaction_t *txn, const void *data, + const char *etag, time_t lastmod) +{ + const char *lock_token = NULL; + unsigned locked = 0; + hdrcache_t hdrcache = txn->req_hdrs; + const char **hdr; + time_t since; + +#ifdef WITH_DAV + struct dav_data *ddata = (struct dav_data *) data; + + /* Check for a write-lock on the source */ + if (ddata && ddata->lock_expire > time(NULL)) { + lock_token = ddata->lock_token; + + switch (txn->meth) { + case METH_DELETE: + case METH_LOCK: + case METH_MOVE: + case METH_POST: + case METH_PUT: + /* State-changing method: Only the lock owner can execute + and MUST provide the correct lock-token in an If header */ + if (strcmp(ddata->lock_ownerid, httpd_userid)) return HTTP_LOCKED; + + locked = 1; + break; + + case METH_UNLOCK: + /* State-changing method: Authorized in meth_unlock() */ + break; + + case METH_ACL: + case METH_MKCALENDAR: + case METH_MKCOL: + case METH_PROPPATCH: + /* State-changing method: Locks on collections unsupported */ + break; + + default: + /* Non-state-changing method: Always allowed */ + break; + } + } +#else + assert(!data); +#endif /* WITH_DAV */ + + /* Per RFC 4918, If is similar to If-Match, but with lock-token submission. + Per Section 5 of HTTPbis, Part 4, LOCK errors supercede preconditions */ + if ((hdr = spool_getheader(hdrcache, "If"))) { + /* State tokens (sync-token, lock-token) and Etags */ + if (!eval_if(hdr[0], etag, lock_token, &locked)) + return HTTP_PRECOND_FAILED; + } + + if (locked) { + /* Correct lock-token was not provided in If header */ + return HTTP_LOCKED; + } + + /* Evaluate other precondition headers per Section 5 of HTTPbis, Part 4 */ + + /* Step 1 */ + if ((hdr = spool_getheader(hdrcache, "If-Match"))) { + if (!etag_match(hdr, etag)) return HTTP_PRECOND_FAILED; + + /* Continue to step 3 */ + } + + /* Step 2 */ + else if ((hdr = spool_getheader(hdrcache, "If-Unmodified-Since"))) { + if (time_from_rfc822(hdr[0], &since)) + return HTTP_BAD_REQUEST; + + if (since && (lastmod > since)) return HTTP_PRECOND_FAILED; + + /* Continue to step 3 */ + } + + /* Step 3 */ + if ((hdr = spool_getheader(hdrcache, "If-None-Match"))) { + if (etag_match(hdr, etag)) { + if (txn->meth == METH_GET || txn->meth == METH_HEAD) + return HTTP_NOT_MODIFIED; + else + return HTTP_PRECOND_FAILED; + } + + /* Continue to step 5 */ + } + + /* Step 4 */ + else if ((txn->meth == METH_GET || txn->meth == METH_HEAD) && + (hdr = spool_getheader(hdrcache, "If-Modified-Since"))) { + if (time_from_rfc822(hdr[0], &since)) + return HTTP_BAD_REQUEST; + + if (lastmod <= since) return HTTP_NOT_MODIFIED; + + /* Continue to step 5 */ + } + + /* Step 5 */ + if (txn->flags.ranges && /* Only if we support Range requests */ + txn->meth == METH_GET && (hdr = spool_getheader(hdrcache, "Range"))) { + + if ((hdr = spool_getheader(hdrcache, "If-Range"))) { + time_from_rfc822(hdr[0], &since); /* error OK here, could be an etag */ + } + + /* Only process Range if If-Range isn't present or validator matches */ + if (!hdr || (since && (lastmod <= since)) || !etagcmp(hdr[0], etag)) + return HTTP_PARTIAL; + } + + /* Step 6 */ + return HTTP_OK; +} + + +const struct mimetype { + const char *ext; + const char *type; + unsigned int compressible; +} mimetypes[] = { + { ".css", "text/css", 1 }, + { ".htm", "text/html", 1 }, + { ".html", "text/html", 1 }, + { ".ics", "text/calendar", 1 }, + { ".ifb", "text/calendar", 1 }, + { ".text", "text/plain", 1 }, + { ".txt", "text/plain", 1 }, + + { ".cgm", "image/cgm", 1 }, + { ".gif", "image/gif", 0 }, + { ".jpg", "image/jpeg", 0 }, + { ".jpeg", "image/jpeg", 0 }, + { ".png", "image/png", 0 }, + { ".svg", "image/svg+xml", 1 }, + { ".tif", "image/tiff", 1 }, + { ".tiff", "image/tiff", 1 }, + + { ".aac", "audio/aac", 0 }, + { ".m4a", "audio/mp4", 0 }, + { ".mp3", "audio/mpeg", 0 }, + { ".mpeg", "audio/mpeg", 0 }, + { ".oga", "audio/ogg", 0 }, + { ".ogg", "audio/ogg", 0 }, + { ".wav", "audio/wav", 0 }, + + { ".avi", "video/x-msvideo", 0 }, + { ".mov", "video/quicktime", 0 }, + { ".m4v", "video/mp4", 0 }, + { ".ogv", "video/ogg", 0 }, + { ".qt", "video/quicktime", 0 }, + { ".wmv", "video/x-ms-wmv", 0 }, + + { ".bz", "application/x-bzip", 0 }, + { ".bz2", "application/x-bzip2", 0 }, + { ".gz", "application/gzip", 0 }, + { ".gzip", "application/gzip", 0 }, + { ".tgz", "application/gzip", 0 }, + { ".zip", "application/zip", 0 }, + + { ".doc", "application/msword", 1 }, + { ".jcs", "application/calendar+json", 1 }, + { ".jfb", "application/calendar+json", 1 }, + { ".js", "application/javascript", 1 }, + { ".json", "application/json", 1 }, + { ".pdf", "application/pdf", 1 }, + { ".ppt", "application/vnd.ms-powerpoint", 1 }, + { ".sh", "application/x-sh", 1 }, + { ".tar", "application/x-tar", 1 }, + { ".xcs", "application/calendar+xml", 1 }, + { ".xfb", "application/calendar+xml", 1 }, + { ".xls", "application/vnd.ms-excel", 1 }, + { ".xml", "application/xml", 1 }, + + { NULL, NULL, 0 } +}; + + +static int list_well_known(struct transaction_t *txn) +{ + static struct buf body = BUF_INITIALIZER; + static time_t lastmod = 0; + struct stat sbuf; + int precond; + + /* stat() imapd.conf for Last-Modified and ETag */ + stat(config_filename, &sbuf); + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%ld-%ld-%ld", + compile_time, sbuf.st_mtime, sbuf.st_size); + sbuf.st_mtime = MAX(compile_time, sbuf.st_mtime); + + /* Check any preconditions, including range request */ + txn->flags.ranges = 1; + precond = check_precond(txn, NULL, buf_cstring(&txn->buf), sbuf.st_mtime); + + switch (precond) { + case HTTP_OK: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, and Expires */ + txn->resp_body.etag = buf_cstring(&txn->buf); + txn->resp_body.lastmod = sbuf.st_mtime; + txn->resp_body.maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + return precond; + } + + if (txn->resp_body.lastmod > lastmod) { + const char *proto = NULL, *host = NULL; + unsigned i, level = 0; + + /* Start HTML */ + buf_reset(&body); + buf_printf_markup(&body, level, HTML_DOCTYPE); + buf_printf_markup(&body, level++, "<html>"); + buf_printf_markup(&body, level++, "<head>"); + buf_printf_markup(&body, level, + "<title>%s</title>", "Well-Known Locations"); + buf_printf_markup(&body, --level, "</head>"); + buf_printf_markup(&body, level++, "<body>"); + buf_printf_markup(&body, level, + "<h2>%s</h2>", "Well-Known Locations"); + buf_printf_markup(&body, level++, "<ul>"); + + /* Add the list of enabled /.well-known/ URLs */ + http_proto_host(txn->req_hdrs, &proto, &host); + for (i = 0; namespaces[i]; i++) { + + if (namespaces[i]->enabled && namespaces[i]->well_known) { + buf_printf_markup(&body, level, + "<li><a href=\"%s://%s%s\">%s</a></li>", + proto, host, namespaces[i]->prefix, + namespaces[i]->well_known); + } + } + + /* Finish HTML */ + buf_printf_markup(&body, --level, "</ul>"); + buf_printf_markup(&body, --level, "</body>"); + buf_printf_markup(&body, --level, "</html>"); + + lastmod = txn->resp_body.lastmod; + } + + /* Output the HTML response */ + txn->resp_body.type = "text/html; charset=utf-8"; + write_body(precond, txn, buf_cstring(&body), buf_len(&body)); + + return 0; +} + + +#define WELL_KNOWN_PREFIX "/.well-known" + +/* Perform a GET/HEAD request */ +static int meth_get(struct transaction_t *txn, + void *params __attribute__((unused))) +{ + int ret = 0, r, fd = -1, precond, len; + const char *prefix, *urls, *path, *ext; + static struct buf pathbuf = BUF_INITIALIZER; + struct stat sbuf; + const char *msg_base = NULL; + unsigned long msg_size = 0; + struct resp_body_t *resp_body = &txn->resp_body; + + /* Check if this is a request for /.well-known/ listing */ + len = strlen(WELL_KNOWN_PREFIX); + if (!strncmp(txn->req_uri->path, WELL_KNOWN_PREFIX, len)) { + if (txn->req_uri->path[len] == '/') len++; + if (txn->req_uri->path[len] == '\0') return list_well_known(txn); + else return HTTP_NOT_FOUND; + } + + /* Serve up static pages */ + prefix = config_getstring(IMAPOPT_HTTPDOCROOT); + if (!prefix) return HTTP_NOT_FOUND; + + if (*prefix != '/') { + /* Remote content */ + struct backend *be; + + be = proxy_findserver(prefix, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + /* Local content */ + if ((urls = config_getstring(IMAPOPT_HTTPALLOWEDURLS))) { + tok_t tok = TOK_INITIALIZER(urls, " \t", TOK_TRIMLEFT|TOK_TRIMRIGHT); + char *token; + + while ((token = tok_next(&tok)) && strcmp(token, txn->req_uri->path)) + tok_fini(&tok); + + if (!token) return HTTP_NOT_FOUND; + } + + buf_setcstr(&pathbuf, prefix); + buf_appendcstr(&pathbuf, txn->req_uri->path); + path = buf_cstring(&pathbuf); + + /* See if path is a directory and look for index.html */ + if (!(r = stat(path, &sbuf)) && S_ISDIR(sbuf.st_mode)) { + buf_appendcstr(&pathbuf, "/index.html"); + path = buf_cstring(&pathbuf); + r = stat(path, &sbuf); + } + + /* See if file exists and get Content-Length & Last-Modified time */ + if (r || !S_ISREG(sbuf.st_mode)) return HTTP_NOT_FOUND; + + if (!resp_body->type) { + /* Caller hasn't specified the Content-Type */ + resp_body->type = "application/octet-stream"; + + if ((ext = strrchr(path, '.'))) { + /* Try to use filename extension to identity Content-Type */ + const struct mimetype *mtype; + + for (mtype = mimetypes; mtype->ext; mtype++) { + if (!strcasecmp(ext, mtype->ext)) { + resp_body->type = mtype->type; + if (!mtype->compressible) { + /* Never compress non-compressible resources */ + txn->resp_body.enc = CE_IDENTITY; + txn->flags.te = TE_NONE; + txn->flags.vary &= ~VARY_AE; + } + break; + } + } + } + } + + /* Generate Etag */ + assert(!buf_len(&txn->buf)); + buf_printf(&txn->buf, "%ld-%ld", (long) sbuf.st_mtime, (long) sbuf.st_size); + + /* Check any preconditions, including range request */ + txn->flags.ranges = 1; + precond = check_precond(txn, NULL, buf_cstring(&txn->buf), sbuf.st_mtime); + + switch (precond) { + case HTTP_OK: + case HTTP_PARTIAL: + case HTTP_NOT_MODIFIED: + /* Fill in ETag, Last-Modified, and Expires */ + resp_body->etag = buf_cstring(&txn->buf); + resp_body->lastmod = sbuf.st_mtime; + resp_body->maxage = 86400; /* 24 hrs */ + txn->flags.cc |= CC_MAXAGE; + if (httpd_userid) txn->flags.cc |= CC_PUBLIC; + + if (precond != HTTP_NOT_MODIFIED) break; + + default: + /* We failed a precondition - don't perform the request */ + resp_body->type = NULL; + return precond; + } + + if (txn->meth == METH_GET) { + /* Open and mmap the file */ + if ((fd = open(path, O_RDONLY)) == -1) return HTTP_SERVER_ERROR; + map_refresh(fd, 1, &msg_base, &msg_size, sbuf.st_size, path, NULL); + } + + write_body(precond, txn, msg_base, sbuf.st_size); + + if (fd != -1) { + map_free(&msg_base, &msg_size); + close(fd); + } + + return ret; +} + + +/* Perform an OPTIONS request */ +EXPORTED int meth_options(struct transaction_t *txn, void *params) +{ + parse_path_t parse_path = (parse_path_t) params; + int r, i; + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Response doesn't have a body, so no Vary */ + txn->flags.vary = 0; + + /* Special case "*" - show all features/methods available on server */ + if (!strcmp(txn->req_uri->path, "*")) { + for (i = 0; namespaces[i]; i++) { + if (namespaces[i]->enabled) + txn->req_tgt.allow |= namespaces[i]->allow; + } + } + else { + if (parse_path) { + /* Parse the path */ + r = parse_path(txn->req_uri->path, &txn->req_tgt, &txn->error.desc); + if (r) return r; + } + + if (txn->flags.cors) { + const char **hdr = + spool_getheader(txn->req_hdrs, "Access-Control-Request-Method"); + + if (hdr) { + /* CORS preflight request */ + unsigned meth; + + txn->flags.cors = CORS_PREFLIGHT; + + /* Check Method against our list of known methods */ + for (meth = 0; (meth < METH_UNKNOWN) && + strcmp(http_methods[meth].name, hdr[0]); meth++); + + if (meth == METH_UNKNOWN) txn->flags.cors = 0; + else { + /* Check Method against those supported by the resource */ + for (i = 0; namespaces[i] && + namespaces[i]->id != txn->req_tgt.namespace; i++); + + if (!namespaces[i]->methods[meth].proc) txn->flags.cors = 0; + } + } + } + } + + response_header(HTTP_OK, txn); + return 0; +} + + +/* Perform an PROPFIND request on "/" iff we support CalDAV */ +static int meth_propfind_root(struct transaction_t *txn, + void *params __attribute__((unused))) +{ + assert(txn); + +#ifdef WITH_DAV + /* Apple iCal and Evolution both check "/" */ + if (!strcmp(txn->req_uri->path, "/") || + !strcmp(txn->req_uri->path, "/dav/")) { + /* Array of known "live" properties */ + const struct prop_entry root_props[] = { + + /* WebDAV ACL (RFC 3744) properties */ + { "principal-collection-set", NS_DAV, PROP_COLLECTION, + propfind_princolset, NULL, NULL }, + + /* WebDAV Current Principal (RFC 5397) properties */ + { "current-user-principal", NS_DAV, PROP_COLLECTION, + propfind_curprin, NULL, NULL }, + + { NULL, 0, 0, NULL, NULL, NULL } + }; + + struct meth_params root_params = { + .lprops = root_props + }; + + /* Make a working copy of target path */ + strlcpy(txn->req_tgt.path, txn->req_uri->path, + sizeof(txn->req_tgt.path)); + txn->req_tgt.tail = txn->req_tgt.path + strlen(txn->req_tgt.path); + + txn->req_tgt.allow |= ALLOW_DAV; + return meth_propfind(txn, &root_params); + } +#endif + + return HTTP_NOT_ALLOWED; +} + + +/* Write cached header to buf, excluding any that might have sensitive data. */ +static void trace_cachehdr(const char *name, const char *contents, void *rock) +{ + struct buf *buf = (struct buf *) rock; + const char **hdr, *sensitive[] = + { "authorization", "cookie", "proxy-authorization", NULL }; + + /* Ignore private headers in our cache */ + if (name[0] == ':') return; + + for (hdr = sensitive; *hdr && strcmp(name, *hdr); hdr++); + + if (!*hdr) buf_printf(buf, "%c%s: %s\r\n", + toupper(name[0]), name+1, contents); +} + +/* Perform an TRACE request */ +EXPORTED int meth_trace(struct transaction_t *txn, void *params) +{ + parse_path_t parse_path = (parse_path_t) params; + const char **hdr; + unsigned long max_fwd = -1; + struct buf *msg = &txn->resp_body.payload; + + /* Response should not be cached */ + txn->flags.cc |= CC_NOCACHE; + + /* Make sure method is allowed */ + if (!(txn->req_tgt.allow & ALLOW_TRACE)) return HTTP_NOT_ALLOWED; + + if ((hdr = spool_getheader(txn->req_hdrs, "Max-Forwards"))) { + max_fwd = strtoul(hdr[0], NULL, 10); + } + + if (max_fwd && parse_path) { + /* Parse the path */ + int r; + + if ((r = parse_path(txn->req_uri->path, + &txn->req_tgt, &txn->error.desc))) return r; + + if (*txn->req_tgt.mboxname) { + /* Locate the mailbox */ + mbentry_t *mbentry = NULL; + + r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL); + if (r) { + syslog(LOG_ERR, "mlookup(%s) failed: %s", + txn->req_tgt.mboxname, error_message(r)); + txn->error.desc = error_message(r); + + switch (r) { + case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN; + case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND; + default: return HTTP_SERVER_ERROR; + } + } + + if (mbentry->server) { + /* Remote mailbox */ + struct backend *be; + + be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid, + &backend_cached, NULL, NULL, httpd_in); + mboxlist_entry_free(&mbentry); + if (!be) return HTTP_UNAVAILABLE; + + return http_pipe_req_resp(be, txn); + } + + mboxlist_entry_free(&mbentry); + + /* Local mailbox */ + } + } + + /* Echo the request back to the client as a message/http: + * + * - Piece the Request-line back together + * - Use all non-sensitive cached headers from client + */ + buf_reset(msg); + buf_printf(msg, "TRACE %s %s\r\n", txn->req_line.uri, txn->req_line.ver); + spool_enum_hdrcache(txn->req_hdrs, &trace_cachehdr, msg); + buf_appendcstr(msg, "\r\n"); + + txn->resp_body.type = "message/http"; + txn->resp_body.len = buf_len(msg); + + write_body(HTTP_OK, txn, buf_cstring(msg), buf_len(msg)); + + return 0; +}
View file
cyrus-imapd-2.5.tar.gz/imap/httpd.h
Added
@@ -0,0 +1,430 @@ +/* httpd.h -- Common state for HTTP/RSS/WebDAV/CalDAV/iSchedule daemon + * + * Copyright (c) 1994-2011 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 HTTPD_H +#define HTTPD_H + +#include <sasl/sasl.h> +#include <libxml/tree.h> +#include <libxml/uri.h> + +#ifdef HAVE_ZLIB +#include <zlib.h> +#endif /* HAVE_ZLIB */ + +#include "annotate.h" /* for strlist */ +#include "hash.h" +#include "http_client.h" +#include "mailbox.h" +#include "spool.h" + +#define MAX_REQ_LINE 8000 /* minimum size per HTTPbis */ +#define MARKUP_INDENT 2 /* # spaces to indent each line of markup */ +#define GZIP_MIN_LEN 300 /* minimum length of data to gzip */ + +/* Supported TLS version for Upgrade */ +#define TLS_VERSION "TLS/1.0" + +/* Supported HTML DOCTYPE */ +#define HTML_DOCTYPE \ + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" " \ + "\"http://www.w3.org/TR/html4/loose.dtd\">" + +#define XML_DECLARATION \ + "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + +/* Macro to return proper response code when user privileges are insufficient */ +#define HTTP_NO_PRIVS \ + (httpd_userid && !is_userid_anonymous(httpd_userid) ? \ + HTTP_FORBIDDEN : HTTP_UNAUTHORIZED) + +/* Macro to access query part of URI */ +#if LIBXML_VERSION >= 20700 +#define URI_QUERY(uri) uri->query_raw +#else +#define URI_QUERY(uri) uri->query +#endif + +/* SASL usage based on availability */ +#if defined(SASL_NEED_HTTP) && defined(SASL_HTTP_REQUEST) + #define HTTP_DIGEST_MECH "DIGEST-MD5" + #define SASL_USAGE_FLAGS (SASL_NEED_HTTP | SASL_SUCCESS_DATA) +#else + #define HTTP_DIGEST_MECH NULL /* not supported by our SASL version */ + #define SASL_USAGE_FLAGS SASL_SUCCESS_DATA +#endif /* SASL_NEED_HTTP */ + +/* Array of HTTP methods known by our server. */ +struct known_meth_t { + const char *name; + unsigned flags; +}; +extern const struct known_meth_t http_methods[]; + +/* Flags for known methods*/ +enum { + METH_NOBODY = (1<<0), /* Method does not expect a body */ +}; + + +/* Path namespaces */ +enum { + URL_NS_DEFAULT = 0, + URL_NS_PRINCIPAL, + URL_NS_CALENDAR, + URL_NS_ADDRESSBOOK, + URL_NS_ISCHEDULE, + URL_NS_DOMAINKEY, + URL_NS_TIMEZONE, + URL_NS_RSS, + URL_NS_DBLOOKUP +}; + +/* Bitmask of features/methods to allow, based on URL */ +enum { + ALLOW_READ = (1<<0), /* Read resources/properties */ + ALLOW_POST = (1<<1), /* Post to a URL */ + ALLOW_WRITE = (1<<2), /* Create/modify/lock resources */ + ALLOW_DELETE = (1<<3), /* Delete resources/collections */ + ALLOW_TRACE = (1<<4), /* TRACE a request */ + ALLOW_DAV = (1<<5), /* WebDAV specific methods/features */ + ALLOW_WRITECOL = (1<<6), /* Create/modify collections */ + ALLOW_CAL = (1<<7), /* CalDAV specific methods/features */ + ALLOW_CAL_AVAIL = (1<<8), /* CalDAV Availability specific features */ + ALLOW_CAL_SCHED = (1<<9), /* CalDAV Scheduling specific features */ + ALLOW_CAL_NOTZ = (1<<10),/* CalDAV TZ by Ref specific features */ + ALLOW_CARD = (1<<11),/* CardDAV specific methods/features */ + ALLOW_ISCHEDULE = (1<<12) /* iSchedule specific methods/features */ +}; + +struct auth_scheme_t { + unsigned idx; /* Index value of the scheme */ + const char *name; /* HTTP auth scheme name */ + const char *saslmech; /* Corresponding SASL mech name */ + unsigned flags; /* Bitmask of requirements/features */ + /* Optional function to send success data */ + void (*send_success)(const char *name, const char *data); + /* Optional function to recv success data */ + const char *(*recv_success)(hdrcache_t hdrs); +}; + +/* Index into available schemes */ +enum { + AUTH_BASIC = 0, + AUTH_DIGEST, + AUTH_SPNEGO, + AUTH_NTLM +}; + +/* Auth scheme flags */ +enum { + AUTH_NEED_PERSIST = (1<<0), /* Persistent connection required */ + AUTH_NEED_REQUEST = (1<<1), /* Request-line required */ + AUTH_SERVER_FIRST = (1<<2), /* SASL mech is server-first */ + AUTH_BASE64 = (1<<3) /* Base64 encode/decode auth data */ +}; + +/* List of HTTP auth schemes that we support */ +extern struct auth_scheme_t auth_schemes[]; + +extern const char *digest_recv_success(hdrcache_t hdrs); + + +/* Request-line context */ +struct request_line_t { + char buf[MAX_REQ_LINE+1]; /* working copy of request-line */ + char *meth; /* method */ + char *uri; /* request-target */ + char *ver; /* HTTP-version */ +}; + + +/* Request target context */ +struct request_target_t { + char path[MAX_MAILBOX_PATH+1]; /* working copy of URL path */ + char *tail; /* tail of original request path */ + unsigned namespace; /* namespace of path */ + char *user; /* ptr to owner of collection (NULL = shared) */ + size_t userlen; + char *collection; /* ptr to collection name */ + size_t collen; + char *resource; /* ptr to resource name */ + size_t reslen; + unsigned flags; /* target-specific flags/meta-data */ + unsigned long allow; /* bitmask of allowed features/methods */ + char mboxname[MAX_MAILBOX_BUFFER+1]; +}; + +/* Request target flags */ +enum { + TGT_SCHED_INBOX = 1, + TGT_SCHED_OUTBOX +}; + +/* Function to parse URI path and generate a mailbox name */ +typedef int (*parse_path_t)(const char *path, + struct request_target_t *tgt, const char **errstr); + +/* Auth challenge context */ +struct auth_challenge_t { + struct auth_scheme_t *scheme; /* Selected AUTH scheme */ + const char *param; /* Server challenge */ +}; + +/* Meta-data for error response */ +struct error_t { + const char *desc; /* Error description */ + unsigned precond; /* [Cal]DAV precondition */ + const char *resource; /* Resource which lacks privileges */ + int rights; /* Privileges needed by resource */ +}; + +struct range { + unsigned long first; + unsigned long last; + struct range *next; +}; + + +/* Meta-data for response body (payload & representation headers) */ +struct resp_body_t { + ulong len; /* Content-Length */ + struct range *range;/* Content-Range */ + const char *fname; /* Content-Dispo */ + unsigned char enc; /* Content-Encoding */ + const char *lang; /* Content-Language */ + const char *loc; /* Content-Location */ + const u_char *md5; /* Content-MD5 */ + const char *type; /* Content-Type */ + unsigned prefs; /* Prefer */ + const char *lock; /* Lock-Token */ + const char *etag; /* ETag */ + time_t lastmod; /* Last-Modified */ + time_t maxage; /* Expires */ + const char *stag; /* Schedule-Tag */ + time_t iserial; /* iSched serial# */ + struct buf payload; /* Payload */ +}; + +/* Transaction flags */ +struct txn_flags_t { + unsigned char ver1_0; /* Request from HTTP/1.0 client */ + unsigned char conn; /* Connection opts on req/resp */ + unsigned char override; /* HTTP method override */ + unsigned char cors; /* Cross-Origin Resource Sharing */ + unsigned char mime; /* MIME-conformant response */ + unsigned char te; /* Transfer-Encoding for resp */ + unsigned char cc; /* Cache-Control directives for resp */ + unsigned char ranges; /* Accept range requests for resource */ + unsigned char vary; /* Headers on which response varied */ + unsigned char trailer; /* Headers which will be in trailer */ +}; + +/* Transaction context */ +struct transaction_t { + unsigned meth; /* Index of Method to be performed */ + struct txn_flags_t flags; /* Flags for this txn */ + struct request_line_t req_line; /* Parsed request-line */ + xmlURIPtr req_uri; /* Parsed request-target URI */ + struct request_target_t req_tgt; /* Parsed request-target path */ + hash_table req_qparams; /* Parsed query params */ + hdrcache_t req_hdrs; /* Cached HTTP headers */ + struct body_t req_body; /* Buffered request body */ + struct auth_challenge_t auth_chal; /* Authentication challenge */ + const char *location; /* Location of resource */ + struct error_t error; /* Error response meta-data */ + struct resp_body_t resp_body; /* Response body meta-data */ +#ifdef HAVE_ZLIB + z_stream zstrm; /* Compression context */ + struct buf zbuf; /* Compression buffer */ +#endif + struct buf buf; /* Working buffer - currently used for: + httpd: + - telemetry of auth'd request + - error desc string + - Location hdr on redirects + - Etag for static docs + http_rss: + - Content-Type for MIME parts + - URL for feed & items + http_caldav: + - precond error resource URL + http_ischedule: + - error desc string + */ +}; + +/* Connection token flags */ +enum { + CONN_CLOSE = (1<<0), + CONN_UPGRADE = (1<<1), + CONN_KEEPALIVE = (1<<2) +}; + +/* Cross-Origin Resource Sharing flags */ +enum { + CORS_NONE = 0, + CORS_SIMPLE = 1, + CORS_PREFLIGHT = 2 +}; + +/* Content-Encoding flags (coding of representation) */ +enum { + CE_IDENTITY = 0, + CE_DEFLATE = (1<<0), + CE_GZIP = (1<<1) +}; + +/* Cache-Control directive flags */ +enum { + CC_REVALIDATE = (1<<0), + CC_NOCACHE = (1<<1), + CC_NOSTORE = (1<<2), + CC_NOTRANSFORM = (1<<3), + CC_PUBLIC = (1<<4), + CC_PRIVATE = (1<<5), + CC_MAXAGE = (1<<6) +}; + +/* Vary header flags (headers used in selecting/producing representation) */ +enum { + VARY_ACCEPT = (1<<0), + VARY_AE = (1<<1), /* Accept-Encoding */ + VARY_BRIEF = (1<<2), + VARY_PREFER = (1<<3) +}; + +/* Trailer header flags */ +enum { + TRAILER_CMD5 = (1<<0) /* Content-MD5 */ +}; + +typedef int (*method_proc_t)(struct transaction_t *txn, void *params); +typedef int (*filter_proc_t)(struct transaction_t *txn, + const char *base, unsigned long len); + +struct method_t { + method_proc_t proc; /* Function to perform the method */ + void *params; /* Parameters to pass to the method */ +}; + +struct namespace_t { + unsigned id; /* Namespace identifier */ + unsigned enabled; /* Is this namespace enabled? */ + const char *prefix; /* Prefix of URL path denoting namespace */ + const char *well_known; /* Any /.well-known/ URI */ + unsigned need_auth; /* Do we need to auth for this namespace? */ + unsigned long allow; /* Bitmask of allowed features/methods */ + void (*init)(struct buf *serverinfo); + void (*auth)(const char *userid); + void (*reset)(void); + void (*shutdown)(void); + struct method_t methods[]; /* Array of functions to perform HTTP methods. + * MUST be an entry for EACH method listed, + * and in the SAME ORDER in which they appear, + * in the http_methods[] array. + * If the method is not supported, + * the function pointer MUST be NULL. + */ +}; + +struct accept { + char *token; + float qual; + struct accept *next; +}; + +extern struct namespace_t namespace_default; +extern struct namespace_t namespace_principal; +extern struct namespace_t namespace_calendar; +extern struct namespace_t namespace_addressbook; +extern struct namespace_t namespace_ischedule; +extern struct namespace_t namespace_domainkey; +extern struct namespace_t namespace_timezone; +extern struct namespace_t namespace_rss; +extern struct namespace_t namespace_dblookup; + + +/* XXX These should be included in struct transaction_t */ +extern struct buf serverinfo; +extern struct backend **backend_cached; +extern struct protstream *httpd_in; +extern struct protstream *httpd_out; +extern int https; +extern int httpd_tls_done; +extern int httpd_timeout; +extern int httpd_userisadmin; +extern int httpd_userisproxyadmin; +extern char *httpd_userid, *proxy_userid; +extern struct auth_state *httpd_authstate; +extern struct namespace httpd_namespace; +extern struct sockaddr_storage httpd_localaddr, httpd_remoteaddr; +extern unsigned long config_httpmodules; +extern int config_httpprettytelemetry; + +extern xmlURIPtr parse_uri(unsigned meth, const char *uri, unsigned path_reqd, + const char **errstr); +extern struct accept *parse_accept(const char **hdr); +extern time_t calc_compile_time(const char *time, const char *date); +extern const char *http_statusline(long code); +extern char *rfc3339date_gen(char *buf, size_t len, time_t t); +extern char *httpdate_gen(char *buf, size_t len, time_t t); +extern void comma_list_hdr(const char *hdr, const char *vals[], + unsigned flags, ...); +extern void response_header(long code, struct transaction_t *txn); +extern void buf_printf_markup(struct buf *buf, unsigned level, + const char *fmt, ...); +extern void error_response(long code, struct transaction_t *txn); +extern void html_response(long code, struct transaction_t *txn, xmlDocPtr html); +extern void xml_response(long code, struct transaction_t *txn, xmlDocPtr xml); +extern void write_body(long code, struct transaction_t *txn, + const char *buf, unsigned len); +extern void write_multipart_body(long code, struct transaction_t *txn, + const char *buf, unsigned len); +extern int meth_options(struct transaction_t *txn, void *params); +extern int meth_trace(struct transaction_t *txn, void *params); +extern int etagcmp(const char *hdr, const char *etag); +extern int check_precond(struct transaction_t *txn, const void *data, + const char *etag, time_t lastmod); + +#endif /* HTTPD_H */
View file
cyrus-imapd-2.5.tar.gz/imap/idled.c
Changed
@@ -301,8 +301,7 @@ /* count the number of mailboxes */ mboxlist_init(0); mboxlist_open(NULL); - cyrusdb_foreach(mbdb, "", 0, NULL, &mbox_count_cb, - &nmbox, NULL); + mboxlist_allmbox("", &mbox_count_cb, &nmbox, /*incdel*/0); mboxlist_close(); mboxlist_done();
View file
cyrus-imapd-2.5.tar.gz/imap/idlemsg.c
Changed
@@ -164,12 +164,18 @@ EXPORTED int idle_send(const struct sockaddr_un *remote, const idle_message_t *msg) { + int flags = 0; + +#ifdef MSG_DONTWAIT + flags |= MSG_DONTWAIT; +#endif + if (idle_sock < 0) return IMAP_SERVER_UNAVAILABLE; if (sendto(idle_sock, (void *) msg, IDLE_MESSAGE_BASE_SIZE+strlen(msg->mboxname)+1, /* 1 for NULL */ - 0, (struct sockaddr *) remote, sizeof(*remote)) == -1) { + flags, (struct sockaddr *) remote, sizeof(*remote)) == -1) { return errno; }
View file
cyrus-imapd-2.5.tar.gz/imap/imap_err.et
Changed
@@ -104,6 +104,9 @@ ec IMAP_MAILBOX_LOCKED, "Mailbox is locked" +ec IMAP_MAILBOX_DISABLED, + "Delivery to mailbox is disabled" + ec IMAP_PARTITION_UNKNOWN, "Unknown/invalid partition"
View file
cyrus-imapd-2.5.tar.gz/imap/imapd.c
Changed
@@ -175,6 +175,7 @@ static void *imapd_tls_comp = NULL; /* TLS compression method, if any */ static int imapd_compress_done = 0; /* have we done a successful compress? */ static const char *plaintextloginalert = NULL; +static int ignorequota = 0; static struct id_data { struct attvaluelist *params; @@ -847,7 +848,7 @@ snmp_connect(); /* ignore return code */ snmp_set_str(SERVER_NAME_VERSION,cyrus_version()); - while ((opt = getopt(argc, argv, "sp:N")) != EOF) { + while ((opt = getopt(argc, argv, "Np:sq")) != EOF) { switch (opt) { case 's': /* imaps (do starttls right away) */ imaps = 1; @@ -864,6 +865,9 @@ * you know what you're doing! */ nosaslpasswdcheck = 1; break; + case 'q': /* don't enforce quotas */ + ignorequota = 1; + break; default: break; } @@ -2213,6 +2217,8 @@ syslog(LOG_NOTICE, "cmdtimer: '%s' '%s' '%s' '%f' '%f' '%f'", imapd_userid ? imapd_userid : "<none>", cmdname, mboxname, cmdtime, nettime, cmdtime + nettime); + /* XXX - this would explode horribly if ptr is pointing into zbuf */ + syslog(LOG_NOTICE, "buf: %.*s", (int)(imapd_in->ptr - imapd_in->buf), imapd_in->buf); } } continue; @@ -3454,7 +3460,7 @@ /* local mailbox */ if (!r) { qdiffs[QUOTA_MESSAGE] = 1; - r = append_check(mailboxname, imapd_authstate, ACL_INSERT, qdiffs); + r = append_check(mailboxname, imapd_authstate, ACL_INSERT, ignorequota ? NULL : qdiffs); } if (r) { eatline(imapd_in, ' '); @@ -3608,7 +3614,7 @@ qdiffs[QUOTA_MESSAGE] = stages.count; r = append_setup(&appendstate, mailboxname, imapd_userid, imapd_authstate, ACL_INSERT, - qdiffs, &imapd_namespace, + ignorequota ? NULL : qdiffs, &imapd_namespace, (imapd_userisadmin || imapd_userisproxyadmin), EVENT_MESSAGE_APPEND); } @@ -5259,7 +5265,8 @@ r = index_copy(imapd_index, sequence, usinguid, mailboxname, ©uid, !config_getswitch(IMAPOPT_SINGLEINSTANCESTORE), &imapd_namespace, - (imapd_userisadmin || imapd_userisproxyadmin), ismove); + (imapd_userisadmin || imapd_userisproxyadmin), ismove, + ignorequota); } imapd_check(NULL, ismove || usinguid); @@ -5349,6 +5356,19 @@ dlist_getatom(extargs, "PARTITION", &partition); dlist_getatom(extargs, "SERVER", &server); + + const char *type = NULL; + + dlist_getatom(extargs, "PARTITION", &partition); + dlist_getatom(extargs, "SERVER", &server); + if (dlist_getatom(extargs, "TYPE", &type)) { + if (!strcasecmp(type, "CALENDAR")) mbtype |= MBTYPE_CALENDAR; + else if (!strcasecmp(type, "ADDRESSBOOK")) mbtype |= MBTYPE_ADDRESSBOOK; + else { + r = IMAP_MAILBOX_BADTYPE; + goto err; + } + } use = dlist_getchild(extargs, "USE"); if (use) { struct dlist *item; @@ -5377,14 +5397,12 @@ // However, this only applies to frontends. If we're a backend, a frontend will // proxy the partition it wishes to create the mailbox on. if ((server || partition) && !imapd_userisadmin) { - if ( - config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_STANDARD || - config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_UNIFIED - ) { + if (config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_STANDARD || + config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_UNIFIED) { if (!config_getstring(IMAPOPT_PROXYSERVERS)) { - prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_PERMISSION_DENIED)); - goto done; + r = IMAP_PERMISSION_DENIED; + goto err; } } } @@ -5398,6 +5416,7 @@ imapd_userid, mailboxname); if (r) { + err: prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); goto done; } @@ -5406,7 +5425,7 @@ char *copy = xstrdup(mailboxname); lcase(copy); - if (strstr(copy, "inbox.inbox")) + if (strstr(copy, "inbox.inbox.")) r = IMAP_MAILBOX_BADNAME; free(copy); @@ -5887,10 +5906,19 @@ char oldextname[MAX_MAILBOX_BUFFER]; char newextname[MAX_MAILBOX_BUFFER]; struct renrock *text = (struct renrock *)rock; - int r; + struct mboxlist_entry *mbentry = NULL; + int r = 0; + + r = mboxlist_lookup(name, &mbentry, NULL); + if (r == IMAP_MAILBOX_NONEXISTENT) { + /* skip these mailboxes */ + r = 0; + goto done; + } + if (r) goto done; if((text->nl + strlen(name + text->ol)) >= MAX_MAILBOX_BUFFER) - return 0; + goto done; strcpy(text->newmailboxname + text->nl, name + text->ol); @@ -5910,13 +5938,21 @@ if(r) { prot_printf(imapd_out, "* NO rename %s %s: %s\r\n", oldextname, newextname, error_message(r)); - if (RENAME_STOP_ON_ERROR) return r; + if (!RENAME_STOP_ON_ERROR) r = 0; } else { /* If we're renaming a user, change quotaroot and ACL */ if (text->rename_user) { user_copyquotaroot(name, text->newmailboxname); user_renameacl(text->namespace, text->newmailboxname, text->acl_olduser, text->acl_newuser); +#ifdef WITH_DAV + if (mbentry->mbtype & (MBTYPE_CALENDAR|MBTYPE_ADDRESSBOOK)) { + struct mailbox *mailbox = NULL; + r = mailbox_open_irl(text->newmailboxname, &mailbox); + if (!r) r = mailbox_add_dav(mailbox); + mailbox_close(&mailbox); + } +#endif } @@ -5924,19 +5960,13 @@ oldextname, newextname); sync_log_mailbox_double(name, text->newmailboxname); - - if (text->rename_user) { - /* allow the replica to get the correct new quotaroot - * and acls copied across */ - sync_log_user(text->newuser); - /* allow the replica to clean up the old meta files */ - sync_log_unuser(text->olduser); - } } +done: + mboxlist_entry_free(&mbentry); prot_flush(imapd_out); - return 0; + return r; } /* @@ -6000,7 +6030,7 @@ if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); - return; + goto done; } if (mbentry->mbtype & MBTYPE_REMOTE) { @@ -6075,7 +6105,7 @@ ); res = pipe_until_tag(s, tag, 0); - + /* make sure we've seen the update */ if (ultraparanoid && res == PROXY_OK) kick_mupdate(); } @@ -6100,7 +6130,7 @@ if (location && !config_partitiondir(location)) { /* invalid partition, assume its a server (remote destination) */ char *server; - + if (strcmp(oldname, newname)) { prot_printf(imapd_out, "%s NO Cross-server or cross-partition move w/rename not supported\r\n", @@ -6288,7 +6318,7 @@ rock.acl_newuser = acl_newuser; rock.partition = location; rock.rename_user = rename_user; - + /* add submailboxes; we pretend we're an admin since we successfully renamed the parent - we're using internal names here */ r = mboxlist_findall(NULL, oldmailboxname, 1, imapd_userid, @@ -6296,8 +6326,14 @@ } /* take care of deleting old ACLs, subscriptions, seen state and quotas */ - if (!r && rename_user) + if (!r && rename_user) { user_deletedata(olduser, 1); + /* allow the replica to get the correct new quotaroot + * and acls copied across */ + sync_log_user(newuser); + /* allow the replica to clean up the old meta files */ + sync_log_unuser(olduser); + } imapd_check(NULL, 0); @@ -7617,6 +7653,8 @@ struct statusdata sdata; int r = 0; + memset(&sdata, 0, sizeof(struct statusdata)); + r = (*imapd_namespace.mboxname_tointernal)(&imapd_namespace, name, imapd_userid, mailboxname); @@ -11186,6 +11224,8 @@ if (!name) return; + memset(&sdata, 0, sizeof(struct statusdata)); + /* first convert "INBOX" to "user.<userid>" */ if (!strncasecmp(name, "inbox", 5) && (!name[5] || name[5] == '.') ) { @@ -11395,8 +11435,13 @@ if (listargs->cmd & LIST_CMD_EXTENDED && attributes & MBOX_ATTRIBUTE_CHILDINFO_SUBSCRIBED) { prot_printf(imapd_out, " (CHILDINFO ("); + /* RFC 5258: + * ; Note 2: The selection options are always returned + * ; quoted, unlike their specification in + * ; the extended LIST command. + */ if (attributes & MBOX_ATTRIBUTE_CHILDINFO_SUBSCRIBED) - prot_printf(imapd_out, "SUBSCRIBED"); + prot_printf(imapd_out, "\"SUBSCRIBED\""); prot_printf(imapd_out, "))"); } @@ -12351,7 +12396,7 @@ "%s NO [COMPRESSIONACTIVE] %s active via TLS\r\n", tag, SSL_COMP_get_name(imapd_tls_comp)); } -#endif defined(HAVE_SSL) && (OPENSSL_VERSION_NUMBER >= 0x0090800fL) +#endif // defined(HAVE_SSL) && (OPENSSL_VERSION_NUMBER >= 0x0090800fL) else if (strcasecmp(alg, "DEFLATE")) { prot_printf(imapd_out, "%s NO Unknown COMPRESS algorithm: %s\r\n", tag, alg);
View file
cyrus-imapd-2.5.tar.gz/imap/imapd.h
Changed
@@ -138,7 +138,7 @@ int silent; int seen; /* for STORE_*_FLAGS */ - bit32 system_flags; + uint32_t system_flags; /* Note that we must pass the user flags as names because the * lookup of user flag names must proceed under the index lock */ strarray_t flags;
View file
cyrus-imapd-2.5.tar.gz/imap/index.c
Changed
@@ -121,8 +121,8 @@ unsigned size, const strarray_t *headers, const strarray_t *headers_not); -static void index_fetchcacheheader(struct index_state *state, struct index_record *record, - const strarray_t *headers, unsigned start_octet, +static void index_fetchcacheheader(struct index_state *state, struct index_record *record, + const strarray_t *headers, unsigned start_octet, unsigned octet_count); static void index_listflags(struct index_state *state); static void index_fetchflags(struct index_state *state, uint32_t msgno); @@ -143,8 +143,9 @@ static int index_copysetup(struct index_state *state, uint32_t msgno, struct copyargs *copyargs); static int index_storeflag(struct index_state *state, - struct index_modified_flags *modified_flags, - uint32_t msgno, struct storeargs *storeargs); + struct index_modified_flags *modified_flags, + uint32_t msgno, struct index_record *record, + struct storeargs *storeargs); static int index_store_annotation(struct index_state *state, uint32_t msgno, struct storeargs *storeargs); static int index_fetchreply(struct index_state *state, uint32_t msgno, @@ -322,7 +323,6 @@ state->internalseen = mailbox_internal_seen(state->mailbox, state->userid); - state->keepingseen = (state->myrights & ACL_SETSEEN); } else { r = mailbox_open_iwl(name, &state->mailbox); @@ -360,6 +360,7 @@ struct index_record record; int numexpunged = 0; struct mboxevent *mboxevent = NULL; + modseq_t oldmodseq; r = index_lock(state); if (r) return r; @@ -389,6 +390,8 @@ if (index_reload_record(state, msgno, &record)) continue; + oldmodseq = im->modseq; + if (!im->isseen) { state->numunseen--; im->isseen = 1; @@ -406,6 +409,10 @@ r = index_rewrite_record(state, msgno, &record); if (r) break; + /* avoid telling again (equivalent to STORE FLAGS.SILENT) */ + if (im->told_modseq == oldmodseq) + im->told_modseq = im->modseq; + mboxevent_extract_record(mboxevent, state->mailbox, &record); } @@ -473,6 +480,7 @@ struct seendata oldsd = SEENDATA_INITIALIZER; struct seendata sd = SEENDATA_INITIALIZER; struct mailbox *mailbox = state->mailbox; + const char *userid = (mailbox->i.options & OPT_IMAP_SHAREDSEEN) ? "anyone" : state->userid; if (!state->seen_dirty) return 0; @@ -492,7 +500,7 @@ return 0; } - r = seen_open(state->userid, SEEN_CREATE, &seendb); + r = seen_open(userid, SEEN_CREATE, &seendb); if (r) return r; r = seen_lockread(seendb, mailbox->uniqueid, &oldsd); @@ -544,9 +552,10 @@ else if (state->userid) { struct seen *seendb = NULL; struct seendata sd = SEENDATA_INITIALIZER; + const char *userid = (mailbox->i.options & OPT_IMAP_SHAREDSEEN) ? "anyone" : state->userid; int r; - r = seen_open(state->userid, SEEN_CREATE, &seendb); + r = seen_open(userid, SEEN_CREATE, &seendb); if (!r) r = seen_read(seendb, mailbox->uniqueid, &sd); seen_close(&seendb); @@ -556,7 +565,7 @@ prot_printf(state->out, "* OK (seen state failure) %s: %s\r\n", error_message(IMAP_NO_CHECKPRESERVE), error_message(r)); syslog(LOG_ERR, "Could not open seen state for %s (%s)", - state->userid, error_message(r)); + userid, error_message(r)); } else { *recentuid = sd.lastuid; @@ -1095,7 +1104,7 @@ seq = _parse_sequence(state, sequence, usinguid); /* set the \Seen flag if necessary - while we still have the lock */ - if (fetchargs->fetchitems & FETCH_SETSEEN && !state->examining) { + if (fetchargs->fetchitems & FETCH_SETSEEN && !state->examining && state->myrights & ACL_SETSEEN) { mboxevent = mboxevent_new(EVENT_MESSAGE_READ); for (msgno = 1; msgno <= state->exists; msgno++) { @@ -1218,7 +1227,7 @@ case STORE_ADD_FLAGS: case STORE_REMOVE_FLAGS: case STORE_REPLACE_FLAGS: - r = index_storeflag(state, &modified_flags, msgno, storeargs); + r = index_storeflag(state, &modified_flags, msgno, &record, storeargs); if (r) break; @@ -1619,8 +1628,12 @@ { int items = STATUS_MESSAGES | STATUS_UIDNEXT | STATUS_UIDVALIDITY | STATUS_HIGHESTMODSEQ | STATUS_RECENT | STATUS_UNSEEN; + + index_refresh(state); + statuscache_fill(sdata, state->userid, state->mailbox, items, state->numrecent, state->numunseen); + return 0; } @@ -1841,7 +1854,8 @@ int nolink, struct namespace *namespace, int isadmin, - int ismove) + int ismove, + int ignorequota) { static struct copyargs copyargs; int i; @@ -1892,7 +1906,7 @@ r = append_setup_mbox(&appendstate, destmailbox, state->userid, state->authstate, ACL_INSERT, - qptr, namespace, isadmin, + ignorequota ? NULL : qptr, namespace, isadmin, ismove ? EVENT_MESSAGE_MOVE : EVENT_MESSAGE_COPY); if (r) goto done; @@ -2475,6 +2489,12 @@ p = buf; while (*p && *p != '\r') { colon = strchr(p, ':'); + /* + * If there is no colon in remaining buffer, + * there is no valid header, leave loop + */ + if (!colon) break; + if (colon && headers_not && headers_not->count) { goodheader = 1; for (l = headers_not->data ; *l ; l++) { @@ -3363,24 +3383,21 @@ * Helper function to perform a STORE command for flags. */ static int index_storeflag(struct index_state *state, - struct index_modified_flags *modified_flags, - uint32_t msgno, struct storeargs *storeargs) + struct index_modified_flags *modified_flags, + uint32_t msgno, struct index_record *record, + struct storeargs *storeargs) { - bit32 old, new; + uint32_t old, new, keep; unsigned i; int dirty = 0; modseq_t oldmodseq; struct index_map *im = &state->map[msgno-1]; - struct index_record record; int r; memset(modified_flags, 0, sizeof(struct index_modified_flags)); oldmodseq = im->modseq; - r = index_reload_record(state, msgno, &record); - if (r) return r; - /* Change \Seen flag. This gets done on the index first and will only be copied into the record later if internalseen is set */ if (state->myrights & ACL_SETSEEN) { @@ -3399,8 +3416,9 @@ } } - old = record.system_flags; - new = storeargs->system_flags; + keep = record->system_flags & FLAGS_INTERNAL; + old = record->system_flags & FLAGS_SYSTEM; + new = storeargs->system_flags & FLAGS_SYSTEM; /* all other updates happen directly to the record */ if (storeargs->operation == STORE_REPLACE_FLAGS) { @@ -3408,55 +3426,55 @@ /* ACL_DELETE handled in index_store() */ if ((old & FLAG_DELETED) != (new & FLAG_DELETED)) { dirty++; - record.system_flags = (old & ~FLAG_DELETED) | (new & FLAG_DELETED); + record->system_flags = (old & ~FLAG_DELETED) | (new & FLAG_DELETED); } } else { if (!(state->myrights & ACL_DELETEMSG)) { if ((old & ~FLAG_DELETED) != (new & ~FLAG_DELETED)) { dirty++; - record.system_flags = (old & FLAG_DELETED) | (new & ~FLAG_DELETED); + record->system_flags = (old & FLAG_DELETED) | (new & ~FLAG_DELETED); } } else { if (old != new) { dirty++; - record.system_flags = new; + record->system_flags = new; } } for (i = 0; i < (MAX_USER_FLAGS/32); i++) { - if (record.user_flags[i] != storeargs->user_flags[i]) { - bit32 changed; + if (record->user_flags[i] != storeargs->user_flags[i]) { + uint32_t changed; dirty++; - changed = ~record.user_flags[i] & storeargs->user_flags[i]; + changed = ~record->user_flags[i] & storeargs->user_flags[i]; if (changed) { modified_flags->added_user_flags[i] = changed; modified_flags->added_flags++; } - changed = record.user_flags[i] & ~storeargs->user_flags[i]; + changed = record->user_flags[i] & ~storeargs->user_flags[i]; if (changed) { modified_flags->removed_user_flags[i] = changed; modified_flags->removed_flags++; } - record.user_flags[i] = storeargs->user_flags[i]; + record->user_flags[i] = storeargs->user_flags[i]; } } } } else if (storeargs->operation == STORE_ADD_FLAGS) { - bit32 added; + uint32_t added; if (~old & new) { dirty++; - record.system_flags = old | new; + record->system_flags = old | new; } for (i = 0; i < (MAX_USER_FLAGS/32); i++) { - added = ~record.user_flags[i] & storeargs->user_flags[i]; + added = ~record->user_flags[i] & storeargs->user_flags[i]; if (added) { dirty++; - record.user_flags[i] |= storeargs->user_flags[i]; + record->user_flags[i] |= storeargs->user_flags[i]; modified_flags->added_user_flags[i] = added; modified_flags->added_flags++; @@ -3464,17 +3482,17 @@ } } else { /* STORE_REMOVE_FLAGS */ - bit32 removed; + uint32_t removed; if (old & new) { dirty++; - record.system_flags &= ~storeargs->system_flags; + record->system_flags &= ~storeargs->system_flags; } for (i = 0; i < (MAX_USER_FLAGS/32); i++) { - removed = record.user_flags[i] & storeargs->user_flags[i]; + removed = record->user_flags[i] & storeargs->user_flags[i]; if (removed) { dirty++; - record.user_flags[i] &= ~storeargs->user_flags[i]; + record->user_flags[i] &= ~storeargs->user_flags[i]; modified_flags->removed_user_flags[i] = removed; modified_flags->removed_flags++; @@ -3500,19 +3518,21 @@ if (state->internalseen) { /* copy the seen flag from the index */ if (im->isseen) - record.system_flags |= FLAG_SEEN; + record->system_flags |= FLAG_SEEN; else - record.system_flags &= ~FLAG_SEEN; + record->system_flags &= ~FLAG_SEEN; } + /* add back the internal tracking flags */ + record->system_flags |= keep; - modified_flags->added_system_flags = ~old & record.system_flags; + modified_flags->added_system_flags = ~old & record->system_flags & FLAGS_SYSTEM; if (modified_flags->added_system_flags) modified_flags->added_flags++; - modified_flags->removed_system_flags = old & ~record.system_flags; + modified_flags->removed_system_flags = old & ~record->system_flags & FLAGS_SYSTEM; if (modified_flags->removed_system_flags) modified_flags->removed_flags++; - r = index_rewrite_record(state, msgno, &record); + r = index_rewrite_record(state, msgno, record); if (r) return r; /* if it's silent and unchanged, update the seen value, but @@ -4219,6 +4239,9 @@ cur->uid = record.uid; + /* useful for convupdates */ + cur->modseq = record.modseq; + did_cache = did_env = did_conv = 0; tmpenv = NULL; @@ -4270,7 +4293,7 @@ cur->from = get_localpart_addr(cacheitem_base(&record, CACHE_FROM)); break; case SORT_MODSEQ: - cur->modseq = record.modseq; + /* already copied above */ break; case SORT_SIZE: cur->size = record.size;
View file
cyrus-imapd-2.5.tar.gz/imap/index.h
Changed
@@ -110,7 +110,6 @@ int internalseen; int skipped_expunge; int seen_dirty; - int keepingseen; int examining; int myrights; unsigned numrecent; @@ -216,14 +215,15 @@ extern int index_scan(struct index_state *state, const char *contents); extern int index_copy(struct index_state *state, - char *sequence, + char *sequence, int usinguid, - char *name, + char *name, char **copyuidp, int nolink, struct namespace *namespace, int isadmin, - int ismove); + int ismove, + int ignorequota); extern int find_thread_algorithm(char *arg); extern int index_open(const char *name, struct index_init *init,
View file
cyrus-imapd-2.5.tar.gz/imap/jcal.c
Added
@@ -0,0 +1,876 @@ +/* jcal.c -- Routines for converting iCalendar to/from jCal + * + * Copyright (c) 1994-2013 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 WITH_JSON + +#include <stdio.h> /* for snprintf() */ +#include <stddef.h> /* for offsetof() macro */ +#include <syslog.h> + +#include "httpd.h" +#include "jcal.h" +#include "xcal.h" +#include "tok.h" +#include "util.h" +#include "version.h" +#include "xstrlcat.h" + + +/* + * Construct a JSON string for an iCalendar Period. + */ +static char *icalperiodtype_as_json_string(struct icalperiodtype p) +{ + static char str[42]; + const char *start; + const char *end; + + start = icaltime_as_iso_string(p.start); + snprintf(str, sizeof(str), "%s/", start); + + if (!icaltime_is_null_time(p.end)) + end = icaltime_as_iso_string(p.end); + else + end = icaldurationtype_as_ical_string(p.duration); + + strlcat(str, end, sizeof(str)); + + return str; +} + + +/* + * Add an iCalendar recur-rule-part to a JSON recur object. + */ +static void icalrecur_add_obj_to_json_object(json_t *jrecur, const char *rpart, + json_t *obj) +{ + json_t *old_rpart = json_object_get(jrecur, rpart); + + if (old_rpart) { + /* Already have a value for this BY* rpart - needs to be an array */ + json_t *byarray; + + if (!json_is_array(old_rpart)) { + /* Create an array from existing value */ + byarray = json_array(); + json_array_append(byarray, old_rpart); + json_object_set_new(jrecur, rpart, byarray); + } + else byarray = old_rpart; + + /* Append value to array */ + json_array_append_new(byarray, obj); + } + else json_object_set_new(jrecur, rpart, obj); +} + +static void icalrecur_add_int_to_json_object(void *jrecur, const char *rpart, + int i) +{ + icalrecur_add_obj_to_json_object(jrecur, rpart, json_integer(i)); +} + +static void icalrecur_add_string_to_json_object(void *jrecur, const char *rpart, + const char *s) +{ + icalrecur_add_obj_to_json_object(jrecur, rpart, json_string(s)); +} + + +/* + * Construct a JSON "structured value" for an iCalendar REQUEST-STATUS. + */ +static json_t *icalreqstattype_as_json_array(struct icalreqstattype stat) +{ + json_t *jstat; + char code[22]; + + icalerror_check_arg_rz((stat.code != ICAL_UNKNOWN_STATUS),"Status"); + + if (!stat.desc) stat.desc = icalenum_reqstat_desc(stat.code); + + jstat = json_array(); + + snprintf(code, sizeof(code), "%u.%u", + icalenum_reqstat_major(stat.code), + icalenum_reqstat_minor(stat.code)); + + json_array_append_new(jstat, json_string(code)); + json_array_append_new(jstat, json_string(stat.desc)); + if (stat.debug) json_array_append_new(jstat, json_string(stat.debug)); + + return jstat; +} + + +/* + * Construct the proper JSON object for an iCalendar value. + */ +static json_t *icalvalue_as_json_object(const icalvalue *value) +{ + const char *str = NULL; + json_t *obj; + + switch (icalvalue_isa(value)) { + case ICAL_BOOLEAN_VALUE: + return (icalvalue_get_integer(value) ? json_true() : json_false()); + + case ICAL_DATE_VALUE: + str = icaltime_as_iso_string(icalvalue_get_date(value)); + break; + + case ICAL_DATETIME_VALUE: + str = icaltime_as_iso_string(icalvalue_get_datetime(value)); + break; + + case ICAL_DATETIMEPERIOD_VALUE: { + struct icaldatetimeperiodtype dtp = + icalvalue_get_datetimeperiod(value); + + if (!icaltime_is_null_time(dtp.time)) + str = icaltime_as_iso_string(dtp.time); + else + str = icalperiodtype_as_json_string(dtp.period); + break; + } + + case ICAL_FLOAT_VALUE: + return json_real(icalvalue_get_float(value)); + + case ICAL_GEO_VALUE: { + struct icalgeotype geo = icalvalue_get_geo(value); + + obj = json_array(); + json_array_append_new(obj, json_real(geo.lat)); + json_array_append_new(obj, json_real(geo.lon)); + return obj; + } + + case ICAL_INTEGER_VALUE: + return json_integer(icalvalue_get_integer(value)); + + case ICAL_PERIOD_VALUE: + str = icalperiodtype_as_json_string(icalvalue_get_period(value)); + break; + + case ICAL_RECUR_VALUE: { + struct icalrecurrencetype recur = icalvalue_get_recur(value); + + obj = json_object(); + icalrecurrencetype_add_as_xxx(&recur, obj, + &icalrecur_add_int_to_json_object, + &icalrecur_add_string_to_json_object); + return obj; + } + + case ICAL_REQUESTSTATUS_VALUE: + return + icalreqstattype_as_json_array(icalvalue_get_requeststatus(value)); + + case ICAL_TRIGGER_VALUE: { + struct icaltriggertype trig = icalvalue_get_trigger(value); + + if (!icaltime_is_null_time(trig.time)) + str = icaltime_as_iso_string(trig.time); + else + str = icaldurationtype_as_ical_string(trig.duration); + break; + } + + case ICAL_UTCOFFSET_VALUE: + str = icalvalue_utcoffset_as_iso_string(value); + break; + + default: + str = icalvalue_as_ical_string(value); + break; + } + + return (str ? json_string(str) : NULL); +} + + +/* + * Add an iCalendar parameter to an existing JSON object. + */ +static void icalparameter_as_json_object_member(icalparameter *param, + json_t *jparams) +{ + icalparameter_kind kind; + const char *kind_string, *value_string; + + kind = icalparameter_isa(param); + + switch (kind) { + case ICAL_X_PARAMETER: + kind_string = icalparameter_get_xname(param); + break; + + case ICAL_IANA_PARAMETER: + kind_string = icalparameter_get_iana_name(param); + break; + + default: + kind_string = icalparameter_kind_to_string(kind); + if (kind_string) break; + + case ICAL_NO_PARAMETER: + case ICAL_ANY_PARAMETER: + icalerror_set_errno(ICAL_BADARG_ERROR); + return; + } + + /* XXX Need to handle multi-valued parameters */ + value_string = icalparameter_get_xvalue(param); + if (!value_string) { + icalparameter_value value = icalparameter_get_value(param); + + if (value) value_string = icalparameter_enum_to_string(value); + } + if (!value_string) return; + + json_object_set_new(jparams, lcase(icalmemory_tmp_copy(kind_string)), + json_string(value_string)); +} + + +/* + * Construct a JSON array for an iCalendar property. + */ +static json_t *icalproperty_as_json_array(icalproperty *prop) +{ + icalproperty_kind prop_kind; + const char *x_name, *property_name = NULL; + icalparameter *param; + const char *type = NULL; + const icalvalue *value; + json_t *jprop, *jparams; + + if (!prop) return NULL; + + prop_kind = icalproperty_isa(prop); + x_name = icalproperty_get_x_name(prop); + + if (prop_kind == ICAL_X_PROPERTY && x_name) + property_name = x_name; + else + property_name = icalproperty_kind_to_string(prop_kind); + + if (!property_name) { + icalerror_warn("Got a property of an unknown kind."); + return NULL; + } + + /* Create property array */ + jprop = json_array(); + + + /* Add property name */ + json_array_append_new(jprop, + json_string(lcase(icalmemory_tmp_copy(property_name)))); + + + /* Add parameters */ + jparams = json_object(); + for (param = icalproperty_get_first_parameter(prop, ICAL_ANY_PARAMETER); + param != 0; + param = icalproperty_get_next_parameter(prop, ICAL_ANY_PARAMETER)) { + + if (icalparameter_isa(param) == ICAL_VALUE_PARAMETER) continue; + + icalparameter_as_json_object_member(param, jparams); + } + json_array_append_new(jprop, jparams); + + + /* Add type */ + type = icalproperty_value_kind_as_string(prop); + json_array_append_new(jprop, json_string(lcase(icalmemory_tmp_copy(type)))); + + + /* Add value */ + value = icalproperty_get_value(prop); + if (value) { + switch (icalproperty_isa(prop)) { + case ICAL_CATEGORIES_PROPERTY: + case ICAL_RESOURCES_PROPERTY: + case ICAL_POLLPROPERTIES_PROPERTY: + if (icalvalue_isa(value) == ICAL_TEXT_VALUE) { + /* Handle multi-valued properties */ + const char *str = icalvalue_as_ical_string(value); + tok_t tok; + + tok_init(&tok, str, ",", TOK_TRIMLEFT|TOK_TRIMRIGHT|TOK_EMPTY); + while ((str = tok_next(&tok))) { + if (*str) json_array_append_new(jprop, json_string(str)); + } + tok_fini(&tok); + break; + } + + default: + json_array_append_new(jprop, icalvalue_as_json_object(value)); + break; + } + } + + return jprop; +} + + +/* + * Construct a JSON array for an iCalendar component. + */ +static json_t *icalcomponent_as_json_array(icalcomponent *comp) +{ + icalcomponent *c; + icalproperty *p; + icalcomponent_kind kind; + const char* kind_string; + json_t *jcomp, *jprops, *jsubs; + + if (!comp) return NULL; + + kind = icalcomponent_isa(comp); + switch (kind) { + case ICAL_NO_COMPONENT: + return NULL; + break; + + case ICAL_X_COMPONENT: + kind_string = ""; //comp->x_name; + break; + + default: + kind_string = icalcomponent_kind_to_string(kind); + } + + + /* Create component array */ + jcomp = json_array(); + + + /* Add component name */ + json_array_append_new(jcomp, + json_string(lcase(icalmemory_tmp_copy(kind_string)))); + + + /* Add properties */ + jprops = json_array(); + for (p = icalcomponent_get_first_property(comp, ICAL_ANY_PROPERTY); + p; + p = icalcomponent_get_next_property(comp, ICAL_ANY_PROPERTY)) { + + json_array_append_new(jprops, icalproperty_as_json_array(p)); + } + json_array_append_new(jcomp, jprops); + + + /* Add sub-components */ + jsubs = json_array(); + for (c = icalcomponent_get_first_component(comp, ICAL_ANY_COMPONENT); + c; + c = icalcomponent_get_next_component(comp, ICAL_ANY_COMPONENT)) { + + json_array_append_new(jsubs, icalcomponent_as_json_array(c)); + } + json_array_append_new(jcomp, jsubs); + + return jcomp; +} + + +/* + * Construct a jCal string for an iCalendar component. + */ +char *icalcomponent_as_jcal_string(icalcomponent *ical) +{ + json_t *jcal; + size_t flags = JSON_PRESERVE_ORDER; + char *buf; + + if (!ical) return NULL; + + jcal = icalcomponent_as_json_array(ical); + + flags |= (config_httpprettytelemetry ? JSON_INDENT(2) : JSON_COMPACT); + buf = json_dumps(jcal, flags); + + json_decref(jcal); + + return buf; +} + + +struct icalrecur_parser { + const char* rule; + char* copy; + char* this_clause; + char* next_clause; + + struct icalrecurrencetype rt; +}; + +extern icalrecurrencetype_frequency icalrecur_string_to_freq(const char* str); +extern void icalrecur_add_byrules(struct icalrecur_parser *parser, short *array, + int size, char* vals); +extern void icalrecur_add_bydayrules(struct icalrecur_parser *parser, + const char* vals); + +static const char *_json_x_value(json_t *jvalue) +{ + static char buf[21]; + + if (json_is_integer(jvalue)) { + snprintf(buf, sizeof(buf), "%" JSON_INTEGER_FORMAT, + json_integer_value(jvalue)); + return buf; + } + else return json_string_value(jvalue); +} + +static const char *json_x_value(json_t *jvalue) +{ + static struct buf buf = BUF_INITIALIZER; + + if (json_is_array(jvalue)) { + size_t i, n = json_array_size(jvalue); + const char *sep = ""; + + buf_reset(&buf); + for (i = 0; i < n; i++) { + buf_printf(&buf, "%s%s", + sep, _json_x_value(json_array_get(jvalue, i))); + sep = ","; + } + return buf_cstring(&buf); + } + else return _json_x_value(jvalue); +} + + +/* + * Construct an iCalendar property value from a JSON object. + */ +static icalvalue *json_object_to_icalvalue(json_t *jvalue, + icalvalue_kind kind) +{ + icalvalue *value = NULL; + int len, i; + + switch (kind) { + case ICAL_BOOLEAN_VALUE: + if (json_is_boolean(jvalue)) + value = icalvalue_new_integer(json_is_true(jvalue)); + else + syslog(LOG_WARNING, "jCal boolean object expected"); + break; + + case ICAL_FLOAT_VALUE: + if (json_is_real(jvalue)) + value = icalvalue_new_float((float) json_real_value(jvalue)); + else + syslog(LOG_WARNING, "jCal double object expected"); + break; + + case ICAL_GEO_VALUE: + /* MUST be an array of 2 doubles */ + if (json_is_array(jvalue) && (len = json_array_size(jvalue)) != 2) { + + for (i = 0; + i < len && json_is_real(json_array_get(jvalue, i)); + i++); + if (i == len) { + struct icalgeotype geo; + + geo.lat = + json_real_value(json_array_get(jvalue, 0)); + geo.lon = + json_real_value(json_array_get(jvalue, 1)); + + value = icalvalue_new_geo(geo); + } + } + if (!value) + syslog(LOG_WARNING, "jCal array object of 2 doubles expected"); + break; + + case ICAL_INTEGER_VALUE: + if (json_is_integer(jvalue)) + value = icalvalue_new_integer((int) json_integer_value(jvalue)); + else if (json_is_string(jvalue)) + value = icalvalue_new_integer(atoi(json_string_value(jvalue))); + else + syslog(LOG_WARNING, "jCal integer object expected"); + break; + + case ICAL_RECUR_VALUE: + if (json_is_object(jvalue)) { + struct icalrecurrencetype *rt = NULL; + const char *key; + json_t *val; + + json_object_foreach(jvalue, key, val) { + rt = icalrecur_add_rule(&rt, key, val, + (int (*)(void *)) &json_integer_value, + (const char * (*)(void *)) &json_x_value); + if (!rt) break; + } + + if (rt && rt->freq != ICAL_NO_RECURRENCE) + value = icalvalue_new_recur(*rt); + } + else + syslog(LOG_WARNING, "jCal object object expected"); + break; + + case ICAL_REQUESTSTATUS_VALUE: + /* MUST be an array of 2-3 strings */ + if (json_is_array(jvalue) && + ((len = json_array_size(jvalue)) == 2 || len == 3)) { + + for (i = 0; + i < len && json_is_string(json_array_get(jvalue, i)); + i++); + if (i == len) { + struct icalreqstattype rst = + { ICAL_UNKNOWN_STATUS, NULL, NULL }; + short maj, min; + + if (sscanf(json_string_value(json_array_get(jvalue, 0)), + "%hd.%hd", &maj, &min) == 2) { + rst.code = icalenum_num_to_reqstat(maj, min); + } + if (rst.code == ICAL_UNKNOWN_STATUS) { + syslog(LOG_WARNING, "Unknown request-status code"); + break; + } + + rst.desc = + json_string_value(json_array_get(jvalue, 1)); + rst.debug = (len < 3) ? NULL : + json_string_value(json_array_get(jvalue, 2)); + + value = icalvalue_new_requeststatus(rst); + } + } + if (!value) + syslog(LOG_WARNING, "jCal array object of 2-3 strings expected"); + break; + + case ICAL_UTCOFFSET_VALUE: + if (json_is_string(jvalue)) { + int utcoffset, hours, minutes, seconds = 0; + char sign; + + if (sscanf(json_string_value(jvalue), "%c%02d:%02d:%02d", + &sign, &hours, &minutes, &seconds) < 3) { + syslog(LOG_WARNING, "Unexpected utc-offset format"); + break; + } + + utcoffset = hours*3600 + minutes*60 + seconds; + + if (sign == '-') utcoffset = -utcoffset; + + value = icalvalue_new_utcoffset(utcoffset); + } + else + syslog(LOG_WARNING, "jCal string object expected"); + break; + + default: + if (json_is_string(jvalue)) + value = icalvalue_new_from_string(kind, + json_string_value(jvalue)); + else + syslog(LOG_WARNING, "jCal string object expected"); + break; + } + + return value; +} + + +/* + * Construct an iCalendar property from a JSON array. + */ +static icalproperty *json_array_to_icalproperty(json_t *jprop) +{ + json_t *jtype, *jparams, *jvaltype, *jvalue; + const char *propname, *typestr, *key; + icalproperty_kind kind; + icalproperty *prop = NULL; + icalvalue_kind valkind; + icalvalue *value; + int len; + + /* Sanity check the types of the jCal property object */ + if (!json_is_array(jprop) || (len = json_array_size(jprop)) < 4) { + syslog(LOG_WARNING, + "jCal component object is not an array of 4+ objects"); + return NULL; + } + + jtype = json_array_get(jprop, 0); + jparams = json_array_get(jprop, 1); + jvaltype = json_array_get(jprop, 2); + + if (!json_is_string(jtype) || + !json_is_object(jparams) || !json_is_string(jvaltype)) { + syslog(LOG_WARNING, "jCal property array contains incorrect objects"); + return NULL; + } + + /* Get the property type */ + propname = ucase(icalmemory_tmp_copy(json_string_value(jtype))); + kind = icalenum_string_to_property_kind(propname); + if (kind == ICAL_NO_PROPERTY) { + syslog(LOG_WARNING, "Unknown jCal property type: %s", propname); + return NULL; + } + + /* Get the value type */ + typestr = json_string_value(jvaltype); + valkind = !strcmp(typestr, "unknown") ? ICAL_X_VALUE : + icalenum_string_to_value_kind(ucase(icalmemory_tmp_copy(typestr))); + if (valkind == ICAL_NO_VALUE) { + syslog(LOG_WARNING, "Unknown jCal value type for %s property: %s", + propname, typestr); + return NULL; + } + else if (valkind == ICAL_TEXT_VALUE) { + /* "text" also includes enumerated types - grab type from property */ + valkind = icalproperty_kind_to_value_kind(kind); + } + + /* Create new property */ + prop = icalproperty_new(kind); + if (!prop) { + syslog(LOG_ERR, "Creation of new %s property failed", propname); + return NULL; + } + if (kind == ICAL_X_PROPERTY) icalproperty_set_x_name(prop, propname); + + /* Add parameters */ + json_object_foreach(jparams, key, jvalue) { + /* XXX Need to handle multi-valued parameters */ + icalproperty_set_parameter_from_string(prop, + ucase(icalmemory_tmp_copy(key)), + json_string_value(jvalue)); + } + + /* Add value */ + jvalue = json_array_get(jprop, 3); + switch (kind) { + case ICAL_CATEGORIES_PROPERTY: + case ICAL_RESOURCES_PROPERTY: + case ICAL_POLLPROPERTIES_PROPERTY: + if (json_is_string(jvalue) && len > 4) { + /* Handle multi-valued properties */ + struct buf buf = BUF_INITIALIZER; + int i; + + buf_setcstr(&buf, json_string_value(jvalue)); + for (i = 4; i < len; i++) { + buf_putc(&buf, ','); + jvalue = json_array_get(jprop, i); + buf_appendcstr(&buf, json_string_value(jvalue)); + } + value = icalvalue_new_from_string(valkind, buf_cstring(&buf)); + buf_free(&buf); + break; + } + + default: + value = json_object_to_icalvalue(jvalue, valkind); + if (!value) { + syslog(LOG_ERR, "Creation of new %s property value failed", + propname); + goto error; + } + break; + } + + icalproperty_set_value(prop, value); + + return prop; + + error: + icalproperty_free(prop); + return NULL; +} + + +/* + * Construct an iCalendar component from a JSON object. + */ +static icalcomponent *json_object_to_icalcomponent(json_t *jobj) +{ + json_t *jtype, *jprops, *jsubs; + const char *type; + icalcomponent_kind kind; + icalcomponent *comp = NULL; + size_t i; + + /* Sanity check the types of the jCal component object */ + if (!json_is_array(jobj) || json_array_size(jobj) != 3) { + syslog(LOG_WARNING, + "jCal component object is not an array of 3 objects"); + return NULL; + } + + jtype = json_array_get(jobj, 0); + jprops = json_array_get(jobj, 1); + jsubs = json_array_get(jobj, 2); + + if (!json_is_string(jtype) || + !json_is_array(jprops) || !json_is_array(jsubs)) { + syslog(LOG_WARNING, "jCal component array contains incorrect objects"); + return NULL; + } + + type = json_string_value(jtype); + kind = icalenum_string_to_component_kind(ucase(icalmemory_tmp_copy(type))); + if (kind == ICAL_NO_COMPONENT) { + syslog(LOG_WARNING, "Unknown jCal component type: %s", type); + return NULL; + } + + /* Create new component */ + comp = icalcomponent_new(kind); + if (!comp) { + syslog(LOG_ERR, "Creation of new %s component failed", type); + return NULL; + } + + /* Add properties */ + for (i = 0; i < json_array_size(jprops); i++) { + icalproperty *prop = + json_array_to_icalproperty(json_array_get(jprops, i)); + + if (!prop) goto error; + + icalcomponent_add_property(comp, prop); + } + + /* Add sub-components */ + for (i = 0; i < json_array_size(jsubs); i++) { + icalcomponent *sub = + json_object_to_icalcomponent(json_array_get(jsubs, i)); + + if (!sub) goto error; + + icalcomponent_add_component(comp, sub); + } + + return comp; + + error: + icalcomponent_free(comp); + return NULL; +} + + +/* + * Construct an iCalendar component from a jCal string. + */ +EXPORTED icalcomponent *jcal_string_as_icalcomponent(const char *str) +{ + json_t *jcal; + json_error_t jerr; + icalcomponent *ical; + + if (!str) return NULL; + + jcal = json_loads(str, 0, &jerr); + if (!jcal) { + syslog(LOG_WARNING, "json parse error: '%s'", jerr.text); + return NULL; + } + + ical = json_object_to_icalcomponent(jcal); + + json_decref(jcal); + + return ical; +} + + +EXPORTED const char *begin_jcal(struct buf *buf) +{ + /* Begin jCal stream */ + buf_reset(buf); + buf_printf_markup(buf, 0, "["); + buf_printf_markup(buf, 1, "\"vcalendar\","); + buf_printf_markup(buf, 1, "["); + buf_printf_markup(buf, 2, "["); + buf_printf_markup(buf, 3, "\"prodid\","); + buf_printf_markup(buf, 3, "{},"); + buf_printf_markup(buf, 3, "\"text\","); + buf_printf_markup(buf, 3, "\"-//CyrusIMAP.org/Cyrus %s//EN\"", + cyrus_version()); + buf_printf_markup(buf, 2, "],"); + buf_printf_markup(buf, 2, "["); + buf_printf_markup(buf, 3, "\"version\","); + buf_printf_markup(buf, 3, "{},"); + buf_printf_markup(buf, 3, "\"text\","); + buf_printf_markup(buf, 3, "\"2.0\""); + buf_printf_markup(buf, 2, "]"); + buf_printf_markup(buf, 1, "],"); + buf_printf_markup(buf, 0, "["); + + return ","; +} + + +EXPORTED void end_jcal(struct buf *buf) +{ + /* End jCal stream */ + buf_setcstr(buf, "]]"); +} + +#endif /* WITH_JSON */
View file
cyrus-imapd-2.5.tar.gz/imap/jcal.h
Added
@@ -0,0 +1,58 @@ +/* jcal.h -- Routines for converting iCalendar to/from jCal + * + * Copyright (c) 1994-2013 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 WITH_JSON + +#include <libical/ical.h> +#include <jansson.h> + +#include "util.h" + +extern char *icalcomponent_as_jcal_string(icalcomponent* comp); +extern icalcomponent *jcal_string_as_icalcomponent(const char *str); +extern const char *begin_jcal(struct buf *buf); +extern void end_jcal(struct buf *buf); + +#endif /* WITH_JSON */
View file
cyrus-imapd-2.5.tar.gz/imap/lmtpd.c
Changed
@@ -71,6 +71,10 @@ #include "autocreate.h" #endif #include "backend.h" +#ifdef WITH_DAV +#include "caldav_db.h" +#include "carddav_db.h" +#endif #include "duplicate.h" #include "exitcodes.h" #include "global.h" @@ -89,6 +93,7 @@ #include "statuscache.h" #include "telemetry.h" #include "tls.h" +#include "userdeny.h" #include "util.h" #include "version.h" #include "xmalloc.h" @@ -233,6 +238,16 @@ quotadb_init(0); quotadb_open(NULL); + /* open the user deny db */ + denydb_init(0); + denydb_open(0); + +#ifdef WITH_DAV + /* so we can do DAV opterations */ + caldav_init(); + carddav_init(); +#endif + /* Initialize the annotatemore db (for sieve on shared mailboxes) */ annotate_init(NULL, NULL); annotatemore_open(); @@ -411,7 +426,7 @@ return 0; } -int fuzzy_match(char *mboxname) +static int fuzzy_match(char *mboxname) { char name[MAX_MAILBOX_BUFFER], prefix[MAX_MAILBOX_BUFFER], *p = NULL; size_t prefixlen; @@ -1022,6 +1037,14 @@ quotadb_close(); quotadb_done(); + denydb_close(); + denydb_done(); + +#ifdef WITH_DAV + carddav_done(); + caldav_done(); +#endif + annotatemore_close(); annotate_done(); @@ -1180,6 +1203,42 @@ mboxlist_entry_free(&mbentry); } + if (!r) { + char msg[MAX_MAILBOX_PATH+1]; + + if (domain) { + snprintf(namebuf, sizeof(namebuf), "%s@%s", user, domain); + user = namebuf; + } + + if (userdeny(user, config_ident, msg, sizeof(msg))) + return IMAP_MAILBOX_DISABLED; + } + + if (!r) { + char msg[MAX_MAILBOX_PATH+1]; + + if (domain) { + snprintf(namebuf, sizeof(namebuf), "%s@%s", user, domain); + user = namebuf; + } + + if (userdeny(user, config_ident, msg, sizeof(msg))) + return IMAP_MAILBOX_DISABLED; + } + + if (!r) { + char msg[MAX_MAILBOX_PATH+1]; + + if (domain) { + snprintf(namebuf, sizeof(namebuf), "%s@%s", user, domain); + user = namebuf; + } + + if (userdeny(user, config_ident, msg, sizeof(msg))) + return IMAP_MAILBOX_DISABLED; + } + if (r) syslog(LOG_DEBUG, "verify_user(%s) failed: %s", namebuf, error_message(r));
View file
cyrus-imapd-2.5.tar.gz/imap/lmtpd.h
Changed
@@ -89,6 +89,4 @@ int quotaoverride, int acloverride); -int fuzzy_match(char *mboxname); - #endif /* LMTPD_H */
View file
cyrus-imapd-2.5.tar.gz/imap/lmtpengine.c
Changed
@@ -217,6 +217,11 @@ prot_printf(pout, "451 4.2.1 Mailbox Reserved\r\n"); break; + case IMAP_MAILBOX_DISABLED: + prot_printf(pout, + "450 4.2.1 Mailbox disabled, not accepting messages\r\n"); + break; + case IMAP_MESSAGE_CONTAINSNULL: prot_printf(pout, "554 5.6.0 Message contains NUL characters\r\n"); break;
View file
cyrus-imapd-2.5.tar.gz/imap/mailbox.c
Changed
@@ -51,6 +51,7 @@ #elif defined(HAVE_STDINT_H) # include <stdint.h> #endif +#include <libical/vcc.h> #include <stdio.h> #include <stdlib.h> #include <string.h> @@ -80,6 +81,10 @@ #include "annotate.h" #include "assert.h" +#ifdef WITH_DAV +#include "caldav_db.h" +#include "carddav_db.h" +#endif /* WITH_DAV */ #include "crc32.h" #include "md5.h" #include "exitcodes.h" @@ -95,7 +100,6 @@ #include "proc.h" #include "retry.h" #include "seen.h" -#include "upgrade_index.h" #include "util.h" #include "sequence.h" #include "statuscache.h" @@ -119,9 +123,10 @@ (m).header_fd = -1; } static int mailbox_index_unlink(struct mailbox *mailbox); -static int mailbox_index_repack(struct mailbox *mailbox); +static int mailbox_index_repack(struct mailbox *mailbox, int version); static int mailbox_lock_index_internal(struct mailbox *mailbox, int locktype); +static void cleanup_stale_expunged(struct mailbox *mailbox); static struct mailboxlist *create_listitem(const char *name) { @@ -372,18 +377,18 @@ return BIT32_MAX; } -const char *cache_base(struct index_record *record) +static const char *cache_base(struct index_record *record) { const char *base = record->crec.base->s; return base + record->crec.offset; } -size_t cache_len(struct index_record *record) +static size_t cache_len(struct index_record *record) { return record->crec.len; } -struct buf *cache_buf(struct index_record *record) +static struct buf *cache_buf(struct index_record *record) { static struct buf staticbuf; @@ -636,14 +641,18 @@ /* try to parse the cache record */ r = cache_parserecord(&mailbox->cache_buf, record->cache_offset, &record->crec); - if (r) goto done; + + /* old-style record */ + if (!record->cache_crc) + goto done; + crc = crc32_buf(cache_buf(record)); if (crc != record->cache_crc) r = IMAP_MAILBOX_CHECKSUM; done: - if (r) + if (r) syslog(LOG_ERR, "IOERROR: invalid cache record for %s uid %u (%s)", mailbox->name, record->uid, error_message(r)); @@ -664,7 +673,7 @@ if (record->cache_offset) return 0; - if (record->cache_crc != crc32_buf(cache_buf(record))) + if (record->cache_crc && record->cache_crc != crc32_buf(cache_buf(record))) return IMAP_MAILBOX_CHECKSUM; offset = lseek(fd, 0L, SEEK_END); @@ -786,6 +795,17 @@ return 0; } +EXPORTED int mailbox_map_record(struct mailbox *mailbox, struct index_record *record, struct buf *buf) +{ + const char *data; + size_t len; + int r = mailbox_map_message(mailbox, record->uid, &data, &len); + if (r) return r; + + buf_init_mmap(buf, data, len); + return 0; +} + /* * Releases the buffer obtained from mailbox_map_message() */ @@ -952,6 +972,10 @@ if (mailbox->i.options & OPT_MAILBOX_DELETED) r = IMAP_MAILBOX_NONEXISTENT; + /* we always nuke expunged if the version is less than 12 */ + if (mailbox->i.minor_version < 12) + cleanup_stale_expunged(mailbox); + done: if (r) mailbox_close(&mailbox); else *mailboxptr = mailbox; @@ -996,6 +1020,32 @@ mailbox_index_dirty(mailbox); } +EXPORTED int mailbox_setversion(struct mailbox *mailbox, int version) +{ + int r = 0; + + if (version && mailbox->i.minor_version != version) { + /* need to re-set the version! */ + struct mailboxlist *listitem = find_listitem(mailbox->name); + int r; + assert(listitem); + + /* release any existing locks */ + mailbox_unlock_index(mailbox, NULL); + + r = mailbox_mboxlock_reopen(listitem, LOCK_NONBLOCKING); + /* we need to re-open the index because we dropped the mboxname lock, + * so the file may have changed */ + if (!r) r = mailbox_open_index(mailbox); + /* lock_internal so DELETED doesn't cause it to appear to be + * NONEXISTENT */ + if (!r) r = mailbox_lock_index_internal(mailbox, LOCK_EXCLUSIVE); + if (!r) r = mailbox_index_repack(mailbox, version); + } + + return r; +} + /* * Close the mailbox 'mailbox', freeing all associated resources. */ @@ -1004,7 +1054,6 @@ int flag; struct mailbox *mailbox = *mailboxptr; struct mailboxlist *listitem; - int expunge_days = config_getint(IMAPOPT_EXPUNGE_DAYS); /* be safe against double-close */ if (!mailbox) return; @@ -1021,20 +1070,6 @@ return; } - /* auto-cleanup */ - if (mailbox->i.first_expunged && - (mailbox->index_locktype == LOCK_EXCLUSIVE)) { - time_t floor = time(NULL) - (expunge_days * 86400); - /* but only if we're more than a full week older than - * the expunge time, * so it doesn't turn into lots - * of bitty rewrites. - * Also, cyr_expire can get first bite if it's been set - * to run... */ - if (mailbox->i.first_expunged < floor - (8 * 86400)) { - mailbox_expunge_cleanup(mailbox, floor, NULL); - /* XXX - handle error code? */ - } - } /* get a re-read of the options field for cleanup purposes */ if (mailbox->index_fd != -1) { if (!mailbox->index_locktype) @@ -1059,7 +1094,7 @@ if (mailbox->i.options & OPT_MAILBOX_DELETED) mailbox_delete_cleanup(mailbox->part, mailbox->name); else if (mailbox->i.options & OPT_MAILBOX_NEEDS_REPACK) - mailbox_index_repack(mailbox); + mailbox_index_repack(mailbox, mailbox->i.minor_version); else if (mailbox->i.options & OPT_MAILBOX_NEEDS_UNLINK) mailbox_index_unlink(mailbox); /* or we missed out - someone else beat us to it */ @@ -1355,16 +1390,42 @@ return ((record->user_flags[userflag/32] & (1<<(userflag&31))) ? 1 : 0); } -HIDDEN int mailbox_buf_to_index_header(const char *buf, struct index_header *i) +static int mailbox_buf_to_index_header(const char *buf, size_t len, + struct index_header *i) { uint32_t crc; bit32 qannot; + size_t minlen; - i->dirty = 0; + if (len < OFFSET_MINOR_VERSION+4) + return IMAP_MAILBOX_BADFORMAT; + + memset(i, 0, sizeof(struct index_header)); i->generation_no = ntohl(*((bit32 *)(buf+OFFSET_GENERATION_NO))); i->format = ntohl(*((bit32 *)(buf+OFFSET_FORMAT))); i->minor_version = ntohl(*((bit32 *)(buf+OFFSET_MINOR_VERSION))); + switch (i->minor_version) { + case 6: + case 7: + minlen = 76; + break; + case 8: + minlen = 92; + break; + case 9: + case 10: + minlen = 96; + break; + case 12: + case 13: + minlen = 128; + break; + default: + return IMAP_MAILBOX_BADFORMAT; + } + if (len < minlen) + return IMAP_MAILBOX_BADFORMAT; i->start_offset = ntohl(*((bit32 *)(buf+OFFSET_START_OFFSET))); i->record_size = ntohl(*((bit32 *)(buf+OFFSET_RECORD_SIZE))); i->num_records = ntohl(*((bit32 *)(buf+OFFSET_NUM_RECORDS))); @@ -1378,7 +1439,9 @@ i->flagged = ntohl(*((bit32 *)(buf+OFFSET_FLAGGED))); i->options = ntohl(*((bit32 *)(buf+OFFSET_MAILBOX_OPTIONS))); i->leaked_cache_records = ntohl(*((bit32 *)(buf+OFFSET_LEAKED_CACHE))); + if (i->minor_version < 8) goto done; i->highestmodseq = align_ntohll(buf+OFFSET_HIGHESTMODSEQ); + if (i->minor_version < 12) goto done; i->deletedmodseq = align_ntohll(buf+OFFSET_DELETEDMODSEQ); i->exists = ntohl(*((bit32 *)(buf+OFFSET_EXISTS))); i->first_expunged = ntohl(*((bit32 *)(buf+OFFSET_FIRST_EXPUNGED))); @@ -1387,20 +1450,31 @@ i->sync_crc = ntohl(*((bit32 *)(buf+OFFSET_SYNC_CRC))); i->recentuid = ntohl(*((bit32 *)(buf+OFFSET_RECENTUID))); i->recenttime = ntohl(*((bit32 *)(buf+OFFSET_RECENTTIME))); - i->header_crc = ntohl(*((bit32 *)(buf+OFFSET_HEADER_CRC))); - i->pop3_show_after = ntohl(*((bit32 *)(buf+OFFSET_POP3_SHOW_AFTER))); - qannot = ntohl(*((bit32 *)(buf+OFFSET_QUOTA_ANNOT_USED))); - /* this field is stored as a 32b unsigned on disk but 64b signed - * in memory, so we need to be careful about sign extension */ - i->quota_annot_used = (quota_t)((unsigned long long)qannot); - i->sync_crc_vers = ntohl(*((bit32 *)(buf+OFFSET_SYNC_CRC_VERS))); + if (i->minor_version > 12) { + i->pop3_show_after = ntohl(*((bit32 *)(buf+OFFSET_POP3_SHOW_AFTER))); + qannot = ntohl(*((bit32 *)(buf+OFFSET_QUOTA_ANNOT_USED))); + /* this field is stored as a 32b unsigned on disk but 64b signed + * in memory, so we need to be careful about sign extension */ + i->quota_annot_used = (quota_t)((unsigned long long)qannot); + i->sync_crc_vers = ntohl(*((bit32 *)(buf+OFFSET_SYNC_CRC_VERS))); + } + + crc = ntohl(*((bit32 *)(buf+OFFSET_HEADER_CRC))); + if (crc != crc32_map(buf, OFFSET_HEADER_CRC)) + return IMAP_MAILBOX_CHECKSUM; + +done: if (!i->exists) i->options |= OPT_POP3_NEW_UIDL; - crc = crc32_map(buf, OFFSET_HEADER_CRC); - if (crc != i->header_crc) - return IMAP_MAILBOX_CHECKSUM; + if (!i->highestmodseq) + i->highestmodseq = 1; + + if (i->minor_version < 12) { + i->deletedmodseq = i->highestmodseq; + i->exists = i->num_records; + } return 0; } @@ -1447,19 +1521,18 @@ if (!mailbox_index_islocked(mailbox, 0)) return IMAP_MAILBOX_LOCKED; - /* and of course it needs to exist and have at least a full - * sized header */ + /* and of course it needs to exist and have at least enough + * header to read the version number */ if (!mailbox->index_base) return IMAP_MAILBOX_BADFORMAT; - if (mailbox->index_size < INDEX_HEADER_SIZE) - return IMAP_MAILBOX_BADFORMAT; /* need to make sure we're reading fresh data! */ map_refresh(mailbox->index_fd, 1, &mailbox->index_base, &mailbox->index_len, mailbox->index_size, "index", mailbox->name); - r = mailbox_buf_to_index_header(mailbox->index_base, &mailbox->i); + r = mailbox_buf_to_index_header(mailbox->index_base, mailbox->index_len, + &mailbox->i); if (r) return r; r = mailbox_refresh_index_map(mailbox); @@ -1471,8 +1544,9 @@ /* * Read an index record from a mapped index file */ -HIDDEN int mailbox_buf_to_index_record(const char *buf, - struct index_record *record) +static int mailbox_buf_to_index_record(const char *buf, + int version, + struct index_record *record) { uint32_t crc; int n; @@ -1480,7 +1554,7 @@ /* tracking fields - initialise */ memset(record, 0, sizeof(struct index_record)); - /* parse buffer in to structure */ + /* parse the shared bits first */ record->uid = ntohl(*((bit32 *)(buf+OFFSET_UID))); record->internaldate = ntohl(*((bit32 *)(buf+OFFSET_INTERNALDATE))); record->sentdate = ntohl(*((bit32 *)(buf+OFFSET_SENTDATE))); @@ -1495,15 +1569,37 @@ } record->content_lines = ntohl(*((bit32 *)(buf+OFFSET_CONTENT_LINES))); record->cache_version = ntohl(*((bit32 *)(buf+OFFSET_CACHE_VERSION))); + + if (version < 8) + return 0; + + if (version < 10) { + /* modseq was at 72 before the GUID move */ + record->modseq = ntohll(*((bit64 *)(buf+72))); + return 0; + } + message_guid_import(&record->guid, (unsigned char *)buf+OFFSET_MESSAGE_GUID); record->modseq = ntohll(*((bit64 *)(buf+OFFSET_MODSEQ))); + if (version < 12) + return 0; + + /* CID got inserted before cache_crc32 in version 12 */ + if (version == 12) { + record->cache_crc = ntohl(*((bit32 *)(buf+88))); + + crc = crc32_map(buf, 92); + if (crc != ntohl(*((bit32 *)(buf+92)))) + return IMAP_MAILBOX_CHECKSUM; + return 0; + } + record->cid = ntohll(*(bit64 *)(buf+OFFSET_CID)); record->cache_crc = ntohl(*((bit32 *)(buf+OFFSET_CACHE_CRC))); - record->record_crc = ntohl(*((bit32 *)(buf+OFFSET_RECORD_CRC))); /* check CRC32 */ crc = crc32_map(buf, OFFSET_RECORD_CRC); - if (crc != record->record_crc) + if (crc != ntohl(*((bit32 *)(buf+OFFSET_RECORD_CRC)))) return IMAP_MAILBOX_CHECKSUM; return 0; @@ -1531,7 +1627,7 @@ buf = mailbox->index_base + offset; - r = mailbox_buf_to_index_record(buf, record); + r = mailbox_buf_to_index_record(buf, mailbox->i.minor_version, record); if (!r) record->recno = recno; @@ -1541,16 +1637,13 @@ /* * bsearch() function to compare two index record buffers by UID */ -static int rec_compar(const void *key, const void *mem) +static int rec_compar(const void *a, const void *b) { - uint32_t uid = *((uint32_t *) key); - struct index_record record; - int r; - - if ((r = mailbox_buf_to_index_record(mem, &record))) return r; + uint32_t uida = *((uint32_t *) a); + uint32_t uidb = *((uint32_t *) b); - if (uid < record.uid) return -1; - return (uid > record.uid); + if (uida < uidb) return -1; + return (uida > uidb); } /* @@ -1564,10 +1657,11 @@ size_t size = mailbox->i.record_size; int r; - mem = bsearch(&uid, base, num_records, size, rec_compar); + mem = bsearch(&uid, base, num_records, size, rec_compar); if (!mem) return CYRUSDB_NOTFOUND; - if ((r = mailbox_buf_to_index_record(mem, record))) return r; + if ((r = mailbox_buf_to_index_record(mem, mailbox->i.minor_version, record))) + return r; record->recno = ((mem - base) / size) + 1; @@ -1587,8 +1681,6 @@ assert(mailbox->index_fd != -1); assert(!mailbox->index_locktype); -restart: - if (locktype == LOCK_EXCLUSIVE) { /* handle read-only case cleanly - we need to re-open read-write first! */ if (mailbox->is_readonly) { @@ -1643,52 +1735,6 @@ } } - /* make sure the mailbox is up to date if we haven't - * already had a successful load */ - if (!mailbox->i.minor_version) { - bit32 minor_version = ntohl(*((bit32 *)(mailbox->index_base+OFFSET_MINOR_VERSION))); - if (minor_version != MAILBOX_MINOR_VERSION) { - struct mailboxlist *listitem = find_listitem(mailbox->name); - int prev_locktype; - - assert(listitem); - - prev_locktype = listitem->l->locktype; - - /* we need to switch to an exclusive lock while upgrading */ - r = mailbox_mboxlock_reopen(listitem, LOCK_EXCLUSIVE); - if (r) return r; - r = mailbox_open_index(mailbox); - if (r) return r; - - /* lie about our index lock status - the exclusive namelock - * provides equivalent properties - and we know it won't - * leak because the 'restart' above will cover up our sins */ - mailbox->index_locktype = LOCK_EXCLUSIVE; - r = upgrade_index(mailbox); - if (r) return r; - - /* recalculate all counts */ - r = mailbox_open_index(mailbox); - if (r) return r; - r = mailbox_read_index_header(mailbox); - if (r) return r; - r = mailbox_index_recalc(mailbox); - if (r) return r; - r = mailbox_commit(mailbox); - if (r) return r; - - /* we have to downgrade again afterwards so a "SELECT" won't - * hold an exclusive lock forever */ - r = mailbox_mboxlock_reopen(listitem, prev_locktype); - if (r) return r; - r = mailbox_open_index(mailbox); - if (r) return r; - - goto restart; - } - } - /* note: it's guaranteed by our outer cyrus.lock lock that the * cyrus.index and cyrus.cache files are never rewritten, so * we're safe to just extend the map if needed */ @@ -1701,7 +1747,8 @@ } /* check the CRC */ - if (mailbox->header_file_crc != mailbox->i.header_file_crc) { + if (mailbox->header_file_crc && mailbox->i.header_file_crc && + mailbox->header_file_crc != mailbox->i.header_file_crc) { syslog(LOG_ERR, "IOERROR: header CRC mismatch %s: %08X %08X", mailbox->name, (unsigned int)mailbox->header_file_crc, (unsigned int)mailbox->i.header_file_crc); @@ -1752,6 +1799,7 @@ if (updatenotifier) updatenotifier(mailbox->name); sync_log_mailbox(mailbox->name); statuscache_invalidate(mailbox->name, sdata); + mailbox->has_changed = 0; } else if (sdata) { @@ -1865,12 +1913,24 @@ bit32 crc; bit32 options = i->options & MAILBOX_OPT_VALID; + memset(buf, 0, INDEX_HEADER_SIZE); /* buffer is always this big, and aligned */ + + assert (i->minor_version >= 6); + *((bit32 *)(buf+OFFSET_GENERATION_NO)) = htonl(i->generation_no); *((bit32 *)(buf+OFFSET_FORMAT)) = htonl(i->format); *((bit32 *)(buf+OFFSET_MINOR_VERSION)) = htonl(i->minor_version); *((bit32 *)(buf+OFFSET_START_OFFSET)) = htonl(i->start_offset); *((bit32 *)(buf+OFFSET_RECORD_SIZE)) = htonl(i->record_size); - *((bit32 *)(buf+OFFSET_NUM_RECORDS)) = htonl(i->num_records); + if (i->minor_version >= 12) { + *((bit32 *)(buf+OFFSET_NUM_RECORDS)) = htonl(i->num_records); + } + else { + /* this was moved to make upgrades clean, because num_records was + * the same as exists back then, we didn't keep expunged in the + * record */ + *((bit32 *)(buf+OFFSET_NUM_RECORDS)) = htonl(i->exists); + } *((bit32 *)(buf+OFFSET_LAST_APPENDDATE)) = htonl(i->last_appenddate); *((bit32 *)(buf+OFFSET_LAST_UID)) = htonl(i->last_uid); @@ -1882,9 +1942,23 @@ *((bit32 *)(buf+OFFSET_DELETED)) = htonl(i->deleted); *((bit32 *)(buf+OFFSET_ANSWERED)) = htonl(i->answered); *((bit32 *)(buf+OFFSET_FLAGGED)) = htonl(i->flagged); + if (i->minor_version < 8) { + /* this was called OFFSET_POP3_NEW_UIDL and was only zero or one */ + *((bit32 *)(buf+OFFSET_MAILBOX_OPTIONS)) = htonl(options&1); + return 0; /* no CRC32 support */ + } + + /* otherwise we have options and modseqs */ *((bit32 *)(buf+OFFSET_MAILBOX_OPTIONS)) = htonl(options); *((bit32 *)(buf+OFFSET_LEAKED_CACHE)) = htonl(i->leaked_cache_records); align_htonll(buf+OFFSET_HIGHESTMODSEQ, i->highestmodseq); + + /* and that's where it stopped until version 2.4.0 with index version 12 (ignoring + * version 11, which doesn't exist in the wild */ + if (i->minor_version < 12) { + return 0; + } + align_htonll(buf+OFFSET_DELETEDMODSEQ, i->deletedmodseq); *((bit32 *)(buf+OFFSET_EXISTS)) = htonl(i->exists); *((bit32 *)(buf+OFFSET_FIRST_EXPUNGED)) = htonl(i->first_expunged); @@ -1893,12 +1967,17 @@ *((bit32 *)(buf+OFFSET_SYNC_CRC)) = htonl(i->sync_crc); *((bit32 *)(buf+OFFSET_RECENTUID)) = htonl(i->recentuid); *((bit32 *)(buf+OFFSET_RECENTTIME)) = htonl(i->recenttime); - *((bit32 *)(buf+OFFSET_POP3_SHOW_AFTER)) = htonl(i->pop3_show_after); - /* this field is 64b in memory but 32b on disk - as it counts - * bytes stored in dbs and the dbs are 32b anyway there should - * be no problem */ - *((bit32 *)(buf+OFFSET_QUOTA_ANNOT_USED)) = htonl((bit32)i->quota_annot_used); - *((bit32 *)(buf+OFFSET_SYNC_CRC_VERS)) = htonl(i->sync_crc_vers); + if (i->minor_version > 12) { + /* these were added in version 13, but replaced zero-byte fields in + * in version 12, so if we don't write them then the CRC will still + * be correct for version 12, since the header size didn't change */ + *((bit32 *)(buf+OFFSET_POP3_SHOW_AFTER)) = htonl(i->pop3_show_after); + /* this field is 64b in memory but 32b on disk - as it counts + * bytes stored in dbs and the dbs are 32b anyway there should + * be no problem */ + *((bit32 *)(buf+OFFSET_QUOTA_ANNOT_USED)) = htonl((bit32)i->quota_annot_used); + *((bit32 *)(buf+OFFSET_SYNC_CRC_VERS)) = htonl(i->sync_crc_vers); + } /* Update checksum */ crc = htonl(crc32_map((char *)buf, OFFSET_HEADER_CRC)); @@ -1969,19 +2048,22 @@ assert(mailbox_index_islocked(mailbox, 1)); - if (mailbox->i.start_offset < INDEX_HEADER_SIZE) - fatal("Mailbox offset bug", EC_SOFTWARE); - mailbox_index_header_to_buf(&mailbox->i, buf); lseek(mailbox->index_fd, 0, SEEK_SET); - n = retry_write(mailbox->index_fd, buf, INDEX_HEADER_SIZE); - if ((unsigned long)n != INDEX_HEADER_SIZE || fsync(mailbox->index_fd)) { + n = retry_write(mailbox->index_fd, buf, mailbox->i.start_offset); + if (n < 0 || fsync(mailbox->index_fd)) { syslog(LOG_ERR, "IOERROR: writing index header for %s: %m", mailbox->name); return IMAP_IOERROR; } + if (config_auditlog && mailbox->modseq_dirty) + syslog(LOG_NOTICE, "auditlog: modseq sessionid=<%s> " + "mailbox=<%s> uniqueid=<%s> highestmodseq=<" MODSEQ_FMT ">", + session_id(), mailbox->name, mailbox->uniqueid, + mailbox->i.highestmodseq); + /* remove all dirty flags! */ mailbox->i.dirty = 0; mailbox->modseq_dirty = 0; @@ -1996,18 +2078,26 @@ /* * Put an index record into a buffer suitable for writing to a file. */ -static bit32 mailbox_index_record_to_buf(struct index_record *record, +static bit32 mailbox_index_record_to_buf(struct index_record *record, int version, unsigned char *buf) { int n; bit32 crc; + memset(buf, 0, INDEX_RECORD_SIZE); + *((bit32 *)(buf+OFFSET_UID)) = htonl(record->uid); *((bit32 *)(buf+OFFSET_INTERNALDATE)) = htonl(record->internaldate); *((bit32 *)(buf+OFFSET_SENTDATE)) = htonl(record->sentdate); *((bit32 *)(buf+OFFSET_SIZE)) = htonl(record->size); *((bit32 *)(buf+OFFSET_HEADER_SIZE)) = htonl(record->header_size); - *((bit32 *)(buf+OFFSET_GMTIME)) = htonl(record->gmtime); + if (version >= 12) { + *((bit32 *)(buf+OFFSET_GMTIME)) = htonl(record->gmtime); + } + else { + /* content_offset was always the same */ + *((bit32 *)(buf+OFFSET_GMTIME)) = htonl(record->header_size); + } *((bit32 *)(buf+OFFSET_CACHE_OFFSET)) = htonl(record->cache_offset); *((bit32 *)(buf+OFFSET_LAST_UPDATED)) = htonl(record->last_updated); *((bit32 *)(buf+OFFSET_SYSTEM_FLAGS)) = htonl(record->system_flags); @@ -2016,10 +2106,34 @@ } *((bit32 *)(buf+OFFSET_CONTENT_LINES)) = htonl(record->content_lines); *((bit32 *)(buf+OFFSET_CACHE_VERSION)) = htonl(record->cache_version); + + /* versions less than 8 had no modseq */ + if (version < 8) { + return 0; + } + + /* versions 8 and 9 only had a smaller UUID, which we will ignore, + * but the modseq existed and was at offset 72 and 76 */ + if (version < 10) { + *((bit32 *)(buf+72)) = htonl(record->modseq); + return 0; + } + + /* otherwise we have the GUID and MODSEQ in their current place */ message_guid_export(&record->guid, buf+OFFSET_MESSAGE_GUID); *((bit64 *)(buf+OFFSET_MODSEQ)) = htonll(record->modseq); + + /* version 12 added the CACHE_CRC and RECORD_CRC, but at a lower point */ + if (version < 13) { + *((bit32 *)(buf+88)) = htonl(record->cache_crc); + /* calculate the checksum */ + crc = crc32_map((char *)buf, 92); + *((bit32 *)(buf+92)) = htonl(crc); + return crc; + } + *((bit64 *)(buf+OFFSET_CID)) = htonll(record->cid); - *((bit32 *)(buf+OFFSET_CACHE_CRC)) = htonl(record->cache_crc); + *((bit32 *)(buf+OFFSET_CACHE_CRC)) = htonl(record->cache_crc); /* calculate the checksum */ crc = crc32_map((char *)buf, OFFSET_RECORD_CRC); @@ -2247,7 +2361,8 @@ MD5Update(&ctx, " ", 1); MD5Update(&ctx, entry, strlen(entry)); MD5Update(&ctx, " ", 1); - MD5Update(&ctx, userid, strlen(userid)); + if (userid) + MD5Update(&ctx, userid, strlen(userid)); MD5Update(&ctx, " ", 1); MD5Update(&ctx, value->s, value->len); @@ -2502,6 +2617,217 @@ return r; } +#ifdef WITH_DAV +static int mailbox_update_carddav(struct mailbox *mailbox, + struct index_record *old, + struct index_record *new) +{ + const char *userid = mboxname_to_userid(mailbox->name); + struct carddav_db *carddavdb = NULL; + struct param *param; + struct body *body = NULL; + struct carddav_data *cdata = NULL; + const char *resource = NULL; + int r = 0; + + /* conditions in which there's nothing to do */ + if (!new) goto done; + if (!userid) goto done; + + /* phantom record - never really existed here */ + if (!old && (new->system_flags & FLAG_EXPUNGED)) + goto done; + + r = mailbox_cacherecord(mailbox, new); + if (r) goto done; + + /* Get resource URL from filename param in Content-Disposition header */ + message_read_bodystructure(new, &body); + for (param = body->disposition_params; param; param = param->next) { + if (!strcmp(param->attribute, "FILENAME")) { + resource = param->value; + } + } + + assert(resource); + + carddavdb = carddav_open_mailbox(mailbox, 0); + + /* Find existing record for this resource */ + carddav_lookup_resource(carddavdb, mailbox->name, resource, 1, &cdata); + + /* XXX - if not matching by UID, skip - this record doesn't refer to the current item */ + + if (new->system_flags & FLAG_EXPUNGED) { + /* is there an existing record? */ + if (!cdata) goto done; + + /* does it still come from this UID? */ + if (cdata->dav.imap_uid != new->uid) goto done; + + /* delete entry */ + r = carddav_delete(carddavdb, cdata->dav.rowid, 0); + } + else { + const char *uid = NULL, *fullname = NULL, *nickname = NULL; + struct buf msg_buf = BUF_INITIALIZER; + VObjectIterator i; + VObject *vcard; + + /* already seen this message, so do we update it? No */ + if (old) goto done; + + /* Load message containing the resource and parse vcard data */ + r = mailbox_map_record(mailbox, new, &msg_buf); + if (r) goto done; + + vcard = Parse_MIME(buf_cstring(&msg_buf) + new->header_size, + new->size - new->header_size); + buf_free(&msg_buf); + if (!vcard) goto done; + + initPropIterator(&i, vcard); + while (moreIteration(&i)) { + VObject *prop = nextVObject(&i); + const char *name = vObjectName(prop); + + if (!strcmp(name, "UID")) { + uid = fakeCString(vObjectUStringZValue(prop)); + } + else if (!strcmp(name, "FN")) { + fullname = fakeCString(vObjectUStringZValue(prop)); + } + if (!strcmp(name, "NICKNAME")) { + nickname = fakeCString(vObjectUStringZValue(prop)); + } + } + + /* Create mapping entry from resource name to UID */ + cdata->dav.mailbox = mailbox->name; + cdata->dav.resource = resource; + cdata->dav.imap_uid = new->uid; + cdata->vcard_uid = uid; + cdata->fullname = fullname; + cdata->nickname = nickname; + + if (!cdata->dav.creationdate) + cdata->dav.creationdate = new->internaldate; + + r = carddav_write(carddavdb, cdata, 0); + } + +done: + if (carddavdb) { + carddav_commit(carddavdb); + carddav_close(carddavdb); + } + + return r; +} + +static int mailbox_update_caldav(struct mailbox *mailbox, + struct index_record *old, + struct index_record *new) +{ + const char *userid = mboxname_to_userid(mailbox->name); + struct caldav_db *caldavdb = NULL; + struct param *param; + struct body *body = NULL; + struct caldav_data *cdata = NULL; + const char *resource = NULL; + const char *sched_tag = NULL; + int r = 0; + + /* conditions in which there's nothing to do */ + if (!new) goto done; + if (!userid) goto done; + + /* phantom record - never really existed here */ + if (!old && (new->system_flags & FLAG_EXPUNGED)) + goto done; + + r = mailbox_cacherecord(mailbox, new); + if (r) goto done; + + /* Get resource URL from filename param in Content-Disposition header */ + message_read_bodystructure(new, &body); + for (param = body->disposition_params; param; param = param->next) { + if (!strcmp(param->attribute, "FILENAME")) { + resource = param->value; + } + else if (!strcmp(param->attribute, "SCHEDULE-TAG")) { + sched_tag = param->value; + } + } + + caldavdb = caldav_open_mailbox(mailbox, 0); + + /* Find existing record for this resource */ + caldav_lookup_resource(caldavdb, mailbox->name, resource, 1, &cdata); + + /* XXX - if not matching by UID, skip - this record doesn't refer to the current item */ + + if (new->system_flags & FLAG_EXPUNGED) { + /* is there an existing record? */ + if (!cdata) goto done; + + /* does it still come from this UID? */ + if (cdata->dav.imap_uid != new->uid) goto done; + + /* delete entry */ + r = caldav_delete(caldavdb, cdata->dav.rowid, 0); + } + else { + struct buf msg_buf = BUF_INITIALIZER; + icalcomponent *ical = NULL; + + /* already seen this message, so do we update it? No */ + if (old) goto done; + + r = mailbox_map_record(mailbox, new, &msg_buf); + if (r) goto done; + + ical = icalparser_parse_string(buf_cstring(&msg_buf) + new->header_size); + buf_free(&msg_buf); + if (!ical) goto done; + + cdata->dav.creationdate = new->internaldate; + cdata->dav.mailbox = mailbox->name; + cdata->dav.imap_uid = new->uid; + cdata->dav.resource = resource; + cdata->sched_tag = sched_tag; + + caldav_make_entry(ical, cdata); + + r = caldav_write(caldavdb, cdata, 0); + + icalcomponent_free(ical); + } + +done: + message_free_body(body); + free(body); + + if (caldavdb) { + caldav_commit(caldavdb); + caldav_close(caldavdb); + } + + return r; +} + +static int mailbox_update_dav(struct mailbox *mailbox, + struct index_record *old, + struct index_record *new) +{ + if (mailbox->mbtype & MBTYPE_ADDRESSBOOK) + return mailbox_update_carddav(mailbox, old, new); + if (mailbox->mbtype & MBTYPE_CALENDAR) + return mailbox_update_caldav(mailbox, old, new); + return 0; +} +#endif // WITH_DAV + /* NOTE: maybe make this able to return error codes if we have * support for transactional mailbox updates later. For now, * we expect callers to have already done all sanity checking */ @@ -2509,6 +2835,14 @@ struct index_record *old, struct index_record *new) { +#ifdef WITH_DAV + int r = 0; + r = mailbox_update_dav(mailbox, old, new); + if (r) return r; +#endif + + /* NOTE - we do these last */ + if (old) mailbox_index_update_counts(mailbox, old, 0); if (new) @@ -2532,7 +2866,8 @@ size_t offset; int expunge_mode = config_getenum(IMAPOPT_EXPUNGE_MODE); int immediate = (expunge_mode == IMAP_ENUM_EXPUNGE_MODE_IMMEDIATE || - expunge_mode == IMAP_ENUM_EXPUNGE_MODE_DEFAULT); + expunge_mode == IMAP_ENUM_EXPUNGE_MODE_DEFAULT || + mailbox->i.minor_version < 12); assert(mailbox_index_islocked(mailbox, 1)); assert(record->recno > 0 && @@ -2578,7 +2913,8 @@ if (record->system_flags & FLAG_UNLINKED) { /* mark required actions */ - if (expunge_mode == IMAP_ENUM_EXPUNGE_MODE_IMMEDIATE) + if (expunge_mode == IMAP_ENUM_EXPUNGE_MODE_IMMEDIATE + || mailbox->i.minor_version < 12) mailbox->i.options |= OPT_MAILBOX_NEEDS_REPACK; mailbox->i.options |= OPT_MAILBOX_NEEDS_UNLINK; } @@ -2595,7 +2931,7 @@ r = mailbox_update_indexes(mailbox, &oldrecord, record); if (r) return r; - mailbox_index_record_to_buf(record, buf); + mailbox_index_record_to_buf(record, mailbox->i.minor_version, buf); offset = mailbox->i.start_offset + (record->recno-1) * mailbox->i.record_size; @@ -2607,8 +2943,8 @@ return IMAP_IOERROR; } - n = retry_write(mailbox->index_fd, buf, INDEX_RECORD_SIZE); - if (n != INDEX_RECORD_SIZE) { + n = retry_write(mailbox->index_fd, buf, mailbox->i.record_size); + if (n < 0) { syslog(LOG_ERR, "IOERROR: writing index record %u for %s: %m", record->recno, mailbox->name); return IMAP_IOERROR; @@ -2710,7 +3046,7 @@ r = mailbox_update_indexes(mailbox, NULL, record); if (r) return r; - mailbox_index_record_to_buf(record, buf); + mailbox_index_record_to_buf(record, mailbox->i.minor_version, buf); recno = mailbox->i.num_records + 1; @@ -2724,8 +3060,8 @@ return IMAP_IOERROR; } - n = retry_write(mailbox->index_fd, buf, INDEX_RECORD_SIZE); - if (n != INDEX_RECORD_SIZE) { + n = retry_write(mailbox->index_fd, buf, mailbox->i.record_size); + if (n < 0) { syslog(LOG_ERR, "IOERROR: appending index record for %s: %m", mailbox->name); return IMAP_IOERROR; @@ -2733,7 +3069,7 @@ mailbox->i.last_uid = record->uid; mailbox->i.num_records = recno; - mailbox->index_size += INDEX_RECORD_SIZE; + mailbox->index_size += mailbox->i.record_size; if (config_auditlog) syslog(LOG_NOTICE, "auditlog: append sessionid=<%s> mailbox=<%s> uniqueid=<%s> uid=<%u> guid=<%s>", @@ -2823,8 +3159,33 @@ return 0; } -HIDDEN int mailbox_repack_setup(struct mailbox *mailbox, - struct mailbox_repack **repackptr) +/* for repack */ +struct mailbox_repack { + struct mailbox *mailbox; + struct index_header i; + struct seqset *seqset; + const char *userid; + int old_version; + int newindex_fd; + int newcache_fd; +}; + +/* clean up memory structures and abort repack */ +static void mailbox_repack_abort(struct mailbox_repack **repackptr) +{ + struct mailbox_repack *repack = *repackptr; + if (!repack) return; /* safe against double-free */ + seqset_free(repack->seqset); + xclose(repack->newcache_fd); + unlink(mailbox_meta_newfname(repack->mailbox, META_CACHE)); + xclose(repack->newindex_fd); + unlink(mailbox_meta_newfname(repack->mailbox, META_INDEX)); + free(repack); + *repackptr = NULL; +} + +static int mailbox_repack_setup(struct mailbox *mailbox, int version, + struct mailbox_repack **repackptr) { struct mailbox_repack *repack = xzmalloc(sizeof(struct mailbox_repack)); const char *fname; @@ -2856,6 +3217,78 @@ /* update the generation number */ repack->i.generation_no++; + /* track the version number */ + repack->old_version = repack->i.minor_version; + repack->i.minor_version = version; + switch (version) { + case 6: + repack->i.start_offset = 76; + repack->i.record_size = 60; + break; + case 7: + repack->i.start_offset = 76; + repack->i.record_size = 72; + break; + case 8: + repack->i.start_offset = 92; + repack->i.record_size = 80; + break; + case 9: + repack->i.start_offset = 96; + repack->i.record_size = 80; + break; + case 10: + repack->i.start_offset = 96; + repack->i.record_size = 88; + break; + /* 11 was FastMail internal */ + case 12: + repack->i.start_offset = 128; + repack->i.record_size = 96; + break; + case 13: + repack->i.start_offset = 128; + repack->i.record_size = 104; + break; + default: + fatal("index version not supported", EC_SOFTWARE); + } + + /* upgrades or downgrades across version 12 boundary? Sort out seen state */ + if (version >= 12 && repack->old_version < 12) { + /* we need to read the current seen state for the owner */ + struct seendata sd = SEENDATA_INITIALIZER; + int r = IMAP_MAILBOX_NONEXISTENT; + if (mailbox->i.options & OPT_IMAP_SHAREDSEEN) + repack->userid = "anyone"; + else + repack->userid = mboxname_to_userid(mailbox->name); + + if (repack->userid) { + struct seen *seendb = NULL; + r = seen_open(repack->userid, SEEN_SILENT, &seendb); + if (!r) r = seen_read(seendb, mailbox->uniqueid, &sd); + seen_close(&seendb); + } + + if (!r) { + repack->i.recentuid = sd.lastuid; + repack->i.recenttime = sd.lastchange; + repack->seqset = seqset_parse(sd.seenuids, NULL, sd.lastuid); + seen_freedata(&sd); + } + } + else if (version < 12 && repack->old_version >= 12) { + if (mailbox->i.options & OPT_IMAP_SHAREDSEEN) + repack->userid = "anyone"; + else + repack->userid = mboxname_to_userid(mailbox->name); + + /* we need to create the seen state for the owner from the mailbox */ + if (repack->userid) + repack->seqset = seqset_init(mailbox->i.last_uid, SEQ_MERGE); + } + /* zero out some values */ repack->i.num_records = 0; repack->i.quota_mailbox_used = 0; @@ -2880,7 +3313,7 @@ n = retry_write(repack->newcache_fd, buf, 4); if (n == -1) goto fail; - n = retry_write(repack->newindex_fd, buf, INDEX_HEADER_SIZE); + n = retry_write(repack->newindex_fd, buf, repack->i.start_offset); if (n == -1) goto fail; *repackptr = repack; @@ -2891,8 +3324,8 @@ return IMAP_IOERROR; } -HIDDEN int mailbox_repack_add(struct mailbox_repack *repack, - struct index_record *record) +static int mailbox_repack_add(struct mailbox_repack *repack, + struct index_record *record) { int r; int n; @@ -2909,8 +3342,8 @@ header_update_counts(&repack->i, record, 1); /* write the index record out */ - mailbox_index_record_to_buf(record, buf); - n = retry_write(repack->newindex_fd, buf, INDEX_RECORD_SIZE); + mailbox_index_record_to_buf(record, repack->i.minor_version, buf); + n = retry_write(repack->newindex_fd, buf, repack->i.record_size); if (n == -1) return IMAP_IOERROR; @@ -2919,20 +3352,7 @@ return 0; } -/* clean up memory structures and abort repack */ -HIDDEN void mailbox_repack_abort(struct mailbox_repack **repackptr) -{ - struct mailbox_repack *repack = *repackptr; - if (!repack) return; /* safe against double-free */ - xclose(repack->newcache_fd); - unlink(mailbox_meta_newfname(repack->mailbox, META_CACHE)); - xclose(repack->newindex_fd); - unlink(mailbox_meta_newfname(repack->mailbox, META_INDEX)); - free(repack); - *repackptr = NULL; -} - -HIDDEN int mailbox_repack_commit(struct mailbox_repack **repackptr) +static int mailbox_repack_commit(struct mailbox_repack **repackptr) { indexbuffer_t ibuf; unsigned char *buf = ibuf.buf; @@ -2946,13 +3366,31 @@ assert(repack->i.sync_crc_vers == repack->mailbox->i.sync_crc_vers); assert(repack->i.sync_crc == repack->mailbox->i.sync_crc); + if (repack->old_version >= 12 && repack->i.minor_version < 12 + && repack->seqset && repack->userid) { + struct seendata sd = SEENDATA_INITIALIZER; + struct seen *seendb = NULL; + int r = seen_open(repack->userid, SEEN_CREATE, &seendb); + if (!r) r = seen_lockread(seendb, repack->mailbox->uniqueid, &sd); + if (!r) { + sd.lastuid = repack->i.last_uid; + sd.seenuids = seqset_cstring(repack->seqset); + sd.lastread = time(NULL); + sd.lastchange = repack->i.last_appenddate; + r = seen_write(seendb, repack->mailbox->uniqueid, &sd); + /* XXX - syslog on errors? */ + } + seen_close(&seendb); + seen_freedata(&sd); + } + /* rewrite the header with updated details */ mailbox_index_header_to_buf(&repack->i, buf); if (lseek(repack->newindex_fd, 0, SEEK_SET) < 0) goto fail; - if (retry_write(repack->newindex_fd, buf, INDEX_HEADER_SIZE) < 0) + if (retry_write(repack->newindex_fd, buf, repack->i.start_offset) < 0) goto fail; /* ensure everything is committed to disk */ @@ -2982,16 +3420,16 @@ } /* need a mailbox exclusive lock, we're rewriting files */ -static int mailbox_index_repack(struct mailbox *mailbox) +static int mailbox_index_repack(struct mailbox *mailbox, int version) { struct mailbox_repack *repack = NULL; uint32_t recno; struct index_record record; int r = IMAP_IOERROR; - syslog(LOG_INFO, "Repacking mailbox %s", mailbox->name); + syslog(LOG_INFO, "Repacking mailbox %s version %d", mailbox->name, version); - r = mailbox_repack_setup(mailbox, &repack); + r = mailbox_repack_setup(mailbox, version, &repack); if (r) goto fail; for (recno = 1; recno <= mailbox->i.num_records; recno++) { @@ -3001,6 +3439,30 @@ /* been marked for removal, just skip */ if (!record.uid) continue; + /* version changes? */ + if (repack->old_version < 12 && repack->i.minor_version >= 12 && repack->seqset) { + const char *fname = mailbox_message_fname(mailbox, record.uid); + + if (seqset_ismember(repack->seqset, record.uid)) + record.system_flags |= FLAG_SEEN; + else + record.system_flags &= ~FLAG_SEEN; + + /* XXX - re-parse the record iff upgrading past 12 */ + if (message_parse(fname, &record)) { + /* failed to parse, don't try to write out record */ + record.crec.len = 0; + /* and the record is expunged too! */ + record.system_flags |= FLAG_EXPUNGED | FLAG_UNLINKED; + syslog(LOG_ERR, "IOERROR: FATAL - failed to parse file for %s %u, expunging", + repack->mailbox->name, record.uid); + } + } + if (repack->old_version >= 12 && repack->i.minor_version < 12 && repack->seqset) { + seqset_add(repack->seqset, record.uid, record.system_flags & FLAG_SEEN ? 1 : 0); + record.system_flags &= ~FLAG_SEEN; + } + /* we aren't keeping unlinked files, that's kind of the point */ if (record.system_flags & FLAG_UNLINKED) { /* just in case it was left lying around */ @@ -3189,6 +3651,10 @@ EXPORTED int mailbox_internal_seen(struct mailbox *mailbox, const char *userid) { + /* old mailboxes don't have internal seen at all */ + if (mailbox->i.minor_version < 12) + return 0; + /* shared seen - everyone's state is internal */ if (mailbox->i.options & OPT_IMAP_SHAREDSEEN) return 1; @@ -3330,7 +3796,7 @@ r = IMAP_IOERROR; goto done; } - + if (hasquota) { mailbox_set_quotaroot(mailbox, quotaroot); memset(mailbox->quota_previously_used, 0, sizeof(mailbox->quota_previously_used)); @@ -3476,10 +3942,28 @@ return r; } -/* - * Delete and close the mailbox 'mailbox'. Closes 'mailbox' whether - * or not the deletion was successful. Requires a locked mailbox. - */ +#ifdef WITH_DAV +EXPORTED int mailbox_add_dav(struct mailbox *mailbox) +{ + struct index_record record; + uint32_t recno; + int r = 0; + + if (!(mailbox->mbtype & (MBTYPE_ADDRESSBOOK|MBTYPE_CALENDAR))) + return 0; + + for (recno = 1; recno <= mailbox->i.num_records; recno++) { + r = mailbox_read_index_record(mailbox, recno, &record); + if (r) return r; + + r = mailbox_update_dav(mailbox, NULL, &record); + if (r) return r; + } + + return 0; +} +#endif + EXPORTED int mailbox_delete(struct mailbox **mailboxptr) { int r = 0; @@ -3950,7 +4434,6 @@ bit32 eoffset, expungerecord_size; const char *bufp; struct stat sbuf; - int count = 0; int r; /* it's always read-writes */ @@ -3992,12 +4475,8 @@ uid = ntohl(*((bit32 *)(bufp+OFFSET_UID))); fname = mailbox_message_fname(mailbox, uid); unlink(fname); - count++; } - printf("%s removed %d records from stale cyrus.expunge\n", - mailbox->name, count); - fname = mailbox_meta_fname(mailbox, META_EXPUNGE); unlink(fname); @@ -4318,6 +4797,21 @@ } } + if (!record->size) { + /* dang, guess it failed to parse */ + + printf("%s uid %u failed to parse\n", mailbox->name, record->uid); + syslog(LOG_ERR, "%s uid %u failed to parse", mailbox->name, record->uid); + + if (!make_changes) return 0; + + /* otherwise we have issues, mark it unlinked */ + unlink(fname); + record->system_flags |= FLAG_EXPUNGED | FLAG_UNLINKED; + mailbox->i.options |= OPT_MAILBOX_NEEDS_REPACK; + return mailbox_rewrite_index_record(mailbox, record); + } + /* get internaldate from the file if not set */ if (!record->internaldate) { if (did_stat || stat(fname, &sbuf) != -1) @@ -4528,7 +5022,7 @@ mailbox->i.options |= OPT_MAILBOX_NEEDS_REPACK; mailbox_index_dirty(mailbox); - mailbox_index_record_to_buf(record, buf); + mailbox_index_record_to_buf(record, mailbox->i.minor_version, buf); offset = mailbox->i.start_offset + (record->recno-1) * mailbox->i.record_size; @@ -4540,8 +5034,8 @@ return IMAP_IOERROR; } - n = retry_write(mailbox->index_fd, buf, INDEX_RECORD_SIZE); - if (n != INDEX_RECORD_SIZE) { + n = retry_write(mailbox->index_fd, buf, mailbox->i.record_size); + if (n < 0) { syslog(LOG_ERR, "IOERROR: writing index record %u for %s: %m", record->recno, mailbox->name); return IMAP_IOERROR;
View file
cyrus-imapd-2.5.tar.gz/imap/mailbox.h
Changed
@@ -65,6 +65,12 @@ "\"The best thing about this system was that it had lots of goals.\"\n" \ "\t--Jim Morris on Andrew\n") + +/* NOTE: the mailbox minor version must be changed whenever any on-disk + * format changes are made to any mailbox files. It is also important to + * make sure all the mailbox upgrade and downgrade code in mailbox.c is + * changed to be able to convert both backwards and forwards between the + * new version and all supported previous versions */ #define MAILBOX_MINOR_VERSION 13 #define MAILBOX_CACHE_MINOR_VERSION 3 @@ -74,6 +80,7 @@ #define FNAME_SQUAT "/cyrus.squat" #define FNAME_EXPUNGE "/cyrus.expunge" #define FNAME_ANNOTATIONS "/cyrus.annotations" +#define FNAME_DAV "/cyrus.dav" enum meta_filename { META_HEADER = 1, @@ -81,7 +88,8 @@ META_CACHE, META_SQUAT, META_EXPUNGE, - META_ANNOTATIONS + META_ANNOTATIONS, + META_DAV }; #define MAILBOX_FNAME_LEN 256 @@ -134,7 +142,6 @@ modseq_t modseq; bit64 cid; bit32 cache_crc; - bit32 record_crc; /* metadata */ uint32_t recno; @@ -178,7 +185,6 @@ uint32_t recentuid; time_t recenttime; - uint32_t header_crc; time_t pop3_show_after; quota_t quota_annot_used; }; @@ -309,6 +315,12 @@ #define FLAG_UNLINKED (1<<30) #define FLAG_EXPUNGED (1U<<31) +#define FLAGS_SYSTEM (FLAG_ANSWERED|FLAG_FLAGGED|FLAG_DELETED|FLAG_DRAFT|FLAG_SEEN) +#define FLAGS_INTERNAL (FLAG_UNLINKED|FLAG_EXPUNGED) +/* for replication */ +#define FLAGS_LOCAL (FLAG_UNLINKED) +#define FLAGS_GLOBAL (FLAGS_SYSTEM|FLAG_EXPUNGED) + #define OPT_POP3_NEW_UIDL (1<<0) /* added for Outlook stupidity */ /* NOTE: not used anymore - but don't reuse it */ #define OPT_IMAP_CONDSTORE (1<<1) /* added for CONDSTORE extension */ @@ -422,6 +434,7 @@ /* map individual messages in */ extern int mailbox_map_message(struct mailbox *mailbox, unsigned long uid, const char **basep, size_t *lenp); +extern int mailbox_map_record(struct mailbox *mailbox, struct index_record *record, struct buf *buf); extern void mailbox_unmap_message(struct mailbox *mailbox, unsigned long uid, const char **basep, size_t *lenp); @@ -435,12 +448,11 @@ struct index_record *record); char *mailbox_cache_get_msgid(struct mailbox *mailbox, struct index_record *record); + +/* field-based lookup functions */ const char *cacheitem_base(struct index_record *record, int field); unsigned cacheitem_size(struct index_record *record, int field); struct buf *cacheitem_buf(struct index_record *record, int field); -const char *cache_base(struct index_record *record); -size_t cache_len(struct index_record *record); -struct buf *cache_buf(struct index_record *record); /* opening and closing */ extern int mailbox_open_iwl(const char *name, @@ -517,29 +529,12 @@ extern int mailbox_reconstruct(const char *name, int flags); extern void mailbox_make_uniqueid(struct mailbox *mailbox); +extern int mailbox_setversion(struct mailbox *mailbox, int version); /* for upgrade index */ -extern int mailbox_buf_to_index_record(const char *buf, - struct index_record *record); -extern int mailbox_buf_to_index_header(const char *buf, - struct index_header *i); - -/* for repack */ -struct mailbox_repack { - struct mailbox *mailbox; - struct index_header i; - int newindex_fd; - int newcache_fd; -}; #define MAILBOX_CRC_VERSION_MIN 1 #define MAILBOX_CRC_VERSION_MAX 2 -extern int mailbox_repack_setup(struct mailbox *mailbox, - struct mailbox_repack **repackptr); -extern int mailbox_repack_add(struct mailbox_repack *repack, - struct index_record *record); -extern void mailbox_repack_abort(struct mailbox_repack **repackptr); -extern int mailbox_repack_commit(struct mailbox_repack **repackptr); extern int mailbox_index_recalc(struct mailbox *mailbox); #define mailbox_quota_check(mailbox, delta) \ @@ -560,4 +555,6 @@ uint32_t mailbox_sync_crc(struct mailbox *mailbox, unsigned vers, int recalc); unsigned mailbox_best_crcvers(unsigned minvers, unsigned maxvers); +extern int mailbox_add_dav(struct mailbox *mailbox); + #endif /* INCLUDED_MAILBOX_H */
View file
cyrus-imapd-2.5.tar.gz/imap/mbdump.c
Changed
@@ -59,6 +59,7 @@ #include <utime.h> #include "annotate.h" +#include "dav_util.h" #include "exitcodes.h" #include "global.h" #include "imap/imap_err.h" @@ -457,11 +458,12 @@ { META_INDEX, "cyrus.index" }, { META_CACHE, "cyrus.cache" }, { META_EXPUNGE, "cyrus.expunge" }, + { META_DAV, "cyrus.dav" }, { 0, NULL } }; -enum { SEEN_DB = 0, SUBS_DB = 1, MBOXKEY_DB = 2 }; -static int NUM_USER_DATA_FILES = 3; +enum { SEEN_DB = 0, SUBS_DB = 1, MBOXKEY_DB = 2, DAV_DB = 3 }; +static int NUM_USER_DATA_FILES = 4; EXPORTED int dump_mailbox(const char *tag, struct mailbox *mailbox, uint32_t uid_start, int oldversion, @@ -618,6 +620,16 @@ fname = mboxkey_getpath(userid); ftag = "MBOXKEY"; break; +#ifdef WITH_DAV + case DAV_DB: { + struct buf dav_file = BUF_INITIALIZER; + + dav_getpath_byuserid(&dav_file, userid); + fname = (char *) buf_cstring(&dav_file); + ftag = "DAV"; + break; + } +#endif default: fatal("unknown user data file", EC_OSFILE); } @@ -724,10 +736,11 @@ return r; } -static int cleanup_seen_cb(char *name, - int matchlen __attribute__((unused)), - int maycreate __attribute__((unused)), - void *rock) +static int cleanup_seen_cb(void *rock, + const char *key, + size_t keylen, + const char *val __attribute__((unused)), + size_t vallen __attribute__((unused))) { struct seen *seendb = (struct seen *)rock; int r; @@ -736,6 +749,7 @@ struct seendata sd = SEENDATA_INITIALIZER; unsigned recno; struct index_record record; + char *name = xstrndup(key, keylen); r = mailbox_open_iwl(name, &mailbox); if (r) goto done; @@ -768,6 +782,7 @@ mailbox->i.recenttime = sd.lastread; done: + free(name); seqset_free(seq); mailbox_close(&mailbox); return r; @@ -786,10 +801,10 @@ /* no need to do inbox, it will have upgraded OK, just * the subfolders */ - snprintf(buf, sizeof(buf), "%s.*", mbname); + snprintf(buf, sizeof(buf), "%s.", mbname); r = seen_open(userid, SEEN_SILENT, &seendb); - if (!r) mboxlist_findall(NULL, buf, 1, NULL, NULL, cleanup_seen_cb, seendb); + if (!r) mboxlist_allmbox(buf, cleanup_seen_cb, seendb, /*incdel*/0); seen_close(&seendb); return 0; @@ -1052,6 +1067,14 @@ char *s = user_hash_subs(userid); strlcpy(fnamebuf, s, sizeof(fnamebuf)); free(s); +#ifdef WITH_DAV + } else if (userid && !strcmp(file.s, "DAV")) { + /* overwriting this outright is absolutely what we want to do */ + struct buf dav_file = BUF_INITIALIZER; + dav_getpath_byuserid(&dav_file, userid); + strlcpy(fnamebuf, buf_cstring(&dav_file), sizeof(fnamebuf)); + buf_free(&dav_file); +#endif } else if (userid && !strcmp(file.s, "SEEN")) { seen_file = seen_getpath(userid);
View file
cyrus-imapd-2.5.tar.gz/imap/mbexamine.c
Changed
@@ -112,10 +112,6 @@ fatal("must run as the Cyrus user", EC_USAGE); } - /* Ensure we're up-to-date on the index file format */ - assert(INDEX_HEADER_SIZE == (OFFSET_HEADER_CRC+4)); - assert(INDEX_RECORD_SIZE == (OFFSET_RECORD_CRC+4)); - while ((opt = getopt(argc, argv, "C:u:s:q")) != EOF) { switch (opt) { case 'C': /* alt config file */
View file
cyrus-imapd-2.5.tar.gz/imap/mboxevent.c
Changed
@@ -54,6 +54,10 @@ #include "annotate.h" #include "assert.h" +#ifdef WITH_DAV +#include "caldav_db.h" +#include "carddav_db.h" +#endif /* WITH_DAV */ #include "exitcodes.h" #include "global.h" #include "imapurl.h" @@ -64,6 +68,7 @@ #include "imap/mboxname.h" #include "imap/notify.h" +#include "imap/message.h" #define MESSAGE_EVENTS (EVENT_MESSAGE_APPEND|EVENT_MESSAGE_EXPIRE|\ @@ -128,11 +133,14 @@ /* 20 */ { EVENT_ACL_RIGHTS, "aclRights", EVENT_PARAM_STRING, 0, 0 }, /* 21 */ { EVENT_USER, "user", EVENT_PARAM_STRING, 0, 0 }, /* 22 */ { EVENT_MESSAGE_SIZE, "messageSize", EVENT_PARAM_INT, 0, 0 }, - /* 23 */ { EVENT_ENVELOPE, "vnd.cmu.envelope", EVENT_PARAM_STRING, 0, 0 }, - /* 24 */ { EVENT_SESSIONID, "vnd.cmu.sessionId", EVENT_PARAM_STRING, 0, 0 }, - /* 25 */ { EVENT_BODYSTRUCTURE, "bodyStructure", EVENT_PARAM_STRING, 0, 0 }, + /* 23 */ { EVENT_MBTYPE, "vnd.cmu.mbtype", EVENT_PARAM_STRING, 0, 0 }, + /* 24 */ { EVENT_DAV_FILENAME, "vnd.cmu.davFilename", EVENT_PARAM_STRING, 0, 0 }, + /* 25 */ { EVENT_DAV_UID, "vnd.cmu.davUid", EVENT_PARAM_STRING, 0, 0 }, + /* 26 */ { EVENT_ENVELOPE, "vnd.cmu.envelope", EVENT_PARAM_STRING, 0, 0 }, + /* 27 */ { EVENT_SESSIONID, "vnd.cmu.sessionId", EVENT_PARAM_STRING, 0, 0 }, + /* 28 */ { EVENT_BODYSTRUCTURE, "bodyStructure", EVENT_PARAM_STRING, 0, 0 }, /* always at end to let the parser to easily truncate this part */ - /* 26 */ { EVENT_MESSAGE_CONTENT, "messageContent", EVENT_PARAM_STRING, 0, 0 } + /* 29 */ { EVENT_MESSAGE_CONTENT, "messageContent", EVENT_PARAM_STRING, 0, 0 } }, STRARRAY_INITIALIZER, { 0, 0 }, NULL, STRARRAY_INITIALIZER, NULL, NULL, NULL }; @@ -363,6 +371,8 @@ (type & (EVENT_MESSAGE_APPEND|EVENT_MESSAGE_NEW))); case EVENT_MAILBOX_ID: return (type & MAILBOX_EVENTS); + case EVENT_MBTYPE: + return (type & MAILBOX_EVENTS); case EVENT_MAX_MESSAGES: return type & QUOTA_EVENTS; case EVENT_MESSAGE_CONTENT: @@ -371,6 +381,12 @@ case EVENT_MESSAGE_SIZE: return (extra_params & IMAP_ENUM_EVENT_EXTRA_PARAMS_MESSAGESIZE) && (type & (EVENT_MESSAGE_APPEND|EVENT_MESSAGE_NEW)); + case EVENT_DAV_FILENAME: + return (extra_params & IMAP_ENUM_EVENT_EXTRA_PARAMS_VND_CMU_DAVFILENAME) && + (type & EVENT_CALENDAR); + case EVENT_DAV_UID: + return (extra_params & IMAP_ENUM_EVENT_EXTRA_PARAMS_VND_CMU_DAVUID) && + (type & EVENT_CALENDAR); case EVENT_MESSAGES: if (type & (EVENT_QUOTA_EXCEED|EVENT_QUOTA_WITHIN)) return 1; @@ -435,7 +451,7 @@ EXPORTED void mboxevent_notify(struct mboxevent *mboxevents) { enum event_type type; - struct mboxevent *event, *next; + struct mboxevent *event; char stimestamp[TIMESTAMP_MAX+1]; char *formatted_message; @@ -443,42 +459,41 @@ if (!mboxevents) return; - event = mboxevents; - - /* swap FlagsSet and FlagsClear notification order depending the presence of - * the \Seen flag because it changes the value of vnd.cmu.unseenMessages */ - if (event->type == EVENT_FLAGS_SET && - event->next && - event->next->type == EVENT_FLAGS_CLEAR && - strarray_find_case(&event->next->flagnames, "\\Seen", 0) >= 0) { - - next = event->next; - event->next = next->next; - next->next = event; - event = next; - } - /* loop over the chained list of events */ - do { + for (event = mboxevents; event; event = event->next) { if (event->type == EVENT_CANCELLED) - goto next; + continue; + + /* swap FlagsSet and FlagsClear notification order depending the presence of + * the \Seen flag because it changes the value of vnd.cmu.unseenMessages. + * kinda bogus because it only finds two next to each other, but hey */ + if (event->type == EVENT_FLAGS_SET && + event->next && + event->next->type == EVENT_FLAGS_CLEAR && + strarray_find_case(&event->next->flagnames, "\\Seen", 0) >= 0) { + + struct mboxevent *other = event->next; + event->next = other->next; + other->next = event; + event = other; + } /* verify that at least one message has been added depending the event type */ if (event->type & (MESSAGE_EVENTS|FLAGS_EVENTS)) { if (event->type & (EVENT_MESSAGE_NEW|EVENT_MESSAGE_APPEND)) { if (!event->params[EVENT_URI].filled) - goto next; + continue; } else if (event->uidset == NULL) - goto next; + continue; } /* others quota are not supported by RFC 5423 */ if ((event->type & QUOTA_EVENTS) && !event->params[EVENT_DISK_QUOTA].filled && !event->params[EVENT_MAX_MESSAGES].filled) - goto next; + continue; /* finish to fill event parameters structure */ @@ -540,11 +555,7 @@ free(formatted_message); } while (strarray_size(&event->flagnames) > 0); - - next: - event = event->next; } - while (event); return; } @@ -744,6 +755,49 @@ xstrndup(cacheitem_base(record, CACHE_BODYSTRUCTURE), cacheitem_size(record, CACHE_BODYSTRUCTURE))); } + +#ifdef WITH_DAV + /* add caldav items */ + if ((mailbox->mbtype & (MBTYPES_DAV)) && + (mboxevent_expected_param(event->type, EVENT_DAV_FILENAME) || + mboxevent_expected_param(event->type, EVENT_DAV_UID))) { + struct body *body = NULL; + const char *resource = NULL; + struct param *param; + + if (mailbox_cacherecord(mailbox, record)) + return; + message_read_bodystructure(record, &body); + + for (param = body->disposition_params; param; param = param->next) { + if (!strcmp(param->attribute, "FILENAME")) { + resource = param->value; + } + } + + if (resource) + FILL_STRING_PARAM(event, EVENT_DAV_FILENAME, xstrdup(resource)); + + if (mboxevent_expected_param(event->type, EVENT_DAV_UID)) { + if (mailbox->mbtype & MBTYPE_ADDRESSBOOK) { + struct carddav_db *carddavdb = NULL; + struct carddav_data *cdata = NULL; + carddavdb = carddav_open_mailbox(mailbox, 0); + carddav_lookup_resource(carddavdb, mailbox->name, resource, 0, &cdata); + FILL_STRING_PARAM(event, EVENT_DAV_UID, xstrdup(cdata->vcard_uid)); + carddav_close(carddavdb); + } + if (mailbox->mbtype & MBTYPE_CALENDAR) { + struct caldav_db *caldavdb = NULL; + struct caldav_data *cdata = NULL; + caldavdb = caldav_open_mailbox(mailbox, 0); + caldav_lookup_resource(caldavdb, mailbox->name, resource, 0, &cdata); + FILL_STRING_PARAM(event, EVENT_DAV_UID, xstrdup(cdata->ical_uid)); + caldav_close(caldavdb); + } + } + } +#endif // WITH_DAV } void mboxevent_extract_copied_record(struct mboxevent *event, @@ -953,6 +1007,9 @@ imapurl_toURL(url, &imapurl); FILL_STRING_PARAM(event, EVENT_URI, xstrdup(url)); + FILL_STRING_PARAM(event, EVENT_MBTYPE, + xstrdup(mboxlist_mbtype_to_string(mailbox->mbtype))); + /* mailbox related events also require mailboxID */ if (event->type & MAILBOX_EVENTS) { FILL_STRING_PARAM(event, EVENT_MAILBOX_ID, xstrdup(url)); @@ -1012,6 +1069,9 @@ static const char *event_to_name(enum event_type type) { + if (type == (EVENT_MESSAGE_NEW|EVENT_CALENDAR)) + return "MessageNew"; + switch (type) { case EVENT_MESSAGE_APPEND: return "MessageAppend";
View file
cyrus-imapd-2.5.tar.gz/imap/mboxevent.h
Changed
@@ -80,13 +80,14 @@ EVENT_MAILBOX_RENAME = (1<<17), EVENT_MAILBOX_SUBSCRIBE = (1<<18), EVENT_MAILBOX_UNSUBSCRIBE = (1<<19), - EVENT_ACL_CHANGE = (1<<20) + EVENT_ACL_CHANGE = (1<<20), + EVENT_CALENDAR = (1<<21) }; /* The number representing the last available position in * event_param, which should always be messageContent. */ -#define MAX_PARAM 26 +#define MAX_PARAM 29 /* * event parameters defined in RFC 5423 - Internet Message Store Events @@ -120,10 +121,13 @@ /* 20 */ EVENT_ACL_RIGHTS, /* 21 */ EVENT_USER, /* 22 */ EVENT_MESSAGE_SIZE, - /* 23 */ EVENT_ENVELOPE, - /* 24 */ EVENT_SESSIONID, - /* 25 */ EVENT_BODYSTRUCTURE, - /* 26 */ EVENT_MESSAGE_CONTENT + /* 23 */ EVENT_MBTYPE, + /* 24 */ EVENT_DAV_FILENAME, + /* 25 */ EVENT_DAV_UID, + /* 26 */ EVENT_ENVELOPE, + /* 27 */ EVENT_SESSIONID, + /* 28 */ EVENT_BODYSTRUCTURE, + /* 29 */ EVENT_MESSAGE_CONTENT };
View file
cyrus-imapd-2.5.tar.gz/imap/mboxlist.c
Changed
@@ -174,6 +174,10 @@ buf_putc(&buf, 'r'); if (mbtype & MBTYPE_RESERVE) buf_putc(&buf, 'z'); + if (mbtype & MBTYPE_CALENDAR) + buf_putc(&buf, 'c'); + if (mbtype & MBTYPE_ADDRESSBOOK) + buf_putc(&buf, 'a'); return buf_cstring(&buf); } @@ -252,20 +256,6 @@ /* never get here */ } -static char *_parse_acl(struct dlist *di) -{ - struct buf buf = BUF_INITIALIZER; - struct dlist *ai; - - /* gotta make it all tabby again */ - for (ai = di->head; ai; ai = ai->next) { - buf_printf(&buf, "%s\t", ai->name); - buf_printf(&buf, "%s\t", dlist_cstring(ai)); - } - - return buf_release(&buf); -} - EXPORTED uint32_t mboxlist_string_to_mbtype(const char *string) { uint32_t mbtype = 0; @@ -274,6 +264,12 @@ for (; *string; string++) { switch (*string) { + case 'a': + mbtype |= MBTYPE_ADDRESSBOOK; + break; + case 'c': + mbtype |= MBTYPE_CALENDAR; + break; case 'd': mbtype |= MBTYPE_DELETED; break; @@ -295,6 +291,58 @@ return mbtype; } +struct parseentry_rock { + struct mboxlist_entry *mbentry; + struct buf *aclbuf; + int doingacl; +}; + +int parseentry_cb(int type, struct dlistsax_data *d) +{ + struct parseentry_rock *rock = (struct parseentry_rock *)d->rock; + + switch(type) { + case DLISTSAX_KVLISTSTART: + if (!strcmp(buf_cstring(&d->kbuf), "A")) { + rock->doingacl = 1; + } + break; + case DLISTSAX_KVLISTEND: + rock->doingacl = 0; + break; + case DLISTSAX_STRING: + if (rock->doingacl) { + buf_append(rock->aclbuf, &d->kbuf); + buf_putc(rock->aclbuf, '\t'); + buf_append(rock->aclbuf, &d->buf); + buf_putc(rock->aclbuf, '\t'); + } + else { + const char *key = buf_cstring(&d->kbuf); + if (!strcmp(key, "I")) { + rock->mbentry->uniqueid = buf_newcstring(&d->buf); + } + else if (!strcmp(key, "M")) { + rock->mbentry->mtime = atoi(buf_cstring(&d->buf)); + } + else if (!strcmp(key, "P")) { + rock->mbentry->partition = buf_newcstring(&d->buf); + } + else if (!strcmp(key, "S")) { + rock->mbentry->server = buf_newcstring(&d->buf); + } + else if (!strcmp(key, "T")) { + rock->mbentry->mbtype = mboxlist_string_to_mbtype(buf_cstring(&d->buf)); + } + else if (!strcmp(key, "V")) { + rock->mbentry->uidvalidity = atoi(buf_cstring(&d->buf)); + } + } + } + + return 0; +} + /* * parse a record read from the mailboxes.db into its parts. * @@ -311,6 +359,7 @@ const char *name, size_t namelen, const char *data, size_t datalen) { + static struct buf aclbuf; int r = IMAP_MAILBOX_BADFORMAT; char *freeme = NULL; char **target; @@ -328,39 +377,13 @@ /* check for DLIST mboxlist */ if (*data == '%') { - struct dlist *dl = NULL; - struct dlist *di; - r = dlist_parsemap(&dl, 0, data, datalen); - if (r) goto done; - if (!dl) goto done; - for (di = dl->head; di; di = di->next) { - switch(di->name[0]) { - case 'A': - mbentry->acl = _parse_acl(di); - break; - case 'I': - mbentry->uniqueid = xstrdupnull(dlist_cstring(di)); - break; - case 'M': - mbentry->mtime = dlist_num(di); - break; - case 'P': - mbentry->partition = xstrdupnull(dlist_cstring(di)); - break; - case 'S': - mbentry->server = xstrdupnull(dlist_cstring(di)); - break; - case 'T': - mbentry->mbtype = mboxlist_string_to_mbtype(dlist_cstring(di)); - break; - case 'V': - mbentry->uidvalidity = dlist_num(di); - break; - } - } - dlist_free(&dl); - - r = 0; + struct parseentry_rock rock; + memset(&rock, 0, sizeof(struct parseentry_rock)); + rock.mbentry = mbentry; + rock.aclbuf = &aclbuf; + aclbuf.len = 0; + r = dlist_parsesax(data, datalen, 0, parseentry_cb, &rock); + if (!r) mbentry->acl = buf_newcstring(&aclbuf); goto done; } @@ -783,6 +806,8 @@ if (r) goto done; } + r = 0; + done: if (r || !newacl) free(acl); else *newacl = acl; @@ -1192,6 +1217,9 @@ r = mboxlist_lookup(name, &mbentry, NULL); if (r) goto done; + if (mbentry->mbtype & MBTYPE_DELETED) + goto done; + isremote = mbentry->mbtype & MBTYPE_REMOTE; /* check if user has Delete right (we've already excluded non-admins @@ -1773,7 +1801,7 @@ mode = ACL_MODE_REMOVE; } /* do not allow non-admin user to remove the admin rights from mailbox owner */ - if (!isadmin && isidentifiermbox) { + if (!isadmin && isidentifiermbox && mode != ACL_MODE_ADD) { int has_admin_rights = mboxlist_have_admin_rights(rights); if ((has_admin_rights && mode == ACL_MODE_REMOVE) || (!has_admin_rights && mode != ACL_MODE_REMOVE)) { @@ -2026,6 +2054,7 @@ struct glob *g = rock->g; long matchlen; mbentry_t *mbentry = NULL; + int ret = 0; /* don't list mailboxes outside of the default domain */ if (!rock->domainlen && !rock->isadmin && memchr(key, '!', keylen)) return 0; @@ -2034,7 +2063,7 @@ if (rock->inboxoffset) { char namebuf[MAX_MAILBOX_BUFFER]; - if(keylen >= (int) sizeof(namebuf)) { + if (keylen >= (int) sizeof(namebuf)) { syslog(LOG_ERR, "oversize keylen in mboxlist.c:find_p()"); return 0; } @@ -2076,16 +2105,6 @@ return 0; } - /* Suppress deleted hierarchy unless admin: overrides ACL_LOOKUP test */ - if (!rock->isadmin) { - char namebuf[MAX_MAILBOX_BUFFER]; - - memcpy(namebuf, key, keylen); - namebuf[keylen] = '\0'; - if (mboxname_isdeletedmailbox(namebuf, NULL)) - return 0; - } - /* subs DB has empty keys */ if (rock->issubs) return 1; @@ -2094,26 +2113,32 @@ if (mboxlist_parse_entry(&mbentry, key, keylen, data, datalen)) return 0; - if (mbentry->mbtype & MBTYPE_DELETED) { - mboxlist_entry_free(&mbentry); - return 0; - } + /* nobody sees tombstones */ + if (mbentry->mbtype & MBTYPE_DELETED) + goto done; /* check acl */ if (!rock->isadmin) { - int rights = cyrus_acl_myrights(rock->auth_state, mbentry->acl); + /* always suppress deleted for non-admin */ + if (mboxname_isdeletedmailbox(mbentry->name, NULL)) goto done; - if (!(rights & ACL_LOOKUP)) { - mboxlist_entry_free(&mbentry); - return 0; - } - } + /* also suppress calendar */ + if (mboxname_iscalendarmailbox(mbentry->name, mbentry->mbtype)) goto done; - mboxlist_entry_free(&mbentry); + /* and addressbook */ + if (mboxname_isaddressbookmailbox(mbentry->name, mbentry->mbtype)) goto done; + + /* check the acls */ + if (!(cyrus_acl_myrights(rock->auth_state, mbentry->acl) & ACL_LOOKUP)) goto done; + } /* if we get here, close enough for us to spend the time acting interested */ - return 1; + ret = 1; + +done: + mboxlist_entry_free(&mbentry); + return ret; } static int check_name(struct find_rock *rock, @@ -2250,19 +2275,15 @@ EXPORTED int mboxlist_allmbox(const char *prefix, foreach_cb *proc, void *rock, int incdel) { - int r; char *search = prefix ? (char *)prefix : ""; - if (incdel) - r = cyrusdb_foreach(mbdb, search, strlen(search), NULL, proc, rock, 0); - else - r = cyrusdb_foreach(mbdb, search, strlen(search), skipdel_cb, proc, rock, 0); - - return r; + return cyrusdb_foreach(mbdb, search, strlen(search), + incdel ? NULL : skipdel_cb, + proc, rock, 0); } EXPORTED int mboxlist_allusermbox(const char *userid, foreach_cb *proc, - void *rock, int include_deleted) + void *rock, int incdel) { char *inbox = mboxname_user_mbox(userid, 0); size_t inboxlen = strlen(inbox); @@ -2272,22 +2293,31 @@ int r; r = cyrusdb_fetch(mbdb, inbox, inboxlen, &data, &datalen, NULL); - if (r) goto done; - - /* process inbox first */ - r = proc(rock, inbox, inboxlen, data, datalen); + if (!r) { + /* process inbox first */ + if (incdel || skipdel_cb(rock, inbox, inboxlen, data, datalen)) + r = proc(rock, inbox, inboxlen, data, datalen); + } + else if (r == CYRUSDB_NOTFOUND) { + /* don't process inbox! */ + r = 0; + } if (r) goto done; /* process all the sub folders */ - r = cyrusdb_foreach(mbdb, search, strlen(search), NULL, proc, rock, 0); + r = cyrusdb_foreach(mbdb, search, strlen(search), + incdel ? NULL : skipdel_cb, + proc, rock, 0); if (r) goto done; /* don't check if delayed delete is enabled, maybe the caller wants to * clean up deleted stuff after it's been turned off */ - if (include_deleted) { + if (incdel) { const char *prefix = config_getstring(IMAPOPT_DELETEDPREFIX); char *name = strconcat(prefix, ".", inbox, ".", (char *)NULL); - r = cyrusdb_foreach(mbdb, name, strlen(name), NULL, proc, rock, 0); + r = cyrusdb_foreach(mbdb, name, strlen(name), + incdel ? NULL : skipdel_cb, + proc, rock, 0); free(name); }
View file
cyrus-imapd-2.5.tar.gz/imap/mboxlist.h
Changed
@@ -68,6 +68,11 @@ #define MBTYPE_NETNEWS (1<<2) /* Netnews Mailbox - NO LONGER USED */ #define MBTYPE_MOVING (1<<3) /* Mailbox in mid-transfer (part is remotehost!localpart) */ #define MBTYPE_DELETED (1<<4) /* Mailbox has been deleted, but not yet cleaned up */ +#define MBTYPE_CALENDAR (1<<5) /* Calendar Mailbox */ +#define MBTYPE_ADDRESSBOOK (1<<6) /* Addressbook Mailbox */ + +#define MBTYPES_DAV (MBTYPE_CALENDAR|MBTYPE_ADDRESSBOOK) +#define MBTYPES_NONIMAP (MBTYPE_NETNEWS|MBTYPES_DAV) /* master name of the mailboxes file */ #define FNAME_MBOXLIST "/mailboxes.db"
View file
cyrus-imapd-2.5.tar.gz/imap/mboxname.c
Changed
@@ -263,6 +263,48 @@ * one end to the other and such. Yay flexibility. */ +EXPORTED int mboxname_parts_to_internal(struct mboxname_parts *parts, + char *result) +{ + char *p = result; + size_t sz = MAX_MAILBOX_NAME; + size_t len; + const char *dp = config_getstring(IMAPOPT_DELETEDPREFIX); + const char *pf = ""; + + if (parts->domain) { + len = snprintf(p, sz, "%s!", parts->domain); + p += len; + sz -= len; + if (!sz) return IMAP_MAILBOX_BADNAME; + } + + if (parts->is_deleted) { + len = snprintf(p, sz, "%s%s", pf, dp); + p += len; + sz -= len; + if (!sz) return IMAP_MAILBOX_BADNAME; + pf = "."; + } + + if (parts->userid) { + len = snprintf(p, sz, "%suser.%s", pf, parts->userid); + p += len; + sz -= len; + if (!sz) return IMAP_MAILBOX_BADNAME; + pf = "."; + } + + if (parts->box) { + len = snprintf(p, sz, "%s%s", pf, parts->box); + p += len; + sz -= len; + if (!sz) return IMAP_MAILBOX_BADNAME; + } + + return 0; +} + /* Handle conversion from the standard namespace to the internal namespace */ static int mboxname_tointernal(struct namespace *namespace, const char *name, const char *userid, char *result) @@ -565,41 +607,84 @@ } /* Handle conversion from the internal namespace to the alternate namespace */ -static int mboxname_toexternal_alt(struct namespace *namespace, const char *mboxname, +static int mboxname_toexternal_alt(struct namespace *namespace, const char *name, const char *userid, char *result) { - char iresult[MAX_MAILBOX_NAME]; - int r = 0; + char *domain; + size_t userlen, resultlen; - // Make this abundantly simple - mboxname_toexternal(namespace, mboxname, userid, iresult); + /* Blank the result, just in case */ + result[0] = '\0'; - r = strncasecmp(iresult, "inbox", 5); + if(strlen(name) > MAX_MAILBOX_NAME) return IMAP_MAILBOX_BADNAME; + + if (!userid) return IMAP_MAILBOX_BADNAME; + + userlen = strlen(userid); + + if (config_virtdomains && (domain = strchr(userid, '@'))) { + size_t domainlen = strlen(domain); - if (!r) { - if (iresult[5] == '\0') { - sprintf(result, "%s", iresult); - } else if (iresult[5] == namespace->hier_sep) { - sprintf(result, "%s", iresult+6); + userlen = domain - userid; + + if (!strncmp(name, domain+1, domainlen-1) && + name[domainlen-1] == '!') { + name += domainlen; } + } - return 0; + /* Personal (INBOX) namespace */ + if (!strncasecmp(name, "inbox", 5) && + (name[5] == '\0' || name[5] == '.')) { + if (name[5] == '\0') + strcpy(result, name); + else + strcpy(result, name+6); + } + /* paranoia - this shouldn't be needed */ + else if (!strncmp(name, "user.", 5) && + !strncmp(name+5, userid, userlen) && + (name[5+userlen] == '\0' || + name[5+userlen] == '.')) { + if (name[5+userlen] == '\0') + strcpy(result, "INBOX"); + else + strcpy(result, name+5+userlen+1); } - r = strncasecmp(iresult, "user", 4); + /* Other Users namespace */ + else if (!strncmp(name, "user", 4) && + (name[4] == '\0' || name[4] == '.')) { + size_t prefixlen = strlen(namespace->prefix[NAMESPACE_USER]); - if (!r) { - if (iresult[4] == namespace->hier_sep) { - // The namespace already has a hierarchy separator - sprintf(result, "%s%s", namespace->prefix[NAMESPACE_USER], iresult+5); - } + if ((prefixlen > MAX_MAILBOX_NAME) || + ((name[4] == '.') && + ((prefixlen+1+strlen(name+5)) > MAX_MAILBOX_NAME))) + return IMAP_MAILBOX_BADNAME; - return 0; + sprintf(result, "%.*s", + (int) (prefixlen-1), namespace->prefix[NAMESPACE_USER]); + resultlen = strlen(result); + if (name[4] == '.') { + sprintf(result+resultlen, "%c%s", namespace->hier_sep, name+5); + } } - // The namespace already has a hierarchy separator - sprintf(result, "%s%s", namespace->prefix[NAMESPACE_SHARED], iresult); + /* Shared namespace */ + else { + /* special case: LIST/LSUB "" % */ + if (!strncmp(name, namespace->prefix[NAMESPACE_SHARED], + strlen(namespace->prefix[NAMESPACE_SHARED])-1)) { + strcpy(result, name); + } + else { + strcpy(result, namespace->prefix[NAMESPACE_SHARED]); + strcat(result, name); + } + } + /* Translate any separators in mailboxname */ + mboxname_hiersep_toexternal(namespace, result, 0); return 0; } @@ -831,6 +916,74 @@ } /* + * If (internal) mailbox 'name' is a CALENDAR mailbox + * returns boolean + */ +int mboxname_iscalendarmailbox(const char *name, int mbtype) +{ + static const char *calendarprefix = NULL; + static int calendarprefix_len = 0; + const char *p; + const char *start = name; + + if (mbtype & MBTYPE_CALENDAR) return 1; /* Only works on backends */ + + if (!calendarprefix) { + calendarprefix = config_getstring(IMAPOPT_CALENDARPREFIX); + if (calendarprefix) calendarprefix_len = strlen(calendarprefix); + } + + /* if the prefix is blank, then no calendars */ + if (!calendarprefix_len) return 0; + + /* step past the domain part */ + if (config_virtdomains && (p = strchr(start, '!'))) + start = p + 1; + + /* step past the user part */ + if (!strncmp(start, "user.", 5) && (p = strchr(start+5, '.'))) + start = p + 1; + + return ((!strncmp(start, calendarprefix, calendarprefix_len) && + (start[calendarprefix_len] == '\0' || + start[calendarprefix_len] == '.')) ? 1 : 0); +} + +/* + * If (internal) mailbox 'name' is a ADDRESSBOOK mailbox + * returns boolean + */ +int mboxname_isaddressbookmailbox(const char *name, int mbtype) +{ + static const char *addressbookprefix = NULL; + static int addressbookprefix_len = 0; + const char *p; + const char *start = name; + + if (mbtype & MBTYPE_ADDRESSBOOK) return 1; /* Only works on backends */ + + if (!addressbookprefix) { + addressbookprefix = config_getstring(IMAPOPT_ADDRESSBOOKPREFIX); + if (addressbookprefix) addressbookprefix_len = strlen(addressbookprefix); + } + + /* if the prefix is blank, then no addressbooks */ + if (!addressbookprefix_len) return 0; + + /* step past the domain part */ + if (config_virtdomains && (p = strchr(start, '!'))) + start = p + 1; + + /* step past the user part */ + if (!strncmp(start, "user.", 5) && (p = strchr(start+5, '.'))) + start = p + 1; + + return ((!strncmp(start, addressbookprefix, addressbookprefix_len) && + (start[addressbookprefix_len] == '\0' || + start[addressbookprefix_len] == '.')) ? 1 : 0); +} + +/* * Translate (internal) inboxname into corresponding userid. */ EXPORTED const char *mboxname_to_userid(const char *mboxname) @@ -971,7 +1124,7 @@ return 0; } -int mboxname_userid_to_parts(const char *userid, struct mboxname_parts *parts) +EXPORTED int mboxname_userid_to_parts(const char *userid, struct mboxname_parts *parts) { char *b, *e; /* beginning and end of string parts */ @@ -992,7 +1145,7 @@ return 0; } -HIDDEN void mboxname_init_parts(struct mboxname_parts *parts) +EXPORTED void mboxname_init_parts(struct mboxname_parts *parts) { memset(parts, 0, sizeof(*parts)); } @@ -1006,43 +1159,6 @@ } /* - * Apply additional restrictions on netnews mailbox names. - * Cannot have all-numeric name components. - */ -int mboxname_netnewscheck(const char *name) -{ - int c; - int sawnonnumeric = 0; - - while ((c = *name++)!=0) { - switch (c) { - case '.': - if (!sawnonnumeric) return IMAP_MAILBOX_BADNAME; - sawnonnumeric = 0; - break; - - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - break; - - default: - sawnonnumeric = 1; - break; - } - } - if (!sawnonnumeric) return IMAP_MAILBOX_BADNAME; - return 0; -} - -/* * Apply site policy restrictions on mailbox names. * Restrictions are hardwired for now. */ @@ -1339,6 +1455,11 @@ metaflag = IMAP_ENUM_METAPARTITION_FILES_ANNOTATIONS; filename = FNAME_ANNOTATIONS; break; + case META_DAV: + snprintf(confkey, 256, "metadir-dav-%s", partition); + metaflag = IMAP_ENUM_METAPARTITION_FILES_DAV; + filename = FNAME_DAV; + break; case 0: break; default:
View file
cyrus-imapd-2.5.tar.gz/imap/mboxname.h
Changed
@@ -147,6 +147,13 @@ */ int mboxname_to_parts(const char *mboxname, struct mboxname_parts *parts); int mboxname_userid_to_parts(const char *userid, struct mboxname_parts *parts); + +/* + * Create an (internal) mboxname from parts + */ + +int mboxname_parts_to_internal(struct mboxname_parts *parts, char *target); + /* * Cleanup up a mboxname_parts structure. */ @@ -154,6 +161,18 @@ void mboxname_free_parts(struct mboxname_parts *parts); +/* + * If (internal) mailbox 'name' is a CALENDAR mailbox + * returns boolean + */ +int mboxname_iscalendarmailbox(const char *name, int mbtype); + +/* + * If (internal) mailbox 'name' is a ADDRESSBOOK mailbox + * returns boolean + */ +int mboxname_isaddressbookmailbox(const char *name, int mbtype); + /* check if one mboxname is a parent or same as the other */ int mboxname_is_prefix(const char *longstr, const char *shortstr); @@ -197,8 +216,6 @@ */ int mboxname_policycheck(const char *name); -int mboxname_netnewscheck(const char *name); - void mboxname_todeleted(const char *name, char *result, int withtime); /*
View file
cyrus-imapd-2.5.tar.gz/imap/mbtool.c
Changed
@@ -113,10 +113,6 @@ fatal("must run as the Cyrus user", EC_USAGE); } - /* Ensure we're up-to-date on the index file format */ - assert(INDEX_HEADER_SIZE == (OFFSET_HEADER_CRC+4)); - assert(INDEX_RECORD_SIZE == (OFFSET_RECORD_CRC+4)); - while ((opt = getopt(argc, argv, "C:t")) != EOF) { switch (opt) { case 'C': /* alt config file */
View file
cyrus-imapd-2.5.tar.gz/imap/partlist.c
Changed
@@ -46,11 +46,14 @@ #include <stdlib.h> #include <string.h> #include <syslog.h> +#include <sys/stat.h> #include <sys/statvfs.h> #include <sys/types.h> + #include "libconfig.h" #include "partlist.h" +#include "util.h" #include "xmalloc.h" @@ -437,8 +440,8 @@ { partlist_conf_t *part_list_conf = (partlist_conf_t *)rock; partlist_t *part_list = part_list_conf->part_list; - int key_prefix_len = (part_list_conf->key_prefix ? strlen(part_list_conf->key_prefix) : 0); - int i; + size_t key_prefix_len = (part_list_conf->key_prefix ? strlen(part_list_conf->key_prefix) : 0); + unsigned i; if (key_prefix_len) { if ((strncmp(part_list_conf->key_prefix, key, key_prefix_len) != 0) || (strlen(key) <= key_prefix_len)) {
View file
cyrus-imapd-2.5.tar.gz/imap/proc.c
Changed
@@ -82,24 +82,41 @@ static char *procfname = 0; -static char *proc_getdir(void) +static char *proc_getpath(pid_t pid, int isnew) { - return strconcat(config_dir, FNAME_PROCDIR, (char *)NULL); + struct buf buf = BUF_INITIALIZER; + + if (config_getstring(IMAPOPT_PROC_PATH)) { + const char *procpath = config_getstring(IMAPOPT_PROC_PATH); + + if (procpath[0] != '/') + fatal("proc path must be fully qualified", EC_CONFIG); + + if (strlen(procpath) < 2) + fatal("proc path must not be '/'", EC_CONFIG); + + buf_setcstr(&buf, procpath); + + if (buf.s[buf.len-1] != '/') + buf_putc(&buf, '/'); + } + else { + buf_setcstr(&buf, config_dir); + buf_appendcstr(&buf, FNAME_PROCDIR); + } + + if (pid) + buf_printf(&buf, "%u", pid); + + if (isnew) + buf_appendcstr(&buf, ".new"); + + return buf_release(&buf); } -static char *proc_getpath(pid_t pid, int isnew) +static char *proc_getdir(void) { - char *path; - - assert(pid > 0); - path = xmalloc(strlen(config_dir)+sizeof(FNAME_PROCDIR)+16); - sprintf(path, "%s%s/%s%u%s", - config_dir, - FNAME_PROCDIR, - (isnew ? "." : ""), - (unsigned)pid, - (isnew ? ".new" : "")); - return path; + return proc_getpath(0, 0); } EXPORTED int proc_register(const char *servicename, const char *clienthost,
View file
cyrus-imapd-2.5.tar.gz/imap/reconstruct.c
Changed
@@ -77,6 +77,12 @@ #include "acl.h" #include "assert.h" +#include "bsearch.h" +#ifdef WITH_DAV +#include "caldav_db.h" +#include "carddav_db.h" +#endif +#include "crc32.h" #include "hash.h" #include "imap/global.h" #include "exitcodes.h" @@ -113,6 +119,7 @@ extern cyrus_acl_canonproc_t mboxlist_ensureOwnerRights; static int reconstruct_flags = RECONSTRUCT_MAKE_CHANGES | RECONSTRUCT_DO_STAT; +static int setversion = 0; int main(int argc, char **argv) { @@ -131,13 +138,9 @@ fatal("must run as the Cyrus user", EC_USAGE); } - /* Ensure we're up-to-date on the index file format */ - assert(INDEX_HEADER_SIZE == (OFFSET_HEADER_CRC+4)); - assert(INDEX_RECORD_SIZE == (OFFSET_RECORD_CRC+4)); - construct_hash_table(&unqid_table, 2047, 1); - while ((opt = getopt(argc, argv, "C:kp:rmfsxgGqRUoOn")) != EOF) { + while ((opt = getopt(argc, argv, "C:kp:rmfsxgGqRUoOnV:")) != EOF) { switch (opt) { case 'C': /* alt config file */ alt_config = optarg; @@ -203,6 +206,10 @@ reconstruct_flags |= RECONSTRUCT_REMOVE_ODDFILES; break; + case 'V': + setversion = atoi(optarg); + break; + default: usage(); } @@ -233,6 +240,11 @@ quotadb_init(0); quotadb_open(NULL); +#ifdef WITH_DAV + caldav_init(); + carddav_init(); +#endif + /* Deal with nonexistent mailboxes */ if (start_part) { /* We were handed a mailbox that does not exist currently */ @@ -370,6 +382,10 @@ quotadb_done(); partlist_local_done(); +#ifdef WITH_DAV + carddav_done(); + caldav_done(); +#endif cyrus_done(); @@ -444,6 +460,17 @@ printf("%s\n", buf); strncpy(outpath, mailbox_meta_fname(mailbox, META_HEADER), MAX_MAILBOX_NAME); + + if (setversion) { + /* need to re-set the version! */ + int r = mailbox_setversion(mailbox, setversion); + if (r) { + printf("FAILED TO REPACK %s with new version %s\n", buf, error_message(r)); + } + else { + printf("Repacked %s to version %d\n", buf, setversion); + } + } mailbox_close(&mailbox); if (discovered) {
View file
cyrus-imapd-2.5.tar.gz/imap/sequence.c
Changed
@@ -341,7 +341,7 @@ * Return the last number in the sequence, or 0 * if the sequence is empty. */ -HIDDEN unsigned seqset_last(const struct seqset *seq) +EXPORTED unsigned seqset_last(const struct seqset *seq) { return (seq->len ? seq->set[seq->len-1].high : 0); } @@ -384,7 +384,7 @@ * Merge the numbers in seqset `b' into seqset `a'. */ /* NOTE - not sort safe! */ -EXPORTED void seqset_join(struct seqset *a, struct seqset *b) +EXPORTED void seqset_join(struct seqset *a, const struct seqset *b) { if (a->len + b->len > a->alloc) { a->alloc = a->len + b->len;
View file
cyrus-imapd-2.5.tar.gz/imap/sequence.h
Changed
@@ -74,13 +74,13 @@ extern struct seqset *seqset_parse(const char *sequence, struct seqset *set, unsigned maxval); -extern void seqset_join(struct seqset *a, struct seqset *b); +extern void seqset_join(struct seqset *a, const struct seqset *b); extern void seqset_append(struct seqset **l, char *sequence, unsigned maxval); extern int seqset_ismember(struct seqset *set, unsigned num); extern unsigned seqset_getnext(struct seqset *set); extern unsigned seqset_first(const struct seqset *set); extern unsigned seqset_last(const struct seqset *set); extern char *seqset_cstring(const struct seqset *set); -extern void seqset_free(struct seqset *l); +extern void seqset_free(struct seqset *set); #endif /* SEQUENCE_H */
View file
cyrus-imapd-2.5.tar.gz/imap/smmapd.c
Changed
@@ -105,6 +105,7 @@ extern int optind; static struct protstream *map_in, *map_out; +static const char *smmapd_clienthost; /* current namespace */ static struct namespace map_namespace; @@ -130,6 +131,8 @@ prot_free(map_out); } + smmapd_clienthost = "[local]"; + map_in = map_out = NULL; cyrus_reset_stdio(); @@ -216,12 +219,14 @@ char **argv __attribute__((unused)), char **envp __attribute__((unused))) { - + const char *localip, *remoteip; map_in = prot_new(0, 0); map_out = prot_new(1, 1); prot_setflushonread(map_in, map_out); prot_settimeout(map_in, 360); + smmapd_clienthost = get_clienthost(0, &localip, &remoteip); + if (begin_handling() != 0) shut_down(0); /* prepare for new connection */ @@ -463,19 +468,25 @@ switch (r) { case -1: - /* reply already sent */ - break; + /* reply already sent */ + break; case 0: + if (config_getswitch(IMAPOPT_AUDITLOG)) + syslog(LOG_NOTICE, "auditlog: ok userid=<%s> client=<%s>", key, smmapd_clienthost); prot_printf(map_out, SIZE_T_FMT ":OK %s,", 3+strlen(key), key); break; case IMAP_MAILBOX_NONEXISTENT: + if (config_getswitch(IMAPOPT_AUDITLOG)) + syslog(LOG_NOTICE, "auditlog: nonexistent userid=<%s> client=<%s>", key, smmapd_clienthost); prot_printf(map_out, SIZE_T_FMT ":NOTFOUND %s,", 9+strlen(error_message(r)), error_message(r)); break; case IMAP_QUOTA_EXCEEDED: + if (config_getswitch(IMAPOPT_AUDITLOG)) + syslog(LOG_NOTICE, "auditlog: overquota userid=<%s> client=<%s>", key, smmapd_clienthost); if (!config_getswitch(IMAPOPT_LMTP_OVER_QUOTA_PERM_FAILURE)) { prot_printf(map_out, SIZE_T_FMT ":TEMP %s,", strlen(error_message(r))+5, error_message(r)); @@ -484,6 +495,8 @@ /* fall through - permanent failure */ default: + if (config_getswitch(IMAPOPT_AUDITLOG)) + syslog(LOG_NOTICE, "auditlog: failed userid=<%s> client=<%s>", key, smmapd_clienthost); if (errstring) prot_printf(map_out, SIZE_T_FMT ":PERM %s (%s),", 5+strlen(error_message(r))+3+strlen(errstring),
View file
cyrus-imapd-2.5.tar.gz/imap/statuscache_db.c
Changed
@@ -63,6 +63,7 @@ #include "mailbox.h" #include "seen.h" #include "util.h" +#include "xmalloc.h" #include "xstrlcpy.h" #include "statuscache.h"
View file
cyrus-imapd-2.5.tar.gz/imap/sync_client.c
Changed
@@ -103,6 +103,9 @@ static int connect_once = 0; static int background = 0; static int do_compress = 0; +static int no_copyback = 0; + +static char *prev_userid; #define CAPA_CRC_VERSIONS (CAPA_COMPRESS<<1) @@ -294,44 +297,55 @@ struct sync_msgid_list *part_list) { const char *cmd = "RESERVE"; - struct sync_msgid *msgid; + struct sync_msgid *msgid = part_list->head; struct sync_folder *folder; - struct dlist *kl; + struct dlist *kl = NULL; struct dlist *kin = NULL; struct dlist *ki; - int r; - - if (!part_list->toupload) - return 0; /* nothing to reserve */ + int r = 0; if (!replica_folders->head) return 0; /* nowhere to reserve */ - kl = dlist_newkvlist(NULL, cmd); - dlist_setatom(kl, "PARTITION", partition); + while (msgid) { + int n = 0; - ki = dlist_newlist(kl, "MBOXNAME"); - for (folder = replica_folders->head; folder; folder = folder->next) - dlist_setatom(ki, "MBOXNAME", folder->name); + if (!part_list->toupload) + goto done; /* got them all */ - ki = dlist_newlist(kl, "GUID"); - for (msgid = part_list->head; msgid; msgid = msgid->next) { - if (!msgid->need_upload) continue; - dlist_setatom(ki, "GUID", message_guid_encode(&msgid->guid)); - /* we will re-add the "need upload" if we get a MISSING response */ - msgid->need_upload = 0; - part_list->toupload--; - } + kl = dlist_newkvlist(NULL, cmd); + dlist_setatom(kl, "PARTITION", partition); - sync_send_apply(kl, sync_out); - dlist_free(&kl); + ki = dlist_newlist(kl, "MBOXNAME"); + for (folder = replica_folders->head; folder; folder = folder->next) + dlist_setatom(ki, "MBOXNAME", folder->name); - r = sync_parse_response(cmd, sync_in, &kin); - if (r) return r; + ki = dlist_newlist(kl, "GUID"); + for (; msgid; msgid = msgid->next) { + if (!msgid->need_upload) continue; + if (n > 8192) break; + dlist_setatom(ki, "GUID", message_guid_encode(&msgid->guid)); + /* we will re-add the "need upload" if we get a MISSING response */ + msgid->need_upload = 0; + part_list->toupload--; + n++; + } - r = mark_missing(kin, part_list); - dlist_free(&kin); + sync_send_apply(kl, sync_out); + + r = sync_parse_response(cmd, sync_in, &kin); + if (r) goto done; + r = mark_missing(kin, part_list); + if (r) goto done; + + dlist_free(&kl); + dlist_free(&kin); + } + +done: + dlist_free(&kl); + dlist_free(&kin); return r; } @@ -1000,103 +1014,105 @@ const struct sync_annot_list *rannots, struct dlist *kaction) { - int diff = 0; int i; int r; - /* are there any differences? */ - if (mp->modseq != rp->modseq) - diff = 1; - else if (mp->last_updated != rp->last_updated) - diff = 1; - else if (mp->internaldate != rp->internaldate) - diff = 1; - else if (mp->system_flags != rp->system_flags) - diff = 1; - else if (!message_guid_equal(&mp->guid, &rp->guid)) - diff = 1; - else if (mp->cid != rp->cid) - diff = 1; - else if (diff_annotations(mannots, rannots)) - diff = 1; - else { - for (i = 0; i < MAX_USER_FLAGS/32; i++) { - if (mp->user_flags[i] != rp->user_flags[i]) - diff = 1; - } - } + /* if both ends are expunged, then we do no more processing. This + * allows a split brain cleanup to not break things forever. It + * does mean that an expunged message might not replicate in that + * case, but the only way to fix this is add ANOTHER special flag + * for BROKEN and only ignore GUID mismatches in that case, after + * moving the message up. I guess we could force UNLINK immediately + * too... hmm. Not today. */ - /* if differences we'll have to rewrite to bump the modseq - * so that regular replication will cause an update */ - if (diff) { - /* interesting case - expunged locally */ - if (mp->system_flags & FLAG_EXPUNGED) { - /* if expunged, fall through - the rewrite will lift - * the modseq to force the change to stick */ + if ((mp->system_flags & FLAG_EXPUNGED) && (rp->system_flags & FLAG_EXPUNGED)) + return 0; + + /* first of all, check that GUID matches. If not, we have had a split + * brain, so the messages both need to be fixed up to their new UIDs. + * After this function succeeds, both the local and remote copies of this + * current UID will be actually EXPUNGED, so the earlier return applies. */ + if (!message_guid_equal(&mp->guid, &rp->guid)) { + char *mguid = xstrdup(message_guid_encode(&mp->guid)); + char *rguid = xstrdup(message_guid_encode(&rp->guid)); + syslog(LOG_ERR, "SYNCERROR: guid mismatch %s %u (%s %s)", + mailbox->name, mp->uid, rguid, mguid); + free(rguid); + free(mguid); + /* we will need to renumber both ends to get in sync */ + + /* ORDERING - always lower GUID first */ + if (message_guid_cmp(&mp->guid, &rp->guid) > 0) { + r = copyback_one_record(mailbox, rp, rannots, kaction); + if (!r) r = renumber_one_record(mp, kaction); } - else if (rp->system_flags & FLAG_EXPUNGED) { - /* mark expunged - rewrite will cause both sides to agree - * again */ - mp->system_flags |= FLAG_EXPUNGED; + else { + r = renumber_one_record(mp, kaction); + if (!r) r = copyback_one_record(mailbox, rp, rannots, kaction); } - /* general case */ - else { - if (!message_guid_equal(&mp->guid, &rp->guid)) { - char *mguid = xstrdup(message_guid_encode(&mp->guid)); - char *rguid = xstrdup(message_guid_encode(&rp->guid)); - syslog(LOG_ERR, "SYNCERROR: guid mismatch %s %u (%s %s)", - mailbox->name, mp->uid, rguid, mguid); - free(rguid); - free(mguid); - /* we will need to renumber both ends to get in sync */ - - /* ORDERING - always lower GUID first */ - if (message_guid_cmp(&mp->guid, &rp->guid) > 0) { - r = copyback_one_record(mailbox, rp, rannots, kaction); - if (!r) r = renumber_one_record(mp, kaction); - } - else { - r = renumber_one_record(mp, kaction); - if (!r) r = copyback_one_record(mailbox, rp, - rannots, kaction); - } + return r; + } - return r; - } + /* are there any differences? */ + if (mp->modseq != rp->modseq) + goto diff; + if (mp->last_updated != rp->last_updated) + goto diff; + if (mp->internaldate != rp->internaldate) + goto diff; + if ((mp->system_flags & FLAGS_GLOBAL) != rp->system_flags) + goto diff; + if (mp->cid != rp->cid) + goto diff; + if (diff_annotations(mannots, rannots)) + goto diff; + for (i = 0; i < MAX_USER_FLAGS/32; i++) { + if (mp->user_flags[i] != rp->user_flags[i]) + goto diff; + } + + /* no changes found, whoopee */ + return 0; - /* is the replica "newer"? */ - if (rp->modseq > mp->modseq && - rp->last_updated >= mp->last_updated) { - /* then copy all the flag data over from the replica */ - mp->system_flags = rp->system_flags; - for (i = 0; i < MAX_USER_FLAGS/32; i++) - mp->user_flags[i] = rp->user_flags[i]; + diff: + /* if differences we'll have to rewrite to bump the modseq + * so that regular replication will cause an update */ - log_mismatch("more recent on replica", mailbox, mp, rp); + /* interesting case - expunged locally */ + if (mp->system_flags & FLAG_EXPUNGED) { + /* if expunged, fall through - the rewrite will lift + * the modseq to force the change to stick */ + } + else if (rp->system_flags & FLAG_EXPUNGED) { + /* mark expunged - rewrite will cause both sides to agree + * again */ + mp->system_flags |= FLAG_EXPUNGED; + } - if (kaction) { - r = apply_annotations(mailbox, mp, mannots, rannots, 0); - if (r) return r; - } - } - else { - if (kaction) { - r = apply_annotations(mailbox, mp, mannots, rannots, 1); - if (r) return r; - } - } + /* otherwise, is the replica "newer"? Better grab those flags */ + else { + if (rp->modseq > mp->modseq && + rp->last_updated >= mp->last_updated) { + log_mismatch("more recent on replica", mailbox, mp, rp); + /* then copy all the flag data over from the replica */ + mp->system_flags = (rp->system_flags & FLAGS_GLOBAL) | + (mp->system_flags & FLAGS_LOCAL); + mp->cid = rp->cid; + for (i = 0; i < MAX_USER_FLAGS/32; i++) + mp->user_flags[i] = rp->user_flags[i]; } + } - /* are we making changes yet? */ - if (!kaction) return 0; + /* are we making changes yet? */ + if (!kaction) return 0; - /* this will bump the modseq and force a resync either way :) */ - r = mailbox_rewrite_index_record(mailbox, mp); - if (r) return r; - } + /* even expunged messages get annotations synced */ + r = apply_annotations(mailbox, mp, mannots, rannots, 0); + if (r) return r; - return 0; + /* this will bump the modseq and force a resync either way :) */ + return mailbox_rewrite_index_record(mailbox, mp); } @@ -1523,6 +1539,9 @@ { int r = update_mailbox_once(local, remote, reserve_guids, 0); + /* never retry - other end should always sync cleanly */ + if (no_copyback) return r; + if (r == IMAP_AGAIN) { r = mailbox_full_update(local->name); if (!r) r = update_mailbox_once(local, remote, reserve_guids, 1); @@ -1721,28 +1740,6 @@ /* ====================================================================== */ -static void folderlist_remove(struct sync_folder_list *list, const char *prefix) -{ - struct sync_folder *item; - - for (item = list->head; item; item = item->next) { - if (mboxname_is_prefix(item->name, prefix)) - item->mark = 1; - } -} - -static void renamelist_remove(struct sync_rename_list *list, const char *prefix) -{ - struct sync_rename *item; - - for (item = list->head; item; item = item->next) { - if (mboxname_is_prefix(item->oldname, prefix)) { - list->done++; - item->done = 1; - } - } -} - static int do_folders(struct sync_name_list *mboxname_list, struct sync_folder_list *replica_folders, int delete_remote) { @@ -1772,22 +1769,8 @@ if (rfolder->mark) continue; rfolder->mark = 1; - /* does it need a rename? */ - if (strcmp(mfolder->name, rfolder->name)) { - sync_rename_list_add(rename_folders, mfolder->uniqueid, rfolder->name, - mfolder->name, mfolder->part, mfolder->uidvalidity); - if (mboxname_isusermailbox(mfolder->name, 1) && - mboxname_isusermailbox(rfolder->name, 1)) { - /* user rename - strip all other folders from consideration. They - * will be picked up by the full user sync later */ - folderlist_remove(master_folders, mfolder->name); - renamelist_remove(rename_folders, rfolder->name); - folderlist_remove(replica_folders, rfolder->name); - } - } - - /* partition change is a rename too */ - else if (strcmp(mfolder->part, rfolder->part)) { + /* does it need a rename? partition change is a rename too */ + if (strcmp(mfolder->name, rfolder->name) || strcmp(mfolder->part, rfolder->part)) { sync_rename_list_add(rename_folders, mfolder->uniqueid, rfolder->name, mfolder->name, mfolder->part, mfolder->uidvalidity); } @@ -1933,20 +1916,27 @@ struct sync_name_list *quotalist; }; -static int do_mailbox_info(char *name, - int matchlen __attribute__((unused)), - int maycreate __attribute__((unused)), - void *rock) +static int do_mailbox_info(void *rock, + const char *key, size_t keylen, + const char *data __attribute__((unused)), + size_t datalen __attribute__((unused))) { - int r; struct mailbox *mailbox = NULL; struct mboxinfo *info = (struct mboxinfo *)rock; + char *name = xstrndup(key, keylen); + int r = 0; r = mailbox_open_irl(name, &mailbox); /* doesn't exist? Probably not finished creating or removing yet */ - if (r == IMAP_MAILBOX_NONEXISTENT) return 0; - if (r == IMAP_MAILBOX_RESERVED) return 0; - if (r) return r; + if (r == IMAP_MAILBOX_NONEXISTENT) { + r = 0; + goto done; + } + if (r == IMAP_MAILBOX_RESERVED) { + r = 0; + goto done; + } + if (r) goto done; if (info->quotalist && mailbox->quotaroot) { if (!sync_name_lookup(info->quotalist, mailbox->quotaroot)) @@ -1957,7 +1947,10 @@ addmbox(name, 0, 0, info->mboxlist); - return 0; +done: + free(name); + + return r; } static int do_user_quota(struct sync_name_list *master_quotaroots, @@ -2002,27 +1995,23 @@ info.mboxlist = mboxname_list; info.quotalist = master_quotaroots; + /* XXX - convert all this to an allusermbox */ + /* Generate full list of folders on client side */ (sync_namespace.mboxname_tointernal)(&sync_namespace, "INBOX", user, buf); - do_mailbox_info(buf, 0, 0, &info); + do_mailbox_info(&info, buf, strlen(buf), NULL, 0); /* deleted namespace items if enabled */ if (mboxlist_delayed_delete_isenabled()) { char deletedname[MAX_MAILBOX_BUFFER]; mboxname_todeleted(buf, deletedname, 0); - strlcat(deletedname, ".*", sizeof(deletedname)); - r = (sync_namespace.mboxlist_findall)(&sync_namespace, deletedname, 1, - user, NULL, do_mailbox_info, - &info); + r = mboxlist_allmbox(deletedname, do_mailbox_info, &info, /*incdel*/1); } /* subfolders */ if (!r) { - strlcat(buf, ".*", sizeof(buf)); - r = (sync_namespace.mboxlist_findall)(&sync_namespace, buf, 1, - user, NULL, do_mailbox_info, - &info); + r = mboxlist_allmbox(buf, do_mailbox_info, &info, /*incdel*/0); } /* we know all the folders present on the master, so it's safe to delete @@ -2294,21 +2283,6 @@ } } -static void remove_folder(char *name, struct sync_action_list *list, - int chk_child) -{ - struct sync_action *action; - size_t len = strlen(name); - - for (action = list->head ; action ; action = action->next) { - if (!strncmp(name, action->name, len) && - ((action->name[len] == '\0') || - (chk_child && (action->name[len] == '.')))) { - action->active = 0; - } - } -} - /* ====================================================================== */ static int do_sync_mailboxes(struct sync_name_list *mboxname_list, @@ -2402,61 +2376,21 @@ /* Optimise out redundant clauses */ for (action = user_list->head; action; action = action->next) { - char inboxname[MAX_MAILBOX_BUFFER]; - char deletedname[MAX_MAILBOX_BUFFER]; - - /* USER action overrides any MAILBOX action on any of the - * user's mailboxes or any META, SEEN or SUB/UNSUB - * action for same user */ - (sync_namespace.mboxname_tointernal)(&sync_namespace, "INBOX", - action->user, inboxname); - remove_folder(inboxname, mailbox_list, 1); - remove_folder(inboxname, unmailbox_list, 1); - - /* remove deleted namespace items as well */ - if (mboxlist_delayed_delete_isenabled()) { - mboxname_todeleted(inboxname, deletedname, 0); - remove_folder(deletedname, mailbox_list, 1); - remove_folder(deletedname, unmailbox_list, 1); - } - /* remove per-user items */ remove_meta(action->user, meta_list); remove_meta(action->user, seen_list); remove_meta(action->user, sub_list); - - /* add back in inbox, because it may have a rename to detect, woot */ - sync_action_list_add(mailbox_list, inboxname, NULL); } /* duplicate removal for unuser - we also strip all the user events */ for (action = unuser_list->head; action; action = action->next) { - char inboxname[MAX_MAILBOX_BUFFER]; - char deletedname[MAX_MAILBOX_BUFFER]; - - /* USER action overrides any MAILBOX action on any of the - * user's mailboxes or any META, SEEN or SUB/UNSUB - * action for same user */ - (sync_namespace.mboxname_tointernal)(&sync_namespace, "INBOX", - action->user, inboxname); - remove_folder(inboxname, mailbox_list, 1); - remove_folder(inboxname, unmailbox_list, 1); - - /* remove deleted namespace items as well */ - if (mboxlist_delayed_delete_isenabled()) { - mboxname_todeleted(inboxname, deletedname, 0); - remove_folder(deletedname, mailbox_list, 1); - remove_folder(deletedname, unmailbox_list, 1); - } - /* remove per-user items */ remove_meta(action->user, meta_list); remove_meta(action->user, seen_list); remove_meta(action->user, sub_list); - remove_meta(action->user, user_list); - /* add back in inbox, because it may have a rename to detect, woot */ - sync_action_list_add(mailbox_list, inboxname, NULL); + /* unuser trumps user */ + remove_meta(action->user, user_list); } for (action = meta_list->head; action; action = action->next) { @@ -2994,6 +2928,39 @@ } } +static int cb_allmbox(void *rock __attribute__((unused)), + const char *key, size_t keylen, + const char *val __attribute__((unused)), + size_t vallen __attribute__((unused))) +{ + char *mboxname = xstrndup(key, keylen); + const char *userid; + int r = 0; + + if (mboxname_isdeletedmailbox(mboxname, NULL)) + goto done; + + userid = mboxname_to_userid(mboxname); + + if (userid && strcmpsafe(userid, prev_userid)) { + printf("%s\n", userid); + r = do_user(userid); + if (r) { + if (verbose) + fprintf(stderr, "Error from do_user(%s): bailing out!\n", userid); + syslog(LOG_ERR, "Error in do_user(%s): bailing out!", userid); + goto done; + } + free(prev_userid); + prev_userid = xstrdup(userid); + } + +done: + free(mboxname); + + return r; +} + /* ====================================================================== */ static struct sasl_callback mysasl_cb[] = { @@ -3006,6 +2973,7 @@ MODE_UNKNOWN = -1, MODE_REPEAT, MODE_USER, + MODE_ALLUSER, MODE_MAILBOX, MODE_META }; @@ -3036,7 +3004,7 @@ setbuf(stdout, NULL); - while ((opt = getopt(argc, argv, "C:vlS:F:f:w:t:d:n:rRumsoz")) != EOF) { + while ((opt = getopt(argc, argv, "C:vlS:F:f:w:t:d:n:rRumsozOA")) != EOF) { switch (opt) { case 'C': /* alt config file */ alt_config = optarg; @@ -3093,6 +3061,12 @@ mode = MODE_REPEAT; break; + case 'A': + if (mode != MODE_UNKNOWN) + fatal("Mutually exclusive options defined", EC_USAGE); + mode = MODE_ALLUSER; + break; + case 'u': if (mode != MODE_UNKNOWN) fatal("Mutually exclusive options defined", EC_USAGE); @@ -3119,6 +3093,11 @@ #endif break; + case 'O': + /* don't copy changes back from server */ + no_copyback = 1; + break; + default: usage("sync_client"); } @@ -3230,6 +3209,16 @@ replica_disconnect(); break; + case MODE_ALLUSER: + /* Open up connection to server */ + replica_connect(channel); + + if (mboxlist_allmbox(optind < argc ? argv[optind] : NULL, cb_allmbox, NULL, 0)) + exit_rc = 1; + + replica_disconnect(); + break; + case MODE_MAILBOX: /* Open up connection to server */ replica_connect(channel);
View file
cyrus-imapd-2.5.tar.gz/imap/sync_reset.c
Changed
@@ -126,39 +126,11 @@ static int reset_single(const char *userid) { - struct sync_name_list *list = NULL; + struct sync_name_list *sublist = sync_name_list_create(); + struct sync_name_list *mblist = sync_name_list_create(); struct sync_name *item; - char buf[MAX_MAILBOX_NAME]; int r = 0; - /* Nuke subscriptions */ - list = sync_name_list_create(); - r = mboxlist_allsubs(userid, addmbox_sub, list); - if (r) goto fail; - - /* ignore failures here - the subs file gets deleted soon anyway */ - for (item = list->head; item; item = item->next) { - (void)mboxlist_changesub(item->name, userid, sync_authstate, 0, 0, 0); - } - sync_name_list_free(&list); - - /* Nuke normal folders */ - list = sync_name_list_create(); - - (sync_namespacep->mboxname_tointernal)(sync_namespacep, "INBOX", - userid, buf); - - /* deleted namespace items if enabled */ - if (mboxlist_delayed_delete_isenabled()) { - char deletedname[MAX_MAILBOX_BUFFER]; - mboxname_todeleted(buf, deletedname, 0); - strlcat(deletedname, ".*", sizeof(deletedname)); - r = (sync_namespace.mboxlist_findall)(&sync_namespace, deletedname, 1, - sync_userid, sync_authstate, - addmbox, (void *)list); - if (r) goto fail; - } - /* XXX: adding an entry to userdeny_db here would avoid the need to * protect against new logins with external proxy rules - Cyrus could * maintain its own safety */ @@ -166,29 +138,34 @@ /* first, disconnect all current connections for this user */ proc_killuser(userid); - strlcat(buf, ".*", sizeof(buf)); - r = (sync_namespacep->mboxlist_findall)(sync_namespacep, buf, 1, - sync_userid, sync_authstate, - addmbox, (void *)list); + r = mboxlist_allsubs(userid, addmbox_sub, sublist); if (r) goto fail; - for (item = list->head; item; item = item->next) { - r = mboxlist_deletemailbox(item->name, 1, sync_userid, - sync_authstate, NULL, 0, 1, 0); - if (r) goto fail; + /* ignore failures here - the subs file gets deleted soon anyway */ + for (item = sublist->head; item; item = item->next) { + (void)mboxlist_changesub(item->name, userid, sync_authstate, 0, 0, 0); } - /* Nuke inbox (recursive nuke possible?) */ - (sync_namespacep->mboxname_tointernal)(sync_namespacep, "INBOX", - userid, buf); - r = mboxlist_deletemailbox(buf, 1, sync_userid, - sync_authstate, NULL, 0, 1, 0); - if (r && (r != IMAP_MAILBOX_NONEXISTENT)) goto fail; + r = mboxlist_allusermbox(userid, addmbox_sub, mblist, /*incdel*/1); + if (r) goto fail; + + for (item = mblist->head; item; item = item->next) { + r = mboxlist_deletemailbox(item->name, 1, sync_userid, + sync_authstate, NULL, 0, 1, 0); + if (r == IMAP_MAILBOX_NONEXISTENT) { + printf("skipping already removed mailbox %s\n", item->name); + } + else if (r) goto fail; + /* XXX - cheap and nasty hack around actually cleaning up the entry */ + r = mboxlist_deleteremote(item->name, NULL); + if (r) goto fail; + } r = user_deletedata(userid, 1); fail: - sync_name_list_free(&list); + sync_name_list_free(&sublist); + sync_name_list_free(&mblist); return r; }
View file
cyrus-imapd-2.5.tar.gz/imap/sync_server.c
Changed
@@ -74,6 +74,11 @@ #include "annotate.h" #include "append.h" #include "auth.h" +#ifdef WITH_DAV +#include "caldav_db.h" +#include "carddav_db.h" +#include "dav_db.h" +#endif /* WITH_DAV */ #include "dlist.h" #include "exitcodes.h" #include "global.h" @@ -129,6 +134,8 @@ static int sync_starttls_done = 0; static int sync_compress_done = 0; +static int opt_force = 0; + /* commands that have specific names */ static void cmdloop(void); static void cmd_authenticate(char *mech, char *resp); @@ -216,6 +223,7 @@ sync_saslconn = NULL; } sync_starttls_done = 0; + sync_compress_done = 0; if(saslprops.iplocalport) { free(saslprops.iplocalport); @@ -252,11 +260,14 @@ /* load the SASL plugins */ global_sasl_init(1, 1, mysasl_cb); - while ((opt = getopt(argc, argv, "p:")) != EOF) { + while ((opt = getopt(argc, argv, "p:f")) != EOF) { switch(opt) { case 'p': /* external protection */ extprops_ssf = atoi(optarg); break; + case 'f': + opt_force = 1; + break; default: usage(); } @@ -284,6 +295,12 @@ statuscache_open(); } +#ifdef WITH_DAV + dav_init(); + caldav_init(); + carddav_init(); +#endif + return 0; } @@ -444,6 +461,12 @@ proc_cleanup(); +#ifdef WITH_DAV + carddav_done(); + caldav_done(); + dav_done(); +#endif + if (config_getswitch(IMAPOPT_STATUSCACHE)) { statuscache_close(); statuscache_done(); @@ -1246,32 +1269,42 @@ /* found a match, check for updates */ if (rrecord.uid == mrecord.uid) { + /* if they're both EXPUNGED then ignore everything else */ + if ((mrecord.system_flags & FLAG_EXPUNGED) && + (rrecord.system_flags & FLAG_EXPUNGED)) + continue; + /* higher modseq on the replica is an error */ if (rrecord.modseq > mrecord.modseq) { - syslog(LOG_ERR, "SYNCERROR: higher modseq on replica %s %u", + if (opt_force) { + syslog(LOG_NOTICE, "forcesync: higher modseq on replica %s %u (" MODSEQ_FMT " > " MODSEQ_FMT ")", + mailbox->name, mrecord.uid, rrecord.modseq, mrecord.modseq); + } + else { + syslog(LOG_ERR, "SYNCERROR: higher modseq on replica %s %u (" MODSEQ_FMT " > " MODSEQ_FMT ")", + mailbox->name, mrecord.uid, rrecord.modseq, mrecord.modseq); + r = IMAP_SYNC_CHECKSUM; + goto out; + } + } + + /* GUID mismatch is an error straight away, it only ever happens if we + * had a split brain - and it will take a full sync to sort out the mess */ + if (!message_guid_equal(&mrecord.guid, &rrecord.guid)) { + syslog(LOG_ERR, "SYNCERROR: guid mismatch %s %u", mailbox->name, mrecord.uid); r = IMAP_SYNC_CHECKSUM; goto out; } - /* everything else only matters if we're not expunged */ - if (!(mrecord.system_flags & FLAG_EXPUNGED)) { - /* GUID mismatch on a non-expunged record is an error straight away */ - if (!message_guid_equal(&mrecord.guid, &rrecord.guid)) { - syslog(LOG_ERR, "SYNCERROR: guid mismatch %s %u", - mailbox->name, mrecord.uid); - r = IMAP_SYNC_CHECKSUM; - goto out; - } - - /* if it's already expunged on the replica, but alive on the master, - * that's bad */ - if (rrecord.system_flags & FLAG_EXPUNGED) { - syslog(LOG_ERR, "SYNCERROR: expunged on replica %s %u", - mailbox->name, mrecord.uid); - r = IMAP_SYNC_CHECKSUM; - goto out; - } + /* if it's already expunged on the replica, but alive on the master, + * that's bad */ + if (!(mrecord.system_flags & FLAG_EXPUNGED) && + (rrecord.system_flags & FLAG_EXPUNGED)) { + syslog(LOG_ERR, "SYNCERROR: expunged on replica %s %u", + mailbox->name, mrecord.uid); + r = IMAP_SYNC_CHECKSUM; + goto out; } /* skip out on the first pass */ @@ -1280,8 +1313,8 @@ rrecord.modseq = mrecord.modseq; rrecord.last_updated = mrecord.last_updated; rrecord.internaldate = mrecord.internaldate; - rrecord.system_flags = (mrecord.system_flags & ~FLAG_UNLINKED) | - (rrecord.system_flags & FLAG_UNLINKED); + rrecord.system_flags = (mrecord.system_flags & FLAGS_GLOBAL) | + (rrecord.system_flags & FLAGS_LOCAL); for (i = 0; i < MAX_USER_FLAGS/32; i++) rrecord.user_flags[i] = mrecord.user_flags[i]; @@ -1440,35 +1473,63 @@ annotate_state_begin(astate); if (strcmp(mailbox->uniqueid, uniqueid)) { - syslog(LOG_ERR, "Mailbox uniqueid changed %s (%s => %s) - retry", - mboxname, mailbox->uniqueid, uniqueid); - r = IMAP_MAILBOX_MOVED; - goto done; + if (opt_force) { + syslog(LOG_NOTICE, "forcesync: fixing uniqueid %s (%s => %s)", + mboxname, mailbox->uniqueid, uniqueid); + free(mailbox->uniqueid); + mailbox->uniqueid = xstrdup(uniqueid); + mailbox->header_dirty = 1; + } + else { + syslog(LOG_ERR, "Mailbox uniqueid changed %s (%s => %s) - retry", + mboxname, mailbox->uniqueid, uniqueid); + r = IMAP_MAILBOX_MOVED; + goto done; + } } /* skip out now, it's going to mismatch for sure! */ if (highestmodseq < mailbox->i.highestmodseq) { - syslog(LOG_ERR, "higher modseq on replica %s - " - MODSEQ_FMT " < " MODSEQ_FMT, - mboxname, highestmodseq, mailbox->i.highestmodseq); - r = IMAP_SYNC_CHECKSUM; - goto done; + if (opt_force) { + syslog(LOG_NOTICE, "forcesync: higher modseq on replica %s - " + MODSEQ_FMT " < " MODSEQ_FMT, + mboxname, highestmodseq, mailbox->i.highestmodseq); + } + else { + syslog(LOG_ERR, "higher modseq on replica %s - " + MODSEQ_FMT " < " MODSEQ_FMT, + mboxname, highestmodseq, mailbox->i.highestmodseq); + r = IMAP_SYNC_CHECKSUM; + goto done; + } } /* skip out now, it's going to mismatch for sure! */ if (uidvalidity < mailbox->i.uidvalidity) { - syslog(LOG_ERR, "higher uidvalidity on replica %s - %u < %u", - mboxname, uidvalidity, mailbox->i.uidvalidity); - r = IMAP_SYNC_CHECKSUM; - goto done; + if (opt_force) { + syslog(LOG_NOTICE, "forcesync: higher uidvalidity on replica %s - %u < %u", + mboxname, uidvalidity, mailbox->i.uidvalidity); + } + else { + syslog(LOG_ERR, "higher uidvalidity on replica %s - %u < %u", + mboxname, uidvalidity, mailbox->i.uidvalidity); + r = IMAP_SYNC_CHECKSUM; + goto done; + } } /* skip out now, it's going to mismatch for sure! */ if (last_uid < mailbox->i.last_uid) { - syslog(LOG_ERR, "higher last_uid on replica %s - %u < %u", - mboxname, last_uid, mailbox->i.last_uid); - r = IMAP_SYNC_CHECKSUM; - goto done; + if (opt_force) { + syslog(LOG_NOTICE, "forcesync: higher last_uid on replica %s - %u < %u", + mboxname, last_uid, mailbox->i.last_uid); + } + else { + syslog(LOG_ERR, "higher last_uid on replica %s - %u < %u", + mboxname, last_uid, mailbox->i.last_uid); + r = IMAP_SYNC_CHECKSUM; + goto done; + } } /* always take the ACL from the master, it's not versioned */ @@ -1502,7 +1563,9 @@ } mailbox_index_dirty(mailbox); - assert(mailbox->i.last_uid <= last_uid); + if (!opt_force) { + assert(mailbox->i.last_uid <= last_uid); + } mailbox->i.last_uid = last_uid; mailbox->i.recentuid = recentuid; mailbox->i.recenttime = recenttime; @@ -1519,8 +1582,8 @@ } /* this happens rarely, so let us know */ - if (mailbox->i.uidvalidity < uidvalidity) { - syslog(LOG_ERR, "%s uidvalidity higher on master, updating %u => %u", + if (mailbox->i.uidvalidity != uidvalidity) { + syslog(LOG_NOTICE, "%s uidvalidity changed, updating %u => %u", mailbox->name, mailbox->i.uidvalidity, uidvalidity); mailbox->i.uidvalidity = uidvalidity; }
View file
cyrus-imapd-2.5.tar.gz/imap/sync_support.c
Changed
@@ -1124,8 +1124,8 @@ { struct sync_annot *item = xzmalloc(sizeof(struct sync_annot)); - item->entry = xstrdup(entry); - item->userid = xstrdup(userid); + item->entry = xstrdupnull(entry); + item->userid = xstrdupnull(userid); buf_copy(&item->value, value); item->mark = 0; @@ -1703,9 +1703,9 @@ diff++; if (!diff) - diff = strcmp(a->entry, b->entry); + diff = strcmpnull(a->entry, b->entry); if (!diff) - diff = strcmp(a->userid, b->userid); + diff = strcmpnull(a->userid, b->userid); if (!diff && diff_value) diff = buf_cmp(&a->value, &b->value);
View file
cyrus-imapd-2.5.tar.gz/imap/tls.c
Changed
@@ -812,7 +812,7 @@ } else { STACK_OF(X509_NAME) *CAnames = SSL_load_client_CA_file(client_ca_file); - if (!CAnames || sk_num(CAnames) < 1) { + if (!CAnames || sk_num((_STACK *)CAnames) < 1) { syslog(LOG_ERR, "TLS server engine: No client CA certs specified. " "Client side certs may not work"); @@ -1359,12 +1359,12 @@ } } - if (var_server_cert && strlen(var_server_cert) == 0) + if (!var_server_cert || strlen(var_server_cert) == 0) client_cert = NULL; else client_cert = xstrdup(var_server_cert); - if (var_server_key && strlen(var_server_key) == 0) + if (!var_server_key || strlen(var_server_key) == 0) client_key = NULL; else client_key = xstrdup(var_server_key);
View file
cyrus-imapd-2.5.tar.gz/imap/tls_th-lock.c
Changed
@@ -46,16 +46,15 @@ OPENSSL_free(lock_count); } -void pthreads_locking_callback( - int mode, - int type, - char *file, - int line -) { +void pthreads_locking_callback(int mode, int type, + char *file __attribute__((unused)), + int line __attribute__((unused))) +{ if (mode & CRYPTO_LOCK) { pthread_mutex_lock(&(lock_cs[type])); lock_count[type]++; - } else { + } + else { pthread_mutex_unlock(&(lock_cs[type])); } }
View file
cyrus-imapd-2.5.tar.gz/imap/tz_err.et
Added
@@ -0,0 +1,72 @@ +# timezone_err.et -- Error codes for the Cyrus Timezone Service +# +# Copyright (c) 1994-2014 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. +# + +error_table tz + +ec TZ_INVALID_ACTION, + "invalid-action" + +ec TZ_INVALID_TZID, + "invalid-tzid" + +ec TZ_INVALID_PATTERN, + "invalid-pattern" + +ec TZ_INVALID_FORMAT, + "invalid-format" + +ec TZ_INVALID_START, + "invalid-start" + +ec TZ_INVALID_END, + "invalid-end" + +ec TZ_INVALID_CHANGEDSINCE, + "invalid-changedsince" + +ec TZ_INVALID_TRUNCATE, + "invalid-truncate" + +ec TZ_NOT_FOUND, + "tzid-not-found" + +end
View file
cyrus-imapd-2.5.tar.gz/imap/unexpunge.c
Changed
@@ -71,14 +71,15 @@ static int verbose = 0; static int unsetdeleted = 0; +static const char *addflag = NULL; static void usage(void) { fprintf(stderr, "unexpunge [-C <altconfig>] -l <mailbox>\n" - "unexpunge [-C <altconfig>] -t time-interval [ -d ] [ -v ] mailbox\n" - "unexpunge [-C <altconfig>] -a [-d] [-v] <mailbox>\n" - "unexpunge [-C <altconfig>] -u [-d] [-v] <mailbox> <uid>...\n"); + "unexpunge [-C <altconfig>] -t time-interval [-d] [-v] [-f flag] mailbox\n" + "unexpunge [-C <altconfig>] -a [-d] [-v] [-f flag] <mailbox>\n" + "unexpunge [-C <altconfig>] -u [-d] [-v] [-f flag] <mailbox> <uid>...\n"); exit(-1); } @@ -173,18 +174,20 @@ const char *mboxname) { uint32_t recno; - uint32_t olduid; struct index_record record; + struct index_record newrecord; + annotate_state_t *astate = NULL; unsigned uidnum = 0; char oldfname[MAX_MAILBOX_PATH]; const char *fname; + const char *userid = mboxname_to_userid(mailbox->name); int r = 0; *numrestored = 0; for (recno = 1; recno <= mailbox->i.num_records; recno++) { r = mailbox_read_index_record(mailbox, recno, &record); - if (r) return r; + if (r) goto done; /* still active */ if (!(record.system_flags & FLAG_EXPUNGED)) @@ -208,38 +211,62 @@ /* otherwise we want this one */ } - /* mark the old one unlinked so we don't see it again */ - olduid = record.uid; - record.system_flags |= FLAG_UNLINKED; - r = mailbox_rewrite_index_record(mailbox, &record); - if (r) return r; + /* work on a copy */ + newrecord = record; /* duplicate the old filename */ - fname = mailbox_message_fname(mailbox, olduid); + fname = mailbox_message_fname(mailbox, record.uid); xstrncpy(oldfname, fname, MAX_MAILBOX_PATH); /* bump the UID, strip the flags */ - record.uid = mailbox->i.last_uid + 1; - record.system_flags &= ~(FLAG_UNLINKED|FLAG_EXPUNGED); + newrecord.uid = mailbox->i.last_uid + 1; + newrecord.system_flags &= ~FLAG_EXPUNGED; if (unsetdeleted) - record.system_flags &= ~FLAG_DELETED; + newrecord.system_flags &= ~FLAG_DELETED; /* copy the message file */ - fname = mailbox_message_fname(mailbox, record.uid); + fname = mailbox_message_fname(mailbox, newrecord.uid); r = mailbox_copyfile(oldfname, fname, 0); - if (r) return r; + if (r) goto done; + + /* add the flag if requested */ + if (addflag) { + int userflag = 0; + r = mailbox_user_flag(mailbox, addflag, &userflag, 1); + if (r) goto done; + newrecord.user_flags[userflag/32] |= 1<<(userflag&31); + } /* and append the new record */ - mailbox_append_index_record(mailbox, &record); + r = mailbox_append_index_record(mailbox, &newrecord); + if (r) goto done; + + /* ensure we have an astate connected to the destination + * mailbox, so that the annotation txn will be committed + * when we close the mailbox */ + r = mailbox_get_annotate_state(mailbox, newrecord.uid, &astate); + if (r) goto done; + + /* and copy over any annotations */ + r = annotate_msg_copy(mailbox, record.uid, + mailbox, newrecord.uid, + userid); + if (r) goto done; if (verbose) printf("Unexpunged %s: %u => %u\n", - mboxname, olduid, record.uid); + mboxname, record.uid, newrecord.uid); + + /* mark the old one unlinked so we don't see it again */ + record.system_flags |= FLAG_UNLINKED; + r = mailbox_rewrite_index_record(mailbox, &record); + if (r) goto done; (*numrestored)++; } - return 0; +done: + return r; } int main(int argc, char *argv[]) @@ -261,7 +288,7 @@ fatal("must run as the Cyrus user", EC_USAGE); } - while ((opt = getopt(argc, argv, "C:laudt:v")) != EOF) { + while ((opt = getopt(argc, argv, "C:laudt:f:v")) != EOF) { switch (opt) { case 'C': /* alt config file */ alt_config = optarg; @@ -312,6 +339,10 @@ unsetdeleted = 1; break; + case 'f': + addflag = optarg; + break; + case 'v': verbose = 1; break; @@ -337,6 +368,11 @@ sync_log_init(); + if (addflag && addflag[0] == '\\') { + syslog(LOG_ERR, "can't set a system flag"); + fatal("can't set a system flag", EC_SOFTWARE); + } + /* Set namespace -- force standard (internal) */ if ((r = mboxname_init_namespace(&unex_namespace, 1)) != 0) { syslog(LOG_ERR, "%s", error_message(r));
View file
cyrus-imapd-2.5.tar.gz/imap/upgrade_index.c
Deleted
@@ -1,425 +0,0 @@ -/* upgrade_index.c -- Mailbox upgrade routines - * - * 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. - */ -#include <config.h> - -#ifdef HAVE_UNISTD_H -#include <unistd.h> -#endif -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <syslog.h> -#include <utime.h> - -#ifdef HAVE_DIRENT_H -# include <dirent.h> -# define NAMLEN(dirent) strlen((dirent)->d_name) -#else -# define dirent direct -# define NAMLEN(dirent) (dirent)->d_namlen -# if HAVE_SYS_NDIR_H -# include <sys/ndir.h> -# endif -# if HAVE_SYS_DIR_H -# include <sys/dir.h> -# endif -# if HAVE_NDIR_H -# include <ndir.h> -# endif -#endif - -#include "annotate.h" -#include "assert.h" -#include "crc32.h" -#include "global.h" -#include "imap/imap_err.h" -#include "mailbox.h" -#include "message.h" -#include "map.h" -#include "seen.h" -#include "util.h" -#include "sequence.h" -#include "xmalloc.h" - -struct expunge_data { - uint32_t uid; - const char *base; -}; - -static int sort_expunge(const void *a, const void *b) -{ - struct expunge_data *ea = (struct expunge_data *)a; - struct expunge_data *eb = (struct expunge_data *)b; - return ea->uid - eb->uid; -} - -static void upgrade_index_record(struct mailbox *mailbox, - const char *buf, - struct index_record *record, - int record_size, - int oldversion) -{ - char recordbuf[INDEX_RECORD_SIZE]; - struct utimbuf settime; - const char *fname; - - assert(record_size <= INDEX_RECORD_SIZE); - - memset(recordbuf, 0, INDEX_RECORD_SIZE); - memcpy(recordbuf, buf, record_size); - - /* do the initial parse. Ignore the result, crc32 will mismatch - * for sure */ - mailbox_buf_to_index_record(recordbuf, record); - - if (oldversion == 12) { - /* avoid re-parsing the message by copying the old cache_crc, - * but only if the old RECORD_CRC matches */ - if (crc32_map(buf, 92) == ntohl(*((bit32 *)(buf+92)))) { - record->cache_crc = ntohl(*((bit32 *)(buf+88))); - /* we need to read the cache record in here, so that repack - * will write it out to a new cache file */ - if (!mailbox_cacherecord(mailbox, record)) - return; - /* record failed, drop through */ - record->cache_offset = 0; - } - /* CRC failed, drop through */ - } - - fname = mailbox_message_fname(mailbox, record->uid); - - if (message_parse(fname, record)) { - /* failed to create, don't try to write */ - record->crec.len = 0; - /* and the record is expunged too! */ - record->system_flags |= FLAG_EXPUNGED | FLAG_UNLINKED; - syslog(LOG_ERR, "IOERROR: FATAL - failed to parse " - "file %s for upgrade, expunging", fname); - return; - } - - /* update the mtime to match the internaldate */ - settime.actime = settime.modtime = record->internaldate; - utime(fname, &settime); -} - -/* - * Upgrade an index/expunge file for 'mailbox' - */ -HIDDEN int upgrade_index(struct mailbox *mailbox) -{ - uint32_t recno, erecno; - uint32_t uid, euid; - unsigned long oldmapnum; - unsigned long oldnum_records; - unsigned long expunge_num = 0; - uint32_t oldminor_version, oldstart_offset, oldrecord_size; - indexbuffer_t headerbuf; - const char *bufp = NULL; - char *hbuf = (char *)headerbuf.buf; - const char *fname; - const char *datadirname; - struct stat sbuf; - struct seqset *seq = NULL; - struct index_record record; - int expunge_fd = -1; - const char *expunge_base = NULL; - size_t expunge_len = 0; /* mapped size */ - unsigned long emapnum; - bit32 eversion = 0, eoffset = 0, expungerecord_size = 0; - struct expunge_data *expunge_data = NULL; - struct mailbox_repack *repack = NULL; - int r; - - if (mailbox->index_size < OFFSET_NUM_RECORDS) - return IMAP_MAILBOX_BADFORMAT; - - oldminor_version = ntohl(*((bit32 *)(mailbox->index_base+OFFSET_MINOR_VERSION))); - oldstart_offset = ntohl(*((bit32 *)(mailbox->index_base+OFFSET_START_OFFSET))); - oldrecord_size = ntohl(*((bit32 *)(mailbox->index_base+OFFSET_RECORD_SIZE))); - oldnum_records = ntohl(*((bit32 *)(mailbox->index_base+OFFSET_NUM_RECORDS))); - - /* bogus data at the start of the index file? */ - if (!oldstart_offset || !oldrecord_size) - return IMAP_MAILBOX_BADFORMAT; - - oldmapnum = (mailbox->index_size - oldstart_offset) / oldrecord_size; - if (oldmapnum < oldnum_records) { - syslog(LOG_ERR, "upgrade: %s map doesn't fit, shrinking index %lu to %lu", - mailbox->name, oldnum_records, oldmapnum); - oldnum_records = oldmapnum; - } - - /* check if someone else already upgraded the index! */ - if (oldminor_version == MAILBOX_MINOR_VERSION) - goto done; - - /* check that the data directory exists. If not, it may be that - * something isn't correctly mounted. We don't want to wipe out - * all the index records due to IOERRORs just because the admin - * made a temporary mistake */ - datadirname = mailbox_message_fname(mailbox, 0); - if (stat(datadirname, &sbuf)) { - syslog(LOG_ERR, "IOERROR: unable to find data directory %s " - "for mailbox %s, refusing to upgrade", - datadirname, mailbox->name); - return IMAP_IOERROR; - } - - /* Copy existing header so we can upgrade it */ - memset(hbuf, 0, INDEX_HEADER_SIZE); - if (oldstart_offset > INDEX_HEADER_SIZE) - memcpy(hbuf, mailbox->index_base, INDEX_HEADER_SIZE); - else - memcpy(hbuf, mailbox->index_base, oldstart_offset); - - /* QUOTA_MAILBOX_USED changed to 64 bit added with minor version 6 */ - if (oldminor_version < 6) { - /* upgrade quota to 64-bits (bump existing fields) */ - memmove(hbuf+OFFSET_QUOTA_MAILBOX_USED + 4, hbuf+OFFSET_QUOTA_MAILBOX_USED, - INDEX_HEADER_SIZE - (OFFSET_QUOTA_MAILBOX_USED + 4)); - /* zero the unused 32-bits */ - *((bit32 *)(hbuf+OFFSET_QUOTA_MAILBOX_USED)) = htonl(0); - } - - /* ignore the result - we EXPECT a CRC32 mismatch */ - mailbox_buf_to_index_header(hbuf, &mailbox->i); - - /* new version fields */ - mailbox->i.minor_version = MAILBOX_MINOR_VERSION; - mailbox->i.start_offset = INDEX_HEADER_SIZE; - mailbox->i.record_size = INDEX_RECORD_SIZE; - - /* upgrade other fields as necessary */ - if (!mailbox->i.highestmodseq) - mailbox->i.highestmodseq = 1; - if (!mailbox->i.uidvalidity) - mailbox->i.uidvalidity = time(0); - - /* minor version wasn't updated religiously in the early days, - * so we need to use the old offset instead */ - if (oldstart_offset < OFFSET_MAILBOX_OPTIONS) - mailbox->i.options = config_getint(IMAPOPT_MAILBOX_DEFAULT_OPTIONS); - - if (oldminor_version < 12) { - struct seendata sd = SEENDATA_INITIALIZER; - const char *owner_userid; - - /* remove the CONDSTORE option - it's implicit now */ - mailbox->i.options &= ~OPT_IMAP_CONDSTORE; - - if (mailbox->i.options & OPT_IMAP_SHAREDSEEN) - owner_userid = "anyone"; - else - owner_userid = mboxname_to_userid(mailbox->name); - - r = mailbox_read_header(mailbox, NULL); - if (r) goto fail; - - /* NEW HEADER FIELDS */ - - /* we'll set this if there are expunged records */ - mailbox->i.first_expunged = 0; - /* we can't know about deletions before the current modseq */ - mailbox->i.deletedmodseq = mailbox->i.highestmodseq; - /* bootstrap CRC matching */ - mailbox->i.header_file_crc = mailbox->header_file_crc; - - /* set up seen tracking for user inside the mailbox */ - if (!owner_userid) { - r = IMAP_MAILBOX_NONEXISTENT; - } else { - struct seen *seendb = NULL; - r = seen_open(owner_userid, SEEN_SILENT, &seendb); - if (!r) r = seen_read(seendb, mailbox->uniqueid, &sd); - seen_close(&seendb); - } - if (r) { /* no seen data? */ - mailbox->i.recentuid = 0; - mailbox->i.recenttime = 0; - } - else { - mailbox->i.recentuid = sd.lastuid; - mailbox->i.recenttime = sd.lastchange; - seq = seqset_parse(sd.seenuids, NULL, sd.lastuid); - seen_freedata(&sd); - } - - /* check for expunge */ - fname = mailbox_meta_fname(mailbox, META_EXPUNGE); - expunge_fd = open(fname, O_RDWR, 0666); - if (expunge_fd == -1) goto no_expunge; - - r = fstat(expunge_fd, &sbuf); - if (r == -1) goto no_expunge; - - if (sbuf.st_size < INDEX_HEADER_SIZE) goto no_expunge; - map_refresh(expunge_fd, 1, &expunge_base, - &expunge_len, sbuf.st_size, "expunge", - mailbox->name); - - /* use the expunge file's header information just in case - * versions are skewed for some reason */ - eversion = ntohl(*((bit32 *)(expunge_base+OFFSET_MINOR_VERSION))); - eoffset = ntohl(*((bit32 *)(expunge_base+OFFSET_START_OFFSET))); - expungerecord_size = ntohl(*((bit32 *)(expunge_base+OFFSET_RECORD_SIZE))); - - /* bogus data at the start of the expunge file? */ - if (!eoffset || !expungerecord_size) - goto no_expunge; - - expunge_num = ntohl(*((bit32 *)(expunge_base+OFFSET_NUM_RECORDS))); - emapnum = (sbuf.st_size - eoffset) / expungerecord_size; - if (emapnum < expunge_num) { - syslog(LOG_ERR, "IOERROR: %s map doesn't fit, shrinking expunge %lu to %lu", - mailbox->name, expunge_num, emapnum); - expunge_num = emapnum; - } - - /* make sure there's space for them */ - expunge_data = xmalloc(expunge_num * sizeof(struct expunge_data)); - - /* find the start offset for each record, and the UID */ - for (erecno = 1; erecno <= expunge_num; erecno++) { - bufp = expunge_base + eoffset + (erecno-1)*expungerecord_size; - expunge_data[erecno-1].uid = ntohl(*((bit32 *)(bufp+OFFSET_UID))); - expunge_data[erecno-1].base = bufp; - } - - /* expunge files were not sorted. So sort them now for easier - * interleaving */ - qsort(expunge_data, expunge_num, - sizeof(struct expunge_data), &sort_expunge); - } -no_expunge: - - r = mailbox_repack_setup(mailbox, &repack); - if (r) goto fail; - - /* Write the rest of new index */ - recno = 1; - erecno = 1; - while (recno <= oldnum_records || erecno <= expunge_num) { - /* read the uid */ - if (recno <= oldnum_records) { - bufp = mailbox->index_base + oldstart_offset + (recno-1)*oldrecord_size; - uid = ntohl(*((bit32 *)(bufp+OFFSET_UID))); - } - else { - uid = UINT32_MAX; - } - if (erecno <= expunge_num) { - euid = expunge_data[erecno-1].uid; - } - else { - euid = UINT32_MAX; - } - - /* case: index UID is first, or the same */ - if (uid <= euid) { - upgrade_index_record(mailbox, bufp, &record, oldrecord_size, - oldminor_version); - recno++; - if (uid == euid) /* duplicate in both, skip expunged */ - erecno++; - } - else { - upgrade_index_record(mailbox, expunge_data[erecno-1].base, - &record, expungerecord_size, eversion); - record.system_flags |= FLAG_EXPUNGED; - erecno++; - } - - /* user seen was merged into the index with version 12 */ - if (oldminor_version < 12 && seqset_ismember(seq, record.uid)) - record.system_flags |= FLAG_SEEN; - - /* CID was added with version 13 */ - if (oldminor_version < 13) - record.cid = 0; - - r = mailbox_repack_add(repack, &record); - if (r) goto fail; - } - - /* older than 13 didn't count MESSAGE or NUMFOLDERS usage for quota */ - if (oldminor_version < 13 && mailbox->quotaroot) { - quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_INITIALIZER; - qdiffs[QUOTA_MESSAGE] = repack->i.exists; - qdiffs[QUOTA_NUMFOLDERS] = 1; - quota_update_useds(mailbox->quotaroot, qdiffs, mailbox->name); - } - - r = mailbox_repack_commit(&repack); - if (r) goto fail; - mailbox->i.dirty = 0; - - /* don't need this file any more! */ - unlink(mailbox_meta_fname(mailbox, META_EXPUNGE)); - - /* XXX - remove seen record */ - - syslog(LOG_INFO, "Index upgrade: %s (%d -> %d)", mailbox->name, - oldminor_version, MAILBOX_MINOR_VERSION); - -done: - if (expunge_fd != -1) close(expunge_fd); - if (expunge_base) map_free(&expunge_base, &expunge_len); - seqset_free(seq); - free(expunge_data); - - return 0; - -fail: - if (expunge_fd != -1) close(expunge_fd); - if (expunge_base) map_free(&expunge_base, &expunge_len); - seqset_free(seq); - free(expunge_data); - - mailbox_repack_abort(&repack); - - syslog(LOG_ERR, "Index upgrade failed: %s", mailbox->name); - - return IMAP_IOERROR; -} -
View file
cyrus-imapd-2.5.tar.gz/imap/upgrade_index.h
Deleted
@@ -1,48 +0,0 @@ -/* upgrade_index.h -- Upgrade from previous versions - * - * 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. - */ - -#ifndef INCLUDED_UPGRADE_INDEX_H -#define INCLUDED_UPGRADE_INDEX_H - -extern int upgrade_index(struct mailbox *mailbox); - -#endif
View file
cyrus-imapd-2.5.tar.gz/imap/xcal.c
Added
@@ -0,0 +1,1293 @@ +/* xcal.c -- Routines for converting iCalendar to/from xCal + * + * Copyright (c) 1994-2013 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 <stdio.h> /* for snprintf() */ +#include <stddef.h> /* for offsetof() macro */ +#include <syslog.h> + +#include <libxml/tree.h> + +#include "httpd.h" +#include "tok.h" +#include "util.h" +#include "version.h" +#include "xcal.h" + + +extern icalvalue_kind icalproperty_kind_to_value_kind(icalproperty_kind kind); +extern const char* icalrecur_freq_to_string(icalrecurrencetype_frequency kind); +extern const char* icalrecur_weekday_to_string(icalrecurrencetype_weekday kind); +#ifdef HAVE_RSCALE +extern const char* icalrecur_skip_to_string(icalrecurrencetype_skip kind); +#endif + + +/* + * Determine the type (kind) of an iCalendar property value. + */ +const char *icalproperty_value_kind_as_string(icalproperty *prop) +{ + icalvalue_kind kind = ICAL_NO_VALUE; + icalparameter *val_param; + + val_param = icalproperty_get_first_parameter(prop, ICAL_VALUE_PARAMETER); + if (val_param) { + /* Use the kind specified in the VALUE param */ + kind = icalparameter_value_to_value_kind( + icalparameter_get_value(val_param)); + } + + if (kind == ICAL_NO_VALUE) { + icalvalue *value = icalproperty_get_value(prop); + + if (value) { + /* Use the kind determined from the property value */ + kind = icalvalue_isa(value); + } + } + + if (kind == ICAL_NO_VALUE) { + /* Use the default kind for the property */ + kind = icalproperty_kind_to_value_kind(icalproperty_isa(prop)); + } + + switch (kind) { + case ICAL_X_VALUE: + return "unknown"; + + case ICAL_ACTION_VALUE: + case ICAL_CARLEVEL_VALUE: + case ICAL_CLASS_VALUE: + case ICAL_CMD_VALUE: + case ICAL_METHOD_VALUE: + case ICAL_QUERYLEVEL_VALUE: + case ICAL_STATUS_VALUE: + case ICAL_TRANSP_VALUE: + return "text"; + + default: + return icalvalue_kind_to_string(kind); + } +} + + +/* + * Construct an ISO.8601.2004 string for an iCalendar Date/Date-Time. + */ +const char *icaltime_as_iso_string(const struct icaltimetype tt) +{ + static char str[21]; + const char *fmt; + + if (tt.is_date) fmt = "%04d-%02d-%02d"; + else if (tt.is_utc) fmt = "%04d-%02d-%02dT%02d:%02d:%02dZ"; + else fmt = "%04d-%02d-%02dT%02d:%02d:%02d"; + + snprintf(str, sizeof(str), fmt, tt.year, tt.month, tt.day, + tt.hour, tt.minute, tt.second); + + return str; +} + + +/* + * Construct an ISO.8601.2004 string for an iCalendar UTC Offset. + */ +const char *icalvalue_utcoffset_as_iso_string(const icalvalue* value) +{ + static char str[10]; + const char *fmt; + int off, h, m, s; + char sign; + + off = icalvalue_get_utcoffset(value); + + if (abs(off) == off) sign = '+'; + else sign = '-'; + + h = off/3600; + m = (off - (h*3600))/ 60; + s = (off - (h*3600) - (m*60)); + + if (s > 0) fmt = "%c%02d:%02d:%02d"; + else fmt = "%c%02d:%02d"; + + snprintf(str, sizeof(str), fmt, sign, abs(h), abs(m), abs(s)); + + return str; +} + + +static const struct { + const char *str; + int limit; + size_t offset; +} recurmap[] = +{ + { "bysecond", ICAL_BY_SECOND_SIZE, + offsetof(struct icalrecurrencetype, by_second) }, + { "byminute", ICAL_BY_MINUTE_SIZE, + offsetof(struct icalrecurrencetype, by_minute) }, + { "byhour", ICAL_BY_HOUR_SIZE, + offsetof(struct icalrecurrencetype, by_hour) }, + { "byday", ICAL_BY_DAY_SIZE, + offsetof(struct icalrecurrencetype, by_day) }, + { "bymonthday", ICAL_BY_MONTHDAY_SIZE, + offsetof(struct icalrecurrencetype, by_month_day) }, + { "byyearday", ICAL_BY_YEARDAY_SIZE, + offsetof(struct icalrecurrencetype, by_year_day) }, + { "byweekno", ICAL_BY_WEEKNO_SIZE, + offsetof(struct icalrecurrencetype, by_week_no) }, + { "bymonth", ICAL_BY_MONTH_SIZE, + offsetof(struct icalrecurrencetype, by_month) }, + { "bysetpos", ICAL_BY_SETPOS_SIZE, + offsetof(struct icalrecurrencetype, by_set_pos) }, + { 0, 0, 0 }, +}; + + +/* + * Add iCalendar recur-rule-parts to a structured element. + */ +void icalrecurrencetype_add_as_xxx(struct icalrecurrencetype *recur, void *obj, + void (*add_int)(void *, const char *, int), + void (*add_str)(void *, const char *, + const char *)) +{ + int i, j; + + if (recur->freq == ICAL_NO_RECURRENCE) return; + + add_str(obj, "freq", icalrecur_freq_to_string(recur->freq)); + +#ifdef HAVE_RSCALE + if (*recur->rscale) { + add_str(obj, "rscale", recur->rscale); + + if (recur->skip != ICAL_SKIP_BACKWARD) + add_str(obj, "skip", icalrecur_skip_to_string(recur->skip)); + } +#endif + + /* until and count are mutually exclusive */ + if (recur->until.year) { + add_str(obj, "until", icaltime_as_iso_string(recur->until)); + } + else if (recur->count) add_int(obj, "count", recur->count); + + if (recur->interval != 1) add_int(obj, "interval", recur->interval); + + /* Monday is the default, so no need to include it */ + if (recur->week_start != ICAL_MONDAY_WEEKDAY && + recur->week_start != ICAL_NO_WEEKDAY) { + const char *daystr; + + daystr = icalrecur_weekday_to_string( + icalrecurrencetype_day_day_of_week(recur->week_start)); + add_str(obj, "wkst", daystr); + } + + /* The BY* parameters can each take a list of values. + * + * Each of the lists is terminated with the value + * ICAL_RECURRENCE_ARRAY_MAX unless the the list is full. + */ + for (j = 0; recurmap[j].str; j++) { + short *array = (short *)((size_t) recur + recurmap[j].offset); + int limit = recurmap[j].limit - 1; + + for (i = 0; i < limit && array[i] != ICAL_RECURRENCE_ARRAY_MAX; i++) { + char temp[20]; + + if (j == 3) { /* BYDAY */ + const char *daystr; + int pos; + + daystr = icalrecur_weekday_to_string( + icalrecurrencetype_day_day_of_week(array[i])); + pos = icalrecurrencetype_day_position(array[i]); + + if (pos != 0) { + snprintf(temp, sizeof(temp), "%d%s", pos, daystr); + daystr = temp; + } + + add_str(obj, recurmap[j].str, daystr); + } +#ifdef HAVE_RSCALE + else if (j == 7 && ICAL_RSCALE_IS_LEAP_MONTH(array[i])) { + snprintf(temp, sizeof(temp), "%dL", ICAL_RSCALE_MONTH_NUM(array[i])); + add_str(obj, recurmap[j].str, temp); + } +#endif + else add_int(obj, recurmap[j].str, array[i]); + } + } +} + + +/* + * Add an XML element for an iCalendar Period. + */ +static void icalperiodtype_add_as_xml_element(xmlNodePtr xtype, + struct icalperiodtype p) +{ + const char *start; + const char *end; + + start = icaltime_as_iso_string(p.start); + xmlNewTextChild(xtype, NULL, BAD_CAST "start", BAD_CAST start); + + if (!icaltime_is_null_time(p.end)) { + end = icaltime_as_iso_string(p.end); + xmlNewTextChild(xtype, NULL, BAD_CAST "end", BAD_CAST end); + } + else { + end = icaldurationtype_as_ical_string(p.duration); + xmlNewTextChild(xtype, NULL, BAD_CAST "duration", BAD_CAST end); + } +} + + +/* + * Add an iCalendar recur-rule-part to a XML recur element. + */ +static void icalrecur_add_int_as_xml_element(void *xrecur, const char *rpart, + int i) +{ + char ibuf[20]; + + snprintf(ibuf, sizeof(ibuf), "%d", i); + xmlNewTextChild((xmlNodePtr) xrecur, NULL, BAD_CAST rpart, BAD_CAST ibuf); +} + +static void icalrecur_add_string_as_xml_element(void *xrecur, const char *rpart, + const char *s) +{ + xmlNewTextChild((xmlNodePtr) xrecur, NULL, BAD_CAST rpart, BAD_CAST s); +} + + +/* + * Construct an XML element for an iCalendar parameter. + */ +static xmlNodePtr icalparameter_as_xml_element(icalparameter *param) +{ + icalparameter_kind kind; + icalparameter_value value; + const char *kind_string, *type_string, *value_string; + xmlNodePtr xparam; + + kind = icalparameter_isa(param); + + switch (kind) { + case ICAL_X_PARAMETER: + kind_string = icalparameter_get_xname(param); + break; + + case ICAL_IANA_PARAMETER: + kind_string = icalparameter_get_iana_name(param); + break; + + default: + kind_string = icalparameter_kind_to_string(kind); + if (kind_string) break; + + case ICAL_NO_PARAMETER: + case ICAL_ANY_PARAMETER: + icalerror_set_errno(ICAL_BADARG_ERROR); + return NULL; + } + + /* Get value type */ + switch (kind) { + case ICAL_ALTREP_PARAMETER: + case ICAL_DIR_PARAMETER: + type_string = "uri"; + break; + + case ICAL_DELEGATEDFROM_PARAMETER: + case ICAL_DELEGATEDTO_PARAMETER: + case ICAL_MEMBER_PARAMETER: + case ICAL_SENTBY_PARAMETER: + type_string = "cal-address"; + break; + + case ICAL_RSVP_PARAMETER: + type_string = "boolean"; + break; + + default: + type_string = "text"; + break; + } + + /* XXX Need to handle multi-valued parameters */ + value = icalparameter_get_value(param); + if (value == ICAL_VALUE_X) value_string = icalparameter_get_xvalue(param); + else value_string = icalparameter_enum_to_string(value); + if (!value_string) return NULL; + + xparam = xmlNewNode(NULL, BAD_CAST lcase(icalmemory_tmp_copy(kind_string))); + xmlNewTextChild(xparam, NULL, BAD_CAST type_string, BAD_CAST value_string); + + return xparam; +} + + +/* + * Add the proper XML element for an iCalendar value. + */ +static void icalproperty_add_value_as_xml_element(xmlNodePtr xprop, + icalproperty *prop) + +{ + const char *type, *str = NULL; + xmlNodePtr xtype; + const icalvalue *value; + char buf[40]; + + /* Add type */ + type = lcase(icalmemory_tmp_copy( + icalproperty_value_kind_as_string(prop))); + xtype = xmlNewChild(xprop, NULL, BAD_CAST type, NULL); + + + /* Add value */ + value = icalproperty_get_value(prop); + + switch (icalvalue_isa(value)) { + case ICAL_DATE_VALUE: + str = icaltime_as_iso_string(icalvalue_get_date(value)); + break; + + case ICAL_DATETIME_VALUE: + str = icaltime_as_iso_string(icalvalue_get_datetime(value)); + break; + + case ICAL_DATETIMEPERIOD_VALUE: { + struct icaldatetimeperiodtype dtp = + icalvalue_get_datetimeperiod(value); + + if (!icaltime_is_null_time(dtp.time)) { + str = icaltime_as_iso_string(dtp.time); + break; + } + else { + icalperiodtype_add_as_xml_element(xtype, dtp.period); + return; + } + } + + case ICAL_GEO_VALUE: { + struct icalgeotype geo = icalvalue_get_geo(value); + + snprintf(buf, sizeof(buf), "%f", geo.lat); + xmlNewTextChild(xtype, NULL, BAD_CAST "latitude", BAD_CAST buf); + snprintf(buf, sizeof(buf), "%f", geo.lon); + xmlNewTextChild(xtype, NULL, BAD_CAST "longitude", BAD_CAST buf); + return; + } + + case ICAL_PERIOD_VALUE: + icalperiodtype_add_as_xml_element(xtype, icalvalue_get_period(value)); + return; + + case ICAL_RECUR_VALUE: { + struct icalrecurrencetype recur = icalvalue_get_recur(value); + + icalrecurrencetype_add_as_xxx(&recur, xtype, + &icalrecur_add_int_as_xml_element, + &icalrecur_add_string_as_xml_element); + return; + } + + case ICAL_REQUESTSTATUS_VALUE: { + struct icalreqstattype stat = icalvalue_get_requeststatus(value); + + if (!stat.desc) stat.desc = icalenum_reqstat_desc(stat.code); + + snprintf(buf, sizeof(buf), "%u.%u", + icalenum_reqstat_major(stat.code), + icalenum_reqstat_minor(stat.code)); + xmlNewTextChild(xtype, NULL, BAD_CAST "code", BAD_CAST buf); + xmlNewTextChild(xtype, NULL, BAD_CAST "description", BAD_CAST stat.desc); + if (stat.debug) + xmlNewTextChild(xtype, NULL, BAD_CAST "data", BAD_CAST stat.debug); + + return; + } + + case ICAL_TRIGGER_VALUE: { + struct icaltriggertype trig = icalvalue_get_trigger(value); + + if (!icaltime_is_null_time(trig.time)) + str = icaltime_as_iso_string(trig.time); + else + str = icaldurationtype_as_ical_string(trig.duration); + break; + } + + case ICAL_UTCOFFSET_VALUE: + str = icalvalue_utcoffset_as_iso_string(value); + break; + + default: + str = icalvalue_as_ical_string(value); + + switch (icalproperty_isa(prop)) { + case ICAL_CATEGORIES_PROPERTY: + case ICAL_RESOURCES_PROPERTY: + case ICAL_POLLPROPERTIES_PROPERTY: + if (strchr(str, ',')) { + /* Handle multi-valued properties */ + tok_t tok; + + tok_init(&tok, str, ",", TOK_TRIMLEFT|TOK_TRIMRIGHT|TOK_EMPTY); + str = tok_next(&tok); + xmlAddChild(xtype, xmlNewText(BAD_CAST str)); + + while ((str = tok_next(&tok))) { + if (*str) { + xtype = xmlNewChild(xprop, NULL, BAD_CAST type, NULL); + xmlAddChild(xtype, xmlNewText(BAD_CAST str)); + } + } + tok_fini(&tok); + return; + } + + default: break; + } + + break; + } + + if (str) xmlAddChild(xtype, xmlNewText(BAD_CAST str)); +} + + +/* + * Construct an XML element for an iCalendar property. + */ +static xmlNodePtr icalproperty_as_xml_element(icalproperty *prop) +{ + icalproperty_kind prop_kind; + const char *x_name, *property_name = NULL; + icalparameter *param; + xmlNodePtr xprop, xparams = NULL; + + if (!prop) return NULL; + + prop_kind = icalproperty_isa(prop); + x_name = icalproperty_get_x_name(prop); + + if (prop_kind == ICAL_X_PROPERTY && x_name) + property_name = x_name; + else + property_name = icalproperty_kind_to_string(prop_kind); + + if (!property_name) { + icalerror_warn("Got a property of an unknown kind."); + return NULL; + } + + /* Create property */ + xprop = xmlNewNode(NULL, + BAD_CAST lcase(icalmemory_tmp_copy(property_name))); + + + /* Add parameters */ + for (param = icalproperty_get_first_parameter(prop, ICAL_ANY_PARAMETER); + param != 0; + param = icalproperty_get_next_parameter(prop, ICAL_ANY_PARAMETER)) { + + if (icalparameter_isa(param) == ICAL_VALUE_PARAMETER) continue; + + if (!xparams) + xparams = xmlNewChild(xprop, NULL, BAD_CAST "parameters", NULL); + + xmlAddChild(xparams, icalparameter_as_xml_element(param)); + } + + + /* Add value */ + icalproperty_add_value_as_xml_element(xprop, prop); + + return xprop; +} + + +/* + * Construct a XML element for an iCalendar component. + */ +static xmlNodePtr icalcomponent_as_xml_element(icalcomponent *comp) +{ + icalcomponent *c; + icalproperty *p; + icalcomponent_kind kind; + const char* kind_string; + xmlNodePtr xcomp, xprops = NULL, xsubs = NULL; + + if (!comp) return NULL; + + kind = icalcomponent_isa(comp); + switch (kind) { + case ICAL_NO_COMPONENT: + return NULL; + break; + + case ICAL_X_COMPONENT: + kind_string = ""; //comp->x_name; + break; + + default: + kind_string = icalcomponent_kind_to_string(kind); + } + + + /* Create component */ + xcomp = xmlNewNode(NULL, + BAD_CAST lcase(icalmemory_tmp_copy(kind_string))); + + + /* Add properties */ + for (p = icalcomponent_get_first_property(comp, ICAL_ANY_PROPERTY); + p; + p = icalcomponent_get_next_property(comp, ICAL_ANY_PROPERTY)) { + + if (!xprops) + xprops = xmlNewChild(xcomp, NULL, BAD_CAST "properties", NULL); + + xmlAddChild(xprops, icalproperty_as_xml_element(p)); + } + + + /* Add sub-components */ + for (c = icalcomponent_get_first_component(comp, ICAL_ANY_COMPONENT); + c; + c = icalcomponent_get_next_component(comp, ICAL_ANY_COMPONENT)) { + + if (!xsubs) + xsubs = xmlNewChild(xcomp, NULL, BAD_CAST "components", NULL); + + xmlAddChild(xsubs, icalcomponent_as_xml_element(c)); + } + + return xcomp; +} + + +/* + * Construct a xcal string for an iCalendar component. + */ +char *icalcomponent_as_xcal_string(icalcomponent *ical) +{ + xmlDocPtr doc; + xmlNodePtr root, xcomp; + xmlChar *buf; + int bufsiz; + + if (!ical) return NULL; + + doc = xmlNewDoc(BAD_CAST "1.0"); + root = xmlNewNode(NULL, BAD_CAST "icalendar"); + xmlNewNs(root, BAD_CAST XML_NS_ICALENDAR, NULL); + xmlDocSetRootElement(doc, root); + + xcomp = icalcomponent_as_xml_element(ical); + + xmlAddChild(root, xcomp); + + if (!xmlStrcmp(xcomp->name, BAD_CAST "vcalendar")) { + /* Complete iCalendar stream */ + xmlDocDumpFormatMemoryEnc(doc, &buf, &bufsiz, "utf-8", + config_httpprettytelemetry); + } + else { + /* Single iCalendar object */ + xmlBufferPtr xbuf = xmlBufferCreate(); + + bufsiz = xmlNodeDump(xbuf, doc, xcomp, + 0, config_httpprettytelemetry); + buf = xmlBufferDetach(xbuf); + xmlBufferFree(xbuf); + } + + xmlFreeDoc(doc); + + return (char *) buf; +} + + +/* Add an iCalendar recurrence rule part to icalrecurrencetype. + * + * XXX The following structure is opaque libical, but for some stupid + * reason the icalrecur_add_by*rules() functions require it even though + * all they use is the rt field. MUST keep this in sync with icalrecur.c + */ +struct icalrecur_parser { + const char* rule; + char* copy; + char* this_clause; + char* next_clause; + + struct icalrecurrencetype rt; +}; + +extern icalrecurrencetype_frequency icalrecur_string_to_freq(const char* str); +#ifdef HAVE_RSCALE +extern icalrecurrencetype_skip icalrecur_string_to_skip(const char* str); +#endif +extern void icalrecur_add_byrules(struct icalrecur_parser *parser, short *array, + int size, char* vals); +extern void icalrecur_add_bydayrules(struct icalrecur_parser *parser, + const char* vals); + +struct icalrecurrencetype *icalrecur_add_rule(struct icalrecurrencetype **rt, + const char *rpart, void *data, + int (*get_int)(void *), + const char* (*get_str)(void *)) +{ + static struct icalrecur_parser parser; + + if (!*rt) { + /* Initialize */ + *rt = &parser.rt; + icalrecurrencetype_clear(*rt); + } + + if (!strcmp(rpart, "freq")) { + (*rt)->freq = icalrecur_string_to_freq(get_str(data)); + } +#ifdef HAVE_RSCALE + else if (!strcmp(rpart, "rscale")) { + (*rt)->rscale = icalmemory_tmp_copy(get_str(data)); + } + else if (!strcmp(rpart, "skip")) { + (*rt)->skip = icalrecur_string_to_skip(get_str(data)); + } +#endif + else if (!strcmp(rpart, "count")) { + (*rt)->count = get_int(data); + } + else if (!strcmp(rpart, "until")) { + (*rt)->until = icaltime_from_string(get_str(data)); + } + else if (!strcmp(rpart, "interval")) { + (*rt)->interval = get_int(data); + if ((*rt)->interval < 1) (*rt)->interval = 1; /* MUST be >= 1 */ + } + else if (!strcmp(rpart, "wkst")) { + (*rt)->week_start = icalrecur_string_to_weekday(get_str(data)); + } + else if (!strcmp(rpart, "byday")) { + icalrecur_add_bydayrules(&parser, get_str(data)); + } + else { + int i; + + for (i = 0; recurmap[i].str && strcmp(rpart, recurmap[i].str); i++); + + if (recurmap[i].str) { + short *array = + (short *)((size_t) *rt + recurmap[i].offset); + int limit = recurmap[i].limit; + + icalrecur_add_byrules(&parser, array, limit, + icalmemory_tmp_copy(get_str(data))); + } + else { + syslog(LOG_WARNING, "Unknown recurrence rule-part: %s", rpart); + icalrecurrencetype_clear(*rt); + *rt = NULL; + } + } + +#ifdef HAVE_RSCALE + /* When "RSCALE" is not present the default is "YES". + When "RSCALE" is present the default is "BACKWARD". */ + if (!(*rt)->rscale) (*rt)->skip = ICAL_SKIP_YES; + else if ((*rt)->skip == ICAL_SKIP_NO) (*rt)->skip = ICAL_SKIP_BACKWARD; +#endif + + return *rt; +} + + +int xmlElementContent_to_int(void *content) +{ + return atoi((const char *) content); +} + +const char *xmlElementContent_to_str(void *content) +{ + return (const char *) content; +} + + +/* + * Construct an iCalendar property value from XML content. + */ +static icalvalue *xml_element_to_icalvalue(xmlNodePtr xtype, + icalvalue_kind kind) +{ + icalvalue *value = NULL; + xmlNodePtr node; + xmlChar *content = NULL; + + switch (kind) { + + case ICAL_GEO_VALUE: { + struct icalgeotype geo; + + node = xmlFirstElementChild(xtype); + if (!node) { + syslog(LOG_WARNING, "Missing <latitude> XML element"); + break; + } + else if (xmlStrcmp(node->name, BAD_CAST "latitude")) { + syslog(LOG_WARNING, + "Expected <latitude> XML element, received %s", node->name); + break; + } + + content = xmlNodeGetContent(node); + geo.lat = atof((const char *) content); + + node = xmlNextElementSibling(node); + if (!node) { + syslog(LOG_WARNING, "Missing <longitude> XML element"); + break; + } + else if (xmlStrcmp(node->name, BAD_CAST "longitude")) { + syslog(LOG_WARNING, + "Expected <longitude> XML element, received %s", node->name); + break; + } + + xmlFree(content); + content = xmlNodeGetContent(node); + geo.lon = atof((const char *) content); + + value = icalvalue_new_geo(geo); + + break; + } + + case ICAL_PERIOD_VALUE: { + struct icalperiodtype p; + + p.start = p.end = icaltime_null_time(); + p.duration = icaldurationtype_from_int(0); + + node = xmlFirstElementChild(xtype); + if (!node) { + syslog(LOG_WARNING, "Missing <start> XML element"); + break; + } + else if (xmlStrcmp(node->name, BAD_CAST "start")) { + syslog(LOG_WARNING, + "Expected <start> XML element, received %s", node->name); + break; + } + + content = xmlNodeGetContent(node); + p.start = icaltime_from_string((const char *) content); + if (icaltime_is_null_time(p.start)) break; + + node = xmlNextElementSibling(node); + if (!node) { + syslog(LOG_WARNING, "Missing <end> / <duration> XML element"); + break; + } + else if (!xmlStrcmp(node->name, BAD_CAST "end")) { + xmlFree(content); + content = xmlNodeGetContent(node); + p.end = icaltime_from_string((const char *) content); + if (icaltime_is_null_time(p.end)) break; + } + else if (!xmlStrcmp(node->name, BAD_CAST "duration")) { + xmlFree(content); + content = xmlNodeGetContent(node); + p.duration = icaldurationtype_from_string((const char *) content); + if (icaldurationtype_as_int(p.duration) == 0) break; + } + else { + syslog(LOG_WARNING, + "Expected <end> / <duration> XML element, received %s", + node->name); + break; + } + + value = icalvalue_new_period(p); + + break; + } + + case ICAL_RECUR_VALUE: { + struct icalrecurrencetype *rt = NULL; + + for (node = xmlFirstElementChild(xtype); node; + node = xmlNextElementSibling(node)) { + + content = xmlNodeGetContent(node); + rt = icalrecur_add_rule(&rt, (const char *) node->name, content, + &xmlElementContent_to_int, + &xmlElementContent_to_str); + xmlFree(content); + content = NULL; + if (!rt) break; + } + + if (rt && rt->freq != ICAL_NO_RECURRENCE) + value = icalvalue_new_recur(*rt); + + break; + } + + case ICAL_REQUESTSTATUS_VALUE: { + struct icalreqstattype rst = { ICAL_UNKNOWN_STATUS, NULL, NULL }; + short maj, min; + + node = xmlFirstElementChild(xtype); + if (!node) { + syslog(LOG_WARNING, "Missing <code> XML element"); + break; + } + else if (xmlStrcmp(node->name, BAD_CAST "code")) { + syslog(LOG_WARNING, + "Expected <code> XML element, received %s", node->name); + break; + } + + content = xmlNodeGetContent(node); + if (sscanf((const char *) content, "%hd.%hd", &maj, &min) == 2) { + rst.code = icalenum_num_to_reqstat(maj, min); + } + if (rst.code == ICAL_UNKNOWN_STATUS) { + syslog(LOG_WARNING, "Unknown request-status code"); + break; + } + + node = xmlNextElementSibling(node); + if (!node) { + syslog(LOG_WARNING, "Missing <description> XML element"); + break; + } + else if (xmlStrcmp(node->name, BAD_CAST "description")) { + syslog(LOG_WARNING, + "Expected <description> XML element, received %s", + node->name); + break; + } + + xmlFree(content); + content = xmlNodeGetContent(node); + rst.desc = (const char *) content; + + node = xmlNextElementSibling(node); + if (node) { + if (xmlStrcmp(node->name, BAD_CAST "data")) { + syslog(LOG_WARNING, + "Expected <data> XML element, received %s", node->name); + break; + } + + xmlFree(content); + content = xmlNodeGetContent(node); + rst.debug = (const char *) content; + } + + value = icalvalue_new_requeststatus(rst); + break; + } + + case ICAL_UTCOFFSET_VALUE: { + int n, utcoffset, hours, minutes, seconds = 0; + char sign; + + content = xmlNodeGetContent(xtype); + n = sscanf((const char *) content, "%c%02d:%02d:%02d", + &sign, &hours, &minutes, &seconds); + + if (n < 3) { + syslog(LOG_WARNING, "Unexpected utc-offset format"); + break; + } + + utcoffset = hours*3600 + minutes*60 + seconds; + + if (sign == '-') utcoffset = -utcoffset; + + value = icalvalue_new_utcoffset(utcoffset); + break; + } + + default: + content = xmlNodeGetContent(xtype); + value = icalvalue_new_from_string(kind, (const char *) content); + break; + } + + if (content) xmlFree(content); + + return value; +} + + +/* + * Construct an iCalendar property from a XML element. + */ +static icalproperty *xml_element_to_icalproperty(xmlNodePtr xprop) +{ + const char *propname, *typestr; + icalproperty_kind kind; + icalproperty *prop = NULL; + icalvalue_kind valkind; + icalvalue *value; + xmlNodePtr node; + + /* Get the property type */ + propname = ucase(icalmemory_tmp_copy((const char *) xprop->name)); + kind = icalenum_string_to_property_kind(propname); + if (kind == ICAL_NO_PROPERTY) { + syslog(LOG_WARNING, "Unknown xCal property type: %s", propname); + return NULL; + } + + /* Create new property */ + prop = icalproperty_new(kind); + if (!prop) { + syslog(LOG_ERR, "Creation of new %s property failed", propname); + return NULL; + } + if (kind == ICAL_X_PROPERTY) icalproperty_set_x_name(prop, propname); + + + /* Add parameters */ + node = xmlFirstElementChild(xprop); + if (node && !xmlStrcmp(node->name, BAD_CAST "parameters")) { + xmlNodePtr xparam; + + for (xparam = xmlFirstElementChild(node); xparam; + xparam = xmlNextElementSibling(xparam)) { + char *paramname = + ucase(icalmemory_tmp_copy((const char *) xparam->name)); + xmlChar *paramval = xmlNodeGetContent(xmlFirstElementChild(xparam)); + + /* XXX Need to handle multi-valued parameters */ + icalproperty_set_parameter_from_string(prop, paramname, + (const char *) paramval); + + xmlFree(paramval); + } + + node = xmlNextElementSibling(node); + } + + /* Get the value type */ + if (!node) { + syslog(LOG_WARNING, "Missing xCal value for %s property", + propname); + return NULL; + } + typestr = ucase(icalmemory_tmp_copy((const char *) node->name)); + valkind = !strcmp(typestr, "UNKNOWN") ? ICAL_X_VALUE : + icalenum_string_to_value_kind(typestr); + if (valkind == ICAL_NO_VALUE) { + syslog(LOG_WARNING, "Unknown xCal value type for %s property: %s", + propname, typestr); + return NULL; + } + else if (valkind == ICAL_TEXT_VALUE) { + /* "text" also includes enumerated types - grab type from property */ + valkind = icalproperty_kind_to_value_kind(kind); + } + + + /* Add value */ + switch (kind) { + case ICAL_CATEGORIES_PROPERTY: + case ICAL_RESOURCES_PROPERTY: + case ICAL_POLLPROPERTIES_PROPERTY: + if (valkind == ICAL_TEXT_VALUE) { + /* Handle multi-valued properties */ + struct buf buf = BUF_INITIALIZER; + xmlChar *content = NULL; + + content = xmlNodeGetContent(node); + buf_setcstr(&buf, (const char *) content); + free(content); + + while ((node = xmlNextElementSibling(node))) { + buf_putc(&buf, ','); + content = xmlNodeGetContent(node); + buf_appendcstr(&buf, (const char *) content); + free(content); + } + + value = icalvalue_new_from_string(valkind, buf_cstring(&buf)); + buf_free(&buf); + break; + } + + default: + value = xml_element_to_icalvalue(node, valkind); + if (!value) { + syslog(LOG_ERR, "Parsing %s property value failed", propname); + goto error; + } + } + + icalproperty_set_value(prop, value); + + + /* Sanity check */ + if ((node = xmlNextElementSibling(node))) { + syslog(LOG_WARNING, + "Unexpected XML element in property: %s", node->name); + goto error; + } + + return prop; + + error: + icalproperty_free(prop); + return NULL; +} + + +/* + * Construct an iCalendar component from a XML element. + */ +static icalcomponent *xml_element_to_icalcomponent(xmlNodePtr xcomp) +{ + icalcomponent_kind kind; + icalcomponent *comp = NULL; + xmlNodePtr node, xprop, xsub; + + if (!xcomp) return NULL; + + /* Get component type */ + kind = + icalenum_string_to_component_kind( + ucase(icalmemory_tmp_copy((const char *) xcomp->name))); + if (kind == ICAL_NO_COMPONENT) { + syslog(LOG_WARNING, "Unknown xCal component type: %s", xcomp->name); + return NULL; + } + + /* Create new component */ + comp = icalcomponent_new(kind); + if (!comp) { + syslog(LOG_ERR, "Creation of new %s component failed", xcomp->name); + return NULL; + } + + /* Add properties */ + node = xmlFirstElementChild(xcomp); + if (!node || xmlStrcmp(node->name, BAD_CAST "properties")) { + syslog(LOG_WARNING, + "Expected <properties> XML element, received %s", node->name); + goto error; + } + for (xprop = xmlFirstElementChild(node); xprop; + xprop = xmlNextElementSibling(xprop)) { + icalproperty *prop = xml_element_to_icalproperty(xprop); + + if (!prop) goto error; + + icalcomponent_add_property(comp, prop); + } + + /* Add sub-components */ + if (!(node = xmlNextElementSibling(node))) return comp; + + if (xmlStrcmp(node->name, BAD_CAST "components")) { + syslog(LOG_WARNING, + "Expected <components> XML element, received %s", node->name); + goto error; + } + + for (xsub = xmlFirstElementChild(node); xsub; + xsub = xmlNextElementSibling(xsub)) { + icalcomponent *sub = xml_element_to_icalcomponent(xsub); + + if (!sub) goto error; + + icalcomponent_add_component(comp, sub); + } + + /* Sanity check */ + if ((node = xmlNextElementSibling(node))) { + syslog(LOG_WARNING, + "Unexpected XML element in component: %s", node->name); + goto error; + } + + return comp; + + error: + icalcomponent_free(comp); + return NULL; +} + + +/* + * Construct an iCalendar component from an xCal string. + */ +icalcomponent *xcal_string_as_icalcomponent(const char *str) +{ + xmlParserCtxtPtr ctxt; + xmlDocPtr doc = NULL; + xmlNodePtr root; + icalcomponent *ical = NULL; + + if (!str) return NULL; + + /* Parse the XML request */ + ctxt = xmlNewParserCtxt(); + if (ctxt) { + doc = xmlCtxtReadMemory(ctxt, str, strlen(str), NULL, NULL, + XML_PARSE_NOWARNING); + xmlFreeParserCtxt(ctxt); + } + if (!doc) { + syslog(LOG_WARNING, "XML parse error"); + return NULL; + } + + /* Get root element */ + if (!(root = xmlDocGetRootElement(doc)) || + xmlStrcmp(root->name, BAD_CAST "icalendar") || + xmlStrcmp(root->ns->href, BAD_CAST XML_NS_ICALENDAR)) { + syslog(LOG_WARNING, + "XML root element is not %s:icalendar", XML_NS_ICALENDAR); + goto done; + } + + ical = xml_element_to_icalcomponent(xmlFirstElementChild(root)); + + done: + xmlFreeDoc(doc); + + return ical; +} + + +const char *begin_xcal(struct buf *buf) +{ + /* Begin xCal stream */ + buf_reset(buf); + buf_printf_markup(buf, 0, "<?xml version=\"1.0\" encoding=\"utf-8\"?>"); + buf_printf_markup(buf, 0, "<icalendar xmlns=\"%s\">", XML_NS_ICALENDAR); + buf_printf_markup(buf, 1, "<vcalendar>"); + buf_printf_markup(buf, 2, "<properties>"); + buf_printf_markup(buf, 3, "<prodid>"); + buf_printf_markup(buf, 4, "<text>-//CyrusIMAP.org/Cyrus %s//EN</text>", + cyrus_version()); + buf_printf_markup(buf, 3, "</prodid>"); + buf_printf_markup(buf, 3, "<version>"); + buf_printf_markup(buf, 4, "<text>2.0</text>"); + buf_printf_markup(buf, 3, "</version>"); + buf_printf_markup(buf, 2, "</properties>"); + buf_printf_markup(buf, 2, "<components>"); + + return ""; +} + + +void end_xcal(struct buf *buf) +{ + /* End xCal stream */ + buf_reset(buf); + buf_printf_markup(buf, 2, "</components>"); + buf_printf_markup(buf, 1, "</vcalendar>"); + buf_printf_markup(buf, 0, "</icalendar>"); +} + + +/* libxml2 replacement functions for those missing in older versions */ +#if (LIBXML_VERSION < 20800) +xmlChar *xmlBufferDetach(xmlBufferPtr buf) +{ + xmlChar *ret; + + if (!buf) return NULL; + + ret = buf->content; + buf->content = NULL; + buf->use = buf->size = 0; + + return ret; +} + + +#if (LIBXML_VERSION < 20703) +xmlNodePtr xmlFirstElementChild(xmlNodePtr node) +{ + if (!node) return NULL; + + for (node = node->children; node; node = node->next) { + if (node->type == XML_ELEMENT_NODE) return (node); + } + + return NULL; +} + + +xmlNodePtr xmlNextElementSibling(xmlNodePtr node) +{ + if (!node) return NULL; + + for (node = node->next; node; node = node->next) { + if (node->type == XML_ELEMENT_NODE) return (node); + } + + return NULL; +} +#endif /* < 2.7.3 */ +#endif /* < 2.8.0 */
View file
cyrus-imapd-2.5.tar.gz/imap/xcal.h
Added
@@ -0,0 +1,87 @@ +/* xcal.h -- Routines for converting iCalendar to/from xCal + * + * Copyright (c) 1994-2013 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 <libical/ical.h> + +#include "util.h" + +#ifndef HAVE_VPOLL +/* Allow us to compile without #ifdef HAVE_VPOLL everywhere */ +#define ICAL_POLLPROPERTIES_PROPERTY ICAL_NO_PROPERTY +#endif + +#define XML_NS_ICALENDAR "urn:ietf:params:xml:ns:icalendar-2.0" + +extern const char *icalproperty_value_kind_as_string(icalproperty *prop); +extern const char *icaltime_as_iso_string(const struct icaltimetype tt); +extern const char *icalvalue_utcoffset_as_iso_string(const icalvalue* value); +extern void icalrecurrencetype_add_as_xxx(struct icalrecurrencetype *recur, + void *obj, + void (*add_int)(void *, const char *, + int), + void (*add_str)(void *, const char *, + const char *)); +extern struct icalrecurrencetype * +icalrecur_add_rule(struct icalrecurrencetype **rt, + const char *rpart, void *data, + int (*get_int)(void *), + const char* (*get_str)(void *)); + +extern char *icalcomponent_as_xcal_string(icalcomponent* comp); +extern icalcomponent *xcal_string_as_icalcomponent(const char *str); +extern const char *begin_xcal(struct buf *buf); +extern void end_xcal(struct buf *buf); + +/* libxml2 replacement functions for those missing in older versions */ +#if (LIBXML_VERSION < 20800) +#include <libxml/tree.h> + +extern xmlChar *xmlBufferDetach(xmlBufferPtr buf); + +#if (LIBXML_VERSION < 20703) +extern xmlNodePtr xmlFirstElementChild(xmlNodePtr parent); +extern xmlNodePtr xmlNextElementSibling(xmlNodePtr node); +#endif /* < 2.7.3 */ +#endif /* < 2.8.0 */
View file
cyrus-imapd-2.5.tar.gz/imap/zoneinfo_db.c
Added
@@ -0,0 +1,325 @@ +/* zoneinfo_db.c -- zoneinfo DB routines + * + * Copyright (c) 1994-2013 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 <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> + +#include "assert.h" +#include "cyrusdb.h" +#include "exitcodes.h" +#include "global.h" +#include "tok.h" +#include "util.h" +#include "xmalloc.h" + +#include "zoneinfo_db.h" + +#define DB config_zoneinfo_db + +struct db *zoneinfodb; +static int zoneinfo_dbopen = 0; +static struct buf databuf = BUF_INITIALIZER; + +EXPORTED int zoneinfo_open(const char *fname) +{ + int ret; + char *tofree = NULL; + + if (!fname) fname = config_getstring(IMAPOPT_ZONEINFO_DB_PATH); + + /* create db file name */ + if (!fname) { + tofree = strconcat(config_dir, FNAME_ZONEINFODB, (char *)NULL); + fname = tofree; + } + + ret = cyrusdb_open(DB, fname, CYRUSDB_CREATE, &zoneinfodb); + if (ret != 0) { + syslog(LOG_ERR, "DBERROR: opening %s: %s", fname, + cyrusdb_strerror(ret)); + } + else zoneinfo_dbopen = 1; + + free(tofree); + + return ret; +} + +void zoneinfo_close(struct txn *tid) +{ + if (zoneinfo_dbopen) { + int r; + + if (tid) { + r = cyrusdb_commit(zoneinfodb, tid); + if (r) { + syslog(LOG_ERR, "DBERROR: error committing zoneinfo: %s", + cyrusdb_strerror(r)); + } + } + r = cyrusdb_close(zoneinfodb); + if (r) { + syslog(LOG_ERR, "DBERROR: error closing zoneinfo: %s", + cyrusdb_strerror(r)); + } + zoneinfo_dbopen = 0; + + buf_free(&databuf); + } +} + +void zoneinfo_done(void) +{ + /* DB->done() handled by cyrus_done() */ +} + +static int parse_zoneinfo(const char *data, int datalen, + struct zoneinfo *zi, int all) +{ + const char *dend = data + datalen; + unsigned version; + char *p; + + memset(zi, 0, sizeof(struct zoneinfo)); + + /* version SP type SP dtstamp SP (string *(TAB string)) */ + + version = strtoul(data, &p, 10); + if (version != ZONEINFO_VERSION) return CYRUSDB_IOERROR; + + if (p < dend) zi->type = strtoul(p, &p, 10); + if (p < dend) zi->dtstamp = strtol(p, &p, 10); + + if (all && p < dend) { + size_t len = dend - ++p; + char *str = xstrndup(p, len); + tok_t tok; + + tok_initm(&tok, str, "\t", TOK_FREEBUFFER); + while ((str = tok_next(&tok))) appendstrlist(&zi->data, str); + tok_fini(&tok); + } + + return 0; +} + +EXPORTED int zoneinfo_lookup(const char *tzid, struct zoneinfo *zi) +{ + const char *data = NULL; + size_t datalen; + int r; + + /* Don't access DB if it hasn't been opened */ + if (!zoneinfo_dbopen) return CYRUSDB_INTERNAL; + + assert(tzid); + + /* Check if there is an entry in the database */ + do { + r = cyrusdb_fetch(zoneinfodb, tzid, strlen(tzid), &data, &datalen, NULL); + } while (r == CYRUSDB_AGAIN); + + if (r || !data || (datalen < 6)) return r ? r : CYRUSDB_IOERROR; + + if (!zi) return 0; + + return parse_zoneinfo(data, datalen, zi, 1); +} + +EXPORTED int zoneinfo_store(const char *tzid, struct zoneinfo *zi, struct txn **tid) +{ + struct strlist *sl; + const char *sep; + int r; + + /* Don't access DB if it hasn't been opened */ + if (!zoneinfo_dbopen) return CYRUSDB_INTERNAL; + + assert(tzid && zi); + + /* version SP type SP dtstamp SP (string *(TAB string)) */ + buf_reset(&databuf); + buf_printf(&databuf, "%u %u %ld ", ZONEINFO_VERSION, zi->type, zi->dtstamp); + for (sl = zi->data, sep = ""; sl; sl = sl->next, sep = "\t") + buf_printf(&databuf, "%s%s", sep, sl->s); + + r = cyrusdb_store(zoneinfodb, tzid, strlen(tzid), + buf_cstring(&databuf), buf_len(&databuf), tid); + + if (r != CYRUSDB_OK) { + syslog(LOG_ERR, "DBERROR: error updating zoneinfo: %s (%s)", + tzid, cyrusdb_strerror(r)); + } + + return r; +} + + +static int tzmatch(const char *str, const char *pat) +{ + for ( ; *pat; str++, pat++) { + /* End of string and more pattern */ + if (!*str && *pat != '*') return 0; + + switch (*pat) { + case '*': + /* Collapse consecutive stars */ + while (*++pat == '*') continue; + + /* Trailing star matches anything */ + if (!*pat) return 1; + + while (*str) if (tzmatch(str++, pat)) return 1; + return 0; + + case ' ': + case '_': + /* Treat ' ' == '_' */ + if (*str != ' ' && *str != '_') return 0; + break; + + default: + /* Case-insensitive comparison */ + if (tolower(*str) != tolower(*pat)) return 0; + break; + } + } + + /* Did we reach end of string? */ + return (!*str); +} + + +struct findrock { + const char *find; + int tzid_only; + time_t changedsince; + int (*proc)(); + void *rock; +}; + +static int find_p(void *rock, + const char *tzid, size_t tzidlen, + const char *data, size_t datalen) +{ + struct findrock *frock = (struct findrock *) rock; + struct zoneinfo zi; + + if (parse_zoneinfo(data, datalen, &zi, 0)) return 0; + + switch (zi.type) { + case ZI_INFO: return 0; + + case ZI_LINK: + if (frock->tzid_only) return 0; + break; + + case ZI_ZONE: + if (zi.dtstamp <= frock->changedsince) return 0; + break; + } + + if (!frock->find) return 1; + if (frock->tzid_only) return (tzidlen == strlen(frock->find)); + return tzmatch(tzid, frock->find); +} + +static int find_cb(void *rock, + const char *tzid, size_t tzidlen, + const char *data, size_t datalen) +{ + struct findrock *frock = (struct findrock *) rock; + struct zoneinfo zi; + int r; + + r = parse_zoneinfo(data, datalen, &zi, 1); + if (!r) { + struct strlist *linkto = NULL; + + if (zi.type == ZI_LINK) { + linkto = zi.data; + zi.data = NULL; + + tzid = linkto->s; + tzidlen = strlen(tzid); + r = zoneinfo_lookup(tzid, &zi); + } + if (!r) r = (*frock->proc)(tzid, tzidlen, &zi, frock->rock); + freestrlist(zi.data); + freestrlist(linkto); + } + + return r; +} + +EXPORTED int zoneinfo_find(const char *find, int tzid_only, time_t changedsince, + int (*proc)(), void *rock) +{ + struct findrock frock; + + /* Don't access DB if it hasn't been opened */ + if (!zoneinfo_dbopen) return CYRUSDB_INTERNAL; + + assert(proc); + + frock.find = find; + frock.tzid_only = tzid_only; + frock.changedsince = changedsince; + frock.proc = proc; + frock.rock = rock; + + if (!find || !tzid_only) find = ""; + + /* process each matching entry in our database */ + return cyrusdb_foreach(zoneinfodb, (char *) find, strlen(find), + &find_p, &find_cb, &frock, NULL); +}
View file
cyrus-imapd-2.5.tar.gz/imap/zoneinfo_db.h
Added
@@ -0,0 +1,97 @@ +/* zoneinfo_db.h -- zoneinfo DB routines + * + * Copyright (c) 1994-2013 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 ZONEINFO_DB_H +#define ZONEINFO_DB_H + +#include <time.h> + +#include "annotate.h" /* for strlist functionality */ + +/* name of the zoneinfo directory */ +#define FNAME_ZONEINFODIR "/zoneinfo" + +/* name of the zoneinfo database */ +#define FNAME_ZONEINFODB "/zoneinfo.db" +#define ZONEINFO_VERSION 1 + +#define INFO_TZID ".info" +#define zoneinfo_lookup_info(zi) zoneinfo_lookup(INFO_TZID, zi) + +struct zoneinfo { + unsigned type; + time_t dtstamp; + struct strlist *data; +}; + +/* zoneinfo record types */ +enum { + ZI_ZONE = 0, + ZI_LINK, + ZI_INFO +}; + +/* open the zoneinfo db */ +extern int zoneinfo_open(const char *name); + +/* lookup a single zoneinfo entry and return result, or error if it + doesn't exist or doesn't have the fields we need */ +extern int zoneinfo_lookup(const char *tzid, struct zoneinfo *zi); + +/* store a zoneinfo entry */ +extern int zoneinfo_store(const char *tzid, struct zoneinfo *zi, + struct txn **tid); + +/* process all zoneinfo entries (optionally matching 'find') */ +extern int zoneinfo_find(const char *find, int tzid_only, time_t changedsince, + int (*proc)(const char *tzid, int tzidlen, + struct zoneinfo *zi, void *rock), + void *rock); + +/* close the database (optionally committing txn) */ +extern void zoneinfo_close(struct txn *tid); + +/* done with database stuff */ +extern void zoneinfo_done(void); + +#endif /* ZONEINFO_DB_H */
View file
cyrus-imapd-2.5.tar.gz/lib/charset.c
Changed
@@ -49,6 +49,35 @@ #include "chartable.h" #include "util.h" +#define U_REPLACEMENT 0xfffd + +#define unicode_isvalid(c) \ + (!((c >= 0xd800 && c <= 0xdfff) || ((unsigned)c > 0x10ffff))) + +char QPSAFECHAR[256] = { +/* control chars are unsafe */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +/* http://en.wikipedia.org/wiki/Quoted-printable */ +/* All printable ASCII characters (decimal values between 33 and 126) */ +/* may be represented by themselves, except "=" (decimal 61). */ + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, +/* all high bits are unsafe */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + struct qp_state { int isheader; int bytesleft; @@ -1348,3 +1377,41 @@ return (b64_len ? retval : NULL); } +/* returns a buffer which the caller must free */ +EXPORTED char *charset_encode_mimeheader(const char *header, size_t len) +{ + struct buf buf = BUF_INITIALIZER; + size_t n; + int need_quote = 0; + + if (!header) return NULL; + + if (!len) len = strlen(header); + + for (n = 0; n < len; n++) { + unsigned char this = header[n]; + if (QPSAFECHAR[this] || this == ' ') continue; + need_quote = 1; + break; + } + + if (need_quote) { + buf_printf(&buf, "=?UTF-8?Q?"); + for (n = 0; n < len; n++) { + unsigned char this = header[n]; + if (QPSAFECHAR[this]) { + buf_putc(&buf, (char)this); + } + else { + buf_printf(&buf, "=%02X", this); + } + } + buf_printf(&buf, "?="); + } + else { + buf_setmap(&buf, header, len); + } + + return buf_release(&buf); +} +
View file
cyrus-imapd-2.5.tar.gz/lib/charset.h
Changed
@@ -82,6 +82,7 @@ extern char *charset_to_utf8(const char *msg_base, size_t len, charset_index charset, int encoding); extern int charset_search_mimeheader(const char *substr, comp_pat *pat, const char *s, int flags); +extern char *charset_encode_mimeheader(const char *header, size_t len); /* Definitions for charset_extractfile */ /* These constants are passed into the index_search_text_receiver_t callback to
View file
cyrus-imapd-2.5.tar.gz/lib/charset/UnicodeData.txt
Changed
@@ -165,10 +165,10 @@ 00A4;CURRENCY SIGN;Sc;0;ET;;;;;N;;;;; 00A5;YEN SIGN;Sc;0;ET;;;;;N;;;;; 00A6;BROKEN BAR;So;0;ON;;;;;N;BROKEN VERTICAL BAR;;;; -00A7;SECTION SIGN;So;0;ON;;;;;N;;;;; +00A7;SECTION SIGN;Po;0;ON;;;;;N;;;;; 00A8;DIAERESIS;Sk;0;ON;<compat> 0020 0308;;;;N;SPACING DIAERESIS;;;; 00A9;COPYRIGHT SIGN;So;0;ON;;;;;N;;;;; -00AA;FEMININE ORDINAL INDICATOR;Ll;0;L;<super> 0061;;;;N;;;;; +00AA;FEMININE ORDINAL INDICATOR;Lo;0;L;<super> 0061;;;;N;;;;; 00AB;LEFT-POINTING DOUBLE ANGLE QUOTATION MARK;Pi;0;ON;;;;;Y;LEFT POINTING GUILLEMET;;;; 00AC;NOT SIGN;Sm;0;ON;;;;;N;;;;; 00AD;SOFT HYPHEN;Cf;0;BN;;;;;N;;;;; @@ -180,11 +180,11 @@ 00B3;SUPERSCRIPT THREE;No;0;EN;<super> 0033;;3;3;N;SUPERSCRIPT DIGIT THREE;;;; 00B4;ACUTE ACCENT;Sk;0;ON;<compat> 0020 0301;;;;N;SPACING ACUTE;;;; 00B5;MICRO SIGN;Ll;0;L;<compat> 03BC;;;;N;;;039C;;039C -00B6;PILCROW SIGN;So;0;ON;;;;;N;PARAGRAPH SIGN;;;; +00B6;PILCROW SIGN;Po;0;ON;;;;;N;PARAGRAPH SIGN;;;; 00B7;MIDDLE DOT;Po;0;ON;;;;;N;;;;; 00B8;CEDILLA;Sk;0;ON;<compat> 0020 0327;;;;N;SPACING CEDILLA;;;; 00B9;SUPERSCRIPT ONE;No;0;EN;<super> 0031;;1;1;N;SUPERSCRIPT DIGIT ONE;;;; -00BA;MASCULINE ORDINAL INDICATOR;Ll;0;L;<super> 006F;;;;N;;;;; +00BA;MASCULINE ORDINAL INDICATOR;Lo;0;L;<super> 006F;;;;N;;;;; 00BB;RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK;Pf;0;ON;;;;;Y;RIGHT POINTING GUILLEMET;;;; 00BC;VULGAR FRACTION ONE QUARTER;No;0;ON;<fraction> 0031 2044 0034;;;1/4;N;FRACTION ONE QUARTER;;;; 00BD;VULGAR FRACTION ONE HALF;No;0;ON;<fraction> 0031 2044 0032;;;1/2;N;FRACTION ONE HALF;;;; @@ -602,23 +602,23 @@ 0259;LATIN SMALL LETTER SCHWA;Ll;0;L;;;;;N;;;018F;;018F 025A;LATIN SMALL LETTER SCHWA WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER SCHWA HOOK;;;; 025B;LATIN SMALL LETTER OPEN E;Ll;0;L;;;;;N;LATIN SMALL LETTER EPSILON;;0190;;0190 -025C;LATIN SMALL LETTER REVERSED OPEN E;Ll;0;L;;;;;N;LATIN SMALL LETTER REVERSED EPSILON;;;; +025C;LATIN SMALL LETTER REVERSED OPEN E;Ll;0;L;;;;;N;LATIN SMALL LETTER REVERSED EPSILON;;A7AB;;A7AB 025D;LATIN SMALL LETTER REVERSED OPEN E WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER REVERSED EPSILON HOOK;;;; 025E;LATIN SMALL LETTER CLOSED REVERSED OPEN E;Ll;0;L;;;;;N;LATIN SMALL LETTER CLOSED REVERSED EPSILON;;;; 025F;LATIN SMALL LETTER DOTLESS J WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER DOTLESS J BAR;;;; 0260;LATIN SMALL LETTER G WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER G HOOK;;0193;;0193 -0261;LATIN SMALL LETTER SCRIPT G;Ll;0;L;;;;;N;;;;; +0261;LATIN SMALL LETTER SCRIPT G;Ll;0;L;;;;;N;;;A7AC;;A7AC 0262;LATIN LETTER SMALL CAPITAL G;Ll;0;L;;;;;N;;;;; 0263;LATIN SMALL LETTER GAMMA;Ll;0;L;;;;;N;;;0194;;0194 0264;LATIN SMALL LETTER RAMS HORN;Ll;0;L;;;;;N;LATIN SMALL LETTER BABY GAMMA;;;; 0265;LATIN SMALL LETTER TURNED H;Ll;0;L;;;;;N;;;A78D;;A78D -0266;LATIN SMALL LETTER H WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER H HOOK;;;; +0266;LATIN SMALL LETTER H WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER H HOOK;;A7AA;;A7AA 0267;LATIN SMALL LETTER HENG WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER HENG HOOK;;;; 0268;LATIN SMALL LETTER I WITH STROKE;Ll;0;L;;;;;N;LATIN SMALL LETTER BARRED I;;0197;;0197 0269;LATIN SMALL LETTER IOTA;Ll;0;L;;;;;N;;;0196;;0196 026A;LATIN LETTER SMALL CAPITAL I;Ll;0;L;;;;;N;;;;; 026B;LATIN SMALL LETTER L WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;2C62;;2C62 -026C;LATIN SMALL LETTER L WITH BELT;Ll;0;L;;;;;N;LATIN SMALL LETTER L BELT;;;; +026C;LATIN SMALL LETTER L WITH BELT;Ll;0;L;;;;;N;LATIN SMALL LETTER L BELT;;A7AD;;A7AD 026D;LATIN SMALL LETTER L WITH RETROFLEX HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER L RETROFLEX HOOK;;;; 026E;LATIN SMALL LETTER LEZH;Ll;0;L;;;;;N;LATIN SMALL LETTER L YOGH;;;; 026F;LATIN SMALL LETTER TURNED M;Ll;0;L;;;;;N;;;019C;;019C @@ -645,7 +645,7 @@ 0284;LATIN SMALL LETTER DOTLESS J WITH STROKE AND HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER DOTLESS J BAR HOOK;;;; 0285;LATIN SMALL LETTER SQUAT REVERSED ESH;Ll;0;L;;;;;N;;;;; 0286;LATIN SMALL LETTER ESH WITH CURL;Ll;0;L;;;;;N;LATIN SMALL LETTER ESH CURL;;;; -0287;LATIN SMALL LETTER TURNED T;Ll;0;L;;;;;N;;;;; +0287;LATIN SMALL LETTER TURNED T;Ll;0;L;;;;;N;;;A7B1;;A7B1 0288;LATIN SMALL LETTER T WITH RETROFLEX HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER T RETROFLEX HOOK;;01AE;;01AE 0289;LATIN SMALL LETTER U BAR;Ll;0;L;;;;;N;;;0244;;0244 028A;LATIN SMALL LETTER UPSILON;Ll;0;L;;;;;N;;;01B1;;01B1 @@ -668,7 +668,7 @@ 029B;LATIN LETTER SMALL CAPITAL G WITH HOOK;Ll;0;L;;;;;N;LATIN LETTER SMALL CAPITAL G HOOK;;;; 029C;LATIN LETTER SMALL CAPITAL H;Ll;0;L;;;;;N;;;;; 029D;LATIN SMALL LETTER J WITH CROSSED-TAIL;Ll;0;L;;;;;N;LATIN SMALL LETTER CROSSED-TAIL J;;;; -029E;LATIN SMALL LETTER TURNED K;Ll;0;L;;;;;N;;;;; +029E;LATIN SMALL LETTER TURNED K;Ll;0;L;;;;;N;;;A7B0;;A7B0 029F;LATIN LETTER SMALL CAPITAL L;Ll;0;L;;;;;N;;;;; 02A0;LATIN SMALL LETTER Q WITH HOOK;Ll;0;L;;;;;N;LATIN SMALL LETTER Q HOOK;;;; 02A1;LATIN LETTER GLOTTAL STOP WITH STROKE;Ll;0;L;;;;;N;LATIN LETTER GLOTTAL STOP BAR;;;; @@ -891,6 +891,7 @@ 037C;GREEK SMALL DOTTED LUNATE SIGMA SYMBOL;Ll;0;L;;;;;N;;;03FE;;03FE 037D;GREEK SMALL REVERSED DOTTED LUNATE SIGMA SYMBOL;Ll;0;L;;;;;N;;;03FF;;03FF 037E;GREEK QUESTION MARK;Po;0;ON;003B;;;;N;;;;; +037F;GREEK CAPITAL LETTER YOT;Lu;0;L;;;;;N;;;;03F3; 0384;GREEK TONOS;Sk;0;ON;<compat> 0020 0301;;;;N;GREEK SPACING TONOS;;;; 0385;GREEK DIALYTIKA TONOS;Sk;0;ON;00A8 0301;;;;N;GREEK SPACING DIAERESIS TONOS;;;; 0386;GREEK CAPITAL LETTER ALPHA WITH TONOS;Lu;0;L;0391 0301;;;;N;GREEK CAPITAL LETTER ALPHA TONOS;;;03AC; @@ -999,7 +1000,7 @@ 03F0;GREEK KAPPA SYMBOL;Ll;0;L;<compat> 03BA;;;;N;GREEK SMALL LETTER SCRIPT KAPPA;;039A;;039A 03F1;GREEK RHO SYMBOL;Ll;0;L;<compat> 03C1;;;;N;GREEK SMALL LETTER TAILED RHO;;03A1;;03A1 03F2;GREEK LUNATE SIGMA SYMBOL;Ll;0;L;<compat> 03C2;;;;N;GREEK SMALL LETTER LUNATE SIGMA;;03F9;;03F9 -03F3;GREEK LETTER YOT;Ll;0;L;;;;;N;;;;; +03F3;GREEK LETTER YOT;Ll;0;L;;;;;N;;;037F;;037F 03F4;GREEK CAPITAL THETA SYMBOL;Lu;0;L;<compat> 0398;;;;N;;;;03B8; 03F5;GREEK LUNATE EPSILON SYMBOL;Ll;0;L;<compat> 03B5;;;;N;;;0395;;0395 03F6;GREEK REVERSED LUNATE EPSILON SYMBOL;Sm;0;ON;;;;;N;;;;; @@ -1308,6 +1309,14 @@ 0525;CYRILLIC SMALL LETTER PE WITH DESCENDER;Ll;0;L;;;;;N;;;0524;;0524 0526;CYRILLIC CAPITAL LETTER SHHA WITH DESCENDER;Lu;0;L;;;;;N;;;;0527; 0527;CYRILLIC SMALL LETTER SHHA WITH DESCENDER;Ll;0;L;;;;;N;;;0526;;0526 +0528;CYRILLIC CAPITAL LETTER EN WITH LEFT HOOK;Lu;0;L;;;;;N;;;;0529; +0529;CYRILLIC SMALL LETTER EN WITH LEFT HOOK;Ll;0;L;;;;;N;;;0528;;0528 +052A;CYRILLIC CAPITAL LETTER DZZHE;Lu;0;L;;;;;N;;;;052B; +052B;CYRILLIC SMALL LETTER DZZHE;Ll;0;L;;;;;N;;;052A;;052A +052C;CYRILLIC CAPITAL LETTER DCHE;Lu;0;L;;;;;N;;;;052D; +052D;CYRILLIC SMALL LETTER DCHE;Ll;0;L;;;;;N;;;052C;;052C +052E;CYRILLIC CAPITAL LETTER EL WITH DESCENDER;Lu;0;L;;;;;N;;;;052F; +052F;CYRILLIC SMALL LETTER EL WITH DESCENDER;Ll;0;L;;;;;N;;;052E;;052E 0531;ARMENIAN CAPITAL LETTER AYB;Lu;0;L;;;;;N;;;;0561; 0532;ARMENIAN CAPITAL LETTER BEN;Lu;0;L;;;;;N;;;;0562; 0533;ARMENIAN CAPITAL LETTER GIM;Lu;0;L;;;;;N;;;;0563; @@ -1394,6 +1403,9 @@ 0587;ARMENIAN SMALL LIGATURE ECH YIWN;Ll;0;L;<compat> 0565 0582;;;;N;;;;; 0589;ARMENIAN FULL STOP;Po;0;L;;;;;N;ARMENIAN PERIOD;;;; 058A;ARMENIAN HYPHEN;Pd;0;ON;;;;;N;;;;; +058D;RIGHT-FACING ARMENIAN ETERNITY SIGN;So;0;ON;;;;;N;;;;; +058E;LEFT-FACING ARMENIAN ETERNITY SIGN;So;0;ON;;;;;N;;;;; +058F;ARMENIAN DRAM SIGN;Sc;0;ET;;;;;N;;;;; 0591;HEBREW ACCENT ETNAHTA;Mn;220;NSM;;;;;N;;;;; 0592;HEBREW ACCENT SEGOL;Mn;230;NSM;;;;;N;;;;; 0593;HEBREW ACCENT SHALSHELET;Mn;230;NSM;;;;;N;;;;; @@ -1485,6 +1497,8 @@ 0601;ARABIC SIGN SANAH;Cf;0;AN;;;;;N;;;;; 0602;ARABIC FOOTNOTE MARKER;Cf;0;AN;;;;;N;;;;; 0603;ARABIC SIGN SAFHA;Cf;0;AN;;;;;N;;;;; +0604;ARABIC SIGN SAMVAT;Cf;0;AN;;;;;N;;;;; +0605;ARABIC NUMBER MARK ABOVE;Cf;0;AN;;;;;N;;;;; 0606;ARABIC-INDIC CUBE ROOT;Sm;0;ON;;;;;N;;;;; 0607;ARABIC-INDIC FOURTH ROOT;Sm;0;ON;;;;;N;;;;; 0608;ARABIC RAY;Sm;0;AL;;;;;N;;;;; @@ -1507,6 +1521,7 @@ 0619;ARABIC SMALL DAMMA;Mn;31;NSM;;;;;N;;;;; 061A;ARABIC SMALL KASRA;Mn;32;NSM;;;;;N;;;;; 061B;ARABIC SEMICOLON;Po;0;AL;;;;;N;;;;; +061C;ARABIC LETTER MARK;Cf;0;AL;;;;;N;;;;; 061E;ARABIC TRIPLE DOT PUNCTUATION MARK;Po;0;AL;;;;;N;;;;; 061F;ARABIC QUESTION MARK;Po;0;AL;;;;;N;;;;; 0620;ARABIC LETTER KASHMIRI YEH;Lo;0;AL;;;;;N;;;;; @@ -1747,7 +1762,7 @@ 070B;SYRIAC HARKLEAN OBELUS;Po;0;AL;;;;;N;;;;; 070C;SYRIAC HARKLEAN METOBELUS;Po;0;AL;;;;;N;;;;; 070D;SYRIAC HARKLEAN ASTERISCUS;Po;0;AL;;;;;N;;;;; -070F;SYRIAC ABBREVIATION MARK;Cf;0;AN;;;;;N;;;;; +070F;SYRIAC ABBREVIATION MARK;Cf;0;AL;;;;;N;;;;; 0710;SYRIAC LETTER ALAPH;Lo;0;AL;;;;;N;;;;; 0711;SYRIAC LETTER SUPERSCRIPT ALAPH;Mn;36;NSM;;;;;N;;;;; 0712;SYRIAC LETTER BETH;Lo;0;AL;;;;;N;;;;; @@ -2057,6 +2072,53 @@ 085A;MANDAIC VOCALIZATION MARK;Mn;220;NSM;;;;;N;;;;; 085B;MANDAIC GEMINATION MARK;Mn;220;NSM;;;;;N;;;;; 085E;MANDAIC PUNCTUATION;Po;0;R;;;;;N;;;;; +08A0;ARABIC LETTER BEH WITH SMALL V BELOW;Lo;0;AL;;;;;N;;;;; +08A1;ARABIC LETTER BEH WITH HAMZA ABOVE;Lo;0;AL;;;;;N;;;;; +08A2;ARABIC LETTER JEEM WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;; +08A3;ARABIC LETTER TAH WITH TWO DOTS ABOVE;Lo;0;AL;;;;;N;;;;; +08A4;ARABIC LETTER FEH WITH DOT BELOW AND THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;; +08A5;ARABIC LETTER QAF WITH DOT BELOW;Lo;0;AL;;;;;N;;;;; +08A6;ARABIC LETTER LAM WITH DOUBLE BAR;Lo;0;AL;;;;;N;;;;; +08A7;ARABIC LETTER MEEM WITH THREE DOTS ABOVE;Lo;0;AL;;;;;N;;;;; +08A8;ARABIC LETTER YEH WITH TWO DOTS BELOW AND HAMZA ABOVE;Lo;0;AL;;;;;N;;;;; +08A9;ARABIC LETTER YEH WITH TWO DOTS BELOW AND DOT ABOVE;Lo;0;AL;;;;;N;;;;; +08AA;ARABIC LETTER REH WITH LOOP;Lo;0;AL;;;;;N;;;;; +08AB;ARABIC LETTER WAW WITH DOT WITHIN;Lo;0;AL;;;;;N;;;;; +08AC;ARABIC LETTER ROHINGYA YEH;Lo;0;AL;;;;;N;;;;; +08AD;ARABIC LETTER LOW ALEF;Lo;0;AL;;;;;N;;;;; +08AE;ARABIC LETTER DAL WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;;;;; +08AF;ARABIC LETTER SAD WITH THREE DOTS BELOW;Lo;0;AL;;;;;N;;;;; +08B0;ARABIC LETTER GAF WITH INVERTED STROKE;Lo;0;AL;;;;;N;;;;; +08B1;ARABIC LETTER STRAIGHT WAW;Lo;0;AL;;;;;N;;;;; +08B2;ARABIC LETTER ZAIN WITH INVERTED V ABOVE;Lo;0;AL;;;;;N;;;;; +08E4;ARABIC CURLY FATHA;Mn;230;NSM;;;;;N;;;;; +08E5;ARABIC CURLY DAMMA;Mn;230;NSM;;;;;N;;;;; +08E6;ARABIC CURLY KASRA;Mn;220;NSM;;;;;N;;;;; +08E7;ARABIC CURLY FATHATAN;Mn;230;NSM;;;;;N;;;;; +08E8;ARABIC CURLY DAMMATAN;Mn;230;NSM;;;;;N;;;;; +08E9;ARABIC CURLY KASRATAN;Mn;220;NSM;;;;;N;;;;; +08EA;ARABIC TONE ONE DOT ABOVE;Mn;230;NSM;;;;;N;;;;; +08EB;ARABIC TONE TWO DOTS ABOVE;Mn;230;NSM;;;;;N;;;;; +08EC;ARABIC TONE LOOP ABOVE;Mn;230;NSM;;;;;N;;;;; +08ED;ARABIC TONE ONE DOT BELOW;Mn;220;NSM;;;;;N;;;;; +08EE;ARABIC TONE TWO DOTS BELOW;Mn;220;NSM;;;;;N;;;;; +08EF;ARABIC TONE LOOP BELOW;Mn;220;NSM;;;;;N;;;;; +08F0;ARABIC OPEN FATHATAN;Mn;27;NSM;;;;;N;;;;; +08F1;ARABIC OPEN DAMMATAN;Mn;28;NSM;;;;;N;;;;; +08F2;ARABIC OPEN KASRATAN;Mn;29;NSM;;;;;N;;;;; +08F3;ARABIC SMALL HIGH WAW;Mn;230;NSM;;;;;N;;;;; +08F4;ARABIC FATHA WITH RING;Mn;230;NSM;;;;;N;;;;; +08F5;ARABIC FATHA WITH DOT ABOVE;Mn;230;NSM;;;;;N;;;;; +08F6;ARABIC KASRA WITH DOT BELOW;Mn;220;NSM;;;;;N;;;;; +08F7;ARABIC LEFT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; +08F8;ARABIC RIGHT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; +08F9;ARABIC LEFT ARROWHEAD BELOW;Mn;220;NSM;;;;;N;;;;; +08FA;ARABIC RIGHT ARROWHEAD BELOW;Mn;220;NSM;;;;;N;;;;; +08FB;ARABIC DOUBLE RIGHT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; +08FC;ARABIC DOUBLE RIGHT ARROWHEAD ABOVE WITH DOT;Mn;230;NSM;;;;;N;;;;; +08FD;ARABIC RIGHT ARROWHEAD ABOVE WITH DOT;Mn;230;NSM;;;;;N;;;;; +08FE;ARABIC DAMMA WITH DOT;Mn;230;NSM;;;;;N;;;;; +08FF;ARABIC MARK SIDEWAYS NOON GHUNNA;Mn;230;NSM;;;;;N;;;;; 0900;DEVANAGARI SIGN INVERTED CANDRABINDU;Mn;0;NSM;;;;;N;;;;; 0901;DEVANAGARI SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;; 0902;DEVANAGARI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;; @@ -2177,6 +2239,7 @@ 0975;DEVANAGARI LETTER AW;Lo;0;L;;;;;N;;;;; 0976;DEVANAGARI LETTER UE;Lo;0;L;;;;;N;;;;; 0977;DEVANAGARI LETTER UUE;Lo;0;L;;;;;N;;;;; +0978;DEVANAGARI LETTER MARWARI DDA;Lo;0;L;;;;;N;;;;; 0979;DEVANAGARI LETTER ZHA;Lo;0;L;;;;;N;;;;; 097A;DEVANAGARI LETTER HEAVY YA;Lo;0;L;;;;;N;;;;; 097B;DEVANAGARI LETTER GGA;Lo;0;L;;;;;N;;;;; @@ -2184,6 +2247,7 @@ 097D;DEVANAGARI LETTER GLOTTAL STOP;Lo;0;L;;;;;N;;;;; 097E;DEVANAGARI LETTER DDDA;Lo;0;L;;;;;N;;;;; 097F;DEVANAGARI LETTER BBA;Lo;0;L;;;;;N;;;;; +0980;BENGALI ANJI;Lo;0;L;;;;;N;;;;; 0981;BENGALI SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;; 0982;BENGALI SIGN ANUSVARA;Mc;0;L;;;;;N;;;;; 0983;BENGALI SIGN VISARGA;Mc;0;L;;;;;N;;;;; @@ -2437,6 +2501,7 @@ 0AED;GUJARATI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; 0AEE;GUJARATI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; 0AEF;GUJARATI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +0AF0;GUJARATI ABBREVIATION SIGN;Po;0;L;;;;;N;;;;; 0AF1;GUJARATI RUPEE SIGN;Sc;0;ET;;;;;N;;;;; 0B01;ORIYA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;; 0B02;ORIYA SIGN ANUSVARA;Mc;0;L;;;;;N;;;;; @@ -2600,6 +2665,7 @@ 0BF8;TAMIL AS ABOVE SIGN;So;0;ON;;;;;N;;;;; 0BF9;TAMIL RUPEE SIGN;Sc;0;ET;;;;;N;;;;; 0BFA;TAMIL NUMBER SIGN;So;0;ON;;;;;N;;;;; +0C00;TELUGU SIGN COMBINING CANDRABINDU ABOVE;Mn;0;NSM;;;;;N;;;;; 0C01;TELUGU SIGN CANDRABINDU;Mc;0;L;;;;;N;;;;; 0C02;TELUGU SIGN ANUSVARA;Mc;0;L;;;;;N;;;;; 0C03;TELUGU SIGN VISARGA;Mc;0;L;;;;;N;;;;; @@ -2647,6 +2713,7 @@ 0C31;TELUGU LETTER RRA;Lo;0;L;;;;;N;;;;; 0C32;TELUGU LETTER LA;Lo;0;L;;;;;N;;;;; 0C33;TELUGU LETTER LLA;Lo;0;L;;;;;N;;;;; +0C34;TELUGU LETTER LLLA;Lo;0;L;;;;;N;;;;; 0C35;TELUGU LETTER VA;Lo;0;L;;;;;N;;;;; 0C36;TELUGU LETTER SHA;Lo;0;L;;;;;N;;;;; 0C37;TELUGU LETTER SSA;Lo;0;L;;;;;N;;;;; @@ -2693,6 +2760,7 @@ 0C7D;TELUGU FRACTION DIGIT TWO FOR EVEN POWERS OF FOUR;No;0;ON;;;;2;N;;;;; 0C7E;TELUGU FRACTION DIGIT THREE FOR EVEN POWERS OF FOUR;No;0;ON;;;;3;N;;;;; 0C7F;TELUGU SIGN TUUMU;So;0;L;;;;;N;;;;; +0C81;KANNADA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;; 0C82;KANNADA SIGN ANUSVARA;Mc;0;L;;;;;N;;;;; 0C83;KANNADA SIGN VISARGA;Mc;0;L;;;;;N;;;;; 0C85;KANNADA LETTER A;Lo;0;L;;;;;N;;;;; @@ -2779,6 +2847,7 @@ 0CEF;KANNADA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; 0CF1;KANNADA SIGN JIHVAMULIYA;Lo;0;L;;;;;N;;;;; 0CF2;KANNADA SIGN UPADHMANIYA;Lo;0;L;;;;;N;;;;; +0D01;MALAYALAM SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;; 0D02;MALAYALAM SIGN ANUSVARA;Mc;0;L;;;;;N;;;;; 0D03;MALAYALAM SIGN VISARGA;Mc;0;L;;;;;N;;;;; 0D05;MALAYALAM LETTER A;Lo;0;L;;;;;N;;;;; @@ -2954,6 +3023,16 @@ 0DDD;SINHALA VOWEL SIGN KOMBUVA HAA DIGA AELA-PILLA;Mc;0;L;0DDC 0DCA;;;;N;;;;; 0DDE;SINHALA VOWEL SIGN KOMBUVA HAA GAYANUKITTA;Mc;0;L;0DD9 0DDF;;;;N;;;;; 0DDF;SINHALA VOWEL SIGN GAYANUKITTA;Mc;0;L;;;;;N;;;;; +0DE6;SINHALA LITH DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +0DE7;SINHALA LITH DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +0DE8;SINHALA LITH DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +0DE9;SINHALA LITH DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +0DEA;SINHALA LITH DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +0DEB;SINHALA LITH DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +0DEC;SINHALA LITH DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +0DED;SINHALA LITH DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +0DEE;SINHALA LITH DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +0DEF;SINHALA LITH DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; 0DF2;SINHALA VOWEL SIGN DIGA GAETTA-PILLA;Mc;0;L;;;;;N;;;;; 0DF3;SINHALA VOWEL SIGN DIGA GAYANUKITTA;Mc;0;L;;;;;N;;;;; 0DF4;SINHALA PUNCTUATION KUNDDALIYA;Po;0;L;;;;;N;;;;; @@ -3109,6 +3188,8 @@ 0ED9;LAO DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; 0EDC;LAO HO NO;Lo;0;L;<compat> 0EAB 0E99;;;;N;;;;; 0EDD;LAO HO MO;Lo;0;L;<compat> 0EAB 0EA1;;;;N;;;;; +0EDE;LAO LETTER KHMU GO;Lo;0;L;;;;;N;;;;; +0EDF;LAO LETTER KHMU NYO;Lo;0;L;;;;;N;;;;; 0F00;TIBETAN SYLLABLE OM;Lo;0;L;;;;;N;;;;; 0F01;TIBETAN MARK GTER YIG MGO TRUNCATED A;So;0;L;;;;;N;;;;; 0F02;TIBETAN MARK GTER YIG MGO -UM RNAM BCAD MA;So;0;L;;;;;N;;;;; @@ -3129,7 +3210,7 @@ 0F11;TIBETAN MARK RIN CHEN SPUNGS SHAD;Po;0;L;;;;;N;TIBETAN RINCHANPHUNGSHAD;;;; 0F12;TIBETAN MARK RGYA GRAM SHAD;Po;0;L;;;;;N;;;;; 0F13;TIBETAN MARK CARET -DZUD RTAGS ME LONG CAN;So;0;L;;;;;N;;;;; -0F14;TIBETAN MARK GTER TSHEG;So;0;L;;;;;N;TIBETAN COMMA;;;; +0F14;TIBETAN MARK GTER TSHEG;Po;0;L;;;;;N;TIBETAN COMMA;;;; 0F15;TIBETAN LOGOTYPE SIGN CHAD RTAGS;So;0;L;;;;;N;;;;; 0F16;TIBETAN LOGOTYPE SIGN LHAG RTAGS;So;0;L;;;;;N;;;;; 0F17;TIBETAN ASTROLOGICAL SIGN SGRA GCAN -CHAR RTAGS;So;0;L;;;;;N;;;;; @@ -3518,6 +3599,8 @@ 10C3;GEORGIAN CAPITAL LETTER WE;Lu;0;L;;;;;N;;;;2D23; 10C4;GEORGIAN CAPITAL LETTER HAR;Lu;0;L;;;;;N;;;;2D24; 10C5;GEORGIAN CAPITAL LETTER HOE;Lu;0;L;;;;;N;;;;2D25; +10C7;GEORGIAN CAPITAL LETTER YN;Lu;0;L;;;;;N;;;;2D27; +10CD;GEORGIAN CAPITAL LETTER AEN;Lu;0;L;;;;;N;;;;2D2D; 10D0;GEORGIAN LETTER AN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER AN;;;; 10D1;GEORGIAN LETTER BAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER BAN;;;; 10D2;GEORGIAN LETTER GAN;Lo;0;L;;;;;N;GEORGIAN SMALL LETTER GAN;;;; @@ -3563,6 +3646,9 @@ 10FA;GEORGIAN LETTER AIN;Lo;0;L;;;;;N;;;;; 10FB;GEORGIAN PARAGRAPH SEPARATOR;Po;0;L;;;;;N;;;;; 10FC;MODIFIER LETTER GEORGIAN NAR;Lm;0;L;<super> 10DC;;;;N;;;;; +10FD;GEORGIAN LETTER AEN;Lo;0;L;;;;;N;;;;; +10FE;GEORGIAN LETTER HARD SIGN;Lo;0;L;;;;;N;;;;; +10FF;GEORGIAN LETTER LABIAL SIGN;Lo;0;L;;;;;N;;;;; 1100;HANGUL CHOSEONG KIYEOK;Lo;0;L;;;;;N;;;;; 1101;HANGUL CHOSEONG SSANGKIYEOK;Lo;0;L;;;;;N;;;;; 1102;HANGUL CHOSEONG NIEUN;Lo;0;L;;;;;N;;;;; @@ -4148,7 +4234,7 @@ 135D;ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;; 135E;ETHIOPIC COMBINING VOWEL LENGTH MARK;Mn;230;NSM;;;;;N;;;;; 135F;ETHIOPIC COMBINING GEMINATION MARK;Mn;230;NSM;;;;;N;;;;; -1360;ETHIOPIC SECTION MARK;So;0;L;;;;;N;;;;; +1360;ETHIOPIC SECTION MARK;Po;0;L;;;;;N;;;;; 1361;ETHIOPIC WORDSPACE;Po;0;L;;;;;N;;;;; 1362;ETHIOPIC FULL STOP;Po;0;L;;;;;N;;;;; 1363;ETHIOPIC COMMA;Po;0;L;;;;;N;;;;; @@ -5038,6 +5124,14 @@ 16EE;RUNIC ARLAUG SYMBOL;Nl;0;L;;;;17;N;;;;; 16EF;RUNIC TVIMADUR SYMBOL;Nl;0;L;;;;18;N;;;;; 16F0;RUNIC BELGTHOR SYMBOL;Nl;0;L;;;;19;N;;;;; +16F1;RUNIC LETTER K;Lo;0;L;;;;;N;;;;; +16F2;RUNIC LETTER SH;Lo;0;L;;;;;N;;;;; +16F3;RUNIC LETTER OO;Lo;0;L;;;;;N;;;;; +16F4;RUNIC LETTER FRANKS CASKET OS;Lo;0;L;;;;;N;;;;; +16F5;RUNIC LETTER FRANKS CASKET IS;Lo;0;L;;;;;N;;;;; +16F6;RUNIC LETTER FRANKS CASKET EH;Lo;0;L;;;;;N;;;;; +16F7;RUNIC LETTER FRANKS CASKET AC;Lo;0;L;;;;;N;;;;; +16F8;RUNIC LETTER FRANKS CASKET AESC;Lo;0;L;;;;;N;;;;; 1700;TAGALOG LETTER A;Lo;0;L;;;;;N;;;;; 1701;TAGALOG LETTER I;Lo;0;L;;;;;N;;;;; 1702;TAGALOG LETTER U;Lo;0;L;;;;;N;;;;; @@ -5171,8 +5265,8 @@ 17B1;KHMER INDEPENDENT VOWEL QOO TYPE ONE;Lo;0;L;;;;;N;;;;; 17B2;KHMER INDEPENDENT VOWEL QOO TYPE TWO;Lo;0;L;;;;;N;;;;; 17B3;KHMER INDEPENDENT VOWEL QAU;Lo;0;L;;;;;N;;;;; -17B4;KHMER VOWEL INHERENT AQ;Cf;0;L;;;;;N;;;;; -17B5;KHMER VOWEL INHERENT AA;Cf;0;L;;;;;N;;;;; +17B4;KHMER VOWEL INHERENT AQ;Mn;0;NSM;;;;;N;;;;; +17B5;KHMER VOWEL INHERENT AA;Mn;0;NSM;;;;;N;;;;; 17B6;KHMER VOWEL SIGN AA;Mc;0;L;;;;;N;;;;; 17B7;KHMER VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;; 17B8;KHMER VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;; @@ -5247,7 +5341,7 @@ 180B;MONGOLIAN FREE VARIATION SELECTOR ONE;Mn;0;NSM;;;;;N;;;;; 180C;MONGOLIAN FREE VARIATION SELECTOR TWO;Mn;0;NSM;;;;;N;;;;; 180D;MONGOLIAN FREE VARIATION SELECTOR THREE;Mn;0;NSM;;;;;N;;;;; -180E;MONGOLIAN VOWEL SEPARATOR;Zs;0;WS;;;;;N;;;;; +180E;MONGOLIAN VOWEL SEPARATOR;Cf;0;BN;;;;;N;;;;; 1810;MONGOLIAN DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; 1811;MONGOLIAN DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; 1812;MONGOLIAN DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; @@ -5488,6 +5582,8 @@ 191A;LIMBU LETTER SSA;Lo;0;L;;;;;N;;;;; 191B;LIMBU LETTER SA;Lo;0;L;;;;;N;;;;; 191C;LIMBU LETTER HA;Lo;0;L;;;;;N;;;;; +191D;LIMBU LETTER GYAN;Lo;0;L;;;;;N;;;;; +191E;LIMBU LETTER TRA;Lo;0;L;;;;;N;;;;; 1920;LIMBU VOWEL SIGN A;Mn;0;NSM;;;;;N;;;;; 1921;LIMBU VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;; 1922;LIMBU VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;; @@ -5702,7 +5798,7 @@ 1A18;BUGINESE VOWEL SIGN U;Mn;220;NSM;;;;;N;;;;; 1A19;BUGINESE VOWEL SIGN E;Mc;0;L;;;;;N;;;;; 1A1A;BUGINESE VOWEL SIGN O;Mc;0;L;;;;;N;;;;; -1A1B;BUGINESE VOWEL SIGN AE;Mc;0;L;;;;;N;;;;; +1A1B;BUGINESE VOWEL SIGN AE;Mn;0;NSM;;;;;N;;;;; 1A1E;BUGINESE PALLAWA;Po;0;L;;;;;N;;;;; 1A1F;BUGINESE END OF SECTION;Po;0;L;;;;;N;;;;; 1A20;TAI THAM LETTER HIGH KA;Lo;0;L;;;;;N;;;;; @@ -5832,6 +5928,21 @@ 1AAB;TAI THAM SIGN SATKAANKUU;Po;0;L;;;;;N;;;;; 1AAC;TAI THAM SIGN HANG;Po;0;L;;;;;N;;;;; 1AAD;TAI THAM SIGN CAANG;Po;0;L;;;;;N;;;;; +1AB0;COMBINING DOUBLED CIRCUMFLEX ACCENT;Mn;230;NSM;;;;;N;;;;; +1AB1;COMBINING DIAERESIS-RING;Mn;230;NSM;;;;;N;;;;; +1AB2;COMBINING INFINITY;Mn;230;NSM;;;;;N;;;;; +1AB3;COMBINING DOWNWARDS ARROW;Mn;230;NSM;;;;;N;;;;; +1AB4;COMBINING TRIPLE DOT;Mn;230;NSM;;;;;N;;;;; +1AB5;COMBINING X-X BELOW;Mn;220;NSM;;;;;N;;;;; +1AB6;COMBINING WIGGLY LINE BELOW;Mn;220;NSM;;;;;N;;;;; +1AB7;COMBINING OPEN MARK BELOW;Mn;220;NSM;;;;;N;;;;; +1AB8;COMBINING DOUBLE OPEN MARK BELOW;Mn;220;NSM;;;;;N;;;;; +1AB9;COMBINING LIGHT CENTRALIZATION STROKE BELOW;Mn;220;NSM;;;;;N;;;;; +1ABA;COMBINING STRONG CENTRALIZATION STROKE BELOW;Mn;220;NSM;;;;;N;;;;; +1ABB;COMBINING PARENTHESES ABOVE;Mn;230;NSM;;;;;N;;;;; +1ABC;COMBINING DOUBLE PARENTHESES ABOVE;Mn;230;NSM;;;;;N;;;;; +1ABD;COMBINING PARENTHESES BELOW;Mn;220;NSM;;;;;N;;;;; +1ABE;COMBINING PARENTHESES OVERLAY;Me;0;NSM;;;;;N;;;;; 1B00;BALINESE SIGN ULU RICEM;Mn;0;NSM;;;;;N;;;;; 1B01;BALINESE SIGN ULU CANDRA;Mn;0;NSM;;;;;N;;;;; 1B02;BALINESE SIGN CECEK;Mn;0;NSM;;;;;N;;;;; @@ -5996,6 +6107,9 @@ 1BA8;SUNDANESE VOWEL SIGN PAMEPET;Mn;0;NSM;;;;;N;;;;; 1BA9;SUNDANESE VOWEL SIGN PANEULEUNG;Mn;0;NSM;;;;;N;;;;; 1BAA;SUNDANESE SIGN PAMAAEH;Mc;9;L;;;;;N;;;;; +1BAB;SUNDANESE SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;; +1BAC;SUNDANESE CONSONANT SIGN PASANGAN MA;Mn;0;NSM;;;;;N;;;;; +1BAD;SUNDANESE CONSONANT SIGN PASANGAN WA;Mn;0;NSM;;;;;N;;;;; 1BAE;SUNDANESE LETTER KHA;Lo;0;L;;;;;N;;;;; 1BAF;SUNDANESE LETTER SYA;Lo;0;L;;;;;N;;;;; 1BB0;SUNDANESE DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; @@ -6008,6 +6122,12 @@ 1BB7;SUNDANESE DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; 1BB8;SUNDANESE DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; 1BB9;SUNDANESE DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +1BBA;SUNDANESE AVAGRAHA;Lo;0;L;;;;;N;;;;; +1BBB;SUNDANESE LETTER REU;Lo;0;L;;;;;N;;;;; +1BBC;SUNDANESE LETTER LEU;Lo;0;L;;;;;N;;;;; +1BBD;SUNDANESE LETTER BHA;Lo;0;L;;;;;N;;;;; +1BBE;SUNDANESE LETTER FINAL K;Lo;0;L;;;;;N;;;;; +1BBF;SUNDANESE LETTER FINAL M;Lo;0;L;;;;;N;;;;; 1BC0;BATAK LETTER A;Lo;0;L;;;;;N;;;;; 1BC1;BATAK LETTER SIMALUNGUN A;Lo;0;L;;;;;N;;;;; 1BC2;BATAK LETTER HA;Lo;0;L;;;;;N;;;;; @@ -6186,6 +6306,14 @@ 1C7D;OL CHIKI AHAD;Lm;0;L;;;;;N;;;;; 1C7E;OL CHIKI PUNCTUATION MUCAAD;Po;0;L;;;;;N;;;;; 1C7F;OL CHIKI PUNCTUATION DOUBLE MUCAAD;Po;0;L;;;;;N;;;;; +1CC0;SUNDANESE PUNCTUATION BINDU SURYA;Po;0;L;;;;;N;;;;; +1CC1;SUNDANESE PUNCTUATION BINDU PANGLONG;Po;0;L;;;;;N;;;;; +1CC2;SUNDANESE PUNCTUATION BINDU PURNAMA;Po;0;L;;;;;N;;;;; +1CC3;SUNDANESE PUNCTUATION BINDU CAKRA;Po;0;L;;;;;N;;;;; +1CC4;SUNDANESE PUNCTUATION BINDU LEU SATANGA;Po;0;L;;;;;N;;;;; +1CC5;SUNDANESE PUNCTUATION BINDU KA SATANGA;Po;0;L;;;;;N;;;;; +1CC6;SUNDANESE PUNCTUATION BINDU DA SATANGA;Po;0;L;;;;;N;;;;; +1CC7;SUNDANESE PUNCTUATION BINDU BA SATANGA;Po;0;L;;;;;N;;;;; 1CD0;VEDIC TONE KARSHANA;Mn;230;NSM;;;;;N;;;;; 1CD1;VEDIC TONE SHARA;Mn;230;NSM;;;;;N;;;;; 1CD2;VEDIC TONE PRENKHA;Mn;230;NSM;;;;;N;;;;; @@ -6221,6 +6349,12 @@ 1CF0;VEDIC SIGN RTHANG LONG ANUSVARA;Lo;0;L;;;;;N;;;;; 1CF1;VEDIC SIGN ANUSVARA UBHAYATO MUKHA;Lo;0;L;;;;;N;;;;; 1CF2;VEDIC SIGN ARDHAVISARGA;Mc;0;L;;;;;N;;;;; +1CF3;VEDIC SIGN ROTATED ARDHAVISARGA;Mc;0;L;;;;;N;;;;; +1CF4;VEDIC TONE CANDRA ABOVE;Mn;230;NSM;;;;;N;;;;; +1CF5;VEDIC SIGN JIHVAMULIYA;Lo;0;L;;;;;N;;;;; +1CF6;VEDIC SIGN UPADHMANIYA;Lo;0;L;;;;;N;;;;; +1CF8;VEDIC TONE RING ABOVE;Mn;230;NSM;;;;;N;;;;; +1CF9;VEDIC TONE DOUBLE RING ABOVE;Mn;230;NSM;;;;;N;;;;; 1D00;LATIN LETTER SMALL CAPITAL A;Ll;0;L;;;;;N;;;;; 1D01;LATIN LETTER SMALL CAPITAL AE;Ll;0;L;;;;;N;;;;; 1D02;LATIN SMALL LETTER TURNED AE;Ll;0;L;;;;;N;;;;; @@ -6319,15 +6453,15 @@ 1D5F;MODIFIER LETTER SMALL DELTA;Lm;0;L;<super> 03B4;;;;N;;;;; 1D60;MODIFIER LETTER SMALL GREEK PHI;Lm;0;L;<super> 03C6;;;;N;;;;; 1D61;MODIFIER LETTER SMALL CHI;Lm;0;L;<super> 03C7;;;;N;;;;; -1D62;LATIN SUBSCRIPT SMALL LETTER I;Ll;0;L;<sub> 0069;;;;N;;;;; -1D63;LATIN SUBSCRIPT SMALL LETTER R;Ll;0;L;<sub> 0072;;;;N;;;;; -1D64;LATIN SUBSCRIPT SMALL LETTER U;Ll;0;L;<sub> 0075;;;;N;;;;; -1D65;LATIN SUBSCRIPT SMALL LETTER V;Ll;0;L;<sub> 0076;;;;N;;;;; -1D66;GREEK SUBSCRIPT SMALL LETTER BETA;Ll;0;L;<sub> 03B2;;;;N;;;;; -1D67;GREEK SUBSCRIPT SMALL LETTER GAMMA;Ll;0;L;<sub> 03B3;;;;N;;;;; -1D68;GREEK SUBSCRIPT SMALL LETTER RHO;Ll;0;L;<sub> 03C1;;;;N;;;;; -1D69;GREEK SUBSCRIPT SMALL LETTER PHI;Ll;0;L;<sub> 03C6;;;;N;;;;; -1D6A;GREEK SUBSCRIPT SMALL LETTER CHI;Ll;0;L;<sub> 03C7;;;;N;;;;; +1D62;LATIN SUBSCRIPT SMALL LETTER I;Lm;0;L;<sub> 0069;;;;N;;;;; +1D63;LATIN SUBSCRIPT SMALL LETTER R;Lm;0;L;<sub> 0072;;;;N;;;;; +1D64;LATIN SUBSCRIPT SMALL LETTER U;Lm;0;L;<sub> 0075;;;;N;;;;; +1D65;LATIN SUBSCRIPT SMALL LETTER V;Lm;0;L;<sub> 0076;;;;N;;;;; +1D66;GREEK SUBSCRIPT SMALL LETTER BETA;Lm;0;L;<sub> 03B2;;;;N;;;;; +1D67;GREEK SUBSCRIPT SMALL LETTER GAMMA;Lm;0;L;<sub> 03B3;;;;N;;;;; +1D68;GREEK SUBSCRIPT SMALL LETTER RHO;Lm;0;L;<sub> 03C1;;;;N;;;;; +1D69;GREEK SUBSCRIPT SMALL LETTER PHI;Lm;0;L;<sub> 03C6;;;;N;;;;; +1D6A;GREEK SUBSCRIPT SMALL LETTER CHI;Lm;0;L;<sub> 03C7;;;;N;;;;; 1D6B;LATIN SMALL LETTER UE;Ll;0;L;;;;;N;;;;; 1D6C;LATIN SMALL LETTER B WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;; 1D6D;LATIN SMALL LETTER D WITH MIDDLE TILDE;Ll;0;L;;;;;N;;;;; @@ -6452,6 +6586,21 @@ 1DE4;COMBINING LATIN SMALL LETTER S;Mn;230;NSM;;;;;N;;;;; 1DE5;COMBINING LATIN SMALL LETTER LONG S;Mn;230;NSM;;;;;N;;;;; 1DE6;COMBINING LATIN SMALL LETTER Z;Mn;230;NSM;;;;;N;;;;; +1DE7;COMBINING LATIN SMALL LETTER ALPHA;Mn;230;NSM;;;;;N;;;;; +1DE8;COMBINING LATIN SMALL LETTER B;Mn;230;NSM;;;;;N;;;;; +1DE9;COMBINING LATIN SMALL LETTER BETA;Mn;230;NSM;;;;;N;;;;; +1DEA;COMBINING LATIN SMALL LETTER SCHWA;Mn;230;NSM;;;;;N;;;;; +1DEB;COMBINING LATIN SMALL LETTER F;Mn;230;NSM;;;;;N;;;;; +1DEC;COMBINING LATIN SMALL LETTER L WITH DOUBLE MIDDLE TILDE;Mn;230;NSM;;;;;N;;;;; +1DED;COMBINING LATIN SMALL LETTER O WITH LIGHT CENTRALIZATION STROKE;Mn;230;NSM;;;;;N;;;;; +1DEE;COMBINING LATIN SMALL LETTER P;Mn;230;NSM;;;;;N;;;;; +1DEF;COMBINING LATIN SMALL LETTER ESH;Mn;230;NSM;;;;;N;;;;; +1DF0;COMBINING LATIN SMALL LETTER U WITH LIGHT CENTRALIZATION STROKE;Mn;230;NSM;;;;;N;;;;; +1DF1;COMBINING LATIN SMALL LETTER W;Mn;230;NSM;;;;;N;;;;; +1DF2;COMBINING LATIN SMALL LETTER A WITH DIAERESIS;Mn;230;NSM;;;;;N;;;;; +1DF3;COMBINING LATIN SMALL LETTER O WITH DIAERESIS;Mn;230;NSM;;;;;N;;;;; +1DF4;COMBINING LATIN SMALL LETTER U WITH DIAERESIS;Mn;230;NSM;;;;;N;;;;; +1DF5;COMBINING UP TACK ABOVE;Mn;230;NSM;;;;;N;;;;; 1DFC;COMBINING DOUBLE INVERTED BREVE BELOW;Mn;233;NSM;;;;;N;;;;; 1DFD;COMBINING ALMOST EQUAL TO BELOW;Mn;220;NSM;;;;;N;;;;; 1DFE;COMBINING LEFT ARROWHEAD ABOVE;Mn;230;NSM;;;;;N;;;;; @@ -7046,6 +7195,10 @@ 2062;INVISIBLE TIMES;Cf;0;BN;;;;;N;;;;; 2063;INVISIBLE SEPARATOR;Cf;0;BN;;;;;N;;;;; 2064;INVISIBLE PLUS;Cf;0;BN;;;;;N;;;;; +2066;LEFT-TO-RIGHT ISOLATE;Cf;0;LRI;;;;;N;;;;; +2067;RIGHT-TO-LEFT ISOLATE;Cf;0;RLI;;;;;N;;;;; +2068;FIRST STRONG ISOLATE;Cf;0;FSI;;;;;N;;;;; +2069;POP DIRECTIONAL ISOLATE;Cf;0;PDI;;;;;N;;;;; 206A;INHIBIT SYMMETRIC SWAPPING;Cf;0;BN;;;;;N;;;;; 206B;ACTIVATE SYMMETRIC SWAPPING;Cf;0;BN;;;;;N;;;;; 206C;INHIBIT ARABIC FORM SHAPING;Cf;0;BN;;;;;N;;;;; @@ -7120,6 +7273,10 @@ 20B7;SPESMILO SIGN;Sc;0;ET;;;;;N;;;;; 20B8;TENGE SIGN;Sc;0;ET;;;;;N;;;;; 20B9;INDIAN RUPEE SIGN;Sc;0;ET;;;;;N;;;;; +20BA;TURKISH LIRA SIGN;Sc;0;ET;;;;;N;;;;; +20BB;NORDIC MARK SIGN;Sc;0;ET;;;;;N;;;;; +20BC;MANAT SIGN;Sc;0;ET;;;;;N;;;;; +20BD;RUBLE SIGN;Sc;0;ET;;;;;N;;;;; 20D0;COMBINING LEFT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING LEFT HARPOON ABOVE;;;; 20D1;COMBINING RIGHT HARPOON ABOVE;Mn;230;NSM;;;;;N;NON-SPACING RIGHT HARPOON ABOVE;;;; 20D2;COMBINING LONG VERTICAL LINE OVERLAY;Mn;1;NSM;;;;;N;NON-SPACING LONG VERTICAL BAR OVERLAY;;;; @@ -7667,10 +7824,10 @@ 2305;PROJECTIVE;So;0;ON;;;;;N;;;;; 2306;PERSPECTIVE;So;0;ON;;;;;N;;;;; 2307;WAVY LINE;So;0;ON;;;;;N;;;;; -2308;LEFT CEILING;Sm;0;ON;;;;;Y;;;;; -2309;RIGHT CEILING;Sm;0;ON;;;;;Y;;;;; -230A;LEFT FLOOR;Sm;0;ON;;;;;Y;;;;; -230B;RIGHT FLOOR;Sm;0;ON;;;;;Y;;;;; +2308;LEFT CEILING;Ps;0;ON;;;;;Y;;;;; +2309;RIGHT CEILING;Pe;0;ON;;;;;Y;;;;; +230A;LEFT FLOOR;Ps;0;ON;;;;;Y;;;;; +230B;RIGHT FLOOR;Pe;0;ON;;;;;Y;;;;; 230C;BOTTOM RIGHT CROP;So;0;ON;;;;;N;;;;; 230D;BOTTOM LEFT CROP;So;0;ON;;;;;N;;;;; 230E;TOP RIGHT CROP;So;0;ON;;;;;N;;;;; @@ -7903,6 +8060,13 @@ 23F1;STOPWATCH;So;0;ON;;;;;N;;;;; 23F2;TIMER CLOCK;So;0;ON;;;;;N;;;;; 23F3;HOURGLASS WITH FLOWING SAND;So;0;ON;;;;;N;;;;; +23F4;BLACK MEDIUM LEFT-POINTING TRIANGLE;So;0;ON;;;;;N;;;;; +23F5;BLACK MEDIUM RIGHT-POINTING TRIANGLE;So;0;ON;;;;;N;;;;; +23F6;BLACK MEDIUM UP-POINTING TRIANGLE;So;0;ON;;;;;N;;;;; +23F7;BLACK MEDIUM DOWN-POINTING TRIANGLE;So;0;ON;;;;;N;;;;; +23F8;DOUBLE VERTICAL BAR;So;0;ON;;;;;N;;;;; +23F9;BLACK SQUARE FOR STOP;So;0;ON;;;;;N;;;;; +23FA;BLACK CIRCLE FOR RECORD;So;0;ON;;;;;N;;;;; 2400;SYMBOL FOR NULL;So;0;ON;;;;;N;GRAPHIC FOR NULL;;;; 2401;SYMBOL FOR START OF HEADING;So;0;ON;;;;;N;GRAPHIC FOR START OF HEADING;;;; 2402;SYMBOL FOR START OF TEXT;So;0;ON;;;;;N;GRAPHIC FOR START OF TEXT;;;; @@ -8625,6 +8789,7 @@ 26FD;FUEL PUMP;So;0;ON;;;;;N;;;;; 26FE;CUP ON BLACK SQUARE;So;0;ON;;;;;N;;;;; 26FF;WHITE FLAG WITH HORIZONTAL MIDDLE BLACK STRIPE;So;0;ON;;;;;N;;;;; +2700;BLACK SAFETY SCISSORS;So;0;ON;;;;;N;;;;; 2701;UPPER BLADE SCISSORS;So;0;ON;;;;;N;;;;; 2702;BLACK SCISSORS;So;0;ON;;;;;N;;;;; 2703;LOWER BLADE SCISSORS;So;0;ON;;;;;N;;;;; @@ -8827,7 +8992,9 @@ 27C8;REVERSE SOLIDUS PRECEDING SUBSET;Sm;0;ON;;;;;Y;;;;; 27C9;SUPERSET PRECEDING SOLIDUS;Sm;0;ON;;;;;Y;;;;; 27CA;VERTICAL BAR WITH HORIZONTAL STROKE;Sm;0;ON;;;;;N;;;;; +27CB;MATHEMATICAL RISING DIAGONAL;Sm;0;ON;;;;;Y;;;;; 27CC;LONG DIVISION;Sm;0;ON;;;;;Y;;;;; +27CD;MATHEMATICAL FALLING DIAGONAL;Sm;0;ON;;;;;Y;;;;; 27CE;SQUARED LOGICAL AND;Sm;0;ON;;;;;N;;;;; 27CF;SQUARED LOGICAL OR;Sm;0;ON;;;;;N;;;;; 27D0;WHITE DIAMOND WITH CENTRED DOT;Sm;0;ON;;;;;N;;;;; @@ -9723,6 +9890,9 @@ 2B4A;LEFTWARDS ARROW ABOVE ALMOST EQUAL TO;Sm;0;ON;;;;;N;;;;; 2B4B;LEFTWARDS ARROW ABOVE REVERSE TILDE OPERATOR;Sm;0;ON;;;;;N;;;;; 2B4C;RIGHTWARDS ARROW ABOVE REVERSE TILDE OPERATOR;Sm;0;ON;;;;;N;;;;; +2B4D;DOWNWARDS TRIANGLE-HEADED ZIGZAG ARROW;So;0;ON;;;;;N;;;;; +2B4E;SHORT SLANTED NORTH ARROW;So;0;ON;;;;;N;;;;; +2B4F;SHORT BACKSLANTED SOUTH ARROW;So;0;ON;;;;;N;;;;; 2B50;WHITE MEDIUM STAR;So;0;ON;;;;;N;;;;; 2B51;BLACK SMALL STAR;So;0;ON;;;;;N;;;;; 2B52;WHITE SMALL STAR;So;0;ON;;;;;N;;;;; @@ -9733,6 +9903,118 @@ 2B57;HEAVY CIRCLE WITH CIRCLE INSIDE;So;0;ON;;;;;N;;;;; 2B58;HEAVY CIRCLE;So;0;ON;;;;;N;;;;; 2B59;HEAVY CIRCLED SALTIRE;So;0;ON;;;;;N;;;;; +2B5A;SLANTED NORTH ARROW WITH HOOKED HEAD;So;0;ON;;;;;N;;;;; +2B5B;BACKSLANTED SOUTH ARROW WITH HOOKED TAIL;So;0;ON;;;;;N;;;;; +2B5C;SLANTED NORTH ARROW WITH HORIZONTAL TAIL;So;0;ON;;;;;N;;;;; +2B5D;BACKSLANTED SOUTH ARROW WITH HORIZONTAL TAIL;So;0;ON;;;;;N;;;;; +2B5E;BENT ARROW POINTING DOWNWARDS THEN NORTH EAST;So;0;ON;;;;;N;;;;; +2B5F;SHORT BENT ARROW POINTING DOWNWARDS THEN NORTH EAST;So;0;ON;;;;;N;;;;; +2B60;LEFTWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B61;UPWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B62;RIGHTWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B63;DOWNWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B64;LEFT RIGHT TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B65;UP DOWN TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B66;NORTH WEST TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B67;NORTH EAST TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B68;SOUTH EAST TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B69;SOUTH WEST TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B6A;LEFTWARDS TRIANGLE-HEADED DASHED ARROW;So;0;ON;;;;;N;;;;; +2B6B;UPWARDS TRIANGLE-HEADED DASHED ARROW;So;0;ON;;;;;N;;;;; +2B6C;RIGHTWARDS TRIANGLE-HEADED DASHED ARROW;So;0;ON;;;;;N;;;;; +2B6D;DOWNWARDS TRIANGLE-HEADED DASHED ARROW;So;0;ON;;;;;N;;;;; +2B6E;CLOCKWISE TRIANGLE-HEADED OPEN CIRCLE ARROW;So;0;ON;;;;;N;;;;; +2B6F;ANTICLOCKWISE TRIANGLE-HEADED OPEN CIRCLE ARROW;So;0;ON;;;;;N;;;;; +2B70;LEFTWARDS TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;; +2B71;UPWARDS TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;; +2B72;RIGHTWARDS TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;; +2B73;DOWNWARDS TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;; +2B76;NORTH WEST TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;; +2B77;NORTH EAST TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;; +2B78;SOUTH EAST TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;; +2B79;SOUTH WEST TRIANGLE-HEADED ARROW TO BAR;So;0;ON;;;;;N;;;;; +2B7A;LEFTWARDS TRIANGLE-HEADED ARROW WITH DOUBLE HORIZONTAL STROKE;So;0;ON;;;;;N;;;;; +2B7B;UPWARDS TRIANGLE-HEADED ARROW WITH DOUBLE HORIZONTAL STROKE;So;0;ON;;;;;N;;;;; +2B7C;RIGHTWARDS TRIANGLE-HEADED ARROW WITH DOUBLE HORIZONTAL STROKE;So;0;ON;;;;;N;;;;; +2B7D;DOWNWARDS TRIANGLE-HEADED ARROW WITH DOUBLE HORIZONTAL STROKE;So;0;ON;;;;;N;;;;; +2B7E;HORIZONTAL TAB KEY;So;0;ON;;;;;N;;;;; +2B7F;VERTICAL TAB KEY;So;0;ON;;;;;N;;;;; +2B80;LEFTWARDS TRIANGLE-HEADED ARROW OVER RIGHTWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B81;UPWARDS TRIANGLE-HEADED ARROW LEFTWARDS OF DOWNWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B82;RIGHTWARDS TRIANGLE-HEADED ARROW OVER LEFTWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B83;DOWNWARDS TRIANGLE-HEADED ARROW LEFTWARDS OF UPWARDS TRIANGLE-HEADED ARROW;So;0;ON;;;;;N;;;;; +2B84;LEFTWARDS TRIANGLE-HEADED PAIRED ARROWS;So;0;ON;;;;;N;;;;; +2B85;UPWARDS TRIANGLE-HEADED PAIRED ARROWS;So;0;ON;;;;;N;;;;; +2B86;RIGHTWARDS TRIANGLE-HEADED PAIRED ARROWS;So;0;ON;;;;;N;;;;; +2B87;DOWNWARDS TRIANGLE-HEADED PAIRED ARROWS;So;0;ON;;;;;N;;;;; +2B88;LEFTWARDS BLACK CIRCLED WHITE ARROW;So;0;ON;;;;;N;;;;; +2B89;UPWARDS BLACK CIRCLED WHITE ARROW;So;0;ON;;;;;N;;;;; +2B8A;RIGHTWARDS BLACK CIRCLED WHITE ARROW;So;0;ON;;;;;N;;;;; +2B8B;DOWNWARDS BLACK CIRCLED WHITE ARROW;So;0;ON;;;;;N;;;;; +2B8C;ANTICLOCKWISE TRIANGLE-HEADED RIGHT U-SHAPED ARROW;So;0;ON;;;;;N;;;;; +2B8D;ANTICLOCKWISE TRIANGLE-HEADED BOTTOM U-SHAPED ARROW;So;0;ON;;;;;N;;;;; +2B8E;ANTICLOCKWISE TRIANGLE-HEADED LEFT U-SHAPED ARROW;So;0;ON;;;;;N;;;;; +2B8F;ANTICLOCKWISE TRIANGLE-HEADED TOP U-SHAPED ARROW;So;0;ON;;;;;N;;;;; +2B90;RETURN LEFT;So;0;ON;;;;;N;;;;; +2B91;RETURN RIGHT;So;0;ON;;;;;N;;;;; +2B92;NEWLINE LEFT;So;0;ON;;;;;N;;;;; +2B93;NEWLINE RIGHT;So;0;ON;;;;;N;;;;; +2B94;FOUR CORNER ARROWS CIRCLING ANTICLOCKWISE;So;0;ON;;;;;N;;;;; +2B95;RIGHTWARDS BLACK ARROW;So;0;ON;;;;;N;;;;; +2B98;THREE-D TOP-LIGHTED LEFTWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +2B99;THREE-D RIGHT-LIGHTED UPWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +2B9A;THREE-D TOP-LIGHTED RIGHTWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +2B9B;THREE-D LEFT-LIGHTED DOWNWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +2B9C;BLACK LEFTWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +2B9D;BLACK UPWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +2B9E;BLACK RIGHTWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +2B9F;BLACK DOWNWARDS EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +2BA0;DOWNWARDS TRIANGLE-HEADED ARROW WITH LONG TIP LEFTWARDS;So;0;ON;;;;;N;;;;; +2BA1;DOWNWARDS TRIANGLE-HEADED ARROW WITH LONG TIP RIGHTWARDS;So;0;ON;;;;;N;;;;; +2BA2;UPWARDS TRIANGLE-HEADED ARROW WITH LONG TIP LEFTWARDS;So;0;ON;;;;;N;;;;; +2BA3;UPWARDS TRIANGLE-HEADED ARROW WITH LONG TIP RIGHTWARDS;So;0;ON;;;;;N;;;;; +2BA4;LEFTWARDS TRIANGLE-HEADED ARROW WITH LONG TIP UPWARDS;So;0;ON;;;;;N;;;;; +2BA5;RIGHTWARDS TRIANGLE-HEADED ARROW WITH LONG TIP UPWARDS;So;0;ON;;;;;N;;;;; +2BA6;LEFTWARDS TRIANGLE-HEADED ARROW WITH LONG TIP DOWNWARDS;So;0;ON;;;;;N;;;;; +2BA7;RIGHTWARDS TRIANGLE-HEADED ARROW WITH LONG TIP DOWNWARDS;So;0;ON;;;;;N;;;;; +2BA8;BLACK CURVED DOWNWARDS AND LEFTWARDS ARROW;So;0;ON;;;;;N;;;;; +2BA9;BLACK CURVED DOWNWARDS AND RIGHTWARDS ARROW;So;0;ON;;;;;N;;;;; +2BAA;BLACK CURVED UPWARDS AND LEFTWARDS ARROW;So;0;ON;;;;;N;;;;; +2BAB;BLACK CURVED UPWARDS AND RIGHTWARDS ARROW;So;0;ON;;;;;N;;;;; +2BAC;BLACK CURVED LEFTWARDS AND UPWARDS ARROW;So;0;ON;;;;;N;;;;; +2BAD;BLACK CURVED RIGHTWARDS AND UPWARDS ARROW;So;0;ON;;;;;N;;;;; +2BAE;BLACK CURVED LEFTWARDS AND DOWNWARDS ARROW;So;0;ON;;;;;N;;;;; +2BAF;BLACK CURVED RIGHTWARDS AND DOWNWARDS ARROW;So;0;ON;;;;;N;;;;; +2BB0;RIBBON ARROW DOWN LEFT;So;0;ON;;;;;N;;;;; +2BB1;RIBBON ARROW DOWN RIGHT;So;0;ON;;;;;N;;;;; +2BB2;RIBBON ARROW UP LEFT;So;0;ON;;;;;N;;;;; +2BB3;RIBBON ARROW UP RIGHT;So;0;ON;;;;;N;;;;; +2BB4;RIBBON ARROW LEFT UP;So;0;ON;;;;;N;;;;; +2BB5;RIBBON ARROW RIGHT UP;So;0;ON;;;;;N;;;;; +2BB6;RIBBON ARROW LEFT DOWN;So;0;ON;;;;;N;;;;; +2BB7;RIBBON ARROW RIGHT DOWN;So;0;ON;;;;;N;;;;; +2BB8;UPWARDS WHITE ARROW FROM BAR WITH HORIZONTAL BAR;So;0;ON;;;;;N;;;;; +2BB9;UP ARROWHEAD IN A RECTANGLE BOX;So;0;ON;;;;;N;;;;; +2BBD;BALLOT BOX WITH LIGHT X;So;0;ON;;;;;N;;;;; +2BBE;CIRCLED X;So;0;ON;;;;;N;;;;; +2BBF;CIRCLED BOLD X;So;0;ON;;;;;N;;;;; +2BC0;BLACK SQUARE CENTRED;So;0;ON;;;;;N;;;;; +2BC1;BLACK DIAMOND CENTRED;So;0;ON;;;;;N;;;;; +2BC2;TURNED BLACK PENTAGON;So;0;ON;;;;;N;;;;; +2BC3;HORIZONTAL BLACK OCTAGON;So;0;ON;;;;;N;;;;; +2BC4;BLACK OCTAGON;So;0;ON;;;;;N;;;;; +2BC5;BLACK MEDIUM UP-POINTING TRIANGLE CENTRED;So;0;ON;;;;;N;;;;; +2BC6;BLACK MEDIUM DOWN-POINTING TRIANGLE CENTRED;So;0;ON;;;;;N;;;;; +2BC7;BLACK MEDIUM LEFT-POINTING TRIANGLE CENTRED;So;0;ON;;;;;N;;;;; +2BC8;BLACK MEDIUM RIGHT-POINTING TRIANGLE CENTRED;So;0;ON;;;;;N;;;;; +2BCA;TOP HALF BLACK CIRCLE;So;0;ON;;;;;N;;;;; +2BCB;BOTTOM HALF BLACK CIRCLE;So;0;ON;;;;;N;;;;; +2BCC;LIGHT FOUR POINTED BLACK CUSP;So;0;ON;;;;;N;;;;; +2BCD;ROTATED LIGHT FOUR POINTED BLACK CUSP;So;0;ON;;;;;N;;;;; +2BCE;WHITE FOUR POINTED CUSP;So;0;ON;;;;;N;;;;; +2BCF;ROTATED WHITE FOUR POINTED CUSP;So;0;ON;;;;;N;;;;; +2BD0;SQUARE POSITION INDICATOR;So;0;ON;;;;;N;;;;; +2BD1;UNCERTAINTY SIGN;So;0;ON;;;;;N;;;;; 2C00;GLAGOLITIC CAPITAL LETTER AZU;Lu;0;L;;;;;N;;;;2C30; 2C01;GLAGOLITIC CAPITAL LETTER BUKY;Lu;0;L;;;;;N;;;;2C31; 2C02;GLAGOLITIC CAPITAL LETTER VEDE;Lu;0;L;;;;;N;;;;2C32; @@ -9855,7 +10137,7 @@ 2C79;LATIN SMALL LETTER TURNED R WITH TAIL;Ll;0;L;;;;;N;;;;; 2C7A;LATIN SMALL LETTER O WITH LOW RING INSIDE;Ll;0;L;;;;;N;;;;; 2C7B;LATIN LETTER SMALL CAPITAL TURNED E;Ll;0;L;;;;;N;;;;; -2C7C;LATIN SUBSCRIPT SMALL LETTER J;Ll;0;L;<sub> 006A;;;;N;;;;; +2C7C;LATIN SUBSCRIPT SMALL LETTER J;Lm;0;L;<sub> 006A;;;;N;;;;; 2C7D;MODIFIER LETTER CAPITAL V;Lm;0;L;<super> 0056;;;;N;;;;; 2C7E;LATIN CAPITAL LETTER S WITH SWASH TAIL;Lu;0;L;;;;;N;;;;023F; 2C7F;LATIN CAPITAL LETTER Z WITH SWASH TAIL;Lu;0;L;;;;;N;;;;0240; @@ -9973,6 +10255,8 @@ 2CEF;COPTIC COMBINING NI ABOVE;Mn;230;NSM;;;;;N;;;;; 2CF0;COPTIC COMBINING SPIRITUS ASPER;Mn;230;NSM;;;;;N;;;;; 2CF1;COPTIC COMBINING SPIRITUS LENIS;Mn;230;NSM;;;;;N;;;;; +2CF2;COPTIC CAPITAL LETTER BOHAIRIC KHEI;Lu;0;L;;;;;N;;;;2CF3; +2CF3;COPTIC SMALL LETTER BOHAIRIC KHEI;Ll;0;L;;;;;N;;;2CF2;;2CF2 2CF9;COPTIC OLD NUBIAN FULL STOP;Po;0;ON;;;;;N;;;;; 2CFA;COPTIC OLD NUBIAN DIRECT QUESTION MARK;Po;0;ON;;;;;N;;;;; 2CFB;COPTIC OLD NUBIAN INDIRECT QUESTION MARK;Po;0;ON;;;;;N;;;;; @@ -10018,6 +10302,8 @@ 2D23;GEORGIAN SMALL LETTER WE;Ll;0;L;;;;;N;;;10C3;;10C3 2D24;GEORGIAN SMALL LETTER HAR;Ll;0;L;;;;;N;;;10C4;;10C4 2D25;GEORGIAN SMALL LETTER HOE;Ll;0;L;;;;;N;;;10C5;;10C5 +2D27;GEORGIAN SMALL LETTER YN;Ll;0;L;;;;;N;;;10C7;;10C7 +2D2D;GEORGIAN SMALL LETTER AEN;Ll;0;L;;;;;N;;;10CD;;10CD 2D30;TIFINAGH LETTER YA;Lo;0;L;;;;;N;;;;; 2D31;TIFINAGH LETTER YAB;Lo;0;L;;;;;N;;;;; 2D32;TIFINAGH LETTER YABH;Lo;0;L;;;;;N;;;;; @@ -10072,6 +10358,8 @@ 2D63;TIFINAGH LETTER YAZ;Lo;0;L;;;;;N;;;;; 2D64;TIFINAGH LETTER TAWELLEMET YAZ;Lo;0;L;;;;;N;;;;; 2D65;TIFINAGH LETTER YAZZ;Lo;0;L;;;;;N;;;;; +2D66;TIFINAGH LETTER YE;Lo;0;L;;;;;N;;;;; +2D67;TIFINAGH LETTER YO;Lo;0;L;;;;;N;;;;; 2D6F;TIFINAGH MODIFIER LETTER LABIALIZATION MARK;Lm;0;L;<super> 2D61;;;;N;;;;; 2D70;TIFINAGH SEPARATOR MARK;Po;0;L;;;;;N;;;;; 2D7F;TIFINAGH CONSONANT JOINER;Mn;9;NSM;;;;;N;;;;; @@ -10236,6 +10524,23 @@ 2E2F;VERTICAL TILDE;Lm;0;ON;;;;;N;;;;; 2E30;RING POINT;Po;0;ON;;;;;N;;;;; 2E31;WORD SEPARATOR MIDDLE DOT;Po;0;ON;;;;;N;;;;; +2E32;TURNED COMMA;Po;0;ON;;;;;N;;;;; +2E33;RAISED DOT;Po;0;ON;;;;;N;;;;; +2E34;RAISED COMMA;Po;0;ON;;;;;N;;;;; +2E35;TURNED SEMICOLON;Po;0;ON;;;;;N;;;;; +2E36;DAGGER WITH LEFT GUARD;Po;0;ON;;;;;N;;;;; +2E37;DAGGER WITH RIGHT GUARD;Po;0;ON;;;;;N;;;;; +2E38;TURNED DAGGER;Po;0;ON;;;;;N;;;;; +2E39;TOP HALF SECTION SIGN;Po;0;ON;;;;;N;;;;; +2E3A;TWO-EM DASH;Pd;0;ON;;;;;N;;;;; +2E3B;THREE-EM DASH;Pd;0;ON;;;;;N;;;;; +2E3C;STENOGRAPHIC FULL STOP;Po;0;ON;;;;;N;;;;; +2E3D;VERTICAL SIX DOTS;Po;0;ON;;;;;N;;;;; +2E3E;WIGGLY VERTICAL LINE;Po;0;ON;;;;;N;;;;; +2E3F;CAPITULUM;Po;0;ON;;;;;N;;;;; +2E40;DOUBLE HYPHEN;Pd;0;ON;;;;;N;;;;; +2E41;REVERSED COMMA;Po;0;ON;;;;;N;;;;; +2E42;DOUBLE LOW-REVERSED-9 QUOTATION MARK;Ps;0;ON;;;;;N;;;;; 2E80;CJK RADICAL REPEAT;So;0;ON;;;;;N;;;;; 2E81;CJK RADICAL CLIFF;So;0;ON;;;;;N;;;;; 2E82;CJK RADICAL SECOND ONE;So;0;ON;;;;;N;;;;; @@ -10623,8 +10928,8 @@ 302B;IDEOGRAPHIC RISING TONE MARK;Mn;228;NSM;;;;;N;;;;; 302C;IDEOGRAPHIC DEPARTING TONE MARK;Mn;232;NSM;;;;;N;;;;; 302D;IDEOGRAPHIC ENTERING TONE MARK;Mn;222;NSM;;;;;N;;;;; -302E;HANGUL SINGLE DOT TONE MARK;Mn;224;NSM;;;;;N;;;;; -302F;HANGUL DOUBLE DOT TONE MARK;Mn;224;NSM;;;;;N;;;;; +302E;HANGUL SINGLE DOT TONE MARK;Mc;224;L;;;;;N;;;;; +302F;HANGUL DOUBLE DOT TONE MARK;Mc;224;L;;;;;N;;;;; 3030;WAVY DASH;Pd;0;ON;;;;;N;;;;; 3031;VERTICAL KANA REPEAT MARK;Lm;0;L;;;;;N;;;;; 3032;VERTICAL KANA REPEAT WITH VOICED SOUND MARK;Lm;0;L;;;;;N;;;;; @@ -11131,14 +11436,14 @@ 3245;CIRCLED IDEOGRAPH KINDERGARTEN;So;0;L;<circle> 5E7C;;;;N;;;;; 3246;CIRCLED IDEOGRAPH SCHOOL;So;0;L;<circle> 6587;;;;N;;;;; 3247;CIRCLED IDEOGRAPH KOTO;So;0;L;<circle> 7B8F;;;;N;;;;; -3248;CIRCLED NUMBER TEN ON BLACK SQUARE;So;0;L;;;;;N;;;;; -3249;CIRCLED NUMBER TWENTY ON BLACK SQUARE;So;0;L;;;;;N;;;;; -324A;CIRCLED NUMBER THIRTY ON BLACK SQUARE;So;0;L;;;;;N;;;;; -324B;CIRCLED NUMBER FORTY ON BLACK SQUARE;So;0;L;;;;;N;;;;; -324C;CIRCLED NUMBER FIFTY ON BLACK SQUARE;So;0;L;;;;;N;;;;; -324D;CIRCLED NUMBER SIXTY ON BLACK SQUARE;So;0;L;;;;;N;;;;; -324E;CIRCLED NUMBER SEVENTY ON BLACK SQUARE;So;0;L;;;;;N;;;;; -324F;CIRCLED NUMBER EIGHTY ON BLACK SQUARE;So;0;L;;;;;N;;;;; +3248;CIRCLED NUMBER TEN ON BLACK SQUARE;No;0;L;;;;10;N;;;;; +3249;CIRCLED NUMBER TWENTY ON BLACK SQUARE;No;0;L;;;;20;N;;;;; +324A;CIRCLED NUMBER THIRTY ON BLACK SQUARE;No;0;L;;;;30;N;;;;; +324B;CIRCLED NUMBER FORTY ON BLACK SQUARE;No;0;L;;;;40;N;;;;; +324C;CIRCLED NUMBER FIFTY ON BLACK SQUARE;No;0;L;;;;50;N;;;;; +324D;CIRCLED NUMBER SIXTY ON BLACK SQUARE;No;0;L;;;;60;N;;;;; +324E;CIRCLED NUMBER SEVENTY ON BLACK SQUARE;No;0;L;;;;70;N;;;;; +324F;CIRCLED NUMBER EIGHTY ON BLACK SQUARE;No;0;L;;;;80;N;;;;; 3250;PARTNERSHIP SIGN;So;0;ON;<square> 0050 0054 0045;;;;N;;;;; 3251;CIRCLED NUMBER TWENTY ONE;No;0;ON;<circle> 0032 0031;;;21;N;;;;; 3252;CIRCLED NUMBER TWENTY TWO;No;0;ON;<circle> 0032 0032;;;22;N;;;;; @@ -11637,7 +11942,7 @@ 4DFE;HEXAGRAM FOR AFTER COMPLETION;So;0;ON;;;;;N;;;;; 4DFF;HEXAGRAM FOR BEFORE COMPLETION;So;0;ON;;;;;N;;;;; 4E00;<CJK Ideograph, First>;Lo;0;L;;;;;N;;;;; -9FCB;<CJK Ideograph, Last>;Lo;0;L;;;;;N;;;;; +9FCC;<CJK Ideograph, Last>;Lo;0;L;;;;;N;;;;; A000;YI SYLLABLE IT;Lo;0;L;;;;;N;;;;; A001;YI SYLLABLE IX;Lo;0;L;;;;;N;;;;; A002;YI SYLLABLE I;Lo;0;L;;;;;N;;;;; @@ -13258,6 +13563,14 @@ A671;COMBINING CYRILLIC HUNDRED MILLIONS SIGN;Me;0;NSM;;;;;N;;;;; A672;COMBINING CYRILLIC THOUSAND MILLIONS SIGN;Me;0;NSM;;;;;N;;;;; A673;SLAVONIC ASTERISK;Po;0;ON;;;;;N;;;;; +A674;COMBINING CYRILLIC LETTER UKRAINIAN IE;Mn;230;NSM;;;;;N;;;;; +A675;COMBINING CYRILLIC LETTER I;Mn;230;NSM;;;;;N;;;;; +A676;COMBINING CYRILLIC LETTER YI;Mn;230;NSM;;;;;N;;;;; +A677;COMBINING CYRILLIC LETTER U;Mn;230;NSM;;;;;N;;;;; +A678;COMBINING CYRILLIC LETTER HARD SIGN;Mn;230;NSM;;;;;N;;;;; +A679;COMBINING CYRILLIC LETTER YERU;Mn;230;NSM;;;;;N;;;;; +A67A;COMBINING CYRILLIC LETTER SOFT SIGN;Mn;230;NSM;;;;;N;;;;; +A67B;COMBINING CYRILLIC LETTER OMEGA;Mn;230;NSM;;;;;N;;;;; A67C;COMBINING CYRILLIC KAVYKA;Mn;230;NSM;;;;;N;;;;; A67D;COMBINING CYRILLIC PAYEROK;Mn;230;NSM;;;;;N;;;;; A67E;CYRILLIC KAVYKA;Po;0;ON;;;;;N;;;;; @@ -13286,6 +13599,13 @@ A695;CYRILLIC SMALL LETTER HWE;Ll;0;L;;;;;N;;;A694;;A694 A696;CYRILLIC CAPITAL LETTER SHWE;Lu;0;L;;;;;N;;;;A697; A697;CYRILLIC SMALL LETTER SHWE;Ll;0;L;;;;;N;;;A696;;A696 +A698;CYRILLIC CAPITAL LETTER DOUBLE O;Lu;0;L;;;;;N;;;;A699; +A699;CYRILLIC SMALL LETTER DOUBLE O;Ll;0;L;;;;;N;;;A698;;A698 +A69A;CYRILLIC CAPITAL LETTER CROSSED O;Lu;0;L;;;;;N;;;;A69B; +A69B;CYRILLIC SMALL LETTER CROSSED O;Ll;0;L;;;;;N;;;A69A;;A69A +A69C;MODIFIER LETTER CYRILLIC HARD SIGN;Lm;0;L;<super> 044A;;;;N;;;;; +A69D;MODIFIER LETTER CYRILLIC SOFT SIGN;Lm;0;L;<super> 044C;;;;N;;;;; +A69F;COMBINING CYRILLIC LETTER IOTIFIED E;Mn;230;NSM;;;;;N;;;;; A6A0;BAMUM LETTER A;Lo;0;L;;;;;N;;;;; A6A1;BAMUM LETTER KA;Lo;0;L;;;;;N;;;;; A6A2;BAMUM LETTER U;Lo;0;L;;;;;N;;;;; @@ -13519,6 +13839,20 @@ A78E;LATIN SMALL LETTER L WITH RETROFLEX HOOK AND BELT;Ll;0;L;;;;;N;;;;; A790;LATIN CAPITAL LETTER N WITH DESCENDER;Lu;0;L;;;;;N;;;;A791; A791;LATIN SMALL LETTER N WITH DESCENDER;Ll;0;L;;;;;N;;;A790;;A790 +A792;LATIN CAPITAL LETTER C WITH BAR;Lu;0;L;;;;;N;;;;A793; +A793;LATIN SMALL LETTER C WITH BAR;Ll;0;L;;;;;N;;;A792;;A792 +A794;LATIN SMALL LETTER C WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;; +A795;LATIN SMALL LETTER H WITH PALATAL HOOK;Ll;0;L;;;;;N;;;;; +A796;LATIN CAPITAL LETTER B WITH FLOURISH;Lu;0;L;;;;;N;;;;A797; +A797;LATIN SMALL LETTER B WITH FLOURISH;Ll;0;L;;;;;N;;;A796;;A796 +A798;LATIN CAPITAL LETTER F WITH STROKE;Lu;0;L;;;;;N;;;;A799; +A799;LATIN SMALL LETTER F WITH STROKE;Ll;0;L;;;;;N;;;A798;;A798 +A79A;LATIN CAPITAL LETTER VOLAPUK AE;Lu;0;L;;;;;N;;;;A79B; +A79B;LATIN SMALL LETTER VOLAPUK AE;Ll;0;L;;;;;N;;;A79A;;A79A +A79C;LATIN CAPITAL LETTER VOLAPUK OE;Lu;0;L;;;;;N;;;;A79D; +A79D;LATIN SMALL LETTER VOLAPUK OE;Ll;0;L;;;;;N;;;A79C;;A79C +A79E;LATIN CAPITAL LETTER VOLAPUK UE;Lu;0;L;;;;;N;;;;A79F; +A79F;LATIN SMALL LETTER VOLAPUK UE;Ll;0;L;;;;;N;;;A79E;;A79E A7A0;LATIN CAPITAL LETTER G WITH OBLIQUE STROKE;Lu;0;L;;;;;N;;;;A7A1; A7A1;LATIN SMALL LETTER G WITH OBLIQUE STROKE;Ll;0;L;;;;;N;;;A7A0;;A7A0 A7A2;LATIN CAPITAL LETTER K WITH OBLIQUE STROKE;Lu;0;L;;;;;N;;;;A7A3; @@ -13529,6 +13863,15 @@ A7A7;LATIN SMALL LETTER R WITH OBLIQUE STROKE;Ll;0;L;;;;;N;;;A7A6;;A7A6 A7A8;LATIN CAPITAL LETTER S WITH OBLIQUE STROKE;Lu;0;L;;;;;N;;;;A7A9; A7A9;LATIN SMALL LETTER S WITH OBLIQUE STROKE;Ll;0;L;;;;;N;;;A7A8;;A7A8 +A7AA;LATIN CAPITAL LETTER H WITH HOOK;Lu;0;L;;;;;N;;;;0266; +A7AB;LATIN CAPITAL LETTER REVERSED OPEN E;Lu;0;L;;;;;N;;;;025C; +A7AC;LATIN CAPITAL LETTER SCRIPT G;Lu;0;L;;;;;N;;;;0261; +A7AD;LATIN CAPITAL LETTER L WITH BELT;Lu;0;L;;;;;N;;;;026C; +A7B0;LATIN CAPITAL LETTER TURNED K;Lu;0;L;;;;;N;;;;029E; +A7B1;LATIN CAPITAL LETTER TURNED T;Lu;0;L;;;;;N;;;;0287; +A7F7;LATIN EPIGRAPHIC LETTER SIDEWAYS I;Lo;0;L;;;;;N;;;;; +A7F8;MODIFIER LETTER CAPITAL H WITH STROKE;Lm;0;L;<super> 0126;;;;N;;;;; +A7F9;MODIFIER LETTER SMALL LIGATURE OE;Lm;0;L;<super> 0153;;;;N;;;;; A7FA;LATIN LETTER SMALL CAPITAL TURNED M;Ll;0;L;;;;;N;;;;; A7FB;LATIN EPIGRAPHIC LETTER REVERSED F;Lo;0;L;;;;;N;;;;; A7FC;LATIN EPIGRAPHIC LETTER REVERSED P;Lo;0;L;;;;;N;;;;; @@ -13959,6 +14302,37 @@ A9D9;JAVANESE DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; A9DE;JAVANESE PADA TIRTA TUMETES;Po;0;L;;;;;N;;;;; A9DF;JAVANESE PADA ISEN-ISEN;Po;0;L;;;;;N;;;;; +A9E0;MYANMAR LETTER SHAN GHA;Lo;0;L;;;;;N;;;;; +A9E1;MYANMAR LETTER SHAN CHA;Lo;0;L;;;;;N;;;;; +A9E2;MYANMAR LETTER SHAN JHA;Lo;0;L;;;;;N;;;;; +A9E3;MYANMAR LETTER SHAN NNA;Lo;0;L;;;;;N;;;;; +A9E4;MYANMAR LETTER SHAN BHA;Lo;0;L;;;;;N;;;;; +A9E5;MYANMAR SIGN SHAN SAW;Mn;0;NSM;;;;;N;;;;; +A9E6;MYANMAR MODIFIER LETTER SHAN REDUPLICATION;Lm;0;L;;;;;N;;;;; +A9E7;MYANMAR LETTER TAI LAING NYA;Lo;0;L;;;;;N;;;;; +A9E8;MYANMAR LETTER TAI LAING FA;Lo;0;L;;;;;N;;;;; +A9E9;MYANMAR LETTER TAI LAING GA;Lo;0;L;;;;;N;;;;; +A9EA;MYANMAR LETTER TAI LAING GHA;Lo;0;L;;;;;N;;;;; +A9EB;MYANMAR LETTER TAI LAING JA;Lo;0;L;;;;;N;;;;; +A9EC;MYANMAR LETTER TAI LAING JHA;Lo;0;L;;;;;N;;;;; +A9ED;MYANMAR LETTER TAI LAING DDA;Lo;0;L;;;;;N;;;;; +A9EE;MYANMAR LETTER TAI LAING DDHA;Lo;0;L;;;;;N;;;;; +A9EF;MYANMAR LETTER TAI LAING NNA;Lo;0;L;;;;;N;;;;; +A9F0;MYANMAR TAI LAING DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +A9F1;MYANMAR TAI LAING DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +A9F2;MYANMAR TAI LAING DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +A9F3;MYANMAR TAI LAING DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +A9F4;MYANMAR TAI LAING DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +A9F5;MYANMAR TAI LAING DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +A9F6;MYANMAR TAI LAING DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +A9F7;MYANMAR TAI LAING DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +A9F8;MYANMAR TAI LAING DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +A9F9;MYANMAR TAI LAING DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +A9FA;MYANMAR LETTER TAI LAING LLA;Lo;0;L;;;;;N;;;;; +A9FB;MYANMAR LETTER TAI LAING DA;Lo;0;L;;;;;N;;;;; +A9FC;MYANMAR LETTER TAI LAING DHA;Lo;0;L;;;;;N;;;;; +A9FD;MYANMAR LETTER TAI LAING BA;Lo;0;L;;;;;N;;;;; +A9FE;MYANMAR LETTER TAI LAING BHA;Lo;0;L;;;;;N;;;;; AA00;CHAM LETTER A;Lo;0;L;;;;;N;;;;; AA01;CHAM LETTER I;Lo;0;L;;;;;N;;;;; AA02;CHAM LETTER U;Lo;0;L;;;;;N;;;;; @@ -14070,6 +14444,10 @@ AA79;MYANMAR SYMBOL AITON TWO;So;0;L;;;;;N;;;;; AA7A;MYANMAR LETTER AITON RA;Lo;0;L;;;;;N;;;;; AA7B;MYANMAR SIGN PAO KAREN TONE;Mc;0;L;;;;;N;;;;; +AA7C;MYANMAR SIGN TAI LAING TONE-2;Mn;0;NSM;;;;;N;;;;; +AA7D;MYANMAR SIGN TAI LAING TONE-5;Mc;0;L;;;;;N;;;;; +AA7E;MYANMAR LETTER SHWE PALAUNG CHA;Lo;0;L;;;;;N;;;;; +AA7F;MYANMAR LETTER SHWE PALAUNG SHA;Lo;0;L;;;;;N;;;;; AA80;TAI VIET LETTER LOW KO;Lo;0;L;;;;;N;;;;; AA81;TAI VIET LETTER HIGH KO;Lo;0;L;;;;;N;;;;; AA82;TAI VIET LETTER LOW KHO;Lo;0;L;;;;;N;;;;; @@ -14142,6 +14520,29 @@ AADD;TAI VIET SYMBOL SAM;Lm;0;L;;;;;N;;;;; AADE;TAI VIET SYMBOL HO HOI;Po;0;L;;;;;N;;;;; AADF;TAI VIET SYMBOL KOI KOI;Po;0;L;;;;;N;;;;; +AAE0;MEETEI MAYEK LETTER E;Lo;0;L;;;;;N;;;;; +AAE1;MEETEI MAYEK LETTER O;Lo;0;L;;;;;N;;;;; +AAE2;MEETEI MAYEK LETTER CHA;Lo;0;L;;;;;N;;;;; +AAE3;MEETEI MAYEK LETTER NYA;Lo;0;L;;;;;N;;;;; +AAE4;MEETEI MAYEK LETTER TTA;Lo;0;L;;;;;N;;;;; +AAE5;MEETEI MAYEK LETTER TTHA;Lo;0;L;;;;;N;;;;; +AAE6;MEETEI MAYEK LETTER DDA;Lo;0;L;;;;;N;;;;; +AAE7;MEETEI MAYEK LETTER DDHA;Lo;0;L;;;;;N;;;;; +AAE8;MEETEI MAYEK LETTER NNA;Lo;0;L;;;;;N;;;;; +AAE9;MEETEI MAYEK LETTER SHA;Lo;0;L;;;;;N;;;;; +AAEA;MEETEI MAYEK LETTER SSA;Lo;0;L;;;;;N;;;;; +AAEB;MEETEI MAYEK VOWEL SIGN II;Mc;0;L;;;;;N;;;;; +AAEC;MEETEI MAYEK VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;; +AAED;MEETEI MAYEK VOWEL SIGN AAI;Mn;0;NSM;;;;;N;;;;; +AAEE;MEETEI MAYEK VOWEL SIGN AU;Mc;0;L;;;;;N;;;;; +AAEF;MEETEI MAYEK VOWEL SIGN AAU;Mc;0;L;;;;;N;;;;; +AAF0;MEETEI MAYEK CHEIKHAN;Po;0;L;;;;;N;;;;; +AAF1;MEETEI MAYEK AHANG KHUDAM;Po;0;L;;;;;N;;;;; +AAF2;MEETEI MAYEK ANJI;Lo;0;L;;;;;N;;;;; +AAF3;MEETEI MAYEK SYLLABLE REPETITION MARK;Lm;0;L;;;;;N;;;;; +AAF4;MEETEI MAYEK WORD REPETITION MARK;Lm;0;L;;;;;N;;;;; +AAF5;MEETEI MAYEK VOWEL SIGN VISARGA;Mc;0;L;;;;;N;;;;; +AAF6;MEETEI MAYEK VIRAMA;Mn;9;NSM;;;;;N;;;;; AB01;ETHIOPIC SYLLABLE TTHU;Lo;0;L;;;;;N;;;;; AB02;ETHIOPIC SYLLABLE TTHI;Lo;0;L;;;;;N;;;;; AB03;ETHIOPIC SYLLABLE TTHAA;Lo;0;L;;;;;N;;;;; @@ -14174,6 +14575,56 @@ AB2C;ETHIOPIC SYLLABLE BBEE;Lo;0;L;;;;;N;;;;; AB2D;ETHIOPIC SYLLABLE BBE;Lo;0;L;;;;;N;;;;; AB2E;ETHIOPIC SYLLABLE BBO;Lo;0;L;;;;;N;;;;; +AB30;LATIN SMALL LETTER BARRED ALPHA;Ll;0;L;;;;;N;;;;; +AB31;LATIN SMALL LETTER A REVERSED-SCHWA;Ll;0;L;;;;;N;;;;; +AB32;LATIN SMALL LETTER BLACKLETTER E;Ll;0;L;;;;;N;;;;; +AB33;LATIN SMALL LETTER BARRED E;Ll;0;L;;;;;N;;;;; +AB34;LATIN SMALL LETTER E WITH FLOURISH;Ll;0;L;;;;;N;;;;; +AB35;LATIN SMALL LETTER LENIS F;Ll;0;L;;;;;N;;;;; +AB36;LATIN SMALL LETTER SCRIPT G WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;; +AB37;LATIN SMALL LETTER L WITH INVERTED LAZY S;Ll;0;L;;;;;N;;;;; +AB38;LATIN SMALL LETTER L WITH DOUBLE MIDDLE TILDE;Ll;0;L;;;;;N;;;;; +AB39;LATIN SMALL LETTER L WITH MIDDLE RING;Ll;0;L;;;;;N;;;;; +AB3A;LATIN SMALL LETTER M WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;; +AB3B;LATIN SMALL LETTER N WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;; +AB3C;LATIN SMALL LETTER ENG WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;; +AB3D;LATIN SMALL LETTER BLACKLETTER O;Ll;0;L;;;;;N;;;;; +AB3E;LATIN SMALL LETTER BLACKLETTER O WITH STROKE;Ll;0;L;;;;;N;;;;; +AB3F;LATIN SMALL LETTER OPEN O WITH STROKE;Ll;0;L;;;;;N;;;;; +AB40;LATIN SMALL LETTER INVERTED OE;Ll;0;L;;;;;N;;;;; +AB41;LATIN SMALL LETTER TURNED OE WITH STROKE;Ll;0;L;;;;;N;;;;; +AB42;LATIN SMALL LETTER TURNED OE WITH HORIZONTAL STROKE;Ll;0;L;;;;;N;;;;; +AB43;LATIN SMALL LETTER TURNED O OPEN-O;Ll;0;L;;;;;N;;;;; +AB44;LATIN SMALL LETTER TURNED O OPEN-O WITH STROKE;Ll;0;L;;;;;N;;;;; +AB45;LATIN SMALL LETTER STIRRUP R;Ll;0;L;;;;;N;;;;; +AB46;LATIN LETTER SMALL CAPITAL R WITH RIGHT LEG;Ll;0;L;;;;;N;;;;; +AB47;LATIN SMALL LETTER R WITHOUT HANDLE;Ll;0;L;;;;;N;;;;; +AB48;LATIN SMALL LETTER DOUBLE R;Ll;0;L;;;;;N;;;;; +AB49;LATIN SMALL LETTER R WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;; +AB4A;LATIN SMALL LETTER DOUBLE R WITH CROSSED-TAIL;Ll;0;L;;;;;N;;;;; +AB4B;LATIN SMALL LETTER SCRIPT R;Ll;0;L;;;;;N;;;;; +AB4C;LATIN SMALL LETTER SCRIPT R WITH RING;Ll;0;L;;;;;N;;;;; +AB4D;LATIN SMALL LETTER BASELINE ESH;Ll;0;L;;;;;N;;;;; +AB4E;LATIN SMALL LETTER U WITH SHORT RIGHT LEG;Ll;0;L;;;;;N;;;;; +AB4F;LATIN SMALL LETTER U BAR WITH SHORT RIGHT LEG;Ll;0;L;;;;;N;;;;; +AB50;LATIN SMALL LETTER UI;Ll;0;L;;;;;N;;;;; +AB51;LATIN SMALL LETTER TURNED UI;Ll;0;L;;;;;N;;;;; +AB52;LATIN SMALL LETTER U WITH LEFT HOOK;Ll;0;L;;;;;N;;;;; +AB53;LATIN SMALL LETTER CHI;Ll;0;L;;;;;N;;;;; +AB54;LATIN SMALL LETTER CHI WITH LOW RIGHT RING;Ll;0;L;;;;;N;;;;; +AB55;LATIN SMALL LETTER CHI WITH LOW LEFT SERIF;Ll;0;L;;;;;N;;;;; +AB56;LATIN SMALL LETTER X WITH LOW RIGHT RING;Ll;0;L;;;;;N;;;;; +AB57;LATIN SMALL LETTER X WITH LONG LEFT LEG;Ll;0;L;;;;;N;;;;; +AB58;LATIN SMALL LETTER X WITH LONG LEFT LEG AND LOW RIGHT RING;Ll;0;L;;;;;N;;;;; +AB59;LATIN SMALL LETTER X WITH LONG LEFT LEG WITH SERIF;Ll;0;L;;;;;N;;;;; +AB5A;LATIN SMALL LETTER Y WITH SHORT RIGHT LEG;Ll;0;L;;;;;N;;;;; +AB5B;MODIFIER BREVE WITH INVERTED BREVE;Sk;0;L;;;;;N;;;;; +AB5C;MODIFIER LETTER SMALL HENG;Lm;0;L;<super> A727;;;;N;;;;; +AB5D;MODIFIER LETTER SMALL L WITH INVERTED LAZY S;Lm;0;L;<super> AB37;;;;N;;;;; +AB5E;MODIFIER LETTER SMALL L WITH MIDDLE TILDE;Lm;0;L;<super> 026B;;;;N;;;;; +AB5F;MODIFIER LETTER SMALL U WITH LEFT HOOK;Lm;0;L;<super> AB52;;;;N;;;;; +AB64;LATIN SMALL LETTER INVERTED ALPHA;Ll;0;L;;;;;N;;;;; +AB65;GREEK LETTER SMALL CAPITAL OMEGA;Ll;0;L;;;;;N;;;;; ABC0;MEETEI MAYEK LETTER KOK;Lo;0;L;;;;;N;;;;; ABC1;MEETEI MAYEK LETTER SAM;Lo;0;L;;;;;N;;;;; ABC2;MEETEI MAYEK LETTER LAI;Lo;0;L;;;;;N;;;;; @@ -14614,6 +15065,8 @@ FA2B;CJK COMPATIBILITY IDEOGRAPH-FA2B;Lo;0;L;98FC;;;;N;;;;; FA2C;CJK COMPATIBILITY IDEOGRAPH-FA2C;Lo;0;L;9928;;;;N;;;;; FA2D;CJK COMPATIBILITY IDEOGRAPH-FA2D;Lo;0;L;9DB4;;;;N;;;;; +FA2E;CJK COMPATIBILITY IDEOGRAPH-FA2E;Lo;0;L;90DE;;;;N;;;;; +FA2F;CJK COMPATIBILITY IDEOGRAPH-FA2F;Lo;0;L;96B7;;;;N;;;;; FA30;CJK COMPATIBILITY IDEOGRAPH-FA30;Lo;0;L;4FAE;;;;N;;;;; FA31;CJK COMPATIBILITY IDEOGRAPH-FA31;Lo;0;L;50E7;;;;N;;;;; FA32;CJK COMPATIBILITY IDEOGRAPH-FA32;Lo;0;L;514D;;;;N;;;;; @@ -15317,8 +15770,8 @@ FD3B;ARABIC LIGATURE ZAH WITH MEEM MEDIAL FORM;Lo;0;AL;<medial> 0638 0645;;;;N;;;;; FD3C;ARABIC LIGATURE ALEF WITH FATHATAN FINAL FORM;Lo;0;AL;<final> 0627 064B;;;;N;;;;; FD3D;ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM;Lo;0;AL;<isolated> 0627 064B;;;;N;;;;; -FD3E;ORNATE LEFT PARENTHESIS;Ps;0;ON;;;;;N;;;;; -FD3F;ORNATE RIGHT PARENTHESIS;Pe;0;ON;;;;;N;;;;; +FD3E;ORNATE LEFT PARENTHESIS;Pe;0;ON;;;;;N;;;;; +FD3F;ORNATE RIGHT PARENTHESIS;Ps;0;ON;;;;;N;;;;; FD50;ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM;Lo;0;AL;<initial> 062A 062C 0645;;;;N;;;;; FD51;ARABIC LIGATURE TEH WITH HAH WITH JEEM FINAL FORM;Lo;0;AL;<final> 062A 062D 062C;;;;N;;;;; FD52;ARABIC LIGATURE TEH WITH HAH WITH JEEM INITIAL FORM;Lo;0;AL;<initial> 062A 062D 062C;;;;N;;;;; @@ -15484,6 +15937,13 @@ FE24;COMBINING MACRON LEFT HALF;Mn;230;NSM;;;;;N;;;;; FE25;COMBINING MACRON RIGHT HALF;Mn;230;NSM;;;;;N;;;;; FE26;COMBINING CONJOINING MACRON;Mn;230;NSM;;;;;N;;;;; +FE27;COMBINING LIGATURE LEFT HALF BELOW;Mn;220;NSM;;;;;N;;;;; +FE28;COMBINING LIGATURE RIGHT HALF BELOW;Mn;220;NSM;;;;;N;;;;; +FE29;COMBINING TILDE LEFT HALF BELOW;Mn;220;NSM;;;;;N;;;;; +FE2A;COMBINING TILDE RIGHT HALF BELOW;Mn;220;NSM;;;;;N;;;;; +FE2B;COMBINING MACRON LEFT HALF BELOW;Mn;220;NSM;;;;;N;;;;; +FE2C;COMBINING MACRON RIGHT HALF BELOW;Mn;220;NSM;;;;;N;;;;; +FE2D;COMBINING CONJOINING MACRON BELOW;Mn;220;NSM;;;;;N;;;;; FE30;PRESENTATION FORM FOR VERTICAL TWO DOT LEADER;Po;0;ON;<vertical> 2025;;;;N;GLYPH FOR VERTICAL TWO DOT LEADER;;;; FE31;PRESENTATION FORM FOR VERTICAL EM DASH;Pd;0;ON;<vertical> 2014;;;;N;GLYPH FOR VERTICAL EM DASH;;;; FE32;PRESENTATION FORM FOR VERTICAL EN DASH;Pd;0;ON;<vertical> 2013;;;;N;GLYPH FOR VERTICAL EN DASH;;;; @@ -16126,7 +16586,7 @@ 100FA;LINEAR B IDEOGRAM VESSEL B305;Lo;0;L;;;;;N;;;;; 10100;AEGEAN WORD SEPARATOR LINE;Po;0;L;;;;;N;;;;; 10101;AEGEAN WORD SEPARATOR DOT;Po;0;ON;;;;;N;;;;; -10102;AEGEAN CHECK MARK;So;0;L;;;;;N;;;;; +10102;AEGEAN CHECK MARK;Po;0;L;;;;;N;;;;; 10107;AEGEAN NUMBER ONE;No;0;L;;;;1;N;;;;; 10108;AEGEAN NUMBER TWO;No;0;L;;;;2;N;;;;; 10109;AEGEAN NUMBER THREE;No;0;L;;;;3;N;;;;; @@ -16256,6 +16716,8 @@ 10188;GREEK GRAMMA SIGN;So;0;ON;;;;;N;;;;; 10189;GREEK TRYBLION BASE SIGN;So;0;ON;;;;;N;;;;; 1018A;GREEK ZERO SIGN;No;0;ON;;;;0;N;;;;; +1018B;GREEK ONE QUARTER SIGN;No;0;ON;;;;1/4;N;;;;; +1018C;GREEK SINUSOID SIGN;So;0;ON;;;;;N;;;;; 10190;ROMAN SEXTANS SIGN;So;0;ON;;;;;N;;;;; 10191;ROMAN UNCIA SIGN;So;0;ON;;;;;N;;;;; 10192;ROMAN SEMUNCIA SIGN;So;0;ON;;;;;N;;;;; @@ -16268,6 +16730,7 @@ 10199;ROMAN DUPONDIUS SIGN;So;0;ON;;;;;N;;;;; 1019A;ROMAN AS SIGN;So;0;ON;;;;;N;;;;; 1019B;ROMAN CENTURIAL SIGN;So;0;ON;;;;;N;;;;; +101A0;GREEK SYMBOL TAU RHO;So;0;ON;;;;;N;;;;; 101D0;PHAISTOS DISC SIGN PEDESTRIAN;So;0;L;;;;;N;;;;; 101D1;PHAISTOS DISC SIGN PLUMED HEAD;So;0;L;;;;;N;;;;; 101D2;PHAISTOS DISC SIGN TATTOOED HEAD;So;0;L;;;;;N;;;;; @@ -16392,6 +16855,34 @@ 102CE;CARIAN LETTER LD2;Lo;0;L;;;;;N;;;;; 102CF;CARIAN LETTER E2;Lo;0;L;;;;;N;;;;; 102D0;CARIAN LETTER UUU3;Lo;0;L;;;;;N;;;;; +102E0;COPTIC EPACT THOUSANDS MARK;Mn;220;NSM;;;;;N;;;;; +102E1;COPTIC EPACT DIGIT ONE;No;0;EN;;;;1;N;;;;; +102E2;COPTIC EPACT DIGIT TWO;No;0;EN;;;;2;N;;;;; +102E3;COPTIC EPACT DIGIT THREE;No;0;EN;;;;3;N;;;;; +102E4;COPTIC EPACT DIGIT FOUR;No;0;EN;;;;4;N;;;;; +102E5;COPTIC EPACT DIGIT FIVE;No;0;EN;;;;5;N;;;;; +102E6;COPTIC EPACT DIGIT SIX;No;0;EN;;;;6;N;;;;; +102E7;COPTIC EPACT DIGIT SEVEN;No;0;EN;;;;7;N;;;;; +102E8;COPTIC EPACT DIGIT EIGHT;No;0;EN;;;;8;N;;;;; +102E9;COPTIC EPACT DIGIT NINE;No;0;EN;;;;9;N;;;;; +102EA;COPTIC EPACT NUMBER TEN;No;0;EN;;;;10;N;;;;; +102EB;COPTIC EPACT NUMBER TWENTY;No;0;EN;;;;20;N;;;;; +102EC;COPTIC EPACT NUMBER THIRTY;No;0;EN;;;;30;N;;;;; +102ED;COPTIC EPACT NUMBER FORTY;No;0;EN;;;;40;N;;;;; +102EE;COPTIC EPACT NUMBER FIFTY;No;0;EN;;;;50;N;;;;; +102EF;COPTIC EPACT NUMBER SIXTY;No;0;EN;;;;60;N;;;;; +102F0;COPTIC EPACT NUMBER SEVENTY;No;0;EN;;;;70;N;;;;; +102F1;COPTIC EPACT NUMBER EIGHTY;No;0;EN;;;;80;N;;;;; +102F2;COPTIC EPACT NUMBER NINETY;No;0;EN;;;;90;N;;;;; +102F3;COPTIC EPACT NUMBER ONE HUNDRED;No;0;EN;;;;100;N;;;;; +102F4;COPTIC EPACT NUMBER TWO HUNDRED;No;0;EN;;;;200;N;;;;; +102F5;COPTIC EPACT NUMBER THREE HUNDRED;No;0;EN;;;;300;N;;;;; +102F6;COPTIC EPACT NUMBER FOUR HUNDRED;No;0;EN;;;;400;N;;;;; +102F7;COPTIC EPACT NUMBER FIVE HUNDRED;No;0;EN;;;;500;N;;;;; +102F8;COPTIC EPACT NUMBER SIX HUNDRED;No;0;EN;;;;600;N;;;;; +102F9;COPTIC EPACT NUMBER SEVEN HUNDRED;No;0;EN;;;;700;N;;;;; +102FA;COPTIC EPACT NUMBER EIGHT HUNDRED;No;0;EN;;;;800;N;;;;; +102FB;COPTIC EPACT NUMBER NINE HUNDRED;No;0;EN;;;;900;N;;;;; 10300;OLD ITALIC LETTER A;Lo;0;L;;;;;N;;;;; 10301;OLD ITALIC LETTER BE;Lo;0;L;;;;;N;;;;; 10302;OLD ITALIC LETTER KE;Lo;0;L;;;;;N;;;;; @@ -16423,6 +16914,7 @@ 1031C;OLD ITALIC LETTER CHE;Lo;0;L;;;;;N;;;;; 1031D;OLD ITALIC LETTER II;Lo;0;L;;;;;N;;;;; 1031E;OLD ITALIC LETTER UU;Lo;0;L;;;;;N;;;;; +1031F;OLD ITALIC LETTER ESS;Lo;0;L;;;;;N;;;;; 10320;OLD ITALIC NUMERAL ONE;No;0;L;;;;1;N;;;;; 10321;OLD ITALIC NUMERAL FIVE;No;0;L;;;;5;N;;;;; 10322;OLD ITALIC NUMERAL TEN;No;0;L;;;;10;N;;;;; @@ -16454,6 +16946,49 @@ 10348;GOTHIC LETTER HWAIR;Lo;0;L;;;;;N;;;;; 10349;GOTHIC LETTER OTHAL;Lo;0;L;;;;;N;;;;; 1034A;GOTHIC LETTER NINE HUNDRED;Nl;0;L;;;;900;N;;;;; +10350;OLD PERMIC LETTER AN;Lo;0;L;;;;;N;;;;; +10351;OLD PERMIC LETTER BUR;Lo;0;L;;;;;N;;;;; +10352;OLD PERMIC LETTER GAI;Lo;0;L;;;;;N;;;;; +10353;OLD PERMIC LETTER DOI;Lo;0;L;;;;;N;;;;; +10354;OLD PERMIC LETTER E;Lo;0;L;;;;;N;;;;; +10355;OLD PERMIC LETTER ZHOI;Lo;0;L;;;;;N;;;;; +10356;OLD PERMIC LETTER DZHOI;Lo;0;L;;;;;N;;;;; +10357;OLD PERMIC LETTER ZATA;Lo;0;L;;;;;N;;;;; +10358;OLD PERMIC LETTER DZITA;Lo;0;L;;;;;N;;;;; +10359;OLD PERMIC LETTER I;Lo;0;L;;;;;N;;;;; +1035A;OLD PERMIC LETTER KOKE;Lo;0;L;;;;;N;;;;; +1035B;OLD PERMIC LETTER LEI;Lo;0;L;;;;;N;;;;; +1035C;OLD PERMIC LETTER MENOE;Lo;0;L;;;;;N;;;;; +1035D;OLD PERMIC LETTER NENOE;Lo;0;L;;;;;N;;;;; +1035E;OLD PERMIC LETTER VOOI;Lo;0;L;;;;;N;;;;; +1035F;OLD PERMIC LETTER PEEI;Lo;0;L;;;;;N;;;;; +10360;OLD PERMIC LETTER REI;Lo;0;L;;;;;N;;;;; +10361;OLD PERMIC LETTER SII;Lo;0;L;;;;;N;;;;; +10362;OLD PERMIC LETTER TAI;Lo;0;L;;;;;N;;;;; +10363;OLD PERMIC LETTER U;Lo;0;L;;;;;N;;;;; +10364;OLD PERMIC LETTER CHERY;Lo;0;L;;;;;N;;;;; +10365;OLD PERMIC LETTER SHOOI;Lo;0;L;;;;;N;;;;; +10366;OLD PERMIC LETTER SHCHOOI;Lo;0;L;;;;;N;;;;; +10367;OLD PERMIC LETTER YRY;Lo;0;L;;;;;N;;;;; +10368;OLD PERMIC LETTER YERU;Lo;0;L;;;;;N;;;;; +10369;OLD PERMIC LETTER O;Lo;0;L;;;;;N;;;;; +1036A;OLD PERMIC LETTER OO;Lo;0;L;;;;;N;;;;; +1036B;OLD PERMIC LETTER EF;Lo;0;L;;;;;N;;;;; +1036C;OLD PERMIC LETTER HA;Lo;0;L;;;;;N;;;;; +1036D;OLD PERMIC LETTER TSIU;Lo;0;L;;;;;N;;;;; +1036E;OLD PERMIC LETTER VER;Lo;0;L;;;;;N;;;;; +1036F;OLD PERMIC LETTER YER;Lo;0;L;;;;;N;;;;; +10370;OLD PERMIC LETTER YERI;Lo;0;L;;;;;N;;;;; +10371;OLD PERMIC LETTER YAT;Lo;0;L;;;;;N;;;;; +10372;OLD PERMIC LETTER IE;Lo;0;L;;;;;N;;;;; +10373;OLD PERMIC LETTER YU;Lo;0;L;;;;;N;;;;; +10374;OLD PERMIC LETTER YA;Lo;0;L;;;;;N;;;;; +10375;OLD PERMIC LETTER IA;Lo;0;L;;;;;N;;;;; +10376;COMBINING OLD PERMIC LETTER AN;Mn;230;NSM;;;;;N;;;;; +10377;COMBINING OLD PERMIC LETTER DOI;Mn;230;NSM;;;;;N;;;;; +10378;COMBINING OLD PERMIC LETTER ZATA;Mn;230;NSM;;;;;N;;;;; +10379;COMBINING OLD PERMIC LETTER NENOE;Mn;230;NSM;;;;;N;;;;; +1037A;COMBINING OLD PERMIC LETTER SII;Mn;230;NSM;;;;;N;;;;; 10380;UGARITIC LETTER ALPA;Lo;0;L;;;;;N;;;;; 10381;UGARITIC LETTER BETA;Lo;0;L;;;;;N;;;;; 10382;UGARITIC LETTER GAMLA;Lo;0;L;;;;;N;;;;; @@ -16703,6 +17238,440 @@ 104A7;OSMANYA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; 104A8;OSMANYA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; 104A9;OSMANYA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +10500;ELBASAN LETTER A;Lo;0;L;;;;;N;;;;; +10501;ELBASAN LETTER BE;Lo;0;L;;;;;N;;;;; +10502;ELBASAN LETTER CE;Lo;0;L;;;;;N;;;;; +10503;ELBASAN LETTER CHE;Lo;0;L;;;;;N;;;;; +10504;ELBASAN LETTER DE;Lo;0;L;;;;;N;;;;; +10505;ELBASAN LETTER NDE;Lo;0;L;;;;;N;;;;; +10506;ELBASAN LETTER DHE;Lo;0;L;;;;;N;;;;; +10507;ELBASAN LETTER EI;Lo;0;L;;;;;N;;;;; +10508;ELBASAN LETTER E;Lo;0;L;;;;;N;;;;; +10509;ELBASAN LETTER FE;Lo;0;L;;;;;N;;;;; +1050A;ELBASAN LETTER GE;Lo;0;L;;;;;N;;;;; +1050B;ELBASAN LETTER GJE;Lo;0;L;;;;;N;;;;; +1050C;ELBASAN LETTER HE;Lo;0;L;;;;;N;;;;; +1050D;ELBASAN LETTER I;Lo;0;L;;;;;N;;;;; +1050E;ELBASAN LETTER JE;Lo;0;L;;;;;N;;;;; +1050F;ELBASAN LETTER KE;Lo;0;L;;;;;N;;;;; +10510;ELBASAN LETTER LE;Lo;0;L;;;;;N;;;;; +10511;ELBASAN LETTER LLE;Lo;0;L;;;;;N;;;;; +10512;ELBASAN LETTER ME;Lo;0;L;;;;;N;;;;; +10513;ELBASAN LETTER NE;Lo;0;L;;;;;N;;;;; +10514;ELBASAN LETTER NA;Lo;0;L;;;;;N;;;;; +10515;ELBASAN LETTER NJE;Lo;0;L;;;;;N;;;;; +10516;ELBASAN LETTER O;Lo;0;L;;;;;N;;;;; +10517;ELBASAN LETTER PE;Lo;0;L;;;;;N;;;;; +10518;ELBASAN LETTER QE;Lo;0;L;;;;;N;;;;; +10519;ELBASAN LETTER RE;Lo;0;L;;;;;N;;;;; +1051A;ELBASAN LETTER RRE;Lo;0;L;;;;;N;;;;; +1051B;ELBASAN LETTER SE;Lo;0;L;;;;;N;;;;; +1051C;ELBASAN LETTER SHE;Lo;0;L;;;;;N;;;;; +1051D;ELBASAN LETTER TE;Lo;0;L;;;;;N;;;;; +1051E;ELBASAN LETTER THE;Lo;0;L;;;;;N;;;;; +1051F;ELBASAN LETTER U;Lo;0;L;;;;;N;;;;; +10520;ELBASAN LETTER VE;Lo;0;L;;;;;N;;;;; +10521;ELBASAN LETTER XE;Lo;0;L;;;;;N;;;;; +10522;ELBASAN LETTER Y;Lo;0;L;;;;;N;;;;; +10523;ELBASAN LETTER ZE;Lo;0;L;;;;;N;;;;; +10524;ELBASAN LETTER ZHE;Lo;0;L;;;;;N;;;;; +10525;ELBASAN LETTER GHE;Lo;0;L;;;;;N;;;;; +10526;ELBASAN LETTER GHAMMA;Lo;0;L;;;;;N;;;;; +10527;ELBASAN LETTER KHE;Lo;0;L;;;;;N;;;;; +10530;CAUCASIAN ALBANIAN LETTER ALT;Lo;0;L;;;;;N;;;;; +10531;CAUCASIAN ALBANIAN LETTER BET;Lo;0;L;;;;;N;;;;; +10532;CAUCASIAN ALBANIAN LETTER GIM;Lo;0;L;;;;;N;;;;; +10533;CAUCASIAN ALBANIAN LETTER DAT;Lo;0;L;;;;;N;;;;; +10534;CAUCASIAN ALBANIAN LETTER EB;Lo;0;L;;;;;N;;;;; +10535;CAUCASIAN ALBANIAN LETTER ZARL;Lo;0;L;;;;;N;;;;; +10536;CAUCASIAN ALBANIAN LETTER EYN;Lo;0;L;;;;;N;;;;; +10537;CAUCASIAN ALBANIAN LETTER ZHIL;Lo;0;L;;;;;N;;;;; +10538;CAUCASIAN ALBANIAN LETTER TAS;Lo;0;L;;;;;N;;;;; +10539;CAUCASIAN ALBANIAN LETTER CHA;Lo;0;L;;;;;N;;;;; +1053A;CAUCASIAN ALBANIAN LETTER YOWD;Lo;0;L;;;;;N;;;;; +1053B;CAUCASIAN ALBANIAN LETTER ZHA;Lo;0;L;;;;;N;;;;; +1053C;CAUCASIAN ALBANIAN LETTER IRB;Lo;0;L;;;;;N;;;;; +1053D;CAUCASIAN ALBANIAN LETTER SHA;Lo;0;L;;;;;N;;;;; +1053E;CAUCASIAN ALBANIAN LETTER LAN;Lo;0;L;;;;;N;;;;; +1053F;CAUCASIAN ALBANIAN LETTER INYA;Lo;0;L;;;;;N;;;;; +10540;CAUCASIAN ALBANIAN LETTER XEYN;Lo;0;L;;;;;N;;;;; +10541;CAUCASIAN ALBANIAN LETTER DYAN;Lo;0;L;;;;;N;;;;; +10542;CAUCASIAN ALBANIAN LETTER CAR;Lo;0;L;;;;;N;;;;; +10543;CAUCASIAN ALBANIAN LETTER JHOX;Lo;0;L;;;;;N;;;;; +10544;CAUCASIAN ALBANIAN LETTER KAR;Lo;0;L;;;;;N;;;;; +10545;CAUCASIAN ALBANIAN LETTER LYIT;Lo;0;L;;;;;N;;;;; +10546;CAUCASIAN ALBANIAN LETTER HEYT;Lo;0;L;;;;;N;;;;; +10547;CAUCASIAN ALBANIAN LETTER QAY;Lo;0;L;;;;;N;;;;; +10548;CAUCASIAN ALBANIAN LETTER AOR;Lo;0;L;;;;;N;;;;; +10549;CAUCASIAN ALBANIAN LETTER CHOY;Lo;0;L;;;;;N;;;;; +1054A;CAUCASIAN ALBANIAN LETTER CHI;Lo;0;L;;;;;N;;;;; +1054B;CAUCASIAN ALBANIAN LETTER CYAY;Lo;0;L;;;;;N;;;;; +1054C;CAUCASIAN ALBANIAN LETTER MAQ;Lo;0;L;;;;;N;;;;; +1054D;CAUCASIAN ALBANIAN LETTER QAR;Lo;0;L;;;;;N;;;;; +1054E;CAUCASIAN ALBANIAN LETTER NOWC;Lo;0;L;;;;;N;;;;; +1054F;CAUCASIAN ALBANIAN LETTER DZYAY;Lo;0;L;;;;;N;;;;; +10550;CAUCASIAN ALBANIAN LETTER SHAK;Lo;0;L;;;;;N;;;;; +10551;CAUCASIAN ALBANIAN LETTER JAYN;Lo;0;L;;;;;N;;;;; +10552;CAUCASIAN ALBANIAN LETTER ON;Lo;0;L;;;;;N;;;;; +10553;CAUCASIAN ALBANIAN LETTER TYAY;Lo;0;L;;;;;N;;;;; +10554;CAUCASIAN ALBANIAN LETTER FAM;Lo;0;L;;;;;N;;;;; +10555;CAUCASIAN ALBANIAN LETTER DZAY;Lo;0;L;;;;;N;;;;; +10556;CAUCASIAN ALBANIAN LETTER CHAT;Lo;0;L;;;;;N;;;;; +10557;CAUCASIAN ALBANIAN LETTER PEN;Lo;0;L;;;;;N;;;;; +10558;CAUCASIAN ALBANIAN LETTER GHEYS;Lo;0;L;;;;;N;;;;; +10559;CAUCASIAN ALBANIAN LETTER RAT;Lo;0;L;;;;;N;;;;; +1055A;CAUCASIAN ALBANIAN LETTER SEYK;Lo;0;L;;;;;N;;;;; +1055B;CAUCASIAN ALBANIAN LETTER VEYZ;Lo;0;L;;;;;N;;;;; +1055C;CAUCASIAN ALBANIAN LETTER TIWR;Lo;0;L;;;;;N;;;;; +1055D;CAUCASIAN ALBANIAN LETTER SHOY;Lo;0;L;;;;;N;;;;; +1055E;CAUCASIAN ALBANIAN LETTER IWN;Lo;0;L;;;;;N;;;;; +1055F;CAUCASIAN ALBANIAN LETTER CYAW;Lo;0;L;;;;;N;;;;; +10560;CAUCASIAN ALBANIAN LETTER CAYN;Lo;0;L;;;;;N;;;;; +10561;CAUCASIAN ALBANIAN LETTER YAYD;Lo;0;L;;;;;N;;;;; +10562;CAUCASIAN ALBANIAN LETTER PIWR;Lo;0;L;;;;;N;;;;; +10563;CAUCASIAN ALBANIAN LETTER KIW;Lo;0;L;;;;;N;;;;; +1056F;CAUCASIAN ALBANIAN CITATION MARK;Po;0;L;;;;;N;;;;; +10600;LINEAR A SIGN AB001;Lo;0;L;;;;;N;;;;; +10601;LINEAR A SIGN AB002;Lo;0;L;;;;;N;;;;; +10602;LINEAR A SIGN AB003;Lo;0;L;;;;;N;;;;; +10603;LINEAR A SIGN AB004;Lo;0;L;;;;;N;;;;; +10604;LINEAR A SIGN AB005;Lo;0;L;;;;;N;;;;; +10605;LINEAR A SIGN AB006;Lo;0;L;;;;;N;;;;; +10606;LINEAR A SIGN AB007;Lo;0;L;;;;;N;;;;; +10607;LINEAR A SIGN AB008;Lo;0;L;;;;;N;;;;; +10608;LINEAR A SIGN AB009;Lo;0;L;;;;;N;;;;; +10609;LINEAR A SIGN AB010;Lo;0;L;;;;;N;;;;; +1060A;LINEAR A SIGN AB011;Lo;0;L;;;;;N;;;;; +1060B;LINEAR A SIGN AB013;Lo;0;L;;;;;N;;;;; +1060C;LINEAR A SIGN AB016;Lo;0;L;;;;;N;;;;; +1060D;LINEAR A SIGN AB017;Lo;0;L;;;;;N;;;;; +1060E;LINEAR A SIGN AB020;Lo;0;L;;;;;N;;;;; +1060F;LINEAR A SIGN AB021;Lo;0;L;;;;;N;;;;; +10610;LINEAR A SIGN AB021F;Lo;0;L;;;;;N;;;;; +10611;LINEAR A SIGN AB021M;Lo;0;L;;;;;N;;;;; +10612;LINEAR A SIGN AB022;Lo;0;L;;;;;N;;;;; +10613;LINEAR A SIGN AB022F;Lo;0;L;;;;;N;;;;; +10614;LINEAR A SIGN AB022M;Lo;0;L;;;;;N;;;;; +10615;LINEAR A SIGN AB023;Lo;0;L;;;;;N;;;;; +10616;LINEAR A SIGN AB023M;Lo;0;L;;;;;N;;;;; +10617;LINEAR A SIGN AB024;Lo;0;L;;;;;N;;;;; +10618;LINEAR A SIGN AB026;Lo;0;L;;;;;N;;;;; +10619;LINEAR A SIGN AB027;Lo;0;L;;;;;N;;;;; +1061A;LINEAR A SIGN AB028;Lo;0;L;;;;;N;;;;; +1061B;LINEAR A SIGN A028B;Lo;0;L;;;;;N;;;;; +1061C;LINEAR A SIGN AB029;Lo;0;L;;;;;N;;;;; +1061D;LINEAR A SIGN AB030;Lo;0;L;;;;;N;;;;; +1061E;LINEAR A SIGN AB031;Lo;0;L;;;;;N;;;;; +1061F;LINEAR A SIGN AB034;Lo;0;L;;;;;N;;;;; +10620;LINEAR A SIGN AB037;Lo;0;L;;;;;N;;;;; +10621;LINEAR A SIGN AB038;Lo;0;L;;;;;N;;;;; +10622;LINEAR A SIGN AB039;Lo;0;L;;;;;N;;;;; +10623;LINEAR A SIGN AB040;Lo;0;L;;;;;N;;;;; +10624;LINEAR A SIGN AB041;Lo;0;L;;;;;N;;;;; +10625;LINEAR A SIGN AB044;Lo;0;L;;;;;N;;;;; +10626;LINEAR A SIGN AB045;Lo;0;L;;;;;N;;;;; +10627;LINEAR A SIGN AB046;Lo;0;L;;;;;N;;;;; +10628;LINEAR A SIGN AB047;Lo;0;L;;;;;N;;;;; +10629;LINEAR A SIGN AB048;Lo;0;L;;;;;N;;;;; +1062A;LINEAR A SIGN AB049;Lo;0;L;;;;;N;;;;; +1062B;LINEAR A SIGN AB050;Lo;0;L;;;;;N;;;;; +1062C;LINEAR A SIGN AB051;Lo;0;L;;;;;N;;;;; +1062D;LINEAR A SIGN AB053;Lo;0;L;;;;;N;;;;; +1062E;LINEAR A SIGN AB054;Lo;0;L;;;;;N;;;;; +1062F;LINEAR A SIGN AB055;Lo;0;L;;;;;N;;;;; +10630;LINEAR A SIGN AB056;Lo;0;L;;;;;N;;;;; +10631;LINEAR A SIGN AB057;Lo;0;L;;;;;N;;;;; +10632;LINEAR A SIGN AB058;Lo;0;L;;;;;N;;;;; +10633;LINEAR A SIGN AB059;Lo;0;L;;;;;N;;;;; +10634;LINEAR A SIGN AB060;Lo;0;L;;;;;N;;;;; +10635;LINEAR A SIGN AB061;Lo;0;L;;;;;N;;;;; +10636;LINEAR A SIGN AB065;Lo;0;L;;;;;N;;;;; +10637;LINEAR A SIGN AB066;Lo;0;L;;;;;N;;;;; +10638;LINEAR A SIGN AB067;Lo;0;L;;;;;N;;;;; +10639;LINEAR A SIGN AB069;Lo;0;L;;;;;N;;;;; +1063A;LINEAR A SIGN AB070;Lo;0;L;;;;;N;;;;; +1063B;LINEAR A SIGN AB073;Lo;0;L;;;;;N;;;;; +1063C;LINEAR A SIGN AB074;Lo;0;L;;;;;N;;;;; +1063D;LINEAR A SIGN AB076;Lo;0;L;;;;;N;;;;; +1063E;LINEAR A SIGN AB077;Lo;0;L;;;;;N;;;;; +1063F;LINEAR A SIGN AB078;Lo;0;L;;;;;N;;;;; +10640;LINEAR A SIGN AB079;Lo;0;L;;;;;N;;;;; +10641;LINEAR A SIGN AB080;Lo;0;L;;;;;N;;;;; +10642;LINEAR A SIGN AB081;Lo;0;L;;;;;N;;;;; +10643;LINEAR A SIGN AB082;Lo;0;L;;;;;N;;;;; +10644;LINEAR A SIGN AB085;Lo;0;L;;;;;N;;;;; +10645;LINEAR A SIGN AB086;Lo;0;L;;;;;N;;;;; +10646;LINEAR A SIGN AB087;Lo;0;L;;;;;N;;;;; +10647;LINEAR A SIGN A100-102;Lo;0;L;;;;;N;;;;; +10648;LINEAR A SIGN AB118;Lo;0;L;;;;;N;;;;; +10649;LINEAR A SIGN AB120;Lo;0;L;;;;;N;;;;; +1064A;LINEAR A SIGN A120B;Lo;0;L;;;;;N;;;;; +1064B;LINEAR A SIGN AB122;Lo;0;L;;;;;N;;;;; +1064C;LINEAR A SIGN AB123;Lo;0;L;;;;;N;;;;; +1064D;LINEAR A SIGN AB131A;Lo;0;L;;;;;N;;;;; +1064E;LINEAR A SIGN AB131B;Lo;0;L;;;;;N;;;;; +1064F;LINEAR A SIGN A131C;Lo;0;L;;;;;N;;;;; +10650;LINEAR A SIGN AB164;Lo;0;L;;;;;N;;;;; +10651;LINEAR A SIGN AB171;Lo;0;L;;;;;N;;;;; +10652;LINEAR A SIGN AB180;Lo;0;L;;;;;N;;;;; +10653;LINEAR A SIGN AB188;Lo;0;L;;;;;N;;;;; +10654;LINEAR A SIGN AB191;Lo;0;L;;;;;N;;;;; +10655;LINEAR A SIGN A301;Lo;0;L;;;;;N;;;;; +10656;LINEAR A SIGN A302;Lo;0;L;;;;;N;;;;; +10657;LINEAR A SIGN A303;Lo;0;L;;;;;N;;;;; +10658;LINEAR A SIGN A304;Lo;0;L;;;;;N;;;;; +10659;LINEAR A SIGN A305;Lo;0;L;;;;;N;;;;; +1065A;LINEAR A SIGN A306;Lo;0;L;;;;;N;;;;; +1065B;LINEAR A SIGN A307;Lo;0;L;;;;;N;;;;; +1065C;LINEAR A SIGN A308;Lo;0;L;;;;;N;;;;; +1065D;LINEAR A SIGN A309A;Lo;0;L;;;;;N;;;;; +1065E;LINEAR A SIGN A309B;Lo;0;L;;;;;N;;;;; +1065F;LINEAR A SIGN A309C;Lo;0;L;;;;;N;;;;; +10660;LINEAR A SIGN A310;Lo;0;L;;;;;N;;;;; +10661;LINEAR A SIGN A311;Lo;0;L;;;;;N;;;;; +10662;LINEAR A SIGN A312;Lo;0;L;;;;;N;;;;; +10663;LINEAR A SIGN A313A;Lo;0;L;;;;;N;;;;; +10664;LINEAR A SIGN A313B;Lo;0;L;;;;;N;;;;; +10665;LINEAR A SIGN A313C;Lo;0;L;;;;;N;;;;; +10666;LINEAR A SIGN A314;Lo;0;L;;;;;N;;;;; +10667;LINEAR A SIGN A315;Lo;0;L;;;;;N;;;;; +10668;LINEAR A SIGN A316;Lo;0;L;;;;;N;;;;; +10669;LINEAR A SIGN A317;Lo;0;L;;;;;N;;;;; +1066A;LINEAR A SIGN A318;Lo;0;L;;;;;N;;;;; +1066B;LINEAR A SIGN A319;Lo;0;L;;;;;N;;;;; +1066C;LINEAR A SIGN A320;Lo;0;L;;;;;N;;;;; +1066D;LINEAR A SIGN A321;Lo;0;L;;;;;N;;;;; +1066E;LINEAR A SIGN A322;Lo;0;L;;;;;N;;;;; +1066F;LINEAR A SIGN A323;Lo;0;L;;;;;N;;;;; +10670;LINEAR A SIGN A324;Lo;0;L;;;;;N;;;;; +10671;LINEAR A SIGN A325;Lo;0;L;;;;;N;;;;; +10672;LINEAR A SIGN A326;Lo;0;L;;;;;N;;;;; +10673;LINEAR A SIGN A327;Lo;0;L;;;;;N;;;;; +10674;LINEAR A SIGN A328;Lo;0;L;;;;;N;;;;; +10675;LINEAR A SIGN A329;Lo;0;L;;;;;N;;;;; +10676;LINEAR A SIGN A330;Lo;0;L;;;;;N;;;;; +10677;LINEAR A SIGN A331;Lo;0;L;;;;;N;;;;; +10678;LINEAR A SIGN A332;Lo;0;L;;;;;N;;;;; +10679;LINEAR A SIGN A333;Lo;0;L;;;;;N;;;;; +1067A;LINEAR A SIGN A334;Lo;0;L;;;;;N;;;;; +1067B;LINEAR A SIGN A335;Lo;0;L;;;;;N;;;;; +1067C;LINEAR A SIGN A336;Lo;0;L;;;;;N;;;;; +1067D;LINEAR A SIGN A337;Lo;0;L;;;;;N;;;;; +1067E;LINEAR A SIGN A338;Lo;0;L;;;;;N;;;;; +1067F;LINEAR A SIGN A339;Lo;0;L;;;;;N;;;;; +10680;LINEAR A SIGN A340;Lo;0;L;;;;;N;;;;; +10681;LINEAR A SIGN A341;Lo;0;L;;;;;N;;;;; +10682;LINEAR A SIGN A342;Lo;0;L;;;;;N;;;;; +10683;LINEAR A SIGN A343;Lo;0;L;;;;;N;;;;; +10684;LINEAR A SIGN A344;Lo;0;L;;;;;N;;;;; +10685;LINEAR A SIGN A345;Lo;0;L;;;;;N;;;;; +10686;LINEAR A SIGN A346;Lo;0;L;;;;;N;;;;; +10687;LINEAR A SIGN A347;Lo;0;L;;;;;N;;;;; +10688;LINEAR A SIGN A348;Lo;0;L;;;;;N;;;;; +10689;LINEAR A SIGN A349;Lo;0;L;;;;;N;;;;; +1068A;LINEAR A SIGN A350;Lo;0;L;;;;;N;;;;; +1068B;LINEAR A SIGN A351;Lo;0;L;;;;;N;;;;; +1068C;LINEAR A SIGN A352;Lo;0;L;;;;;N;;;;; +1068D;LINEAR A SIGN A353;Lo;0;L;;;;;N;;;;; +1068E;LINEAR A SIGN A354;Lo;0;L;;;;;N;;;;; +1068F;LINEAR A SIGN A355;Lo;0;L;;;;;N;;;;; +10690;LINEAR A SIGN A356;Lo;0;L;;;;;N;;;;; +10691;LINEAR A SIGN A357;Lo;0;L;;;;;N;;;;; +10692;LINEAR A SIGN A358;Lo;0;L;;;;;N;;;;; +10693;LINEAR A SIGN A359;Lo;0;L;;;;;N;;;;; +10694;LINEAR A SIGN A360;Lo;0;L;;;;;N;;;;; +10695;LINEAR A SIGN A361;Lo;0;L;;;;;N;;;;; +10696;LINEAR A SIGN A362;Lo;0;L;;;;;N;;;;; +10697;LINEAR A SIGN A363;Lo;0;L;;;;;N;;;;; +10698;LINEAR A SIGN A364;Lo;0;L;;;;;N;;;;; +10699;LINEAR A SIGN A365;Lo;0;L;;;;;N;;;;; +1069A;LINEAR A SIGN A366;Lo;0;L;;;;;N;;;;; +1069B;LINEAR A SIGN A367;Lo;0;L;;;;;N;;;;; +1069C;LINEAR A SIGN A368;Lo;0;L;;;;;N;;;;; +1069D;LINEAR A SIGN A369;Lo;0;L;;;;;N;;;;; +1069E;LINEAR A SIGN A370;Lo;0;L;;;;;N;;;;; +1069F;LINEAR A SIGN A371;Lo;0;L;;;;;N;;;;; +106A0;LINEAR A SIGN A400-VAS;Lo;0;L;;;;;N;;;;; +106A1;LINEAR A SIGN A401-VAS;Lo;0;L;;;;;N;;;;; +106A2;LINEAR A SIGN A402-VAS;Lo;0;L;;;;;N;;;;; +106A3;LINEAR A SIGN A403-VAS;Lo;0;L;;;;;N;;;;; +106A4;LINEAR A SIGN A404-VAS;Lo;0;L;;;;;N;;;;; +106A5;LINEAR A SIGN A405-VAS;Lo;0;L;;;;;N;;;;; +106A6;LINEAR A SIGN A406-VAS;Lo;0;L;;;;;N;;;;; +106A7;LINEAR A SIGN A407-VAS;Lo;0;L;;;;;N;;;;; +106A8;LINEAR A SIGN A408-VAS;Lo;0;L;;;;;N;;;;; +106A9;LINEAR A SIGN A409-VAS;Lo;0;L;;;;;N;;;;; +106AA;LINEAR A SIGN A410-VAS;Lo;0;L;;;;;N;;;;; +106AB;LINEAR A SIGN A411-VAS;Lo;0;L;;;;;N;;;;; +106AC;LINEAR A SIGN A412-VAS;Lo;0;L;;;;;N;;;;; +106AD;LINEAR A SIGN A413-VAS;Lo;0;L;;;;;N;;;;; +106AE;LINEAR A SIGN A414-VAS;Lo;0;L;;;;;N;;;;; +106AF;LINEAR A SIGN A415-VAS;Lo;0;L;;;;;N;;;;; +106B0;LINEAR A SIGN A416-VAS;Lo;0;L;;;;;N;;;;; +106B1;LINEAR A SIGN A417-VAS;Lo;0;L;;;;;N;;;;; +106B2;LINEAR A SIGN A418-VAS;Lo;0;L;;;;;N;;;;; +106B3;LINEAR A SIGN A501;Lo;0;L;;;;;N;;;;; +106B4;LINEAR A SIGN A502;Lo;0;L;;;;;N;;;;; +106B5;LINEAR A SIGN A503;Lo;0;L;;;;;N;;;;; +106B6;LINEAR A SIGN A504;Lo;0;L;;;;;N;;;;; +106B7;LINEAR A SIGN A505;Lo;0;L;;;;;N;;;;; +106B8;LINEAR A SIGN A506;Lo;0;L;;;;;N;;;;; +106B9;LINEAR A SIGN A508;Lo;0;L;;;;;N;;;;; +106BA;LINEAR A SIGN A509;Lo;0;L;;;;;N;;;;; +106BB;LINEAR A SIGN A510;Lo;0;L;;;;;N;;;;; +106BC;LINEAR A SIGN A511;Lo;0;L;;;;;N;;;;; +106BD;LINEAR A SIGN A512;Lo;0;L;;;;;N;;;;; +106BE;LINEAR A SIGN A513;Lo;0;L;;;;;N;;;;; +106BF;LINEAR A SIGN A515;Lo;0;L;;;;;N;;;;; +106C0;LINEAR A SIGN A516;Lo;0;L;;;;;N;;;;; +106C1;LINEAR A SIGN A520;Lo;0;L;;;;;N;;;;; +106C2;LINEAR A SIGN A521;Lo;0;L;;;;;N;;;;; +106C3;LINEAR A SIGN A523;Lo;0;L;;;;;N;;;;; +106C4;LINEAR A SIGN A524;Lo;0;L;;;;;N;;;;; +106C5;LINEAR A SIGN A525;Lo;0;L;;;;;N;;;;; +106C6;LINEAR A SIGN A526;Lo;0;L;;;;;N;;;;; +106C7;LINEAR A SIGN A527;Lo;0;L;;;;;N;;;;; +106C8;LINEAR A SIGN A528;Lo;0;L;;;;;N;;;;; +106C9;LINEAR A SIGN A529;Lo;0;L;;;;;N;;;;; +106CA;LINEAR A SIGN A530;Lo;0;L;;;;;N;;;;; +106CB;LINEAR A SIGN A531;Lo;0;L;;;;;N;;;;; +106CC;LINEAR A SIGN A532;Lo;0;L;;;;;N;;;;; +106CD;LINEAR A SIGN A534;Lo;0;L;;;;;N;;;;; +106CE;LINEAR A SIGN A535;Lo;0;L;;;;;N;;;;; +106CF;LINEAR A SIGN A536;Lo;0;L;;;;;N;;;;; +106D0;LINEAR A SIGN A537;Lo;0;L;;;;;N;;;;; +106D1;LINEAR A SIGN A538;Lo;0;L;;;;;N;;;;; +106D2;LINEAR A SIGN A539;Lo;0;L;;;;;N;;;;; +106D3;LINEAR A SIGN A540;Lo;0;L;;;;;N;;;;; +106D4;LINEAR A SIGN A541;Lo;0;L;;;;;N;;;;; +106D5;LINEAR A SIGN A542;Lo;0;L;;;;;N;;;;; +106D6;LINEAR A SIGN A545;Lo;0;L;;;;;N;;;;; +106D7;LINEAR A SIGN A547;Lo;0;L;;;;;N;;;;; +106D8;LINEAR A SIGN A548;Lo;0;L;;;;;N;;;;; +106D9;LINEAR A SIGN A549;Lo;0;L;;;;;N;;;;; +106DA;LINEAR A SIGN A550;Lo;0;L;;;;;N;;;;; +106DB;LINEAR A SIGN A551;Lo;0;L;;;;;N;;;;; +106DC;LINEAR A SIGN A552;Lo;0;L;;;;;N;;;;; +106DD;LINEAR A SIGN A553;Lo;0;L;;;;;N;;;;; +106DE;LINEAR A SIGN A554;Lo;0;L;;;;;N;;;;; +106DF;LINEAR A SIGN A555;Lo;0;L;;;;;N;;;;; +106E0;LINEAR A SIGN A556;Lo;0;L;;;;;N;;;;; +106E1;LINEAR A SIGN A557;Lo;0;L;;;;;N;;;;; +106E2;LINEAR A SIGN A559;Lo;0;L;;;;;N;;;;; +106E3;LINEAR A SIGN A563;Lo;0;L;;;;;N;;;;; +106E4;LINEAR A SIGN A564;Lo;0;L;;;;;N;;;;; +106E5;LINEAR A SIGN A565;Lo;0;L;;;;;N;;;;; +106E6;LINEAR A SIGN A566;Lo;0;L;;;;;N;;;;; +106E7;LINEAR A SIGN A568;Lo;0;L;;;;;N;;;;; +106E8;LINEAR A SIGN A569;Lo;0;L;;;;;N;;;;; +106E9;LINEAR A SIGN A570;Lo;0;L;;;;;N;;;;; +106EA;LINEAR A SIGN A571;Lo;0;L;;;;;N;;;;; +106EB;LINEAR A SIGN A572;Lo;0;L;;;;;N;;;;; +106EC;LINEAR A SIGN A573;Lo;0;L;;;;;N;;;;; +106ED;LINEAR A SIGN A574;Lo;0;L;;;;;N;;;;; +106EE;LINEAR A SIGN A575;Lo;0;L;;;;;N;;;;; +106EF;LINEAR A SIGN A576;Lo;0;L;;;;;N;;;;; +106F0;LINEAR A SIGN A577;Lo;0;L;;;;;N;;;;; +106F1;LINEAR A SIGN A578;Lo;0;L;;;;;N;;;;; +106F2;LINEAR A SIGN A579;Lo;0;L;;;;;N;;;;; +106F3;LINEAR A SIGN A580;Lo;0;L;;;;;N;;;;; +106F4;LINEAR A SIGN A581;Lo;0;L;;;;;N;;;;; +106F5;LINEAR A SIGN A582;Lo;0;L;;;;;N;;;;; +106F6;LINEAR A SIGN A583;Lo;0;L;;;;;N;;;;; +106F7;LINEAR A SIGN A584;Lo;0;L;;;;;N;;;;; +106F8;LINEAR A SIGN A585;Lo;0;L;;;;;N;;;;; +106F9;LINEAR A SIGN A586;Lo;0;L;;;;;N;;;;; +106FA;LINEAR A SIGN A587;Lo;0;L;;;;;N;;;;; +106FB;LINEAR A SIGN A588;Lo;0;L;;;;;N;;;;; +106FC;LINEAR A SIGN A589;Lo;0;L;;;;;N;;;;; +106FD;LINEAR A SIGN A591;Lo;0;L;;;;;N;;;;; +106FE;LINEAR A SIGN A592;Lo;0;L;;;;;N;;;;; +106FF;LINEAR A SIGN A594;Lo;0;L;;;;;N;;;;; +10700;LINEAR A SIGN A595;Lo;0;L;;;;;N;;;;; +10701;LINEAR A SIGN A596;Lo;0;L;;;;;N;;;;; +10702;LINEAR A SIGN A598;Lo;0;L;;;;;N;;;;; +10703;LINEAR A SIGN A600;Lo;0;L;;;;;N;;;;; +10704;LINEAR A SIGN A601;Lo;0;L;;;;;N;;;;; +10705;LINEAR A SIGN A602;Lo;0;L;;;;;N;;;;; +10706;LINEAR A SIGN A603;Lo;0;L;;;;;N;;;;; +10707;LINEAR A SIGN A604;Lo;0;L;;;;;N;;;;; +10708;LINEAR A SIGN A606;Lo;0;L;;;;;N;;;;; +10709;LINEAR A SIGN A608;Lo;0;L;;;;;N;;;;; +1070A;LINEAR A SIGN A609;Lo;0;L;;;;;N;;;;; +1070B;LINEAR A SIGN A610;Lo;0;L;;;;;N;;;;; +1070C;LINEAR A SIGN A611;Lo;0;L;;;;;N;;;;; +1070D;LINEAR A SIGN A612;Lo;0;L;;;;;N;;;;; +1070E;LINEAR A SIGN A613;Lo;0;L;;;;;N;;;;; +1070F;LINEAR A SIGN A614;Lo;0;L;;;;;N;;;;; +10710;LINEAR A SIGN A615;Lo;0;L;;;;;N;;;;; +10711;LINEAR A SIGN A616;Lo;0;L;;;;;N;;;;; +10712;LINEAR A SIGN A617;Lo;0;L;;;;;N;;;;; +10713;LINEAR A SIGN A618;Lo;0;L;;;;;N;;;;; +10714;LINEAR A SIGN A619;Lo;0;L;;;;;N;;;;; +10715;LINEAR A SIGN A620;Lo;0;L;;;;;N;;;;; +10716;LINEAR A SIGN A621;Lo;0;L;;;;;N;;;;; +10717;LINEAR A SIGN A622;Lo;0;L;;;;;N;;;;; +10718;LINEAR A SIGN A623;Lo;0;L;;;;;N;;;;; +10719;LINEAR A SIGN A624;Lo;0;L;;;;;N;;;;; +1071A;LINEAR A SIGN A626;Lo;0;L;;;;;N;;;;; +1071B;LINEAR A SIGN A627;Lo;0;L;;;;;N;;;;; +1071C;LINEAR A SIGN A628;Lo;0;L;;;;;N;;;;; +1071D;LINEAR A SIGN A629;Lo;0;L;;;;;N;;;;; +1071E;LINEAR A SIGN A634;Lo;0;L;;;;;N;;;;; +1071F;LINEAR A SIGN A637;Lo;0;L;;;;;N;;;;; +10720;LINEAR A SIGN A638;Lo;0;L;;;;;N;;;;; +10721;LINEAR A SIGN A640;Lo;0;L;;;;;N;;;;; +10722;LINEAR A SIGN A642;Lo;0;L;;;;;N;;;;; +10723;LINEAR A SIGN A643;Lo;0;L;;;;;N;;;;; +10724;LINEAR A SIGN A644;Lo;0;L;;;;;N;;;;; +10725;LINEAR A SIGN A645;Lo;0;L;;;;;N;;;;; +10726;LINEAR A SIGN A646;Lo;0;L;;;;;N;;;;; +10727;LINEAR A SIGN A648;Lo;0;L;;;;;N;;;;; +10728;LINEAR A SIGN A649;Lo;0;L;;;;;N;;;;; +10729;LINEAR A SIGN A651;Lo;0;L;;;;;N;;;;; +1072A;LINEAR A SIGN A652;Lo;0;L;;;;;N;;;;; +1072B;LINEAR A SIGN A653;Lo;0;L;;;;;N;;;;; +1072C;LINEAR A SIGN A654;Lo;0;L;;;;;N;;;;; +1072D;LINEAR A SIGN A655;Lo;0;L;;;;;N;;;;; +1072E;LINEAR A SIGN A656;Lo;0;L;;;;;N;;;;; +1072F;LINEAR A SIGN A657;Lo;0;L;;;;;N;;;;; +10730;LINEAR A SIGN A658;Lo;0;L;;;;;N;;;;; +10731;LINEAR A SIGN A659;Lo;0;L;;;;;N;;;;; +10732;LINEAR A SIGN A660;Lo;0;L;;;;;N;;;;; +10733;LINEAR A SIGN A661;Lo;0;L;;;;;N;;;;; +10734;LINEAR A SIGN A662;Lo;0;L;;;;;N;;;;; +10735;LINEAR A SIGN A663;Lo;0;L;;;;;N;;;;; +10736;LINEAR A SIGN A664;Lo;0;L;;;;;N;;;;; +10740;LINEAR A SIGN A701 A;Lo;0;L;;;;;N;;;;; +10741;LINEAR A SIGN A702 B;Lo;0;L;;;;;N;;;;; +10742;LINEAR A SIGN A703 D;Lo;0;L;;;;;N;;;;; +10743;LINEAR A SIGN A704 E;Lo;0;L;;;;;N;;;;; +10744;LINEAR A SIGN A705 F;Lo;0;L;;;;;N;;;;; +10745;LINEAR A SIGN A706 H;Lo;0;L;;;;;N;;;;; +10746;LINEAR A SIGN A707 J;Lo;0;L;;;;;N;;;;; +10747;LINEAR A SIGN A708 K;Lo;0;L;;;;;N;;;;; +10748;LINEAR A SIGN A709 L;Lo;0;L;;;;;N;;;;; +10749;LINEAR A SIGN A709-2 L2;Lo;0;L;;;;;N;;;;; +1074A;LINEAR A SIGN A709-3 L3;Lo;0;L;;;;;N;;;;; +1074B;LINEAR A SIGN A709-4 L4;Lo;0;L;;;;;N;;;;; +1074C;LINEAR A SIGN A709-6 L6;Lo;0;L;;;;;N;;;;; +1074D;LINEAR A SIGN A710 W;Lo;0;L;;;;;N;;;;; +1074E;LINEAR A SIGN A711 X;Lo;0;L;;;;;N;;;;; +1074F;LINEAR A SIGN A712 Y;Lo;0;L;;;;;N;;;;; +10750;LINEAR A SIGN A713 OMEGA;Lo;0;L;;;;;N;;;;; +10751;LINEAR A SIGN A714 ABB;Lo;0;L;;;;;N;;;;; +10752;LINEAR A SIGN A715 BB;Lo;0;L;;;;;N;;;;; +10753;LINEAR A SIGN A717 DD;Lo;0;L;;;;;N;;;;; +10754;LINEAR A SIGN A726 EYYY;Lo;0;L;;;;;N;;;;; +10755;LINEAR A SIGN A732 JE;Lo;0;L;;;;;N;;;;; +10760;LINEAR A SIGN A800;Lo;0;L;;;;;N;;;;; +10761;LINEAR A SIGN A801;Lo;0;L;;;;;N;;;;; +10762;LINEAR A SIGN A802;Lo;0;L;;;;;N;;;;; +10763;LINEAR A SIGN A803;Lo;0;L;;;;;N;;;;; +10764;LINEAR A SIGN A804;Lo;0;L;;;;;N;;;;; +10765;LINEAR A SIGN A805;Lo;0;L;;;;;N;;;;; +10766;LINEAR A SIGN A806;Lo;0;L;;;;;N;;;;; +10767;LINEAR A SIGN A807;Lo;0;L;;;;;N;;;;; 10800;CYPRIOT SYLLABLE A;Lo;0;R;;;;;N;;;;; 10801;CYPRIOT SYLLABLE E;Lo;0;R;;;;;N;;;;; 10802;CYPRIOT SYLLABLE I;Lo;0;R;;;;;N;;;;; @@ -16789,6 +17758,78 @@ 1085D;IMPERIAL ARAMAIC NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;; 1085E;IMPERIAL ARAMAIC NUMBER ONE THOUSAND;No;0;R;;;;1000;N;;;;; 1085F;IMPERIAL ARAMAIC NUMBER TEN THOUSAND;No;0;R;;;;10000;N;;;;; +10860;PALMYRENE LETTER ALEPH;Lo;0;R;;;;;N;;;;; +10861;PALMYRENE LETTER BETH;Lo;0;R;;;;;N;;;;; +10862;PALMYRENE LETTER GIMEL;Lo;0;R;;;;;N;;;;; +10863;PALMYRENE LETTER DALETH;Lo;0;R;;;;;N;;;;; +10864;PALMYRENE LETTER HE;Lo;0;R;;;;;N;;;;; +10865;PALMYRENE LETTER WAW;Lo;0;R;;;;;N;;;;; +10866;PALMYRENE LETTER ZAYIN;Lo;0;R;;;;;N;;;;; +10867;PALMYRENE LETTER HETH;Lo;0;R;;;;;N;;;;; +10868;PALMYRENE LETTER TETH;Lo;0;R;;;;;N;;;;; +10869;PALMYRENE LETTER YODH;Lo;0;R;;;;;N;;;;; +1086A;PALMYRENE LETTER KAPH;Lo;0;R;;;;;N;;;;; +1086B;PALMYRENE LETTER LAMEDH;Lo;0;R;;;;;N;;;;; +1086C;PALMYRENE LETTER MEM;Lo;0;R;;;;;N;;;;; +1086D;PALMYRENE LETTER FINAL NUN;Lo;0;R;;;;;N;;;;; +1086E;PALMYRENE LETTER NUN;Lo;0;R;;;;;N;;;;; +1086F;PALMYRENE LETTER SAMEKH;Lo;0;R;;;;;N;;;;; +10870;PALMYRENE LETTER AYIN;Lo;0;R;;;;;N;;;;; +10871;PALMYRENE LETTER PE;Lo;0;R;;;;;N;;;;; +10872;PALMYRENE LETTER SADHE;Lo;0;R;;;;;N;;;;; +10873;PALMYRENE LETTER QOPH;Lo;0;R;;;;;N;;;;; +10874;PALMYRENE LETTER RESH;Lo;0;R;;;;;N;;;;; +10875;PALMYRENE LETTER SHIN;Lo;0;R;;;;;N;;;;; +10876;PALMYRENE LETTER TAW;Lo;0;R;;;;;N;;;;; +10877;PALMYRENE LEFT-POINTING FLEURON;So;0;R;;;;;N;;;;; +10878;PALMYRENE RIGHT-POINTING FLEURON;So;0;R;;;;;N;;;;; +10879;PALMYRENE NUMBER ONE;No;0;R;;;;1;N;;;;; +1087A;PALMYRENE NUMBER TWO;No;0;R;;;;2;N;;;;; +1087B;PALMYRENE NUMBER THREE;No;0;R;;;;3;N;;;;; +1087C;PALMYRENE NUMBER FOUR;No;0;R;;;;4;N;;;;; +1087D;PALMYRENE NUMBER FIVE;No;0;R;;;;5;N;;;;; +1087E;PALMYRENE NUMBER TEN;No;0;R;;;;10;N;;;;; +1087F;PALMYRENE NUMBER TWENTY;No;0;R;;;;20;N;;;;; +10880;NABATAEAN LETTER FINAL ALEPH;Lo;0;R;;;;;N;;;;; +10881;NABATAEAN LETTER ALEPH;Lo;0;R;;;;;N;;;;; +10882;NABATAEAN LETTER FINAL BETH;Lo;0;R;;;;;N;;;;; +10883;NABATAEAN LETTER BETH;Lo;0;R;;;;;N;;;;; +10884;NABATAEAN LETTER GIMEL;Lo;0;R;;;;;N;;;;; +10885;NABATAEAN LETTER DALETH;Lo;0;R;;;;;N;;;;; +10886;NABATAEAN LETTER FINAL HE;Lo;0;R;;;;;N;;;;; +10887;NABATAEAN LETTER HE;Lo;0;R;;;;;N;;;;; +10888;NABATAEAN LETTER WAW;Lo;0;R;;;;;N;;;;; +10889;NABATAEAN LETTER ZAYIN;Lo;0;R;;;;;N;;;;; +1088A;NABATAEAN LETTER HETH;Lo;0;R;;;;;N;;;;; +1088B;NABATAEAN LETTER TETH;Lo;0;R;;;;;N;;;;; +1088C;NABATAEAN LETTER FINAL YODH;Lo;0;R;;;;;N;;;;; +1088D;NABATAEAN LETTER YODH;Lo;0;R;;;;;N;;;;; +1088E;NABATAEAN LETTER FINAL KAPH;Lo;0;R;;;;;N;;;;; +1088F;NABATAEAN LETTER KAPH;Lo;0;R;;;;;N;;;;; +10890;NABATAEAN LETTER FINAL LAMEDH;Lo;0;R;;;;;N;;;;; +10891;NABATAEAN LETTER LAMEDH;Lo;0;R;;;;;N;;;;; +10892;NABATAEAN LETTER FINAL MEM;Lo;0;R;;;;;N;;;;; +10893;NABATAEAN LETTER MEM;Lo;0;R;;;;;N;;;;; +10894;NABATAEAN LETTER FINAL NUN;Lo;0;R;;;;;N;;;;; +10895;NABATAEAN LETTER NUN;Lo;0;R;;;;;N;;;;; +10896;NABATAEAN LETTER SAMEKH;Lo;0;R;;;;;N;;;;; +10897;NABATAEAN LETTER AYIN;Lo;0;R;;;;;N;;;;; +10898;NABATAEAN LETTER PE;Lo;0;R;;;;;N;;;;; +10899;NABATAEAN LETTER SADHE;Lo;0;R;;;;;N;;;;; +1089A;NABATAEAN LETTER QOPH;Lo;0;R;;;;;N;;;;; +1089B;NABATAEAN LETTER RESH;Lo;0;R;;;;;N;;;;; +1089C;NABATAEAN LETTER FINAL SHIN;Lo;0;R;;;;;N;;;;; +1089D;NABATAEAN LETTER SHIN;Lo;0;R;;;;;N;;;;; +1089E;NABATAEAN LETTER TAW;Lo;0;R;;;;;N;;;;; +108A7;NABATAEAN NUMBER ONE;No;0;R;;;;1;N;;;;; +108A8;NABATAEAN NUMBER TWO;No;0;R;;;;2;N;;;;; +108A9;NABATAEAN NUMBER THREE;No;0;R;;;;3;N;;;;; +108AA;NABATAEAN NUMBER FOUR;No;0;R;;;;4;N;;;;; +108AB;NABATAEAN CRUCIFORM NUMBER FOUR;No;0;R;;;;4;N;;;;; +108AC;NABATAEAN NUMBER FIVE;No;0;R;;;;5;N;;;;; +108AD;NABATAEAN NUMBER TEN;No;0;R;;;;10;N;;;;; +108AE;NABATAEAN NUMBER TWENTY;No;0;R;;;;20;N;;;;; +108AF;NABATAEAN NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;; 10900;PHOENICIAN LETTER ALF;Lo;0;R;;;;;N;;;;; 10901;PHOENICIAN LETTER BET;Lo;0;R;;;;;N;;;;; 10902;PHOENICIAN LETTER GAML;Lo;0;R;;;;;N;;;;; @@ -16845,6 +17886,64 @@ 10938;LYDIAN LETTER NN;Lo;0;R;;;;;N;;;;; 10939;LYDIAN LETTER C;Lo;0;R;;;;;N;;;;; 1093F;LYDIAN TRIANGULAR MARK;Po;0;R;;;;;N;;;;; +10980;MEROITIC HIEROGLYPHIC LETTER A;Lo;0;R;;;;;N;;;;; +10981;MEROITIC HIEROGLYPHIC LETTER E;Lo;0;R;;;;;N;;;;; +10982;MEROITIC HIEROGLYPHIC LETTER I;Lo;0;R;;;;;N;;;;; +10983;MEROITIC HIEROGLYPHIC LETTER O;Lo;0;R;;;;;N;;;;; +10984;MEROITIC HIEROGLYPHIC LETTER YA;Lo;0;R;;;;;N;;;;; +10985;MEROITIC HIEROGLYPHIC LETTER WA;Lo;0;R;;;;;N;;;;; +10986;MEROITIC HIEROGLYPHIC LETTER BA;Lo;0;R;;;;;N;;;;; +10987;MEROITIC HIEROGLYPHIC LETTER BA-2;Lo;0;R;;;;;N;;;;; +10988;MEROITIC HIEROGLYPHIC LETTER PA;Lo;0;R;;;;;N;;;;; +10989;MEROITIC HIEROGLYPHIC LETTER MA;Lo;0;R;;;;;N;;;;; +1098A;MEROITIC HIEROGLYPHIC LETTER NA;Lo;0;R;;;;;N;;;;; +1098B;MEROITIC HIEROGLYPHIC LETTER NA-2;Lo;0;R;;;;;N;;;;; +1098C;MEROITIC HIEROGLYPHIC LETTER NE;Lo;0;R;;;;;N;;;;; +1098D;MEROITIC HIEROGLYPHIC LETTER NE-2;Lo;0;R;;;;;N;;;;; +1098E;MEROITIC HIEROGLYPHIC LETTER RA;Lo;0;R;;;;;N;;;;; +1098F;MEROITIC HIEROGLYPHIC LETTER RA-2;Lo;0;R;;;;;N;;;;; +10990;MEROITIC HIEROGLYPHIC LETTER LA;Lo;0;R;;;;;N;;;;; +10991;MEROITIC HIEROGLYPHIC LETTER KHA;Lo;0;R;;;;;N;;;;; +10992;MEROITIC HIEROGLYPHIC LETTER HHA;Lo;0;R;;;;;N;;;;; +10993;MEROITIC HIEROGLYPHIC LETTER SA;Lo;0;R;;;;;N;;;;; +10994;MEROITIC HIEROGLYPHIC LETTER SA-2;Lo;0;R;;;;;N;;;;; +10995;MEROITIC HIEROGLYPHIC LETTER SE;Lo;0;R;;;;;N;;;;; +10996;MEROITIC HIEROGLYPHIC LETTER KA;Lo;0;R;;;;;N;;;;; +10997;MEROITIC HIEROGLYPHIC LETTER QA;Lo;0;R;;;;;N;;;;; +10998;MEROITIC HIEROGLYPHIC LETTER TA;Lo;0;R;;;;;N;;;;; +10999;MEROITIC HIEROGLYPHIC LETTER TA-2;Lo;0;R;;;;;N;;;;; +1099A;MEROITIC HIEROGLYPHIC LETTER TE;Lo;0;R;;;;;N;;;;; +1099B;MEROITIC HIEROGLYPHIC LETTER TE-2;Lo;0;R;;;;;N;;;;; +1099C;MEROITIC HIEROGLYPHIC LETTER TO;Lo;0;R;;;;;N;;;;; +1099D;MEROITIC HIEROGLYPHIC LETTER DA;Lo;0;R;;;;;N;;;;; +1099E;MEROITIC HIEROGLYPHIC SYMBOL VIDJ;Lo;0;R;;;;;N;;;;; +1099F;MEROITIC HIEROGLYPHIC SYMBOL VIDJ-2;Lo;0;R;;;;;N;;;;; +109A0;MEROITIC CURSIVE LETTER A;Lo;0;R;;;;;N;;;;; +109A1;MEROITIC CURSIVE LETTER E;Lo;0;R;;;;;N;;;;; +109A2;MEROITIC CURSIVE LETTER I;Lo;0;R;;;;;N;;;;; +109A3;MEROITIC CURSIVE LETTER O;Lo;0;R;;;;;N;;;;; +109A4;MEROITIC CURSIVE LETTER YA;Lo;0;R;;;;;N;;;;; +109A5;MEROITIC CURSIVE LETTER WA;Lo;0;R;;;;;N;;;;; +109A6;MEROITIC CURSIVE LETTER BA;Lo;0;R;;;;;N;;;;; +109A7;MEROITIC CURSIVE LETTER PA;Lo;0;R;;;;;N;;;;; +109A8;MEROITIC CURSIVE LETTER MA;Lo;0;R;;;;;N;;;;; +109A9;MEROITIC CURSIVE LETTER NA;Lo;0;R;;;;;N;;;;; +109AA;MEROITIC CURSIVE LETTER NE;Lo;0;R;;;;;N;;;;; +109AB;MEROITIC CURSIVE LETTER RA;Lo;0;R;;;;;N;;;;; +109AC;MEROITIC CURSIVE LETTER LA;Lo;0;R;;;;;N;;;;; +109AD;MEROITIC CURSIVE LETTER KHA;Lo;0;R;;;;;N;;;;; +109AE;MEROITIC CURSIVE LETTER HHA;Lo;0;R;;;;;N;;;;; +109AF;MEROITIC CURSIVE LETTER SA;Lo;0;R;;;;;N;;;;; +109B0;MEROITIC CURSIVE LETTER ARCHAIC SA;Lo;0;R;;;;;N;;;;; +109B1;MEROITIC CURSIVE LETTER SE;Lo;0;R;;;;;N;;;;; +109B2;MEROITIC CURSIVE LETTER KA;Lo;0;R;;;;;N;;;;; +109B3;MEROITIC CURSIVE LETTER QA;Lo;0;R;;;;;N;;;;; +109B4;MEROITIC CURSIVE LETTER TA;Lo;0;R;;;;;N;;;;; +109B5;MEROITIC CURSIVE LETTER TE;Lo;0;R;;;;;N;;;;; +109B6;MEROITIC CURSIVE LETTER TO;Lo;0;R;;;;;N;;;;; +109B7;MEROITIC CURSIVE LETTER DA;Lo;0;R;;;;;N;;;;; +109BE;MEROITIC CURSIVE LOGOGRAM RMT;Lo;0;R;;;;;N;;;;; +109BF;MEROITIC CURSIVE LOGOGRAM IMN;Lo;0;R;;;;;N;;;;; 10A00;KHAROSHTHI LETTER A;Lo;0;R;;;;;N;;;;; 10A01;KHAROSHTHI VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;; 10A02;KHAROSHTHI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;; @@ -16942,6 +18041,89 @@ 10A7D;OLD SOUTH ARABIAN NUMBER ONE;No;0;R;;;;1;N;;;;; 10A7E;OLD SOUTH ARABIAN NUMBER FIFTY;No;0;R;;;;50;N;;;;; 10A7F;OLD SOUTH ARABIAN NUMERIC INDICATOR;Po;0;R;;;;;N;;;;; +10A80;OLD NORTH ARABIAN LETTER HEH;Lo;0;R;;;;;N;;;;; +10A81;OLD NORTH ARABIAN LETTER LAM;Lo;0;R;;;;;N;;;;; +10A82;OLD NORTH ARABIAN LETTER HAH;Lo;0;R;;;;;N;;;;; +10A83;OLD NORTH ARABIAN LETTER MEEM;Lo;0;R;;;;;N;;;;; +10A84;OLD NORTH ARABIAN LETTER QAF;Lo;0;R;;;;;N;;;;; +10A85;OLD NORTH ARABIAN LETTER WAW;Lo;0;R;;;;;N;;;;; +10A86;OLD NORTH ARABIAN LETTER ES-2;Lo;0;R;;;;;N;;;;; +10A87;OLD NORTH ARABIAN LETTER REH;Lo;0;R;;;;;N;;;;; +10A88;OLD NORTH ARABIAN LETTER BEH;Lo;0;R;;;;;N;;;;; +10A89;OLD NORTH ARABIAN LETTER TEH;Lo;0;R;;;;;N;;;;; +10A8A;OLD NORTH ARABIAN LETTER ES-1;Lo;0;R;;;;;N;;;;; +10A8B;OLD NORTH ARABIAN LETTER KAF;Lo;0;R;;;;;N;;;;; +10A8C;OLD NORTH ARABIAN LETTER NOON;Lo;0;R;;;;;N;;;;; +10A8D;OLD NORTH ARABIAN LETTER KHAH;Lo;0;R;;;;;N;;;;; +10A8E;OLD NORTH ARABIAN LETTER SAD;Lo;0;R;;;;;N;;;;; +10A8F;OLD NORTH ARABIAN LETTER ES-3;Lo;0;R;;;;;N;;;;; +10A90;OLD NORTH ARABIAN LETTER FEH;Lo;0;R;;;;;N;;;;; +10A91;OLD NORTH ARABIAN LETTER ALEF;Lo;0;R;;;;;N;;;;; +10A92;OLD NORTH ARABIAN LETTER AIN;Lo;0;R;;;;;N;;;;; +10A93;OLD NORTH ARABIAN LETTER DAD;Lo;0;R;;;;;N;;;;; +10A94;OLD NORTH ARABIAN LETTER GEEM;Lo;0;R;;;;;N;;;;; +10A95;OLD NORTH ARABIAN LETTER DAL;Lo;0;R;;;;;N;;;;; +10A96;OLD NORTH ARABIAN LETTER GHAIN;Lo;0;R;;;;;N;;;;; +10A97;OLD NORTH ARABIAN LETTER TAH;Lo;0;R;;;;;N;;;;; +10A98;OLD NORTH ARABIAN LETTER ZAIN;Lo;0;R;;;;;N;;;;; +10A99;OLD NORTH ARABIAN LETTER THAL;Lo;0;R;;;;;N;;;;; +10A9A;OLD NORTH ARABIAN LETTER YEH;Lo;0;R;;;;;N;;;;; +10A9B;OLD NORTH ARABIAN LETTER THEH;Lo;0;R;;;;;N;;;;; +10A9C;OLD NORTH ARABIAN LETTER ZAH;Lo;0;R;;;;;N;;;;; +10A9D;OLD NORTH ARABIAN NUMBER ONE;No;0;R;;;;1;N;;;;; +10A9E;OLD NORTH ARABIAN NUMBER TEN;No;0;R;;;;10;N;;;;; +10A9F;OLD NORTH ARABIAN NUMBER TWENTY;No;0;R;;;;20;N;;;;; +10AC0;MANICHAEAN LETTER ALEPH;Lo;0;R;;;;;N;;;;; +10AC1;MANICHAEAN LETTER BETH;Lo;0;R;;;;;N;;;;; +10AC2;MANICHAEAN LETTER BHETH;Lo;0;R;;;;;N;;;;; +10AC3;MANICHAEAN LETTER GIMEL;Lo;0;R;;;;;N;;;;; +10AC4;MANICHAEAN LETTER GHIMEL;Lo;0;R;;;;;N;;;;; +10AC5;MANICHAEAN LETTER DALETH;Lo;0;R;;;;;N;;;;; +10AC6;MANICHAEAN LETTER HE;Lo;0;R;;;;;N;;;;; +10AC7;MANICHAEAN LETTER WAW;Lo;0;R;;;;;N;;;;; +10AC8;MANICHAEAN SIGN UD;So;0;R;;;;;N;;;;; +10AC9;MANICHAEAN LETTER ZAYIN;Lo;0;R;;;;;N;;;;; +10ACA;MANICHAEAN LETTER ZHAYIN;Lo;0;R;;;;;N;;;;; +10ACB;MANICHAEAN LETTER JAYIN;Lo;0;R;;;;;N;;;;; +10ACC;MANICHAEAN LETTER JHAYIN;Lo;0;R;;;;;N;;;;; +10ACD;MANICHAEAN LETTER HETH;Lo;0;R;;;;;N;;;;; +10ACE;MANICHAEAN LETTER TETH;Lo;0;R;;;;;N;;;;; +10ACF;MANICHAEAN LETTER YODH;Lo;0;R;;;;;N;;;;; +10AD0;MANICHAEAN LETTER KAPH;Lo;0;R;;;;;N;;;;; +10AD1;MANICHAEAN LETTER XAPH;Lo;0;R;;;;;N;;;;; +10AD2;MANICHAEAN LETTER KHAPH;Lo;0;R;;;;;N;;;;; +10AD3;MANICHAEAN LETTER LAMEDH;Lo;0;R;;;;;N;;;;; +10AD4;MANICHAEAN LETTER DHAMEDH;Lo;0;R;;;;;N;;;;; +10AD5;MANICHAEAN LETTER THAMEDH;Lo;0;R;;;;;N;;;;; +10AD6;MANICHAEAN LETTER MEM;Lo;0;R;;;;;N;;;;; +10AD7;MANICHAEAN LETTER NUN;Lo;0;R;;;;;N;;;;; +10AD8;MANICHAEAN LETTER SAMEKH;Lo;0;R;;;;;N;;;;; +10AD9;MANICHAEAN LETTER AYIN;Lo;0;R;;;;;N;;;;; +10ADA;MANICHAEAN LETTER AAYIN;Lo;0;R;;;;;N;;;;; +10ADB;MANICHAEAN LETTER PE;Lo;0;R;;;;;N;;;;; +10ADC;MANICHAEAN LETTER FE;Lo;0;R;;;;;N;;;;; +10ADD;MANICHAEAN LETTER SADHE;Lo;0;R;;;;;N;;;;; +10ADE;MANICHAEAN LETTER QOPH;Lo;0;R;;;;;N;;;;; +10ADF;MANICHAEAN LETTER XOPH;Lo;0;R;;;;;N;;;;; +10AE0;MANICHAEAN LETTER QHOPH;Lo;0;R;;;;;N;;;;; +10AE1;MANICHAEAN LETTER RESH;Lo;0;R;;;;;N;;;;; +10AE2;MANICHAEAN LETTER SHIN;Lo;0;R;;;;;N;;;;; +10AE3;MANICHAEAN LETTER SSHIN;Lo;0;R;;;;;N;;;;; +10AE4;MANICHAEAN LETTER TAW;Lo;0;R;;;;;N;;;;; +10AE5;MANICHAEAN ABBREVIATION MARK ABOVE;Mn;230;NSM;;;;;N;;;;; +10AE6;MANICHAEAN ABBREVIATION MARK BELOW;Mn;220;NSM;;;;;N;;;;; +10AEB;MANICHAEAN NUMBER ONE;No;0;R;;;;1;N;;;;; +10AEC;MANICHAEAN NUMBER FIVE;No;0;R;;;;5;N;;;;; +10AED;MANICHAEAN NUMBER TEN;No;0;R;;;;10;N;;;;; +10AEE;MANICHAEAN NUMBER TWENTY;No;0;R;;;;20;N;;;;; +10AEF;MANICHAEAN NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;; +10AF0;MANICHAEAN PUNCTUATION STAR;Po;0;R;;;;;N;;;;; +10AF1;MANICHAEAN PUNCTUATION FLEURON;Po;0;R;;;;;N;;;;; +10AF2;MANICHAEAN PUNCTUATION DOUBLE DOT WITHIN DOT;Po;0;R;;;;;N;;;;; +10AF3;MANICHAEAN PUNCTUATION DOT WITHIN DOT;Po;0;R;;;;;N;;;;; +10AF4;MANICHAEAN PUNCTUATION DOT;Po;0;R;;;;;N;;;;; +10AF5;MANICHAEAN PUNCTUATION TWO DOTS;Po;0;R;;;;;N;;;;; +10AF6;MANICHAEAN PUNCTUATION LINE FILLER;Po;0;R;;;;;N;;;;; 10B00;AVESTAN LETTER A;Lo;0;R;;;;;N;;;;; 10B01;AVESTAN LETTER AA;Lo;0;R;;;;;N;;;;; 10B02;AVESTAN LETTER AO;Lo;0;R;;;;;N;;;;; @@ -17060,6 +18242,35 @@ 10B7D;INSCRIPTIONAL PAHLAVI NUMBER TWENTY;No;0;R;;;;20;N;;;;; 10B7E;INSCRIPTIONAL PAHLAVI NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;; 10B7F;INSCRIPTIONAL PAHLAVI NUMBER ONE THOUSAND;No;0;R;;;;1000;N;;;;; +10B80;PSALTER PAHLAVI LETTER ALEPH;Lo;0;R;;;;;N;;;;; +10B81;PSALTER PAHLAVI LETTER BETH;Lo;0;R;;;;;N;;;;; +10B82;PSALTER PAHLAVI LETTER GIMEL;Lo;0;R;;;;;N;;;;; +10B83;PSALTER PAHLAVI LETTER DALETH;Lo;0;R;;;;;N;;;;; +10B84;PSALTER PAHLAVI LETTER HE;Lo;0;R;;;;;N;;;;; +10B85;PSALTER PAHLAVI LETTER WAW-AYIN-RESH;Lo;0;R;;;;;N;;;;; +10B86;PSALTER PAHLAVI LETTER ZAYIN;Lo;0;R;;;;;N;;;;; +10B87;PSALTER PAHLAVI LETTER HETH;Lo;0;R;;;;;N;;;;; +10B88;PSALTER PAHLAVI LETTER YODH;Lo;0;R;;;;;N;;;;; +10B89;PSALTER PAHLAVI LETTER KAPH;Lo;0;R;;;;;N;;;;; +10B8A;PSALTER PAHLAVI LETTER LAMEDH;Lo;0;R;;;;;N;;;;; +10B8B;PSALTER PAHLAVI LETTER MEM-QOPH;Lo;0;R;;;;;N;;;;; +10B8C;PSALTER PAHLAVI LETTER NUN;Lo;0;R;;;;;N;;;;; +10B8D;PSALTER PAHLAVI LETTER SAMEKH;Lo;0;R;;;;;N;;;;; +10B8E;PSALTER PAHLAVI LETTER PE;Lo;0;R;;;;;N;;;;; +10B8F;PSALTER PAHLAVI LETTER SADHE;Lo;0;R;;;;;N;;;;; +10B90;PSALTER PAHLAVI LETTER SHIN;Lo;0;R;;;;;N;;;;; +10B91;PSALTER PAHLAVI LETTER TAW;Lo;0;R;;;;;N;;;;; +10B99;PSALTER PAHLAVI SECTION MARK;Po;0;R;;;;;N;;;;; +10B9A;PSALTER PAHLAVI TURNED SECTION MARK;Po;0;R;;;;;N;;;;; +10B9B;PSALTER PAHLAVI FOUR DOTS WITH CROSS;Po;0;R;;;;;N;;;;; +10B9C;PSALTER PAHLAVI FOUR DOTS WITH DOT;Po;0;R;;;;;N;;;;; +10BA9;PSALTER PAHLAVI NUMBER ONE;No;0;R;;;;1;N;;;;; +10BAA;PSALTER PAHLAVI NUMBER TWO;No;0;R;;;;2;N;;;;; +10BAB;PSALTER PAHLAVI NUMBER THREE;No;0;R;;;;3;N;;;;; +10BAC;PSALTER PAHLAVI NUMBER FOUR;No;0;R;;;;4;N;;;;; +10BAD;PSALTER PAHLAVI NUMBER TEN;No;0;R;;;;10;N;;;;; +10BAE;PSALTER PAHLAVI NUMBER TWENTY;No;0;R;;;;20;N;;;;; +10BAF;PSALTER PAHLAVI NUMBER ONE HUNDRED;No;0;R;;;;100;N;;;;; 10C00;OLD TURKIC LETTER ORKHON A;Lo;0;R;;;;;N;;;;; 10C01;OLD TURKIC LETTER YENISEI A;Lo;0;R;;;;;N;;;;; 10C02;OLD TURKIC LETTER YENISEI AE;Lo;0;R;;;;;N;;;;; @@ -17272,6 +18483,7 @@ 1106D;BRAHMI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; 1106E;BRAHMI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; 1106F;BRAHMI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +1107F;BRAHMI NUMBER JOINER;Mn;9;NSM;;;;;N;;;;; 11080;KAITHI SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;; 11081;KAITHI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;; 11082;KAITHI SIGN VISARGA;Mc;0;L;;;;;N;;;;; @@ -17338,6 +18550,905 @@ 110BF;KAITHI DOUBLE SECTION MARK;Po;0;L;;;;;N;;;;; 110C0;KAITHI DANDA;Po;0;L;;;;;N;;;;; 110C1;KAITHI DOUBLE DANDA;Po;0;L;;;;;N;;;;; +110D0;SORA SOMPENG LETTER SAH;Lo;0;L;;;;;N;;;;; +110D1;SORA SOMPENG LETTER TAH;Lo;0;L;;;;;N;;;;; +110D2;SORA SOMPENG LETTER BAH;Lo;0;L;;;;;N;;;;; +110D3;SORA SOMPENG LETTER CAH;Lo;0;L;;;;;N;;;;; +110D4;SORA SOMPENG LETTER DAH;Lo;0;L;;;;;N;;;;; +110D5;SORA SOMPENG LETTER GAH;Lo;0;L;;;;;N;;;;; +110D6;SORA SOMPENG LETTER MAH;Lo;0;L;;;;;N;;;;; +110D7;SORA SOMPENG LETTER NGAH;Lo;0;L;;;;;N;;;;; +110D8;SORA SOMPENG LETTER LAH;Lo;0;L;;;;;N;;;;; +110D9;SORA SOMPENG LETTER NAH;Lo;0;L;;;;;N;;;;; +110DA;SORA SOMPENG LETTER VAH;Lo;0;L;;;;;N;;;;; +110DB;SORA SOMPENG LETTER PAH;Lo;0;L;;;;;N;;;;; +110DC;SORA SOMPENG LETTER YAH;Lo;0;L;;;;;N;;;;; +110DD;SORA SOMPENG LETTER RAH;Lo;0;L;;;;;N;;;;; +110DE;SORA SOMPENG LETTER HAH;Lo;0;L;;;;;N;;;;; +110DF;SORA SOMPENG LETTER KAH;Lo;0;L;;;;;N;;;;; +110E0;SORA SOMPENG LETTER JAH;Lo;0;L;;;;;N;;;;; +110E1;SORA SOMPENG LETTER NYAH;Lo;0;L;;;;;N;;;;; +110E2;SORA SOMPENG LETTER AH;Lo;0;L;;;;;N;;;;; +110E3;SORA SOMPENG LETTER EEH;Lo;0;L;;;;;N;;;;; +110E4;SORA SOMPENG LETTER IH;Lo;0;L;;;;;N;;;;; +110E5;SORA SOMPENG LETTER UH;Lo;0;L;;;;;N;;;;; +110E6;SORA SOMPENG LETTER OH;Lo;0;L;;;;;N;;;;; +110E7;SORA SOMPENG LETTER EH;Lo;0;L;;;;;N;;;;; +110E8;SORA SOMPENG LETTER MAE;Lo;0;L;;;;;N;;;;; +110F0;SORA SOMPENG DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +110F1;SORA SOMPENG DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +110F2;SORA SOMPENG DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +110F3;SORA SOMPENG DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +110F4;SORA SOMPENG DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +110F5;SORA SOMPENG DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +110F6;SORA SOMPENG DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +110F7;SORA SOMPENG DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +110F8;SORA SOMPENG DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +110F9;SORA SOMPENG DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +11100;CHAKMA SIGN CANDRABINDU;Mn;230;NSM;;;;;N;;;;; +11101;CHAKMA SIGN ANUSVARA;Mn;230;NSM;;;;;N;;;;; +11102;CHAKMA SIGN VISARGA;Mn;230;NSM;;;;;N;;;;; +11103;CHAKMA LETTER AA;Lo;0;L;;;;;N;;;;; +11104;CHAKMA LETTER I;Lo;0;L;;;;;N;;;;; +11105;CHAKMA LETTER U;Lo;0;L;;;;;N;;;;; +11106;CHAKMA LETTER E;Lo;0;L;;;;;N;;;;; +11107;CHAKMA LETTER KAA;Lo;0;L;;;;;N;;;;; +11108;CHAKMA LETTER KHAA;Lo;0;L;;;;;N;;;;; +11109;CHAKMA LETTER GAA;Lo;0;L;;;;;N;;;;; +1110A;CHAKMA LETTER GHAA;Lo;0;L;;;;;N;;;;; +1110B;CHAKMA LETTER NGAA;Lo;0;L;;;;;N;;;;; +1110C;CHAKMA LETTER CAA;Lo;0;L;;;;;N;;;;; +1110D;CHAKMA LETTER CHAA;Lo;0;L;;;;;N;;;;; +1110E;CHAKMA LETTER JAA;Lo;0;L;;;;;N;;;;; +1110F;CHAKMA LETTER JHAA;Lo;0;L;;;;;N;;;;; +11110;CHAKMA LETTER NYAA;Lo;0;L;;;;;N;;;;; +11111;CHAKMA LETTER TTAA;Lo;0;L;;;;;N;;;;; +11112;CHAKMA LETTER TTHAA;Lo;0;L;;;;;N;;;;; +11113;CHAKMA LETTER DDAA;Lo;0;L;;;;;N;;;;; +11114;CHAKMA LETTER DDHAA;Lo;0;L;;;;;N;;;;; +11115;CHAKMA LETTER NNAA;Lo;0;L;;;;;N;;;;; +11116;CHAKMA LETTER TAA;Lo;0;L;;;;;N;;;;; +11117;CHAKMA LETTER THAA;Lo;0;L;;;;;N;;;;; +11118;CHAKMA LETTER DAA;Lo;0;L;;;;;N;;;;; +11119;CHAKMA LETTER DHAA;Lo;0;L;;;;;N;;;;; +1111A;CHAKMA LETTER NAA;Lo;0;L;;;;;N;;;;; +1111B;CHAKMA LETTER PAA;Lo;0;L;;;;;N;;;;; +1111C;CHAKMA LETTER PHAA;Lo;0;L;;;;;N;;;;; +1111D;CHAKMA LETTER BAA;Lo;0;L;;;;;N;;;;; +1111E;CHAKMA LETTER BHAA;Lo;0;L;;;;;N;;;;; +1111F;CHAKMA LETTER MAA;Lo;0;L;;;;;N;;;;; +11120;CHAKMA LETTER YYAA;Lo;0;L;;;;;N;;;;; +11121;CHAKMA LETTER YAA;Lo;0;L;;;;;N;;;;; +11122;CHAKMA LETTER RAA;Lo;0;L;;;;;N;;;;; +11123;CHAKMA LETTER LAA;Lo;0;L;;;;;N;;;;; +11124;CHAKMA LETTER WAA;Lo;0;L;;;;;N;;;;; +11125;CHAKMA LETTER SAA;Lo;0;L;;;;;N;;;;; +11126;CHAKMA LETTER HAA;Lo;0;L;;;;;N;;;;; +11127;CHAKMA VOWEL SIGN A;Mn;0;NSM;;;;;N;;;;; +11128;CHAKMA VOWEL SIGN I;Mn;0;NSM;;;;;N;;;;; +11129;CHAKMA VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;; +1112A;CHAKMA VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;; +1112B;CHAKMA VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;; +1112C;CHAKMA VOWEL SIGN E;Mc;0;L;;;;;N;;;;; +1112D;CHAKMA VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;; +1112E;CHAKMA VOWEL SIGN O;Mn;0;NSM;11131 11127;;;;N;;;;; +1112F;CHAKMA VOWEL SIGN AU;Mn;0;NSM;11132 11127;;;;N;;;;; +11130;CHAKMA VOWEL SIGN OI;Mn;0;NSM;;;;;N;;;;; +11131;CHAKMA O MARK;Mn;0;NSM;;;;;N;;;;; +11132;CHAKMA AU MARK;Mn;0;NSM;;;;;N;;;;; +11133;CHAKMA VIRAMA;Mn;9;NSM;;;;;N;;;;; +11134;CHAKMA MAAYYAA;Mn;9;NSM;;;;;N;;;;; +11136;CHAKMA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +11137;CHAKMA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +11138;CHAKMA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +11139;CHAKMA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +1113A;CHAKMA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +1113B;CHAKMA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +1113C;CHAKMA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +1113D;CHAKMA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +1113E;CHAKMA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +1113F;CHAKMA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +11140;CHAKMA SECTION MARK;Po;0;L;;;;;N;;;;; +11141;CHAKMA DANDA;Po;0;L;;;;;N;;;;; +11142;CHAKMA DOUBLE DANDA;Po;0;L;;;;;N;;;;; +11143;CHAKMA QUESTION MARK;Po;0;L;;;;;N;;;;; +11150;MAHAJANI LETTER A;Lo;0;L;;;;;N;;;;; +11151;MAHAJANI LETTER I;Lo;0;L;;;;;N;;;;; +11152;MAHAJANI LETTER U;Lo;0;L;;;;;N;;;;; +11153;MAHAJANI LETTER E;Lo;0;L;;;;;N;;;;; +11154;MAHAJANI LETTER O;Lo;0;L;;;;;N;;;;; +11155;MAHAJANI LETTER KA;Lo;0;L;;;;;N;;;;; +11156;MAHAJANI LETTER KHA;Lo;0;L;;;;;N;;;;; +11157;MAHAJANI LETTER GA;Lo;0;L;;;;;N;;;;; +11158;MAHAJANI LETTER GHA;Lo;0;L;;;;;N;;;;; +11159;MAHAJANI LETTER CA;Lo;0;L;;;;;N;;;;; +1115A;MAHAJANI LETTER CHA;Lo;0;L;;;;;N;;;;; +1115B;MAHAJANI LETTER JA;Lo;0;L;;;;;N;;;;; +1115C;MAHAJANI LETTER JHA;Lo;0;L;;;;;N;;;;; +1115D;MAHAJANI LETTER NYA;Lo;0;L;;;;;N;;;;; +1115E;MAHAJANI LETTER TTA;Lo;0;L;;;;;N;;;;; +1115F;MAHAJANI LETTER TTHA;Lo;0;L;;;;;N;;;;; +11160;MAHAJANI LETTER DDA;Lo;0;L;;;;;N;;;;; +11161;MAHAJANI LETTER DDHA;Lo;0;L;;;;;N;;;;; +11162;MAHAJANI LETTER NNA;Lo;0;L;;;;;N;;;;; +11163;MAHAJANI LETTER TA;Lo;0;L;;;;;N;;;;; +11164;MAHAJANI LETTER THA;Lo;0;L;;;;;N;;;;; +11165;MAHAJANI LETTER DA;Lo;0;L;;;;;N;;;;; +11166;MAHAJANI LETTER DHA;Lo;0;L;;;;;N;;;;; +11167;MAHAJANI LETTER NA;Lo;0;L;;;;;N;;;;; +11168;MAHAJANI LETTER PA;Lo;0;L;;;;;N;;;;; +11169;MAHAJANI LETTER PHA;Lo;0;L;;;;;N;;;;; +1116A;MAHAJANI LETTER BA;Lo;0;L;;;;;N;;;;; +1116B;MAHAJANI LETTER BHA;Lo;0;L;;;;;N;;;;; +1116C;MAHAJANI LETTER MA;Lo;0;L;;;;;N;;;;; +1116D;MAHAJANI LETTER RA;Lo;0;L;;;;;N;;;;; +1116E;MAHAJANI LETTER LA;Lo;0;L;;;;;N;;;;; +1116F;MAHAJANI LETTER VA;Lo;0;L;;;;;N;;;;; +11170;MAHAJANI LETTER SA;Lo;0;L;;;;;N;;;;; +11171;MAHAJANI LETTER HA;Lo;0;L;;;;;N;;;;; +11172;MAHAJANI LETTER RRA;Lo;0;L;;;;;N;;;;; +11173;MAHAJANI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;; +11174;MAHAJANI ABBREVIATION SIGN;Po;0;L;;;;;N;;;;; +11175;MAHAJANI SECTION MARK;Po;0;L;;;;;N;;;;; +11176;MAHAJANI LIGATURE SHRI;Lo;0;L;;;;;N;;;;; +11180;SHARADA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;; +11181;SHARADA SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;; +11182;SHARADA SIGN VISARGA;Mc;0;L;;;;;N;;;;; +11183;SHARADA LETTER A;Lo;0;L;;;;;N;;;;; +11184;SHARADA LETTER AA;Lo;0;L;;;;;N;;;;; +11185;SHARADA LETTER I;Lo;0;L;;;;;N;;;;; +11186;SHARADA LETTER II;Lo;0;L;;;;;N;;;;; +11187;SHARADA LETTER U;Lo;0;L;;;;;N;;;;; +11188;SHARADA LETTER UU;Lo;0;L;;;;;N;;;;; +11189;SHARADA LETTER VOCALIC R;Lo;0;L;;;;;N;;;;; +1118A;SHARADA LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;; +1118B;SHARADA LETTER VOCALIC L;Lo;0;L;;;;;N;;;;; +1118C;SHARADA LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;; +1118D;SHARADA LETTER E;Lo;0;L;;;;;N;;;;; +1118E;SHARADA LETTER AI;Lo;0;L;;;;;N;;;;; +1118F;SHARADA LETTER O;Lo;0;L;;;;;N;;;;; +11190;SHARADA LETTER AU;Lo;0;L;;;;;N;;;;; +11191;SHARADA LETTER KA;Lo;0;L;;;;;N;;;;; +11192;SHARADA LETTER KHA;Lo;0;L;;;;;N;;;;; +11193;SHARADA LETTER GA;Lo;0;L;;;;;N;;;;; +11194;SHARADA LETTER GHA;Lo;0;L;;;;;N;;;;; +11195;SHARADA LETTER NGA;Lo;0;L;;;;;N;;;;; +11196;SHARADA LETTER CA;Lo;0;L;;;;;N;;;;; +11197;SHARADA LETTER CHA;Lo;0;L;;;;;N;;;;; +11198;SHARADA LETTER JA;Lo;0;L;;;;;N;;;;; +11199;SHARADA LETTER JHA;Lo;0;L;;;;;N;;;;; +1119A;SHARADA LETTER NYA;Lo;0;L;;;;;N;;;;; +1119B;SHARADA LETTER TTA;Lo;0;L;;;;;N;;;;; +1119C;SHARADA LETTER TTHA;Lo;0;L;;;;;N;;;;; +1119D;SHARADA LETTER DDA;Lo;0;L;;;;;N;;;;; +1119E;SHARADA LETTER DDHA;Lo;0;L;;;;;N;;;;; +1119F;SHARADA LETTER NNA;Lo;0;L;;;;;N;;;;; +111A0;SHARADA LETTER TA;Lo;0;L;;;;;N;;;;; +111A1;SHARADA LETTER THA;Lo;0;L;;;;;N;;;;; +111A2;SHARADA LETTER DA;Lo;0;L;;;;;N;;;;; +111A3;SHARADA LETTER DHA;Lo;0;L;;;;;N;;;;; +111A4;SHARADA LETTER NA;Lo;0;L;;;;;N;;;;; +111A5;SHARADA LETTER PA;Lo;0;L;;;;;N;;;;; +111A6;SHARADA LETTER PHA;Lo;0;L;;;;;N;;;;; +111A7;SHARADA LETTER BA;Lo;0;L;;;;;N;;;;; +111A8;SHARADA LETTER BHA;Lo;0;L;;;;;N;;;;; +111A9;SHARADA LETTER MA;Lo;0;L;;;;;N;;;;; +111AA;SHARADA LETTER YA;Lo;0;L;;;;;N;;;;; +111AB;SHARADA LETTER RA;Lo;0;L;;;;;N;;;;; +111AC;SHARADA LETTER LA;Lo;0;L;;;;;N;;;;; +111AD;SHARADA LETTER LLA;Lo;0;L;;;;;N;;;;; +111AE;SHARADA LETTER VA;Lo;0;L;;;;;N;;;;; +111AF;SHARADA LETTER SHA;Lo;0;L;;;;;N;;;;; +111B0;SHARADA LETTER SSA;Lo;0;L;;;;;N;;;;; +111B1;SHARADA LETTER SA;Lo;0;L;;;;;N;;;;; +111B2;SHARADA LETTER HA;Lo;0;L;;;;;N;;;;; +111B3;SHARADA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;; +111B4;SHARADA VOWEL SIGN I;Mc;0;L;;;;;N;;;;; +111B5;SHARADA VOWEL SIGN II;Mc;0;L;;;;;N;;;;; +111B6;SHARADA VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;; +111B7;SHARADA VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;; +111B8;SHARADA VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;; +111B9;SHARADA VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;; +111BA;SHARADA VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;; +111BB;SHARADA VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;; +111BC;SHARADA VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;; +111BD;SHARADA VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;; +111BE;SHARADA VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;; +111BF;SHARADA VOWEL SIGN AU;Mc;0;L;;;;;N;;;;; +111C0;SHARADA SIGN VIRAMA;Mc;9;L;;;;;N;;;;; +111C1;SHARADA SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;; +111C2;SHARADA SIGN JIHVAMULIYA;Lo;0;L;;;;;N;;;;; +111C3;SHARADA SIGN UPADHMANIYA;Lo;0;L;;;;;N;;;;; +111C4;SHARADA OM;Lo;0;L;;;;;N;;;;; +111C5;SHARADA DANDA;Po;0;L;;;;;N;;;;; +111C6;SHARADA DOUBLE DANDA;Po;0;L;;;;;N;;;;; +111C7;SHARADA ABBREVIATION SIGN;Po;0;L;;;;;N;;;;; +111C8;SHARADA SEPARATOR;Po;0;L;;;;;N;;;;; +111CD;SHARADA SUTRA MARK;Po;0;L;;;;;N;;;;; +111D0;SHARADA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +111D1;SHARADA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +111D2;SHARADA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +111D3;SHARADA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +111D4;SHARADA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +111D5;SHARADA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +111D6;SHARADA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +111D7;SHARADA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +111D8;SHARADA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +111D9;SHARADA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +111DA;SHARADA EKAM;Lo;0;L;;;;;N;;;;; +111E1;SINHALA ARCHAIC DIGIT ONE;No;0;L;;;;1;N;;;;; +111E2;SINHALA ARCHAIC DIGIT TWO;No;0;L;;;;2;N;;;;; +111E3;SINHALA ARCHAIC DIGIT THREE;No;0;L;;;;3;N;;;;; +111E4;SINHALA ARCHAIC DIGIT FOUR;No;0;L;;;;4;N;;;;; +111E5;SINHALA ARCHAIC DIGIT FIVE;No;0;L;;;;5;N;;;;; +111E6;SINHALA ARCHAIC DIGIT SIX;No;0;L;;;;6;N;;;;; +111E7;SINHALA ARCHAIC DIGIT SEVEN;No;0;L;;;;7;N;;;;; +111E8;SINHALA ARCHAIC DIGIT EIGHT;No;0;L;;;;8;N;;;;; +111E9;SINHALA ARCHAIC DIGIT NINE;No;0;L;;;;9;N;;;;; +111EA;SINHALA ARCHAIC NUMBER TEN;No;0;L;;;;10;N;;;;; +111EB;SINHALA ARCHAIC NUMBER TWENTY;No;0;L;;;;20;N;;;;; +111EC;SINHALA ARCHAIC NUMBER THIRTY;No;0;L;;;;30;N;;;;; +111ED;SINHALA ARCHAIC NUMBER FORTY;No;0;L;;;;40;N;;;;; +111EE;SINHALA ARCHAIC NUMBER FIFTY;No;0;L;;;;50;N;;;;; +111EF;SINHALA ARCHAIC NUMBER SIXTY;No;0;L;;;;60;N;;;;; +111F0;SINHALA ARCHAIC NUMBER SEVENTY;No;0;L;;;;70;N;;;;; +111F1;SINHALA ARCHAIC NUMBER EIGHTY;No;0;L;;;;80;N;;;;; +111F2;SINHALA ARCHAIC NUMBER NINETY;No;0;L;;;;90;N;;;;; +111F3;SINHALA ARCHAIC NUMBER ONE HUNDRED;No;0;L;;;;100;N;;;;; +111F4;SINHALA ARCHAIC NUMBER ONE THOUSAND;No;0;L;;;;1000;N;;;;; +11200;KHOJKI LETTER A;Lo;0;L;;;;;N;;;;; +11201;KHOJKI LETTER AA;Lo;0;L;;;;;N;;;;; +11202;KHOJKI LETTER I;Lo;0;L;;;;;N;;;;; +11203;KHOJKI LETTER U;Lo;0;L;;;;;N;;;;; +11204;KHOJKI LETTER E;Lo;0;L;;;;;N;;;;; +11205;KHOJKI LETTER AI;Lo;0;L;;;;;N;;;;; +11206;KHOJKI LETTER O;Lo;0;L;;;;;N;;;;; +11207;KHOJKI LETTER AU;Lo;0;L;;;;;N;;;;; +11208;KHOJKI LETTER KA;Lo;0;L;;;;;N;;;;; +11209;KHOJKI LETTER KHA;Lo;0;L;;;;;N;;;;; +1120A;KHOJKI LETTER GA;Lo;0;L;;;;;N;;;;; +1120B;KHOJKI LETTER GGA;Lo;0;L;;;;;N;;;;; +1120C;KHOJKI LETTER GHA;Lo;0;L;;;;;N;;;;; +1120D;KHOJKI LETTER NGA;Lo;0;L;;;;;N;;;;; +1120E;KHOJKI LETTER CA;Lo;0;L;;;;;N;;;;; +1120F;KHOJKI LETTER CHA;Lo;0;L;;;;;N;;;;; +11210;KHOJKI LETTER JA;Lo;0;L;;;;;N;;;;; +11211;KHOJKI LETTER JJA;Lo;0;L;;;;;N;;;;; +11213;KHOJKI LETTER NYA;Lo;0;L;;;;;N;;;;; +11214;KHOJKI LETTER TTA;Lo;0;L;;;;;N;;;;; +11215;KHOJKI LETTER TTHA;Lo;0;L;;;;;N;;;;; +11216;KHOJKI LETTER DDA;Lo;0;L;;;;;N;;;;; +11217;KHOJKI LETTER DDHA;Lo;0;L;;;;;N;;;;; +11218;KHOJKI LETTER NNA;Lo;0;L;;;;;N;;;;; +11219;KHOJKI LETTER TA;Lo;0;L;;;;;N;;;;; +1121A;KHOJKI LETTER THA;Lo;0;L;;;;;N;;;;; +1121B;KHOJKI LETTER DA;Lo;0;L;;;;;N;;;;; +1121C;KHOJKI LETTER DDDA;Lo;0;L;;;;;N;;;;; +1121D;KHOJKI LETTER DHA;Lo;0;L;;;;;N;;;;; +1121E;KHOJKI LETTER NA;Lo;0;L;;;;;N;;;;; +1121F;KHOJKI LETTER PA;Lo;0;L;;;;;N;;;;; +11220;KHOJKI LETTER PHA;Lo;0;L;;;;;N;;;;; +11221;KHOJKI LETTER BA;Lo;0;L;;;;;N;;;;; +11222;KHOJKI LETTER BBA;Lo;0;L;;;;;N;;;;; +11223;KHOJKI LETTER BHA;Lo;0;L;;;;;N;;;;; +11224;KHOJKI LETTER MA;Lo;0;L;;;;;N;;;;; +11225;KHOJKI LETTER YA;Lo;0;L;;;;;N;;;;; +11226;KHOJKI LETTER RA;Lo;0;L;;;;;N;;;;; +11227;KHOJKI LETTER LA;Lo;0;L;;;;;N;;;;; +11228;KHOJKI LETTER VA;Lo;0;L;;;;;N;;;;; +11229;KHOJKI LETTER SA;Lo;0;L;;;;;N;;;;; +1122A;KHOJKI LETTER HA;Lo;0;L;;;;;N;;;;; +1122B;KHOJKI LETTER LLA;Lo;0;L;;;;;N;;;;; +1122C;KHOJKI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;; +1122D;KHOJKI VOWEL SIGN I;Mc;0;L;;;;;N;;;;; +1122E;KHOJKI VOWEL SIGN II;Mc;0;L;;;;;N;;;;; +1122F;KHOJKI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;; +11230;KHOJKI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;; +11231;KHOJKI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;; +11232;KHOJKI VOWEL SIGN O;Mc;0;L;;;;;N;;;;; +11233;KHOJKI VOWEL SIGN AU;Mc;0;L;;;;;N;;;;; +11234;KHOJKI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;; +11235;KHOJKI SIGN VIRAMA;Mc;9;L;;;;;N;;;;; +11236;KHOJKI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;; +11237;KHOJKI SIGN SHADDA;Mn;0;NSM;;;;;N;;;;; +11238;KHOJKI DANDA;Po;0;L;;;;;N;;;;; +11239;KHOJKI DOUBLE DANDA;Po;0;L;;;;;N;;;;; +1123A;KHOJKI WORD SEPARATOR;Po;0;L;;;;;N;;;;; +1123B;KHOJKI SECTION MARK;Po;0;L;;;;;N;;;;; +1123C;KHOJKI DOUBLE SECTION MARK;Po;0;L;;;;;N;;;;; +1123D;KHOJKI ABBREVIATION SIGN;Po;0;L;;;;;N;;;;; +112B0;KHUDAWADI LETTER A;Lo;0;L;;;;;N;;;;; +112B1;KHUDAWADI LETTER AA;Lo;0;L;;;;;N;;;;; +112B2;KHUDAWADI LETTER I;Lo;0;L;;;;;N;;;;; +112B3;KHUDAWADI LETTER II;Lo;0;L;;;;;N;;;;; +112B4;KHUDAWADI LETTER U;Lo;0;L;;;;;N;;;;; +112B5;KHUDAWADI LETTER UU;Lo;0;L;;;;;N;;;;; +112B6;KHUDAWADI LETTER E;Lo;0;L;;;;;N;;;;; +112B7;KHUDAWADI LETTER AI;Lo;0;L;;;;;N;;;;; +112B8;KHUDAWADI LETTER O;Lo;0;L;;;;;N;;;;; +112B9;KHUDAWADI LETTER AU;Lo;0;L;;;;;N;;;;; +112BA;KHUDAWADI LETTER KA;Lo;0;L;;;;;N;;;;; +112BB;KHUDAWADI LETTER KHA;Lo;0;L;;;;;N;;;;; +112BC;KHUDAWADI LETTER GA;Lo;0;L;;;;;N;;;;; +112BD;KHUDAWADI LETTER GGA;Lo;0;L;;;;;N;;;;; +112BE;KHUDAWADI LETTER GHA;Lo;0;L;;;;;N;;;;; +112BF;KHUDAWADI LETTER NGA;Lo;0;L;;;;;N;;;;; +112C0;KHUDAWADI LETTER CA;Lo;0;L;;;;;N;;;;; +112C1;KHUDAWADI LETTER CHA;Lo;0;L;;;;;N;;;;; +112C2;KHUDAWADI LETTER JA;Lo;0;L;;;;;N;;;;; +112C3;KHUDAWADI LETTER JJA;Lo;0;L;;;;;N;;;;; +112C4;KHUDAWADI LETTER JHA;Lo;0;L;;;;;N;;;;; +112C5;KHUDAWADI LETTER NYA;Lo;0;L;;;;;N;;;;; +112C6;KHUDAWADI LETTER TTA;Lo;0;L;;;;;N;;;;; +112C7;KHUDAWADI LETTER TTHA;Lo;0;L;;;;;N;;;;; +112C8;KHUDAWADI LETTER DDA;Lo;0;L;;;;;N;;;;; +112C9;KHUDAWADI LETTER DDDA;Lo;0;L;;;;;N;;;;; +112CA;KHUDAWADI LETTER RRA;Lo;0;L;;;;;N;;;;; +112CB;KHUDAWADI LETTER DDHA;Lo;0;L;;;;;N;;;;; +112CC;KHUDAWADI LETTER NNA;Lo;0;L;;;;;N;;;;; +112CD;KHUDAWADI LETTER TA;Lo;0;L;;;;;N;;;;; +112CE;KHUDAWADI LETTER THA;Lo;0;L;;;;;N;;;;; +112CF;KHUDAWADI LETTER DA;Lo;0;L;;;;;N;;;;; +112D0;KHUDAWADI LETTER DHA;Lo;0;L;;;;;N;;;;; +112D1;KHUDAWADI LETTER NA;Lo;0;L;;;;;N;;;;; +112D2;KHUDAWADI LETTER PA;Lo;0;L;;;;;N;;;;; +112D3;KHUDAWADI LETTER PHA;Lo;0;L;;;;;N;;;;; +112D4;KHUDAWADI LETTER BA;Lo;0;L;;;;;N;;;;; +112D5;KHUDAWADI LETTER BBA;Lo;0;L;;;;;N;;;;; +112D6;KHUDAWADI LETTER BHA;Lo;0;L;;;;;N;;;;; +112D7;KHUDAWADI LETTER MA;Lo;0;L;;;;;N;;;;; +112D8;KHUDAWADI LETTER YA;Lo;0;L;;;;;N;;;;; +112D9;KHUDAWADI LETTER RA;Lo;0;L;;;;;N;;;;; +112DA;KHUDAWADI LETTER LA;Lo;0;L;;;;;N;;;;; +112DB;KHUDAWADI LETTER VA;Lo;0;L;;;;;N;;;;; +112DC;KHUDAWADI LETTER SHA;Lo;0;L;;;;;N;;;;; +112DD;KHUDAWADI LETTER SA;Lo;0;L;;;;;N;;;;; +112DE;KHUDAWADI LETTER HA;Lo;0;L;;;;;N;;;;; +112DF;KHUDAWADI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;; +112E0;KHUDAWADI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;; +112E1;KHUDAWADI VOWEL SIGN I;Mc;0;L;;;;;N;;;;; +112E2;KHUDAWADI VOWEL SIGN II;Mc;0;L;;;;;N;;;;; +112E3;KHUDAWADI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;; +112E4;KHUDAWADI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;; +112E5;KHUDAWADI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;; +112E6;KHUDAWADI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;; +112E7;KHUDAWADI VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;; +112E8;KHUDAWADI VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;; +112E9;KHUDAWADI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;; +112EA;KHUDAWADI SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;; +112F0;KHUDAWADI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +112F1;KHUDAWADI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +112F2;KHUDAWADI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +112F3;KHUDAWADI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +112F4;KHUDAWADI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +112F5;KHUDAWADI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +112F6;KHUDAWADI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +112F7;KHUDAWADI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +112F8;KHUDAWADI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +112F9;KHUDAWADI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +11301;GRANTHA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;; +11302;GRANTHA SIGN ANUSVARA;Mc;0;L;;;;;N;;;;; +11303;GRANTHA SIGN VISARGA;Mc;0;L;;;;;N;;;;; +11305;GRANTHA LETTER A;Lo;0;L;;;;;N;;;;; +11306;GRANTHA LETTER AA;Lo;0;L;;;;;N;;;;; +11307;GRANTHA LETTER I;Lo;0;L;;;;;N;;;;; +11308;GRANTHA LETTER II;Lo;0;L;;;;;N;;;;; +11309;GRANTHA LETTER U;Lo;0;L;;;;;N;;;;; +1130A;GRANTHA LETTER UU;Lo;0;L;;;;;N;;;;; +1130B;GRANTHA LETTER VOCALIC R;Lo;0;L;;;;;N;;;;; +1130C;GRANTHA LETTER VOCALIC L;Lo;0;L;;;;;N;;;;; +1130F;GRANTHA LETTER EE;Lo;0;L;;;;;N;;;;; +11310;GRANTHA LETTER AI;Lo;0;L;;;;;N;;;;; +11313;GRANTHA LETTER OO;Lo;0;L;;;;;N;;;;; +11314;GRANTHA LETTER AU;Lo;0;L;;;;;N;;;;; +11315;GRANTHA LETTER KA;Lo;0;L;;;;;N;;;;; +11316;GRANTHA LETTER KHA;Lo;0;L;;;;;N;;;;; +11317;GRANTHA LETTER GA;Lo;0;L;;;;;N;;;;; +11318;GRANTHA LETTER GHA;Lo;0;L;;;;;N;;;;; +11319;GRANTHA LETTER NGA;Lo;0;L;;;;;N;;;;; +1131A;GRANTHA LETTER CA;Lo;0;L;;;;;N;;;;; +1131B;GRANTHA LETTER CHA;Lo;0;L;;;;;N;;;;; +1131C;GRANTHA LETTER JA;Lo;0;L;;;;;N;;;;; +1131D;GRANTHA LETTER JHA;Lo;0;L;;;;;N;;;;; +1131E;GRANTHA LETTER NYA;Lo;0;L;;;;;N;;;;; +1131F;GRANTHA LETTER TTA;Lo;0;L;;;;;N;;;;; +11320;GRANTHA LETTER TTHA;Lo;0;L;;;;;N;;;;; +11321;GRANTHA LETTER DDA;Lo;0;L;;;;;N;;;;; +11322;GRANTHA LETTER DDHA;Lo;0;L;;;;;N;;;;; +11323;GRANTHA LETTER NNA;Lo;0;L;;;;;N;;;;; +11324;GRANTHA LETTER TA;Lo;0;L;;;;;N;;;;; +11325;GRANTHA LETTER THA;Lo;0;L;;;;;N;;;;; +11326;GRANTHA LETTER DA;Lo;0;L;;;;;N;;;;; +11327;GRANTHA LETTER DHA;Lo;0;L;;;;;N;;;;; +11328;GRANTHA LETTER NA;Lo;0;L;;;;;N;;;;; +1132A;GRANTHA LETTER PA;Lo;0;L;;;;;N;;;;; +1132B;GRANTHA LETTER PHA;Lo;0;L;;;;;N;;;;; +1132C;GRANTHA LETTER BA;Lo;0;L;;;;;N;;;;; +1132D;GRANTHA LETTER BHA;Lo;0;L;;;;;N;;;;; +1132E;GRANTHA LETTER MA;Lo;0;L;;;;;N;;;;; +1132F;GRANTHA LETTER YA;Lo;0;L;;;;;N;;;;; +11330;GRANTHA LETTER RA;Lo;0;L;;;;;N;;;;; +11332;GRANTHA LETTER LA;Lo;0;L;;;;;N;;;;; +11333;GRANTHA LETTER LLA;Lo;0;L;;;;;N;;;;; +11335;GRANTHA LETTER VA;Lo;0;L;;;;;N;;;;; +11336;GRANTHA LETTER SHA;Lo;0;L;;;;;N;;;;; +11337;GRANTHA LETTER SSA;Lo;0;L;;;;;N;;;;; +11338;GRANTHA LETTER SA;Lo;0;L;;;;;N;;;;; +11339;GRANTHA LETTER HA;Lo;0;L;;;;;N;;;;; +1133C;GRANTHA SIGN NUKTA;Mn;7;NSM;;;;;N;;;;; +1133D;GRANTHA SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;; +1133E;GRANTHA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;; +1133F;GRANTHA VOWEL SIGN I;Mc;0;L;;;;;N;;;;; +11340;GRANTHA VOWEL SIGN II;Mn;0;NSM;;;;;N;;;;; +11341;GRANTHA VOWEL SIGN U;Mc;0;L;;;;;N;;;;; +11342;GRANTHA VOWEL SIGN UU;Mc;0;L;;;;;N;;;;; +11343;GRANTHA VOWEL SIGN VOCALIC R;Mc;0;L;;;;;N;;;;; +11344;GRANTHA VOWEL SIGN VOCALIC RR;Mc;0;L;;;;;N;;;;; +11347;GRANTHA VOWEL SIGN EE;Mc;0;L;;;;;N;;;;; +11348;GRANTHA VOWEL SIGN AI;Mc;0;L;;;;;N;;;;; +1134B;GRANTHA VOWEL SIGN OO;Mc;0;L;11347 1133E;;;;N;;;;; +1134C;GRANTHA VOWEL SIGN AU;Mc;0;L;11347 11357;;;;N;;;;; +1134D;GRANTHA SIGN VIRAMA;Mc;9;L;;;;;N;;;;; +11357;GRANTHA AU LENGTH MARK;Mc;0;L;;;;;N;;;;; +1135D;GRANTHA SIGN PLUTA;Lo;0;L;;;;;N;;;;; +1135E;GRANTHA LETTER VEDIC ANUSVARA;Lo;0;L;;;;;N;;;;; +1135F;GRANTHA LETTER VEDIC DOUBLE ANUSVARA;Lo;0;L;;;;;N;;;;; +11360;GRANTHA LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;; +11361;GRANTHA LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;; +11362;GRANTHA VOWEL SIGN VOCALIC L;Mc;0;L;;;;;N;;;;; +11363;GRANTHA VOWEL SIGN VOCALIC LL;Mc;0;L;;;;;N;;;;; +11366;COMBINING GRANTHA DIGIT ZERO;Mn;230;NSM;;;;;N;;;;; +11367;COMBINING GRANTHA DIGIT ONE;Mn;230;NSM;;;;;N;;;;; +11368;COMBINING GRANTHA DIGIT TWO;Mn;230;NSM;;;;;N;;;;; +11369;COMBINING GRANTHA DIGIT THREE;Mn;230;NSM;;;;;N;;;;; +1136A;COMBINING GRANTHA DIGIT FOUR;Mn;230;NSM;;;;;N;;;;; +1136B;COMBINING GRANTHA DIGIT FIVE;Mn;230;NSM;;;;;N;;;;; +1136C;COMBINING GRANTHA DIGIT SIX;Mn;230;NSM;;;;;N;;;;; +11370;COMBINING GRANTHA LETTER A;Mn;230;NSM;;;;;N;;;;; +11371;COMBINING GRANTHA LETTER KA;Mn;230;NSM;;;;;N;;;;; +11372;COMBINING GRANTHA LETTER NA;Mn;230;NSM;;;;;N;;;;; +11373;COMBINING GRANTHA LETTER VI;Mn;230;NSM;;;;;N;;;;; +11374;COMBINING GRANTHA LETTER PA;Mn;230;NSM;;;;;N;;;;; +11480;TIRHUTA ANJI;Lo;0;L;;;;;N;;;;; +11481;TIRHUTA LETTER A;Lo;0;L;;;;;N;;;;; +11482;TIRHUTA LETTER AA;Lo;0;L;;;;;N;;;;; +11483;TIRHUTA LETTER I;Lo;0;L;;;;;N;;;;; +11484;TIRHUTA LETTER II;Lo;0;L;;;;;N;;;;; +11485;TIRHUTA LETTER U;Lo;0;L;;;;;N;;;;; +11486;TIRHUTA LETTER UU;Lo;0;L;;;;;N;;;;; +11487;TIRHUTA LETTER VOCALIC R;Lo;0;L;;;;;N;;;;; +11488;TIRHUTA LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;; +11489;TIRHUTA LETTER VOCALIC L;Lo;0;L;;;;;N;;;;; +1148A;TIRHUTA LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;; +1148B;TIRHUTA LETTER E;Lo;0;L;;;;;N;;;;; +1148C;TIRHUTA LETTER AI;Lo;0;L;;;;;N;;;;; +1148D;TIRHUTA LETTER O;Lo;0;L;;;;;N;;;;; +1148E;TIRHUTA LETTER AU;Lo;0;L;;;;;N;;;;; +1148F;TIRHUTA LETTER KA;Lo;0;L;;;;;N;;;;; +11490;TIRHUTA LETTER KHA;Lo;0;L;;;;;N;;;;; +11491;TIRHUTA LETTER GA;Lo;0;L;;;;;N;;;;; +11492;TIRHUTA LETTER GHA;Lo;0;L;;;;;N;;;;; +11493;TIRHUTA LETTER NGA;Lo;0;L;;;;;N;;;;; +11494;TIRHUTA LETTER CA;Lo;0;L;;;;;N;;;;; +11495;TIRHUTA LETTER CHA;Lo;0;L;;;;;N;;;;; +11496;TIRHUTA LETTER JA;Lo;0;L;;;;;N;;;;; +11497;TIRHUTA LETTER JHA;Lo;0;L;;;;;N;;;;; +11498;TIRHUTA LETTER NYA;Lo;0;L;;;;;N;;;;; +11499;TIRHUTA LETTER TTA;Lo;0;L;;;;;N;;;;; +1149A;TIRHUTA LETTER TTHA;Lo;0;L;;;;;N;;;;; +1149B;TIRHUTA LETTER DDA;Lo;0;L;;;;;N;;;;; +1149C;TIRHUTA LETTER DDHA;Lo;0;L;;;;;N;;;;; +1149D;TIRHUTA LETTER NNA;Lo;0;L;;;;;N;;;;; +1149E;TIRHUTA LETTER TA;Lo;0;L;;;;;N;;;;; +1149F;TIRHUTA LETTER THA;Lo;0;L;;;;;N;;;;; +114A0;TIRHUTA LETTER DA;Lo;0;L;;;;;N;;;;; +114A1;TIRHUTA LETTER DHA;Lo;0;L;;;;;N;;;;; +114A2;TIRHUTA LETTER NA;Lo;0;L;;;;;N;;;;; +114A3;TIRHUTA LETTER PA;Lo;0;L;;;;;N;;;;; +114A4;TIRHUTA LETTER PHA;Lo;0;L;;;;;N;;;;; +114A5;TIRHUTA LETTER BA;Lo;0;L;;;;;N;;;;; +114A6;TIRHUTA LETTER BHA;Lo;0;L;;;;;N;;;;; +114A7;TIRHUTA LETTER MA;Lo;0;L;;;;;N;;;;; +114A8;TIRHUTA LETTER YA;Lo;0;L;;;;;N;;;;; +114A9;TIRHUTA LETTER RA;Lo;0;L;;;;;N;;;;; +114AA;TIRHUTA LETTER LA;Lo;0;L;;;;;N;;;;; +114AB;TIRHUTA LETTER VA;Lo;0;L;;;;;N;;;;; +114AC;TIRHUTA LETTER SHA;Lo;0;L;;;;;N;;;;; +114AD;TIRHUTA LETTER SSA;Lo;0;L;;;;;N;;;;; +114AE;TIRHUTA LETTER SA;Lo;0;L;;;;;N;;;;; +114AF;TIRHUTA LETTER HA;Lo;0;L;;;;;N;;;;; +114B0;TIRHUTA VOWEL SIGN AA;Mc;0;L;;;;;N;;;;; +114B1;TIRHUTA VOWEL SIGN I;Mc;0;L;;;;;N;;;;; +114B2;TIRHUTA VOWEL SIGN II;Mc;0;L;;;;;N;;;;; +114B3;TIRHUTA VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;; +114B4;TIRHUTA VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;; +114B5;TIRHUTA VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;; +114B6;TIRHUTA VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;; +114B7;TIRHUTA VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;; +114B8;TIRHUTA VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;; +114B9;TIRHUTA VOWEL SIGN E;Mc;0;L;;;;;N;;;;; +114BA;TIRHUTA VOWEL SIGN SHORT E;Mn;0;NSM;;;;;N;;;;; +114BB;TIRHUTA VOWEL SIGN AI;Mc;0;L;114B9 114BA;;;;N;;;;; +114BC;TIRHUTA VOWEL SIGN O;Mc;0;L;114B9 114B0;;;;N;;;;; +114BD;TIRHUTA VOWEL SIGN SHORT O;Mc;0;L;;;;;N;;;;; +114BE;TIRHUTA VOWEL SIGN AU;Mc;0;L;114B9 114BD;;;;N;;;;; +114BF;TIRHUTA SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;; +114C0;TIRHUTA SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;; +114C1;TIRHUTA SIGN VISARGA;Mc;0;L;;;;;N;;;;; +114C2;TIRHUTA SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;; +114C3;TIRHUTA SIGN NUKTA;Mn;7;NSM;;;;;N;;;;; +114C4;TIRHUTA SIGN AVAGRAHA;Lo;0;L;;;;;N;;;;; +114C5;TIRHUTA GVANG;Lo;0;L;;;;;N;;;;; +114C6;TIRHUTA ABBREVIATION SIGN;Po;0;L;;;;;N;;;;; +114C7;TIRHUTA OM;Lo;0;L;;;;;N;;;;; +114D0;TIRHUTA DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +114D1;TIRHUTA DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +114D2;TIRHUTA DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +114D3;TIRHUTA DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +114D4;TIRHUTA DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +114D5;TIRHUTA DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +114D6;TIRHUTA DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +114D7;TIRHUTA DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +114D8;TIRHUTA DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +114D9;TIRHUTA DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +11580;SIDDHAM LETTER A;Lo;0;L;;;;;N;;;;; +11581;SIDDHAM LETTER AA;Lo;0;L;;;;;N;;;;; +11582;SIDDHAM LETTER I;Lo;0;L;;;;;N;;;;; +11583;SIDDHAM LETTER II;Lo;0;L;;;;;N;;;;; +11584;SIDDHAM LETTER U;Lo;0;L;;;;;N;;;;; +11585;SIDDHAM LETTER UU;Lo;0;L;;;;;N;;;;; +11586;SIDDHAM LETTER VOCALIC R;Lo;0;L;;;;;N;;;;; +11587;SIDDHAM LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;; +11588;SIDDHAM LETTER VOCALIC L;Lo;0;L;;;;;N;;;;; +11589;SIDDHAM LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;; +1158A;SIDDHAM LETTER E;Lo;0;L;;;;;N;;;;; +1158B;SIDDHAM LETTER AI;Lo;0;L;;;;;N;;;;; +1158C;SIDDHAM LETTER O;Lo;0;L;;;;;N;;;;; +1158D;SIDDHAM LETTER AU;Lo;0;L;;;;;N;;;;; +1158E;SIDDHAM LETTER KA;Lo;0;L;;;;;N;;;;; +1158F;SIDDHAM LETTER KHA;Lo;0;L;;;;;N;;;;; +11590;SIDDHAM LETTER GA;Lo;0;L;;;;;N;;;;; +11591;SIDDHAM LETTER GHA;Lo;0;L;;;;;N;;;;; +11592;SIDDHAM LETTER NGA;Lo;0;L;;;;;N;;;;; +11593;SIDDHAM LETTER CA;Lo;0;L;;;;;N;;;;; +11594;SIDDHAM LETTER CHA;Lo;0;L;;;;;N;;;;; +11595;SIDDHAM LETTER JA;Lo;0;L;;;;;N;;;;; +11596;SIDDHAM LETTER JHA;Lo;0;L;;;;;N;;;;; +11597;SIDDHAM LETTER NYA;Lo;0;L;;;;;N;;;;; +11598;SIDDHAM LETTER TTA;Lo;0;L;;;;;N;;;;; +11599;SIDDHAM LETTER TTHA;Lo;0;L;;;;;N;;;;; +1159A;SIDDHAM LETTER DDA;Lo;0;L;;;;;N;;;;; +1159B;SIDDHAM LETTER DDHA;Lo;0;L;;;;;N;;;;; +1159C;SIDDHAM LETTER NNA;Lo;0;L;;;;;N;;;;; +1159D;SIDDHAM LETTER TA;Lo;0;L;;;;;N;;;;; +1159E;SIDDHAM LETTER THA;Lo;0;L;;;;;N;;;;; +1159F;SIDDHAM LETTER DA;Lo;0;L;;;;;N;;;;; +115A0;SIDDHAM LETTER DHA;Lo;0;L;;;;;N;;;;; +115A1;SIDDHAM LETTER NA;Lo;0;L;;;;;N;;;;; +115A2;SIDDHAM LETTER PA;Lo;0;L;;;;;N;;;;; +115A3;SIDDHAM LETTER PHA;Lo;0;L;;;;;N;;;;; +115A4;SIDDHAM LETTER BA;Lo;0;L;;;;;N;;;;; +115A5;SIDDHAM LETTER BHA;Lo;0;L;;;;;N;;;;; +115A6;SIDDHAM LETTER MA;Lo;0;L;;;;;N;;;;; +115A7;SIDDHAM LETTER YA;Lo;0;L;;;;;N;;;;; +115A8;SIDDHAM LETTER RA;Lo;0;L;;;;;N;;;;; +115A9;SIDDHAM LETTER LA;Lo;0;L;;;;;N;;;;; +115AA;SIDDHAM LETTER VA;Lo;0;L;;;;;N;;;;; +115AB;SIDDHAM LETTER SHA;Lo;0;L;;;;;N;;;;; +115AC;SIDDHAM LETTER SSA;Lo;0;L;;;;;N;;;;; +115AD;SIDDHAM LETTER SA;Lo;0;L;;;;;N;;;;; +115AE;SIDDHAM LETTER HA;Lo;0;L;;;;;N;;;;; +115AF;SIDDHAM VOWEL SIGN AA;Mc;0;L;;;;;N;;;;; +115B0;SIDDHAM VOWEL SIGN I;Mc;0;L;;;;;N;;;;; +115B1;SIDDHAM VOWEL SIGN II;Mc;0;L;;;;;N;;;;; +115B2;SIDDHAM VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;; +115B3;SIDDHAM VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;; +115B4;SIDDHAM VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;; +115B5;SIDDHAM VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;; +115B8;SIDDHAM VOWEL SIGN E;Mc;0;L;;;;;N;;;;; +115B9;SIDDHAM VOWEL SIGN AI;Mc;0;L;;;;;N;;;;; +115BA;SIDDHAM VOWEL SIGN O;Mc;0;L;115B8 115AF;;;;N;;;;; +115BB;SIDDHAM VOWEL SIGN AU;Mc;0;L;115B9 115AF;;;;N;;;;; +115BC;SIDDHAM SIGN CANDRABINDU;Mn;0;NSM;;;;;N;;;;; +115BD;SIDDHAM SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;; +115BE;SIDDHAM SIGN VISARGA;Mc;0;L;;;;;N;;;;; +115BF;SIDDHAM SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;; +115C0;SIDDHAM SIGN NUKTA;Mn;7;NSM;;;;;N;;;;; +115C1;SIDDHAM SIGN SIDDHAM;Po;0;L;;;;;N;;;;; +115C2;SIDDHAM DANDA;Po;0;L;;;;;N;;;;; +115C3;SIDDHAM DOUBLE DANDA;Po;0;L;;;;;N;;;;; +115C4;SIDDHAM SEPARATOR DOT;Po;0;L;;;;;N;;;;; +115C5;SIDDHAM SEPARATOR BAR;Po;0;L;;;;;N;;;;; +115C6;SIDDHAM REPETITION MARK-1;Po;0;L;;;;;N;;;;; +115C7;SIDDHAM REPETITION MARK-2;Po;0;L;;;;;N;;;;; +115C8;SIDDHAM REPETITION MARK-3;Po;0;L;;;;;N;;;;; +115C9;SIDDHAM END OF TEXT MARK;Po;0;L;;;;;N;;;;; +11600;MODI LETTER A;Lo;0;L;;;;;N;;;;; +11601;MODI LETTER AA;Lo;0;L;;;;;N;;;;; +11602;MODI LETTER I;Lo;0;L;;;;;N;;;;; +11603;MODI LETTER II;Lo;0;L;;;;;N;;;;; +11604;MODI LETTER U;Lo;0;L;;;;;N;;;;; +11605;MODI LETTER UU;Lo;0;L;;;;;N;;;;; +11606;MODI LETTER VOCALIC R;Lo;0;L;;;;;N;;;;; +11607;MODI LETTER VOCALIC RR;Lo;0;L;;;;;N;;;;; +11608;MODI LETTER VOCALIC L;Lo;0;L;;;;;N;;;;; +11609;MODI LETTER VOCALIC LL;Lo;0;L;;;;;N;;;;; +1160A;MODI LETTER E;Lo;0;L;;;;;N;;;;; +1160B;MODI LETTER AI;Lo;0;L;;;;;N;;;;; +1160C;MODI LETTER O;Lo;0;L;;;;;N;;;;; +1160D;MODI LETTER AU;Lo;0;L;;;;;N;;;;; +1160E;MODI LETTER KA;Lo;0;L;;;;;N;;;;; +1160F;MODI LETTER KHA;Lo;0;L;;;;;N;;;;; +11610;MODI LETTER GA;Lo;0;L;;;;;N;;;;; +11611;MODI LETTER GHA;Lo;0;L;;;;;N;;;;; +11612;MODI LETTER NGA;Lo;0;L;;;;;N;;;;; +11613;MODI LETTER CA;Lo;0;L;;;;;N;;;;; +11614;MODI LETTER CHA;Lo;0;L;;;;;N;;;;; +11615;MODI LETTER JA;Lo;0;L;;;;;N;;;;; +11616;MODI LETTER JHA;Lo;0;L;;;;;N;;;;; +11617;MODI LETTER NYA;Lo;0;L;;;;;N;;;;; +11618;MODI LETTER TTA;Lo;0;L;;;;;N;;;;; +11619;MODI LETTER TTHA;Lo;0;L;;;;;N;;;;; +1161A;MODI LETTER DDA;Lo;0;L;;;;;N;;;;; +1161B;MODI LETTER DDHA;Lo;0;L;;;;;N;;;;; +1161C;MODI LETTER NNA;Lo;0;L;;;;;N;;;;; +1161D;MODI LETTER TA;Lo;0;L;;;;;N;;;;; +1161E;MODI LETTER THA;Lo;0;L;;;;;N;;;;; +1161F;MODI LETTER DA;Lo;0;L;;;;;N;;;;; +11620;MODI LETTER DHA;Lo;0;L;;;;;N;;;;; +11621;MODI LETTER NA;Lo;0;L;;;;;N;;;;; +11622;MODI LETTER PA;Lo;0;L;;;;;N;;;;; +11623;MODI LETTER PHA;Lo;0;L;;;;;N;;;;; +11624;MODI LETTER BA;Lo;0;L;;;;;N;;;;; +11625;MODI LETTER BHA;Lo;0;L;;;;;N;;;;; +11626;MODI LETTER MA;Lo;0;L;;;;;N;;;;; +11627;MODI LETTER YA;Lo;0;L;;;;;N;;;;; +11628;MODI LETTER RA;Lo;0;L;;;;;N;;;;; +11629;MODI LETTER LA;Lo;0;L;;;;;N;;;;; +1162A;MODI LETTER VA;Lo;0;L;;;;;N;;;;; +1162B;MODI LETTER SHA;Lo;0;L;;;;;N;;;;; +1162C;MODI LETTER SSA;Lo;0;L;;;;;N;;;;; +1162D;MODI LETTER SA;Lo;0;L;;;;;N;;;;; +1162E;MODI LETTER HA;Lo;0;L;;;;;N;;;;; +1162F;MODI LETTER LLA;Lo;0;L;;;;;N;;;;; +11630;MODI VOWEL SIGN AA;Mc;0;L;;;;;N;;;;; +11631;MODI VOWEL SIGN I;Mc;0;L;;;;;N;;;;; +11632;MODI VOWEL SIGN II;Mc;0;L;;;;;N;;;;; +11633;MODI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;; +11634;MODI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;; +11635;MODI VOWEL SIGN VOCALIC R;Mn;0;NSM;;;;;N;;;;; +11636;MODI VOWEL SIGN VOCALIC RR;Mn;0;NSM;;;;;N;;;;; +11637;MODI VOWEL SIGN VOCALIC L;Mn;0;NSM;;;;;N;;;;; +11638;MODI VOWEL SIGN VOCALIC LL;Mn;0;NSM;;;;;N;;;;; +11639;MODI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;; +1163A;MODI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;; +1163B;MODI VOWEL SIGN O;Mc;0;L;;;;;N;;;;; +1163C;MODI VOWEL SIGN AU;Mc;0;L;;;;;N;;;;; +1163D;MODI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;; +1163E;MODI SIGN VISARGA;Mc;0;L;;;;;N;;;;; +1163F;MODI SIGN VIRAMA;Mn;9;NSM;;;;;N;;;;; +11640;MODI SIGN ARDHACANDRA;Mn;0;NSM;;;;;N;;;;; +11641;MODI DANDA;Po;0;L;;;;;N;;;;; +11642;MODI DOUBLE DANDA;Po;0;L;;;;;N;;;;; +11643;MODI ABBREVIATION SIGN;Po;0;L;;;;;N;;;;; +11644;MODI SIGN HUVA;Lo;0;L;;;;;N;;;;; +11650;MODI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +11651;MODI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +11652;MODI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +11653;MODI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +11654;MODI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +11655;MODI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +11656;MODI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +11657;MODI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +11658;MODI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +11659;MODI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +11680;TAKRI LETTER A;Lo;0;L;;;;;N;;;;; +11681;TAKRI LETTER AA;Lo;0;L;;;;;N;;;;; +11682;TAKRI LETTER I;Lo;0;L;;;;;N;;;;; +11683;TAKRI LETTER II;Lo;0;L;;;;;N;;;;; +11684;TAKRI LETTER U;Lo;0;L;;;;;N;;;;; +11685;TAKRI LETTER UU;Lo;0;L;;;;;N;;;;; +11686;TAKRI LETTER E;Lo;0;L;;;;;N;;;;; +11687;TAKRI LETTER AI;Lo;0;L;;;;;N;;;;; +11688;TAKRI LETTER O;Lo;0;L;;;;;N;;;;; +11689;TAKRI LETTER AU;Lo;0;L;;;;;N;;;;; +1168A;TAKRI LETTER KA;Lo;0;L;;;;;N;;;;; +1168B;TAKRI LETTER KHA;Lo;0;L;;;;;N;;;;; +1168C;TAKRI LETTER GA;Lo;0;L;;;;;N;;;;; +1168D;TAKRI LETTER GHA;Lo;0;L;;;;;N;;;;; +1168E;TAKRI LETTER NGA;Lo;0;L;;;;;N;;;;; +1168F;TAKRI LETTER CA;Lo;0;L;;;;;N;;;;; +11690;TAKRI LETTER CHA;Lo;0;L;;;;;N;;;;; +11691;TAKRI LETTER JA;Lo;0;L;;;;;N;;;;; +11692;TAKRI LETTER JHA;Lo;0;L;;;;;N;;;;; +11693;TAKRI LETTER NYA;Lo;0;L;;;;;N;;;;; +11694;TAKRI LETTER TTA;Lo;0;L;;;;;N;;;;; +11695;TAKRI LETTER TTHA;Lo;0;L;;;;;N;;;;; +11696;TAKRI LETTER DDA;Lo;0;L;;;;;N;;;;; +11697;TAKRI LETTER DDHA;Lo;0;L;;;;;N;;;;; +11698;TAKRI LETTER NNA;Lo;0;L;;;;;N;;;;; +11699;TAKRI LETTER TA;Lo;0;L;;;;;N;;;;; +1169A;TAKRI LETTER THA;Lo;0;L;;;;;N;;;;; +1169B;TAKRI LETTER DA;Lo;0;L;;;;;N;;;;; +1169C;TAKRI LETTER DHA;Lo;0;L;;;;;N;;;;; +1169D;TAKRI LETTER NA;Lo;0;L;;;;;N;;;;; +1169E;TAKRI LETTER PA;Lo;0;L;;;;;N;;;;; +1169F;TAKRI LETTER PHA;Lo;0;L;;;;;N;;;;; +116A0;TAKRI LETTER BA;Lo;0;L;;;;;N;;;;; +116A1;TAKRI LETTER BHA;Lo;0;L;;;;;N;;;;; +116A2;TAKRI LETTER MA;Lo;0;L;;;;;N;;;;; +116A3;TAKRI LETTER YA;Lo;0;L;;;;;N;;;;; +116A4;TAKRI LETTER RA;Lo;0;L;;;;;N;;;;; +116A5;TAKRI LETTER LA;Lo;0;L;;;;;N;;;;; +116A6;TAKRI LETTER VA;Lo;0;L;;;;;N;;;;; +116A7;TAKRI LETTER SHA;Lo;0;L;;;;;N;;;;; +116A8;TAKRI LETTER SA;Lo;0;L;;;;;N;;;;; +116A9;TAKRI LETTER HA;Lo;0;L;;;;;N;;;;; +116AA;TAKRI LETTER RRA;Lo;0;L;;;;;N;;;;; +116AB;TAKRI SIGN ANUSVARA;Mn;0;NSM;;;;;N;;;;; +116AC;TAKRI SIGN VISARGA;Mc;0;L;;;;;N;;;;; +116AD;TAKRI VOWEL SIGN AA;Mn;0;NSM;;;;;N;;;;; +116AE;TAKRI VOWEL SIGN I;Mc;0;L;;;;;N;;;;; +116AF;TAKRI VOWEL SIGN II;Mc;0;L;;;;;N;;;;; +116B0;TAKRI VOWEL SIGN U;Mn;0;NSM;;;;;N;;;;; +116B1;TAKRI VOWEL SIGN UU;Mn;0;NSM;;;;;N;;;;; +116B2;TAKRI VOWEL SIGN E;Mn;0;NSM;;;;;N;;;;; +116B3;TAKRI VOWEL SIGN AI;Mn;0;NSM;;;;;N;;;;; +116B4;TAKRI VOWEL SIGN O;Mn;0;NSM;;;;;N;;;;; +116B5;TAKRI VOWEL SIGN AU;Mn;0;NSM;;;;;N;;;;; +116B6;TAKRI SIGN VIRAMA;Mc;9;L;;;;;N;;;;; +116B7;TAKRI SIGN NUKTA;Mn;7;NSM;;;;;N;;;;; +116C0;TAKRI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +116C1;TAKRI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +116C2;TAKRI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +116C3;TAKRI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +116C4;TAKRI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +116C5;TAKRI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +116C6;TAKRI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +116C7;TAKRI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +116C8;TAKRI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +116C9;TAKRI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +118A0;WARANG CITI CAPITAL LETTER NGAA;Lu;0;L;;;;;N;;;;118C0; +118A1;WARANG CITI CAPITAL LETTER A;Lu;0;L;;;;;N;;;;118C1; +118A2;WARANG CITI CAPITAL LETTER WI;Lu;0;L;;;;;N;;;;118C2; +118A3;WARANG CITI CAPITAL LETTER YU;Lu;0;L;;;;;N;;;;118C3; +118A4;WARANG CITI CAPITAL LETTER YA;Lu;0;L;;;;;N;;;;118C4; +118A5;WARANG CITI CAPITAL LETTER YO;Lu;0;L;;;;;N;;;;118C5; +118A6;WARANG CITI CAPITAL LETTER II;Lu;0;L;;;;;N;;;;118C6; +118A7;WARANG CITI CAPITAL LETTER UU;Lu;0;L;;;;;N;;;;118C7; +118A8;WARANG CITI CAPITAL LETTER E;Lu;0;L;;;;;N;;;;118C8; +118A9;WARANG CITI CAPITAL LETTER O;Lu;0;L;;;;;N;;;;118C9; +118AA;WARANG CITI CAPITAL LETTER ANG;Lu;0;L;;;;;N;;;;118CA; +118AB;WARANG CITI CAPITAL LETTER GA;Lu;0;L;;;;;N;;;;118CB; +118AC;WARANG CITI CAPITAL LETTER KO;Lu;0;L;;;;;N;;;;118CC; +118AD;WARANG CITI CAPITAL LETTER ENY;Lu;0;L;;;;;N;;;;118CD; +118AE;WARANG CITI CAPITAL LETTER YUJ;Lu;0;L;;;;;N;;;;118CE; +118AF;WARANG CITI CAPITAL LETTER UC;Lu;0;L;;;;;N;;;;118CF; +118B0;WARANG CITI CAPITAL LETTER ENN;Lu;0;L;;;;;N;;;;118D0; +118B1;WARANG CITI CAPITAL LETTER ODD;Lu;0;L;;;;;N;;;;118D1; +118B2;WARANG CITI CAPITAL LETTER TTE;Lu;0;L;;;;;N;;;;118D2; +118B3;WARANG CITI CAPITAL LETTER NUNG;Lu;0;L;;;;;N;;;;118D3; +118B4;WARANG CITI CAPITAL LETTER DA;Lu;0;L;;;;;N;;;;118D4; +118B5;WARANG CITI CAPITAL LETTER AT;Lu;0;L;;;;;N;;;;118D5; +118B6;WARANG CITI CAPITAL LETTER AM;Lu;0;L;;;;;N;;;;118D6; +118B7;WARANG CITI CAPITAL LETTER BU;Lu;0;L;;;;;N;;;;118D7; +118B8;WARANG CITI CAPITAL LETTER PU;Lu;0;L;;;;;N;;;;118D8; +118B9;WARANG CITI CAPITAL LETTER HIYO;Lu;0;L;;;;;N;;;;118D9; +118BA;WARANG CITI CAPITAL LETTER HOLO;Lu;0;L;;;;;N;;;;118DA; +118BB;WARANG CITI CAPITAL LETTER HORR;Lu;0;L;;;;;N;;;;118DB; +118BC;WARANG CITI CAPITAL LETTER HAR;Lu;0;L;;;;;N;;;;118DC; +118BD;WARANG CITI CAPITAL LETTER SSUU;Lu;0;L;;;;;N;;;;118DD; +118BE;WARANG CITI CAPITAL LETTER SII;Lu;0;L;;;;;N;;;;118DE; +118BF;WARANG CITI CAPITAL LETTER VIYO;Lu;0;L;;;;;N;;;;118DF; +118C0;WARANG CITI SMALL LETTER NGAA;Ll;0;L;;;;;N;;;118A0;;118A0 +118C1;WARANG CITI SMALL LETTER A;Ll;0;L;;;;;N;;;118A1;;118A1 +118C2;WARANG CITI SMALL LETTER WI;Ll;0;L;;;;;N;;;118A2;;118A2 +118C3;WARANG CITI SMALL LETTER YU;Ll;0;L;;;;;N;;;118A3;;118A3 +118C4;WARANG CITI SMALL LETTER YA;Ll;0;L;;;;;N;;;118A4;;118A4 +118C5;WARANG CITI SMALL LETTER YO;Ll;0;L;;;;;N;;;118A5;;118A5 +118C6;WARANG CITI SMALL LETTER II;Ll;0;L;;;;;N;;;118A6;;118A6 +118C7;WARANG CITI SMALL LETTER UU;Ll;0;L;;;;;N;;;118A7;;118A7 +118C8;WARANG CITI SMALL LETTER E;Ll;0;L;;;;;N;;;118A8;;118A8 +118C9;WARANG CITI SMALL LETTER O;Ll;0;L;;;;;N;;;118A9;;118A9 +118CA;WARANG CITI SMALL LETTER ANG;Ll;0;L;;;;;N;;;118AA;;118AA +118CB;WARANG CITI SMALL LETTER GA;Ll;0;L;;;;;N;;;118AB;;118AB +118CC;WARANG CITI SMALL LETTER KO;Ll;0;L;;;;;N;;;118AC;;118AC +118CD;WARANG CITI SMALL LETTER ENY;Ll;0;L;;;;;N;;;118AD;;118AD +118CE;WARANG CITI SMALL LETTER YUJ;Ll;0;L;;;;;N;;;118AE;;118AE +118CF;WARANG CITI SMALL LETTER UC;Ll;0;L;;;;;N;;;118AF;;118AF +118D0;WARANG CITI SMALL LETTER ENN;Ll;0;L;;;;;N;;;118B0;;118B0 +118D1;WARANG CITI SMALL LETTER ODD;Ll;0;L;;;;;N;;;118B1;;118B1 +118D2;WARANG CITI SMALL LETTER TTE;Ll;0;L;;;;;N;;;118B2;;118B2 +118D3;WARANG CITI SMALL LETTER NUNG;Ll;0;L;;;;;N;;;118B3;;118B3 +118D4;WARANG CITI SMALL LETTER DA;Ll;0;L;;;;;N;;;118B4;;118B4 +118D5;WARANG CITI SMALL LETTER AT;Ll;0;L;;;;;N;;;118B5;;118B5 +118D6;WARANG CITI SMALL LETTER AM;Ll;0;L;;;;;N;;;118B6;;118B6 +118D7;WARANG CITI SMALL LETTER BU;Ll;0;L;;;;;N;;;118B7;;118B7 +118D8;WARANG CITI SMALL LETTER PU;Ll;0;L;;;;;N;;;118B8;;118B8 +118D9;WARANG CITI SMALL LETTER HIYO;Ll;0;L;;;;;N;;;118B9;;118B9 +118DA;WARANG CITI SMALL LETTER HOLO;Ll;0;L;;;;;N;;;118BA;;118BA +118DB;WARANG CITI SMALL LETTER HORR;Ll;0;L;;;;;N;;;118BB;;118BB +118DC;WARANG CITI SMALL LETTER HAR;Ll;0;L;;;;;N;;;118BC;;118BC +118DD;WARANG CITI SMALL LETTER SSUU;Ll;0;L;;;;;N;;;118BD;;118BD +118DE;WARANG CITI SMALL LETTER SII;Ll;0;L;;;;;N;;;118BE;;118BE +118DF;WARANG CITI SMALL LETTER VIYO;Ll;0;L;;;;;N;;;118BF;;118BF +118E0;WARANG CITI DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +118E1;WARANG CITI DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +118E2;WARANG CITI DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +118E3;WARANG CITI DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +118E4;WARANG CITI DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +118E5;WARANG CITI DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +118E6;WARANG CITI DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +118E7;WARANG CITI DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +118E8;WARANG CITI DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +118E9;WARANG CITI DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +118EA;WARANG CITI NUMBER TEN;No;0;L;;;;10;N;;;;; +118EB;WARANG CITI NUMBER TWENTY;No;0;L;;;;20;N;;;;; +118EC;WARANG CITI NUMBER THIRTY;No;0;L;;;;30;N;;;;; +118ED;WARANG CITI NUMBER FORTY;No;0;L;;;;40;N;;;;; +118EE;WARANG CITI NUMBER FIFTY;No;0;L;;;;50;N;;;;; +118EF;WARANG CITI NUMBER SIXTY;No;0;L;;;;60;N;;;;; +118F0;WARANG CITI NUMBER SEVENTY;No;0;L;;;;70;N;;;;; +118F1;WARANG CITI NUMBER EIGHTY;No;0;L;;;;80;N;;;;; +118F2;WARANG CITI NUMBER NINETY;No;0;L;;;;90;N;;;;; +118FF;WARANG CITI OM;Lo;0;L;;;;;N;;;;; +11AC0;PAU CIN HAU LETTER PA;Lo;0;L;;;;;N;;;;; +11AC1;PAU CIN HAU LETTER KA;Lo;0;L;;;;;N;;;;; +11AC2;PAU CIN HAU LETTER LA;Lo;0;L;;;;;N;;;;; +11AC3;PAU CIN HAU LETTER MA;Lo;0;L;;;;;N;;;;; +11AC4;PAU CIN HAU LETTER DA;Lo;0;L;;;;;N;;;;; +11AC5;PAU CIN HAU LETTER ZA;Lo;0;L;;;;;N;;;;; +11AC6;PAU CIN HAU LETTER VA;Lo;0;L;;;;;N;;;;; +11AC7;PAU CIN HAU LETTER NGA;Lo;0;L;;;;;N;;;;; +11AC8;PAU CIN HAU LETTER HA;Lo;0;L;;;;;N;;;;; +11AC9;PAU CIN HAU LETTER GA;Lo;0;L;;;;;N;;;;; +11ACA;PAU CIN HAU LETTER KHA;Lo;0;L;;;;;N;;;;; +11ACB;PAU CIN HAU LETTER SA;Lo;0;L;;;;;N;;;;; +11ACC;PAU CIN HAU LETTER BA;Lo;0;L;;;;;N;;;;; +11ACD;PAU CIN HAU LETTER CA;Lo;0;L;;;;;N;;;;; +11ACE;PAU CIN HAU LETTER TA;Lo;0;L;;;;;N;;;;; +11ACF;PAU CIN HAU LETTER THA;Lo;0;L;;;;;N;;;;; +11AD0;PAU CIN HAU LETTER NA;Lo;0;L;;;;;N;;;;; +11AD1;PAU CIN HAU LETTER PHA;Lo;0;L;;;;;N;;;;; +11AD2;PAU CIN HAU LETTER RA;Lo;0;L;;;;;N;;;;; +11AD3;PAU CIN HAU LETTER FA;Lo;0;L;;;;;N;;;;; +11AD4;PAU CIN HAU LETTER CHA;Lo;0;L;;;;;N;;;;; +11AD5;PAU CIN HAU LETTER A;Lo;0;L;;;;;N;;;;; +11AD6;PAU CIN HAU LETTER E;Lo;0;L;;;;;N;;;;; +11AD7;PAU CIN HAU LETTER I;Lo;0;L;;;;;N;;;;; +11AD8;PAU CIN HAU LETTER O;Lo;0;L;;;;;N;;;;; +11AD9;PAU CIN HAU LETTER U;Lo;0;L;;;;;N;;;;; +11ADA;PAU CIN HAU LETTER UA;Lo;0;L;;;;;N;;;;; +11ADB;PAU CIN HAU LETTER IA;Lo;0;L;;;;;N;;;;; +11ADC;PAU CIN HAU LETTER FINAL P;Lo;0;L;;;;;N;;;;; +11ADD;PAU CIN HAU LETTER FINAL K;Lo;0;L;;;;;N;;;;; +11ADE;PAU CIN HAU LETTER FINAL T;Lo;0;L;;;;;N;;;;; +11ADF;PAU CIN HAU LETTER FINAL M;Lo;0;L;;;;;N;;;;; +11AE0;PAU CIN HAU LETTER FINAL N;Lo;0;L;;;;;N;;;;; +11AE1;PAU CIN HAU LETTER FINAL L;Lo;0;L;;;;;N;;;;; +11AE2;PAU CIN HAU LETTER FINAL W;Lo;0;L;;;;;N;;;;; +11AE3;PAU CIN HAU LETTER FINAL NG;Lo;0;L;;;;;N;;;;; +11AE4;PAU CIN HAU LETTER FINAL Y;Lo;0;L;;;;;N;;;;; +11AE5;PAU CIN HAU RISING TONE LONG;Lo;0;L;;;;;N;;;;; +11AE6;PAU CIN HAU RISING TONE;Lo;0;L;;;;;N;;;;; +11AE7;PAU CIN HAU SANDHI GLOTTAL STOP;Lo;0;L;;;;;N;;;;; +11AE8;PAU CIN HAU RISING TONE LONG FINAL;Lo;0;L;;;;;N;;;;; +11AE9;PAU CIN HAU RISING TONE FINAL;Lo;0;L;;;;;N;;;;; +11AEA;PAU CIN HAU SANDHI GLOTTAL STOP FINAL;Lo;0;L;;;;;N;;;;; +11AEB;PAU CIN HAU SANDHI TONE LONG;Lo;0;L;;;;;N;;;;; +11AEC;PAU CIN HAU SANDHI TONE;Lo;0;L;;;;;N;;;;; +11AED;PAU CIN HAU SANDHI TONE LONG FINAL;Lo;0;L;;;;;N;;;;; +11AEE;PAU CIN HAU SANDHI TONE FINAL;Lo;0;L;;;;;N;;;;; +11AEF;PAU CIN HAU MID-LEVEL TONE;Lo;0;L;;;;;N;;;;; +11AF0;PAU CIN HAU GLOTTAL STOP VARIANT;Lo;0;L;;;;;N;;;;; +11AF1;PAU CIN HAU MID-LEVEL TONE LONG FINAL;Lo;0;L;;;;;N;;;;; +11AF2;PAU CIN HAU MID-LEVEL TONE FINAL;Lo;0;L;;;;;N;;;;; +11AF3;PAU CIN HAU LOW-FALLING TONE LONG;Lo;0;L;;;;;N;;;;; +11AF4;PAU CIN HAU LOW-FALLING TONE;Lo;0;L;;;;;N;;;;; +11AF5;PAU CIN HAU GLOTTAL STOP;Lo;0;L;;;;;N;;;;; +11AF6;PAU CIN HAU LOW-FALLING TONE LONG FINAL;Lo;0;L;;;;;N;;;;; +11AF7;PAU CIN HAU LOW-FALLING TONE FINAL;Lo;0;L;;;;;N;;;;; +11AF8;PAU CIN HAU GLOTTAL STOP FINAL;Lo;0;L;;;;;N;;;;; 12000;CUNEIFORM SIGN A;Lo;0;L;;;;;N;;;;; 12001;CUNEIFORM SIGN A TIMES A;Lo;0;L;;;;;N;;;;; 12002;CUNEIFORM SIGN A TIMES BAD;Lo;0;L;;;;;N;;;;; @@ -18217,6 +20328,48 @@ 1236C;CUNEIFORM SIGN ZU5 TIMES A;Lo;0;L;;;;;N;;;;; 1236D;CUNEIFORM SIGN ZUBUR;Lo;0;L;;;;;N;;;;; 1236E;CUNEIFORM SIGN ZUM;Lo;0;L;;;;;N;;;;; +1236F;CUNEIFORM SIGN KAP ELAMITE;Lo;0;L;;;;;N;;;;; +12370;CUNEIFORM SIGN AB TIMES NUN;Lo;0;L;;;;;N;;;;; +12371;CUNEIFORM SIGN AB2 TIMES A;Lo;0;L;;;;;N;;;;; +12372;CUNEIFORM SIGN AMAR TIMES KUG;Lo;0;L;;;;;N;;;;; +12373;CUNEIFORM SIGN DAG KISIM5 TIMES U2 PLUS MASH;Lo;0;L;;;;;N;;;;; +12374;CUNEIFORM SIGN DAG3;Lo;0;L;;;;;N;;;;; +12375;CUNEIFORM SIGN DISH PLUS SHU;Lo;0;L;;;;;N;;;;; +12376;CUNEIFORM SIGN DUB TIMES SHE;Lo;0;L;;;;;N;;;;; +12377;CUNEIFORM SIGN EZEN TIMES GUD;Lo;0;L;;;;;N;;;;; +12378;CUNEIFORM SIGN EZEN TIMES SHE;Lo;0;L;;;;;N;;;;; +12379;CUNEIFORM SIGN GA2 TIMES AN PLUS KAK PLUS A;Lo;0;L;;;;;N;;;;; +1237A;CUNEIFORM SIGN GA2 TIMES ASH2;Lo;0;L;;;;;N;;;;; +1237B;CUNEIFORM SIGN GE22;Lo;0;L;;;;;N;;;;; +1237C;CUNEIFORM SIGN GIG;Lo;0;L;;;;;N;;;;; +1237D;CUNEIFORM SIGN HUSH;Lo;0;L;;;;;N;;;;; +1237E;CUNEIFORM SIGN KA TIMES ANSHE;Lo;0;L;;;;;N;;;;; +1237F;CUNEIFORM SIGN KA TIMES ASH3;Lo;0;L;;;;;N;;;;; +12380;CUNEIFORM SIGN KA TIMES GISH;Lo;0;L;;;;;N;;;;; +12381;CUNEIFORM SIGN KA TIMES GUD;Lo;0;L;;;;;N;;;;; +12382;CUNEIFORM SIGN KA TIMES HI TIMES ASH2;Lo;0;L;;;;;N;;;;; +12383;CUNEIFORM SIGN KA TIMES LUM;Lo;0;L;;;;;N;;;;; +12384;CUNEIFORM SIGN KA TIMES PA;Lo;0;L;;;;;N;;;;; +12385;CUNEIFORM SIGN KA TIMES SHUL;Lo;0;L;;;;;N;;;;; +12386;CUNEIFORM SIGN KA TIMES TU;Lo;0;L;;;;;N;;;;; +12387;CUNEIFORM SIGN KA TIMES UR2;Lo;0;L;;;;;N;;;;; +12388;CUNEIFORM SIGN LAGAB TIMES GI;Lo;0;L;;;;;N;;;;; +12389;CUNEIFORM SIGN LU2 SHESHIG TIMES BAD;Lo;0;L;;;;;N;;;;; +1238A;CUNEIFORM SIGN LU2 TIMES ESH2 PLUS LAL;Lo;0;L;;;;;N;;;;; +1238B;CUNEIFORM SIGN LU2 TIMES SHU;Lo;0;L;;;;;N;;;;; +1238C;CUNEIFORM SIGN MESH;Lo;0;L;;;;;N;;;;; +1238D;CUNEIFORM SIGN MUSH3 TIMES ZA;Lo;0;L;;;;;N;;;;; +1238E;CUNEIFORM SIGN NA4;Lo;0;L;;;;;N;;;;; +1238F;CUNEIFORM SIGN NIN;Lo;0;L;;;;;N;;;;; +12390;CUNEIFORM SIGN NIN9;Lo;0;L;;;;;N;;;;; +12391;CUNEIFORM SIGN NINDA2 TIMES BAL;Lo;0;L;;;;;N;;;;; +12392;CUNEIFORM SIGN NINDA2 TIMES GI;Lo;0;L;;;;;N;;;;; +12393;CUNEIFORM SIGN NU11 ROTATED NINETY DEGREES;Lo;0;L;;;;;N;;;;; +12394;CUNEIFORM SIGN PESH2 ASTERISK;Lo;0;L;;;;;N;;;;; +12395;CUNEIFORM SIGN PIR2;Lo;0;L;;;;;N;;;;; +12396;CUNEIFORM SIGN SAG TIMES IGI GUNU;Lo;0;L;;;;;N;;;;; +12397;CUNEIFORM SIGN TI2;Lo;0;L;;;;;N;;;;; +12398;CUNEIFORM SIGN UM TIMES ME;Lo;0;L;;;;;N;;;;; 12400;CUNEIFORM NUMERIC SIGN TWO ASH;Nl;0;L;;;;2;N;;;;; 12401;CUNEIFORM NUMERIC SIGN THREE ASH;Nl;0;L;;;;3;N;;;;; 12402;CUNEIFORM NUMERIC SIGN FOUR ASH;Nl;0;L;;;;4;N;;;;; @@ -18267,8 +20420,8 @@ 1242F;CUNEIFORM NUMERIC SIGN THREE SHARU VARIANT FORM;Nl;0;L;;;;3;N;;;;; 12430;CUNEIFORM NUMERIC SIGN FOUR SHARU;Nl;0;L;;;;4;N;;;;; 12431;CUNEIFORM NUMERIC SIGN FIVE SHARU;Nl;0;L;;;;5;N;;;;; -12432;CUNEIFORM NUMERIC SIGN SHAR2 TIMES GAL PLUS DISH;Nl;0;L;;;;;N;;;;; -12433;CUNEIFORM NUMERIC SIGN SHAR2 TIMES GAL PLUS MIN;Nl;0;L;;;;;N;;;;; +12432;CUNEIFORM NUMERIC SIGN SHAR2 TIMES GAL PLUS DISH;Nl;0;L;;;;216000;N;;;;; +12433;CUNEIFORM NUMERIC SIGN SHAR2 TIMES GAL PLUS MIN;Nl;0;L;;;;432000;N;;;;; 12434;CUNEIFORM NUMERIC SIGN ONE BURU;Nl;0;L;;;;1;N;;;;; 12435;CUNEIFORM NUMERIC SIGN TWO BURU;Nl;0;L;;;;2;N;;;;; 12436;CUNEIFORM NUMERIC SIGN THREE BURU;Nl;0;L;;;;3;N;;;;; @@ -18303,8 +20456,8 @@ 12453;CUNEIFORM NUMERIC SIGN FOUR BAN2 VARIANT FORM;Nl;0;L;;;;4;N;;;;; 12454;CUNEIFORM NUMERIC SIGN FIVE BAN2;Nl;0;L;;;;5;N;;;;; 12455;CUNEIFORM NUMERIC SIGN FIVE BAN2 VARIANT FORM;Nl;0;L;;;;5;N;;;;; -12456;CUNEIFORM NUMERIC SIGN NIGIDAMIN;Nl;0;L;;;;;N;;;;; -12457;CUNEIFORM NUMERIC SIGN NIGIDAESH;Nl;0;L;;;;;N;;;;; +12456;CUNEIFORM NUMERIC SIGN NIGIDAMIN;Nl;0;L;;;;2;N;;;;; +12457;CUNEIFORM NUMERIC SIGN NIGIDAESH;Nl;0;L;;;;3;N;;;;; 12458;CUNEIFORM NUMERIC SIGN ONE ESHE3;Nl;0;L;;;;1;N;;;;; 12459;CUNEIFORM NUMERIC SIGN TWO ESHE3;Nl;0;L;;;;2;N;;;;; 1245A;CUNEIFORM NUMERIC SIGN ONE THIRD DISH;Nl;0;L;;;;1/3;N;;;;; @@ -18316,10 +20469,23 @@ 12460;CUNEIFORM NUMERIC SIGN ONE QUARTER ASH;Nl;0;L;;;;1/4;N;;;;; 12461;CUNEIFORM NUMERIC SIGN OLD ASSYRIAN ONE SIXTH;Nl;0;L;;;;1/6;N;;;;; 12462;CUNEIFORM NUMERIC SIGN OLD ASSYRIAN ONE QUARTER;Nl;0;L;;;;1/4;N;;;;; +12463;CUNEIFORM NUMERIC SIGN ONE QUARTER GUR;Nl;0;L;;;;1/4;N;;;;; +12464;CUNEIFORM NUMERIC SIGN ONE HALF GUR;Nl;0;L;;;;1/2;N;;;;; +12465;CUNEIFORM NUMERIC SIGN ELAMITE ONE THIRD;Nl;0;L;;;;1/3;N;;;;; +12466;CUNEIFORM NUMERIC SIGN ELAMITE TWO THIRDS;Nl;0;L;;;;2/3;N;;;;; +12467;CUNEIFORM NUMERIC SIGN ELAMITE FORTY;Nl;0;L;;;;40;N;;;;; +12468;CUNEIFORM NUMERIC SIGN ELAMITE FIFTY;Nl;0;L;;;;50;N;;;;; +12469;CUNEIFORM NUMERIC SIGN FOUR U VARIANT FORM;Nl;0;L;;;;4;N;;;;; +1246A;CUNEIFORM NUMERIC SIGN FIVE U VARIANT FORM;Nl;0;L;;;;5;N;;;;; +1246B;CUNEIFORM NUMERIC SIGN SIX U VARIANT FORM;Nl;0;L;;;;6;N;;;;; +1246C;CUNEIFORM NUMERIC SIGN SEVEN U VARIANT FORM;Nl;0;L;;;;7;N;;;;; +1246D;CUNEIFORM NUMERIC SIGN EIGHT U VARIANT FORM;Nl;0;L;;;;8;N;;;;; +1246E;CUNEIFORM NUMERIC SIGN NINE U VARIANT FORM;Nl;0;L;;;;9;N;;;;; 12470;CUNEIFORM PUNCTUATION SIGN OLD ASSYRIAN WORD DIVIDER;Po;0;L;;;;;N;;;;; 12471;CUNEIFORM PUNCTUATION SIGN VERTICAL COLON;Po;0;L;;;;;N;;;;; 12472;CUNEIFORM PUNCTUATION SIGN DIAGONAL COLON;Po;0;L;;;;;N;;;;; 12473;CUNEIFORM PUNCTUATION SIGN DIAGONAL TRICOLON;Po;0;L;;;;;N;;;;; +12474;CUNEIFORM PUNCTUATION SIGN DIAGONAL QUADCOLON;Po;0;L;;;;;N;;;;; 13000;EGYPTIAN HIEROGLYPH A001;Lo;0;L;;;;;N;;;;; 13001;EGYPTIAN HIEROGLYPH A002;Lo;0;L;;;;;N;;;;; 13002;EGYPTIAN HIEROGLYPH A003;Lo;0;L;;;;;N;;;;; @@ -19960,8 +22126,494 @@ 16A36;BAMUM LETTER PHASE-F KPA;Lo;0;L;;;;;N;;;;; 16A37;BAMUM LETTER PHASE-F SAMBA;Lo;0;L;;;;;N;;;;; 16A38;BAMUM LETTER PHASE-F VUEQ;Lo;0;L;;;;;N;;;;; +16A40;MRO LETTER TA;Lo;0;L;;;;;N;;;;; +16A41;MRO LETTER NGI;Lo;0;L;;;;;N;;;;; +16A42;MRO LETTER YO;Lo;0;L;;;;;N;;;;; +16A43;MRO LETTER MIM;Lo;0;L;;;;;N;;;;; +16A44;MRO LETTER BA;Lo;0;L;;;;;N;;;;; +16A45;MRO LETTER DA;Lo;0;L;;;;;N;;;;; +16A46;MRO LETTER A;Lo;0;L;;;;;N;;;;; +16A47;MRO LETTER PHI;Lo;0;L;;;;;N;;;;; +16A48;MRO LETTER KHAI;Lo;0;L;;;;;N;;;;; +16A49;MRO LETTER HAO;Lo;0;L;;;;;N;;;;; +16A4A;MRO LETTER DAI;Lo;0;L;;;;;N;;;;; +16A4B;MRO LETTER CHU;Lo;0;L;;;;;N;;;;; +16A4C;MRO LETTER KEAAE;Lo;0;L;;;;;N;;;;; +16A4D;MRO LETTER OL;Lo;0;L;;;;;N;;;;; +16A4E;MRO LETTER MAEM;Lo;0;L;;;;;N;;;;; +16A4F;MRO LETTER NIN;Lo;0;L;;;;;N;;;;; +16A50;MRO LETTER PA;Lo;0;L;;;;;N;;;;; +16A51;MRO LETTER OO;Lo;0;L;;;;;N;;;;; +16A52;MRO LETTER O;Lo;0;L;;;;;N;;;;; +16A53;MRO LETTER RO;Lo;0;L;;;;;N;;;;; +16A54;MRO LETTER SHI;Lo;0;L;;;;;N;;;;; +16A55;MRO LETTER THEA;Lo;0;L;;;;;N;;;;; +16A56;MRO LETTER EA;Lo;0;L;;;;;N;;;;; +16A57;MRO LETTER WA;Lo;0;L;;;;;N;;;;; +16A58;MRO LETTER E;Lo;0;L;;;;;N;;;;; +16A59;MRO LETTER KO;Lo;0;L;;;;;N;;;;; +16A5A;MRO LETTER LAN;Lo;0;L;;;;;N;;;;; +16A5B;MRO LETTER LA;Lo;0;L;;;;;N;;;;; +16A5C;MRO LETTER HAI;Lo;0;L;;;;;N;;;;; +16A5D;MRO LETTER RI;Lo;0;L;;;;;N;;;;; +16A5E;MRO LETTER TEK;Lo;0;L;;;;;N;;;;; +16A60;MRO DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +16A61;MRO DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +16A62;MRO DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +16A63;MRO DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +16A64;MRO DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +16A65;MRO DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +16A66;MRO DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +16A67;MRO DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +16A68;MRO DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +16A69;MRO DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +16A6E;MRO DANDA;Po;0;L;;;;;N;;;;; +16A6F;MRO DOUBLE DANDA;Po;0;L;;;;;N;;;;; +16AD0;BASSA VAH LETTER ENNI;Lo;0;L;;;;;N;;;;; +16AD1;BASSA VAH LETTER KA;Lo;0;L;;;;;N;;;;; +16AD2;BASSA VAH LETTER SE;Lo;0;L;;;;;N;;;;; +16AD3;BASSA VAH LETTER FA;Lo;0;L;;;;;N;;;;; +16AD4;BASSA VAH LETTER MBE;Lo;0;L;;;;;N;;;;; +16AD5;BASSA VAH LETTER YIE;Lo;0;L;;;;;N;;;;; +16AD6;BASSA VAH LETTER GAH;Lo;0;L;;;;;N;;;;; +16AD7;BASSA VAH LETTER DHII;Lo;0;L;;;;;N;;;;; +16AD8;BASSA VAH LETTER KPAH;Lo;0;L;;;;;N;;;;; +16AD9;BASSA VAH LETTER JO;Lo;0;L;;;;;N;;;;; +16ADA;BASSA VAH LETTER HWAH;Lo;0;L;;;;;N;;;;; +16ADB;BASSA VAH LETTER WA;Lo;0;L;;;;;N;;;;; +16ADC;BASSA VAH LETTER ZO;Lo;0;L;;;;;N;;;;; +16ADD;BASSA VAH LETTER GBU;Lo;0;L;;;;;N;;;;; +16ADE;BASSA VAH LETTER DO;Lo;0;L;;;;;N;;;;; +16ADF;BASSA VAH LETTER CE;Lo;0;L;;;;;N;;;;; +16AE0;BASSA VAH LETTER UWU;Lo;0;L;;;;;N;;;;; +16AE1;BASSA VAH LETTER TO;Lo;0;L;;;;;N;;;;; +16AE2;BASSA VAH LETTER BA;Lo;0;L;;;;;N;;;;; +16AE3;BASSA VAH LETTER VU;Lo;0;L;;;;;N;;;;; +16AE4;BASSA VAH LETTER YEIN;Lo;0;L;;;;;N;;;;; +16AE5;BASSA VAH LETTER PA;Lo;0;L;;;;;N;;;;; +16AE6;BASSA VAH LETTER WADDA;Lo;0;L;;;;;N;;;;; +16AE7;BASSA VAH LETTER A;Lo;0;L;;;;;N;;;;; +16AE8;BASSA VAH LETTER O;Lo;0;L;;;;;N;;;;; +16AE9;BASSA VAH LETTER OO;Lo;0;L;;;;;N;;;;; +16AEA;BASSA VAH LETTER U;Lo;0;L;;;;;N;;;;; +16AEB;BASSA VAH LETTER EE;Lo;0;L;;;;;N;;;;; +16AEC;BASSA VAH LETTER E;Lo;0;L;;;;;N;;;;; +16AED;BASSA VAH LETTER I;Lo;0;L;;;;;N;;;;; +16AF0;BASSA VAH COMBINING HIGH TONE;Mn;1;NSM;;;;;N;;;;; +16AF1;BASSA VAH COMBINING LOW TONE;Mn;1;NSM;;;;;N;;;;; +16AF2;BASSA VAH COMBINING MID TONE;Mn;1;NSM;;;;;N;;;;; +16AF3;BASSA VAH COMBINING LOW-MID TONE;Mn;1;NSM;;;;;N;;;;; +16AF4;BASSA VAH COMBINING HIGH-LOW TONE;Mn;1;NSM;;;;;N;;;;; +16AF5;BASSA VAH FULL STOP;Po;0;L;;;;;N;;;;; +16B00;PAHAWH HMONG VOWEL KEEB;Lo;0;L;;;;;N;;;;; +16B01;PAHAWH HMONG VOWEL KEEV;Lo;0;L;;;;;N;;;;; +16B02;PAHAWH HMONG VOWEL KIB;Lo;0;L;;;;;N;;;;; +16B03;PAHAWH HMONG VOWEL KIV;Lo;0;L;;;;;N;;;;; +16B04;PAHAWH HMONG VOWEL KAUB;Lo;0;L;;;;;N;;;;; +16B05;PAHAWH HMONG VOWEL KAUV;Lo;0;L;;;;;N;;;;; +16B06;PAHAWH HMONG VOWEL KUB;Lo;0;L;;;;;N;;;;; +16B07;PAHAWH HMONG VOWEL KUV;Lo;0;L;;;;;N;;;;; +16B08;PAHAWH HMONG VOWEL KEB;Lo;0;L;;;;;N;;;;; +16B09;PAHAWH HMONG VOWEL KEV;Lo;0;L;;;;;N;;;;; +16B0A;PAHAWH HMONG VOWEL KAIB;Lo;0;L;;;;;N;;;;; +16B0B;PAHAWH HMONG VOWEL KAIV;Lo;0;L;;;;;N;;;;; +16B0C;PAHAWH HMONG VOWEL KOOB;Lo;0;L;;;;;N;;;;; +16B0D;PAHAWH HMONG VOWEL KOOV;Lo;0;L;;;;;N;;;;; +16B0E;PAHAWH HMONG VOWEL KAWB;Lo;0;L;;;;;N;;;;; +16B0F;PAHAWH HMONG VOWEL KAWV;Lo;0;L;;;;;N;;;;; +16B10;PAHAWH HMONG VOWEL KUAB;Lo;0;L;;;;;N;;;;; +16B11;PAHAWH HMONG VOWEL KUAV;Lo;0;L;;;;;N;;;;; +16B12;PAHAWH HMONG VOWEL KOB;Lo;0;L;;;;;N;;;;; +16B13;PAHAWH HMONG VOWEL KOV;Lo;0;L;;;;;N;;;;; +16B14;PAHAWH HMONG VOWEL KIAB;Lo;0;L;;;;;N;;;;; +16B15;PAHAWH HMONG VOWEL KIAV;Lo;0;L;;;;;N;;;;; +16B16;PAHAWH HMONG VOWEL KAB;Lo;0;L;;;;;N;;;;; +16B17;PAHAWH HMONG VOWEL KAV;Lo;0;L;;;;;N;;;;; +16B18;PAHAWH HMONG VOWEL KWB;Lo;0;L;;;;;N;;;;; +16B19;PAHAWH HMONG VOWEL KWV;Lo;0;L;;;;;N;;;;; +16B1A;PAHAWH HMONG VOWEL KAAB;Lo;0;L;;;;;N;;;;; +16B1B;PAHAWH HMONG VOWEL KAAV;Lo;0;L;;;;;N;;;;; +16B1C;PAHAWH HMONG CONSONANT VAU;Lo;0;L;;;;;N;;;;; +16B1D;PAHAWH HMONG CONSONANT NTSAU;Lo;0;L;;;;;N;;;;; +16B1E;PAHAWH HMONG CONSONANT LAU;Lo;0;L;;;;;N;;;;; +16B1F;PAHAWH HMONG CONSONANT HAU;Lo;0;L;;;;;N;;;;; +16B20;PAHAWH HMONG CONSONANT NLAU;Lo;0;L;;;;;N;;;;; +16B21;PAHAWH HMONG CONSONANT RAU;Lo;0;L;;;;;N;;;;; +16B22;PAHAWH HMONG CONSONANT NKAU;Lo;0;L;;;;;N;;;;; +16B23;PAHAWH HMONG CONSONANT QHAU;Lo;0;L;;;;;N;;;;; +16B24;PAHAWH HMONG CONSONANT YAU;Lo;0;L;;;;;N;;;;; +16B25;PAHAWH HMONG CONSONANT HLAU;Lo;0;L;;;;;N;;;;; +16B26;PAHAWH HMONG CONSONANT MAU;Lo;0;L;;;;;N;;;;; +16B27;PAHAWH HMONG CONSONANT CHAU;Lo;0;L;;;;;N;;;;; +16B28;PAHAWH HMONG CONSONANT NCHAU;Lo;0;L;;;;;N;;;;; +16B29;PAHAWH HMONG CONSONANT HNAU;Lo;0;L;;;;;N;;;;; +16B2A;PAHAWH HMONG CONSONANT PLHAU;Lo;0;L;;;;;N;;;;; +16B2B;PAHAWH HMONG CONSONANT NTHAU;Lo;0;L;;;;;N;;;;; +16B2C;PAHAWH HMONG CONSONANT NAU;Lo;0;L;;;;;N;;;;; +16B2D;PAHAWH HMONG CONSONANT AU;Lo;0;L;;;;;N;;;;; +16B2E;PAHAWH HMONG CONSONANT XAU;Lo;0;L;;;;;N;;;;; +16B2F;PAHAWH HMONG CONSONANT CAU;Lo;0;L;;;;;N;;;;; +16B30;PAHAWH HMONG MARK CIM TUB;Mn;230;NSM;;;;;N;;;;; +16B31;PAHAWH HMONG MARK CIM SO;Mn;230;NSM;;;;;N;;;;; +16B32;PAHAWH HMONG MARK CIM KES;Mn;230;NSM;;;;;N;;;;; +16B33;PAHAWH HMONG MARK CIM KHAV;Mn;230;NSM;;;;;N;;;;; +16B34;PAHAWH HMONG MARK CIM SUAM;Mn;230;NSM;;;;;N;;;;; +16B35;PAHAWH HMONG MARK CIM HOM;Mn;230;NSM;;;;;N;;;;; +16B36;PAHAWH HMONG MARK CIM TAUM;Mn;230;NSM;;;;;N;;;;; +16B37;PAHAWH HMONG SIGN VOS THOM;Po;0;L;;;;;N;;;;; +16B38;PAHAWH HMONG SIGN VOS TSHAB CEEB;Po;0;L;;;;;N;;;;; +16B39;PAHAWH HMONG SIGN CIM CHEEM;Po;0;L;;;;;N;;;;; +16B3A;PAHAWH HMONG SIGN VOS THIAB;Po;0;L;;;;;N;;;;; +16B3B;PAHAWH HMONG SIGN VOS FEEM;Po;0;L;;;;;N;;;;; +16B3C;PAHAWH HMONG SIGN XYEEM NTXIV;So;0;L;;;;;N;;;;; +16B3D;PAHAWH HMONG SIGN XYEEM RHO;So;0;L;;;;;N;;;;; +16B3E;PAHAWH HMONG SIGN XYEEM TOV;So;0;L;;;;;N;;;;; +16B3F;PAHAWH HMONG SIGN XYEEM FAIB;So;0;L;;;;;N;;;;; +16B40;PAHAWH HMONG SIGN VOS SEEV;Lm;0;L;;;;;N;;;;; +16B41;PAHAWH HMONG SIGN MEEJ SUAB;Lm;0;L;;;;;N;;;;; +16B42;PAHAWH HMONG SIGN VOS NRUA;Lm;0;L;;;;;N;;;;; +16B43;PAHAWH HMONG SIGN IB YAM;Lm;0;L;;;;;N;;;;; +16B44;PAHAWH HMONG SIGN XAUS;Po;0;L;;;;;N;;;;; +16B45;PAHAWH HMONG SIGN CIM TSOV ROG;So;0;L;;;;;N;;;;; +16B50;PAHAWH HMONG DIGIT ZERO;Nd;0;L;;0;0;0;N;;;;; +16B51;PAHAWH HMONG DIGIT ONE;Nd;0;L;;1;1;1;N;;;;; +16B52;PAHAWH HMONG DIGIT TWO;Nd;0;L;;2;2;2;N;;;;; +16B53;PAHAWH HMONG DIGIT THREE;Nd;0;L;;3;3;3;N;;;;; +16B54;PAHAWH HMONG DIGIT FOUR;Nd;0;L;;4;4;4;N;;;;; +16B55;PAHAWH HMONG DIGIT FIVE;Nd;0;L;;5;5;5;N;;;;; +16B56;PAHAWH HMONG DIGIT SIX;Nd;0;L;;6;6;6;N;;;;; +16B57;PAHAWH HMONG DIGIT SEVEN;Nd;0;L;;7;7;7;N;;;;; +16B58;PAHAWH HMONG DIGIT EIGHT;Nd;0;L;;8;8;8;N;;;;; +16B59;PAHAWH HMONG DIGIT NINE;Nd;0;L;;9;9;9;N;;;;; +16B5B;PAHAWH HMONG NUMBER TENS;No;0;L;;;;10;N;;;;; +16B5C;PAHAWH HMONG NUMBER HUNDREDS;No;0;L;;;;100;N;;;;; +16B5D;PAHAWH HMONG NUMBER TEN THOUSANDS;No;0;L;;;;10000;N;;;;; +16B5E;PAHAWH HMONG NUMBER MILLIONS;No;0;L;;;;1000000;N;;;;; +16B5F;PAHAWH HMONG NUMBER HUNDRED MILLIONS;No;0;L;;;;100000000;N;;;;; +16B60;PAHAWH HMONG NUMBER TEN BILLIONS;No;0;L;;;;10000000000;N;;;;; +16B61;PAHAWH HMONG NUMBER TRILLIONS;No;0;L;;;;1000000000000;N;;;;; +16B63;PAHAWH HMONG SIGN VOS LUB;Lo;0;L;;;;;N;;;;; +16B64;PAHAWH HMONG SIGN XYOO;Lo;0;L;;;;;N;;;;; +16B65;PAHAWH HMONG SIGN HLI;Lo;0;L;;;;;N;;;;; +16B66;PAHAWH HMONG SIGN THIRD-STAGE HLI;Lo;0;L;;;;;N;;;;; +16B67;PAHAWH HMONG SIGN ZWJ THAJ;Lo;0;L;;;;;N;;;;; +16B68;PAHAWH HMONG SIGN HNUB;Lo;0;L;;;;;N;;;;; +16B69;PAHAWH HMONG SIGN NQIG;Lo;0;L;;;;;N;;;;; +16B6A;PAHAWH HMONG SIGN XIAB;Lo;0;L;;;;;N;;;;; +16B6B;PAHAWH HMONG SIGN NTUJ;Lo;0;L;;;;;N;;;;; +16B6C;PAHAWH HMONG SIGN AV;Lo;0;L;;;;;N;;;;; +16B6D;PAHAWH HMONG SIGN TXHEEJ CEEV;Lo;0;L;;;;;N;;;;; +16B6E;PAHAWH HMONG SIGN MEEJ TSEEB;Lo;0;L;;;;;N;;;;; +16B6F;PAHAWH HMONG SIGN TAU;Lo;0;L;;;;;N;;;;; +16B70;PAHAWH HMONG SIGN LOS;Lo;0;L;;;;;N;;;;; +16B71;PAHAWH HMONG SIGN MUS;Lo;0;L;;;;;N;;;;; +16B72;PAHAWH HMONG SIGN CIM HAIS LUS NTOG NTOG;Lo;0;L;;;;;N;;;;; +16B73;PAHAWH HMONG SIGN CIM CUAM TSHOOJ;Lo;0;L;;;;;N;;;;; +16B74;PAHAWH HMONG SIGN CIM TXWV;Lo;0;L;;;;;N;;;;; +16B75;PAHAWH HMONG SIGN CIM TXWV CHWV;Lo;0;L;;;;;N;;;;; +16B76;PAHAWH HMONG SIGN CIM PUB DAWB;Lo;0;L;;;;;N;;;;; +16B77;PAHAWH HMONG SIGN CIM NRES TOS;Lo;0;L;;;;;N;;;;; +16B7D;PAHAWH HMONG CLAN SIGN TSHEEJ;Lo;0;L;;;;;N;;;;; +16B7E;PAHAWH HMONG CLAN SIGN YEEG;Lo;0;L;;;;;N;;;;; +16B7F;PAHAWH HMONG CLAN SIGN LIS;Lo;0;L;;;;;N;;;;; +16B80;PAHAWH HMONG CLAN SIGN LAUJ;Lo;0;L;;;;;N;;;;; +16B81;PAHAWH HMONG CLAN SIGN XYOOJ;Lo;0;L;;;;;N;;;;; +16B82;PAHAWH HMONG CLAN SIGN KOO;Lo;0;L;;;;;N;;;;; +16B83;PAHAWH HMONG CLAN SIGN HAWJ;Lo;0;L;;;;;N;;;;; +16B84;PAHAWH HMONG CLAN SIGN MUAS;Lo;0;L;;;;;N;;;;; +16B85;PAHAWH HMONG CLAN SIGN THOJ;Lo;0;L;;;;;N;;;;; +16B86;PAHAWH HMONG CLAN SIGN TSAB;Lo;0;L;;;;;N;;;;; +16B87;PAHAWH HMONG CLAN SIGN PHAB;Lo;0;L;;;;;N;;;;; +16B88;PAHAWH HMONG CLAN SIGN KHAB;Lo;0;L;;;;;N;;;;; +16B89;PAHAWH HMONG CLAN SIGN HAM;Lo;0;L;;;;;N;;;;; +16B8A;PAHAWH HMONG CLAN SIGN VAJ;Lo;0;L;;;;;N;;;;; +16B8B;PAHAWH HMONG CLAN SIGN FAJ;Lo;0;L;;;;;N;;;;; +16B8C;PAHAWH HMONG CLAN SIGN YAJ;Lo;0;L;;;;;N;;;;; +16B8D;PAHAWH HMONG CLAN SIGN TSWB;Lo;0;L;;;;;N;;;;; +16B8E;PAHAWH HMONG CLAN SIGN KWM;Lo;0;L;;;;;N;;;;; +16B8F;PAHAWH HMONG CLAN SIGN VWJ;Lo;0;L;;;;;N;;;;; +16F00;MIAO LETTER PA;Lo;0;L;;;;;N;;;;; +16F01;MIAO LETTER BA;Lo;0;L;;;;;N;;;;; +16F02;MIAO LETTER YI PA;Lo;0;L;;;;;N;;;;; +16F03;MIAO LETTER PLA;Lo;0;L;;;;;N;;;;; +16F04;MIAO LETTER MA;Lo;0;L;;;;;N;;;;; +16F05;MIAO LETTER MHA;Lo;0;L;;;;;N;;;;; +16F06;MIAO LETTER ARCHAIC MA;Lo;0;L;;;;;N;;;;; +16F07;MIAO LETTER FA;Lo;0;L;;;;;N;;;;; +16F08;MIAO LETTER VA;Lo;0;L;;;;;N;;;;; +16F09;MIAO LETTER VFA;Lo;0;L;;;;;N;;;;; +16F0A;MIAO LETTER TA;Lo;0;L;;;;;N;;;;; +16F0B;MIAO LETTER DA;Lo;0;L;;;;;N;;;;; +16F0C;MIAO LETTER YI TTA;Lo;0;L;;;;;N;;;;; +16F0D;MIAO LETTER YI TA;Lo;0;L;;;;;N;;;;; +16F0E;MIAO LETTER TTA;Lo;0;L;;;;;N;;;;; +16F0F;MIAO LETTER DDA;Lo;0;L;;;;;N;;;;; +16F10;MIAO LETTER NA;Lo;0;L;;;;;N;;;;; +16F11;MIAO LETTER NHA;Lo;0;L;;;;;N;;;;; +16F12;MIAO LETTER YI NNA;Lo;0;L;;;;;N;;;;; +16F13;MIAO LETTER ARCHAIC NA;Lo;0;L;;;;;N;;;;; +16F14;MIAO LETTER NNA;Lo;0;L;;;;;N;;;;; +16F15;MIAO LETTER NNHA;Lo;0;L;;;;;N;;;;; +16F16;MIAO LETTER LA;Lo;0;L;;;;;N;;;;; +16F17;MIAO LETTER LYA;Lo;0;L;;;;;N;;;;; +16F18;MIAO LETTER LHA;Lo;0;L;;;;;N;;;;; +16F19;MIAO LETTER LHYA;Lo;0;L;;;;;N;;;;; +16F1A;MIAO LETTER TLHA;Lo;0;L;;;;;N;;;;; +16F1B;MIAO LETTER DLHA;Lo;0;L;;;;;N;;;;; +16F1C;MIAO LETTER TLHYA;Lo;0;L;;;;;N;;;;; +16F1D;MIAO LETTER DLHYA;Lo;0;L;;;;;N;;;;; +16F1E;MIAO LETTER KA;Lo;0;L;;;;;N;;;;; +16F1F;MIAO LETTER GA;Lo;0;L;;;;;N;;;;; +16F20;MIAO LETTER YI KA;Lo;0;L;;;;;N;;;;; +16F21;MIAO LETTER QA;Lo;0;L;;;;;N;;;;; +16F22;MIAO LETTER QGA;Lo;0;L;;;;;N;;;;; +16F23;MIAO LETTER NGA;Lo;0;L;;;;;N;;;;; +16F24;MIAO LETTER NGHA;Lo;0;L;;;;;N;;;;; +16F25;MIAO LETTER ARCHAIC NGA;Lo;0;L;;;;;N;;;;; +16F26;MIAO LETTER HA;Lo;0;L;;;;;N;;;;; +16F27;MIAO LETTER XA;Lo;0;L;;;;;N;;;;; +16F28;MIAO LETTER GHA;Lo;0;L;;;;;N;;;;; +16F29;MIAO LETTER GHHA;Lo;0;L;;;;;N;;;;; +16F2A;MIAO LETTER TSSA;Lo;0;L;;;;;N;;;;; +16F2B;MIAO LETTER DZZA;Lo;0;L;;;;;N;;;;; +16F2C;MIAO LETTER NYA;Lo;0;L;;;;;N;;;;; +16F2D;MIAO LETTER NYHA;Lo;0;L;;;;;N;;;;; +16F2E;MIAO LETTER TSHA;Lo;0;L;;;;;N;;;;; +16F2F;MIAO LETTER DZHA;Lo;0;L;;;;;N;;;;; +16F30;MIAO LETTER YI TSHA;Lo;0;L;;;;;N;;;;; +16F31;MIAO LETTER YI DZHA;Lo;0;L;;;;;N;;;;; +16F32;MIAO LETTER REFORMED TSHA;Lo;0;L;;;;;N;;;;; +16F33;MIAO LETTER SHA;Lo;0;L;;;;;N;;;;; +16F34;MIAO LETTER SSA;Lo;0;L;;;;;N;;;;; +16F35;MIAO LETTER ZHA;Lo;0;L;;;;;N;;;;; +16F36;MIAO LETTER ZSHA;Lo;0;L;;;;;N;;;;; +16F37;MIAO LETTER TSA;Lo;0;L;;;;;N;;;;; +16F38;MIAO LETTER DZA;Lo;0;L;;;;;N;;;;; +16F39;MIAO LETTER YI TSA;Lo;0;L;;;;;N;;;;; +16F3A;MIAO LETTER SA;Lo;0;L;;;;;N;;;;; +16F3B;MIAO LETTER ZA;Lo;0;L;;;;;N;;;;; +16F3C;MIAO LETTER ZSA;Lo;0;L;;;;;N;;;;; +16F3D;MIAO LETTER ZZA;Lo;0;L;;;;;N;;;;; +16F3E;MIAO LETTER ZZSA;Lo;0;L;;;;;N;;;;; +16F3F;MIAO LETTER ARCHAIC ZZA;Lo;0;L;;;;;N;;;;; +16F40;MIAO LETTER ZZYA;Lo;0;L;;;;;N;;;;; +16F41;MIAO LETTER ZZSYA;Lo;0;L;;;;;N;;;;; +16F42;MIAO LETTER WA;Lo;0;L;;;;;N;;;;; +16F43;MIAO LETTER AH;Lo;0;L;;;;;N;;;;; +16F44;MIAO LETTER HHA;Lo;0;L;;;;;N;;;;; +16F50;MIAO LETTER NASALIZATION;Lo;0;L;;;;;N;;;;; +16F51;MIAO SIGN ASPIRATION;Mc;0;L;;;;;N;;;;; +16F52;MIAO SIGN REFORMED VOICING;Mc;0;L;;;;;N;;;;; +16F53;MIAO SIGN REFORMED ASPIRATION;Mc;0;L;;;;;N;;;;; +16F54;MIAO VOWEL SIGN A;Mc;0;L;;;;;N;;;;; +16F55;MIAO VOWEL SIGN AA;Mc;0;L;;;;;N;;;;; +16F56;MIAO VOWEL SIGN AHH;Mc;0;L;;;;;N;;;;; +16F57;MIAO VOWEL SIGN AN;Mc;0;L;;;;;N;;;;; +16F58;MIAO VOWEL SIGN ANG;Mc;0;L;;;;;N;;;;; +16F59;MIAO VOWEL SIGN O;Mc;0;L;;;;;N;;;;; +16F5A;MIAO VOWEL SIGN OO;Mc;0;L;;;;;N;;;;; +16F5B;MIAO VOWEL SIGN WO;Mc;0;L;;;;;N;;;;; +16F5C;MIAO VOWEL SIGN W;Mc;0;L;;;;;N;;;;; +16F5D;MIAO VOWEL SIGN E;Mc;0;L;;;;;N;;;;; +16F5E;MIAO VOWEL SIGN EN;Mc;0;L;;;;;N;;;;; +16F5F;MIAO VOWEL SIGN ENG;Mc;0;L;;;;;N;;;;; +16F60;MIAO VOWEL SIGN OEY;Mc;0;L;;;;;N;;;;; +16F61;MIAO VOWEL SIGN I;Mc;0;L;;;;;N;;;;; +16F62;MIAO VOWEL SIGN IA;Mc;0;L;;;;;N;;;;; +16F63;MIAO VOWEL SIGN IAN;Mc;0;L;;;;;N;;;;; +16F64;MIAO VOWEL SIGN IANG;Mc;0;L;;;;;N;;;;; +16F65;MIAO VOWEL SIGN IO;Mc;0;L;;;;;N;;;;; +16F66;MIAO VOWEL SIGN IE;Mc;0;L;;;;;N;;;;; +16F67;MIAO VOWEL SIGN II;Mc;0;L;;;;;N;;;;; +16F68;MIAO VOWEL SIGN IU;Mc;0;L;;;;;N;;;;; +16F69;MIAO VOWEL SIGN ING;Mc;0;L;;;;;N;;;;; +16F6A;MIAO VOWEL SIGN U;Mc;0;L;;;;;N;;;;; +16F6B;MIAO VOWEL SIGN UA;Mc;0;L;;;;;N;;;;; +16F6C;MIAO VOWEL SIGN UAN;Mc;0;L;;;;;N;;;;; +16F6D;MIAO VOWEL SIGN UANG;Mc;0;L;;;;;N;;;;; +16F6E;MIAO VOWEL SIGN UU;Mc;0;L;;;;;N;;;;; +16F6F;MIAO VOWEL SIGN UEI;Mc;0;L;;;;;N;;;;; +16F70;MIAO VOWEL SIGN UNG;Mc;0;L;;;;;N;;;;; +16F71;MIAO VOWEL SIGN Y;Mc;0;L;;;;;N;;;;; +16F72;MIAO VOWEL SIGN YI;Mc;0;L;;;;;N;;;;; +16F73;MIAO VOWEL SIGN AE;Mc;0;L;;;;;N;;;;; +16F74;MIAO VOWEL SIGN AEE;Mc;0;L;;;;;N;;;;; +16F75;MIAO VOWEL SIGN ERR;Mc;0;L;;;;;N;;;;; +16F76;MIAO VOWEL SIGN ROUNDED ERR;Mc;0;L;;;;;N;;;;; +16F77;MIAO VOWEL SIGN ER;Mc;0;L;;;;;N;;;;; +16F78;MIAO VOWEL SIGN ROUNDED ER;Mc;0;L;;;;;N;;;;; +16F79;MIAO VOWEL SIGN AI;Mc;0;L;;;;;N;;;;; +16F7A;MIAO VOWEL SIGN EI;Mc;0;L;;;;;N;;;;; +16F7B;MIAO VOWEL SIGN AU;Mc;0;L;;;;;N;;;;; +16F7C;MIAO VOWEL SIGN OU;Mc;0;L;;;;;N;;;;; +16F7D;MIAO VOWEL SIGN N;Mc;0;L;;;;;N;;;;; +16F7E;MIAO VOWEL SIGN NG;Mc;0;L;;;;;N;;;;; +16F8F;MIAO TONE RIGHT;Mn;0;NSM;;;;;N;;;;; +16F90;MIAO TONE TOP RIGHT;Mn;0;NSM;;;;;N;;;;; +16F91;MIAO TONE ABOVE;Mn;0;NSM;;;;;N;;;;; +16F92;MIAO TONE BELOW;Mn;0;NSM;;;;;N;;;;; +16F93;MIAO LETTER TONE-2;Lm;0;L;;;;;N;;;;; +16F94;MIAO LETTER TONE-3;Lm;0;L;;;;;N;;;;; +16F95;MIAO LETTER TONE-4;Lm;0;L;;;;;N;;;;; +16F96;MIAO LETTER TONE-5;Lm;0;L;;;;;N;;;;; +16F97;MIAO LETTER TONE-6;Lm;0;L;;;;;N;;;;; +16F98;MIAO LETTER TONE-7;Lm;0;L;;;;;N;;;;; +16F99;MIAO LETTER TONE-8;Lm;0;L;;;;;N;;;;; +16F9A;MIAO LETTER REFORMED TONE-1;Lm;0;L;;;;;N;;;;; +16F9B;MIAO LETTER REFORMED TONE-2;Lm;0;L;;;;;N;;;;; +16F9C;MIAO LETTER REFORMED TONE-4;Lm;0;L;;;;;N;;;;; +16F9D;MIAO LETTER REFORMED TONE-5;Lm;0;L;;;;;N;;;;; +16F9E;MIAO LETTER REFORMED TONE-6;Lm;0;L;;;;;N;;;;; +16F9F;MIAO LETTER REFORMED TONE-8;Lm;0;L;;;;;N;;;;; 1B000;KATAKANA LETTER ARCHAIC E;Lo;0;L;;;;;N;;;;; 1B001;HIRAGANA LETTER ARCHAIC YE;Lo;0;L;;;;;N;;;;; +1BC00;DUPLOYAN LETTER H;Lo;0;L;;;;;N;;;;; +1BC01;DUPLOYAN LETTER X;Lo;0;L;;;;;N;;;;; +1BC02;DUPLOYAN LETTER P;Lo;0;L;;;;;N;;;;; +1BC03;DUPLOYAN LETTER T;Lo;0;L;;;;;N;;;;; +1BC04;DUPLOYAN LETTER F;Lo;0;L;;;;;N;;;;; +1BC05;DUPLOYAN LETTER K;Lo;0;L;;;;;N;;;;; +1BC06;DUPLOYAN LETTER L;Lo;0;L;;;;;N;;;;; +1BC07;DUPLOYAN LETTER B;Lo;0;L;;;;;N;;;;; +1BC08;DUPLOYAN LETTER D;Lo;0;L;;;;;N;;;;; +1BC09;DUPLOYAN LETTER V;Lo;0;L;;;;;N;;;;; +1BC0A;DUPLOYAN LETTER G;Lo;0;L;;;;;N;;;;; +1BC0B;DUPLOYAN LETTER R;Lo;0;L;;;;;N;;;;; +1BC0C;DUPLOYAN LETTER P N;Lo;0;L;;;;;N;;;;; +1BC0D;DUPLOYAN LETTER D S;Lo;0;L;;;;;N;;;;; +1BC0E;DUPLOYAN LETTER F N;Lo;0;L;;;;;N;;;;; +1BC0F;DUPLOYAN LETTER K M;Lo;0;L;;;;;N;;;;; +1BC10;DUPLOYAN LETTER R S;Lo;0;L;;;;;N;;;;; +1BC11;DUPLOYAN LETTER TH;Lo;0;L;;;;;N;;;;; +1BC12;DUPLOYAN LETTER SLOAN DH;Lo;0;L;;;;;N;;;;; +1BC13;DUPLOYAN LETTER DH;Lo;0;L;;;;;N;;;;; +1BC14;DUPLOYAN LETTER KK;Lo;0;L;;;;;N;;;;; +1BC15;DUPLOYAN LETTER SLOAN J;Lo;0;L;;;;;N;;;;; +1BC16;DUPLOYAN LETTER HL;Lo;0;L;;;;;N;;;;; +1BC17;DUPLOYAN LETTER LH;Lo;0;L;;;;;N;;;;; +1BC18;DUPLOYAN LETTER RH;Lo;0;L;;;;;N;;;;; +1BC19;DUPLOYAN LETTER M;Lo;0;L;;;;;N;;;;; +1BC1A;DUPLOYAN LETTER N;Lo;0;L;;;;;N;;;;; +1BC1B;DUPLOYAN LETTER J;Lo;0;L;;;;;N;;;;; +1BC1C;DUPLOYAN LETTER S;Lo;0;L;;;;;N;;;;; +1BC1D;DUPLOYAN LETTER M N;Lo;0;L;;;;;N;;;;; +1BC1E;DUPLOYAN LETTER N M;Lo;0;L;;;;;N;;;;; +1BC1F;DUPLOYAN LETTER J M;Lo;0;L;;;;;N;;;;; +1BC20;DUPLOYAN LETTER S J;Lo;0;L;;;;;N;;;;; +1BC21;DUPLOYAN LETTER M WITH DOT;Lo;0;L;;;;;N;;;;; +1BC22;DUPLOYAN LETTER N WITH DOT;Lo;0;L;;;;;N;;;;; +1BC23;DUPLOYAN LETTER J WITH DOT;Lo;0;L;;;;;N;;;;; +1BC24;DUPLOYAN LETTER J WITH DOTS INSIDE AND ABOVE;Lo;0;L;;;;;N;;;;; +1BC25;DUPLOYAN LETTER S WITH DOT;Lo;0;L;;;;;N;;;;; +1BC26;DUPLOYAN LETTER S WITH DOT BELOW;Lo;0;L;;;;;N;;;;; +1BC27;DUPLOYAN LETTER M S;Lo;0;L;;;;;N;;;;; +1BC28;DUPLOYAN LETTER N S;Lo;0;L;;;;;N;;;;; +1BC29;DUPLOYAN LETTER J S;Lo;0;L;;;;;N;;;;; +1BC2A;DUPLOYAN LETTER S S;Lo;0;L;;;;;N;;;;; +1BC2B;DUPLOYAN LETTER M N S;Lo;0;L;;;;;N;;;;; +1BC2C;DUPLOYAN LETTER N M S;Lo;0;L;;;;;N;;;;; +1BC2D;DUPLOYAN LETTER J M S;Lo;0;L;;;;;N;;;;; +1BC2E;DUPLOYAN LETTER S J S;Lo;0;L;;;;;N;;;;; +1BC2F;DUPLOYAN LETTER J S WITH DOT;Lo;0;L;;;;;N;;;;; +1BC30;DUPLOYAN LETTER J N;Lo;0;L;;;;;N;;;;; +1BC31;DUPLOYAN LETTER J N S;Lo;0;L;;;;;N;;;;; +1BC32;DUPLOYAN LETTER S T;Lo;0;L;;;;;N;;;;; +1BC33;DUPLOYAN LETTER S T R;Lo;0;L;;;;;N;;;;; +1BC34;DUPLOYAN LETTER S P;Lo;0;L;;;;;N;;;;; +1BC35;DUPLOYAN LETTER S P R;Lo;0;L;;;;;N;;;;; +1BC36;DUPLOYAN LETTER T S;Lo;0;L;;;;;N;;;;; +1BC37;DUPLOYAN LETTER T R S;Lo;0;L;;;;;N;;;;; +1BC38;DUPLOYAN LETTER W;Lo;0;L;;;;;N;;;;; +1BC39;DUPLOYAN LETTER WH;Lo;0;L;;;;;N;;;;; +1BC3A;DUPLOYAN LETTER W R;Lo;0;L;;;;;N;;;;; +1BC3B;DUPLOYAN LETTER S N;Lo;0;L;;;;;N;;;;; +1BC3C;DUPLOYAN LETTER S M;Lo;0;L;;;;;N;;;;; +1BC3D;DUPLOYAN LETTER K R S;Lo;0;L;;;;;N;;;;; +1BC3E;DUPLOYAN LETTER G R S;Lo;0;L;;;;;N;;;;; +1BC3F;DUPLOYAN LETTER S K;Lo;0;L;;;;;N;;;;; +1BC40;DUPLOYAN LETTER S K R;Lo;0;L;;;;;N;;;;; +1BC41;DUPLOYAN LETTER A;Lo;0;L;;;;;N;;;;; +1BC42;DUPLOYAN LETTER SLOAN OW;Lo;0;L;;;;;N;;;;; +1BC43;DUPLOYAN LETTER OA;Lo;0;L;;;;;N;;;;; +1BC44;DUPLOYAN LETTER O;Lo;0;L;;;;;N;;;;; +1BC45;DUPLOYAN LETTER AOU;Lo;0;L;;;;;N;;;;; +1BC46;DUPLOYAN LETTER I;Lo;0;L;;;;;N;;;;; +1BC47;DUPLOYAN LETTER E;Lo;0;L;;;;;N;;;;; +1BC48;DUPLOYAN LETTER IE;Lo;0;L;;;;;N;;;;; +1BC49;DUPLOYAN LETTER SHORT I;Lo;0;L;;;;;N;;;;; +1BC4A;DUPLOYAN LETTER UI;Lo;0;L;;;;;N;;;;; +1BC4B;DUPLOYAN LETTER EE;Lo;0;L;;;;;N;;;;; +1BC4C;DUPLOYAN LETTER SLOAN EH;Lo;0;L;;;;;N;;;;; +1BC4D;DUPLOYAN LETTER ROMANIAN I;Lo;0;L;;;;;N;;;;; +1BC4E;DUPLOYAN LETTER SLOAN EE;Lo;0;L;;;;;N;;;;; +1BC4F;DUPLOYAN LETTER LONG I;Lo;0;L;;;;;N;;;;; +1BC50;DUPLOYAN LETTER YE;Lo;0;L;;;;;N;;;;; +1BC51;DUPLOYAN LETTER U;Lo;0;L;;;;;N;;;;; +1BC52;DUPLOYAN LETTER EU;Lo;0;L;;;;;N;;;;; +1BC53;DUPLOYAN LETTER XW;Lo;0;L;;;;;N;;;;; +1BC54;DUPLOYAN LETTER U N;Lo;0;L;;;;;N;;;;; +1BC55;DUPLOYAN LETTER LONG U;Lo;0;L;;;;;N;;;;; +1BC56;DUPLOYAN LETTER ROMANIAN U;Lo;0;L;;;;;N;;;;; +1BC57;DUPLOYAN LETTER UH;Lo;0;L;;;;;N;;;;; +1BC58;DUPLOYAN LETTER SLOAN U;Lo;0;L;;;;;N;;;;; +1BC59;DUPLOYAN LETTER OOH;Lo;0;L;;;;;N;;;;; +1BC5A;DUPLOYAN LETTER OW;Lo;0;L;;;;;N;;;;; +1BC5B;DUPLOYAN LETTER OU;Lo;0;L;;;;;N;;;;; +1BC5C;DUPLOYAN LETTER WA;Lo;0;L;;;;;N;;;;; +1BC5D;DUPLOYAN LETTER WO;Lo;0;L;;;;;N;;;;; +1BC5E;DUPLOYAN LETTER WI;Lo;0;L;;;;;N;;;;; +1BC5F;DUPLOYAN LETTER WEI;Lo;0;L;;;;;N;;;;; +1BC60;DUPLOYAN LETTER WOW;Lo;0;L;;;;;N;;;;; +1BC61;DUPLOYAN LETTER NASAL U;Lo;0;L;;;;;N;;;;; +1BC62;DUPLOYAN LETTER NASAL O;Lo;0;L;;;;;N;;;;; +1BC63;DUPLOYAN LETTER NASAL I;Lo;0;L;;;;;N;;;;; +1BC64;DUPLOYAN LETTER NASAL A;Lo;0;L;;;;;N;;;;; +1BC65;DUPLOYAN LETTER PERNIN AN;Lo;0;L;;;;;N;;;;; +1BC66;DUPLOYAN LETTER PERNIN AM;Lo;0;L;;;;;N;;;;; +1BC67;DUPLOYAN LETTER SLOAN EN;Lo;0;L;;;;;N;;;;; +1BC68;DUPLOYAN LETTER SLOAN AN;Lo;0;L;;;;;N;;;;; +1BC69;DUPLOYAN LETTER SLOAN ON;Lo;0;L;;;;;N;;;;; +1BC6A;DUPLOYAN LETTER VOCALIC M;Lo;0;L;;;;;N;;;;; +1BC70;DUPLOYAN AFFIX LEFT HORIZONTAL SECANT;Lo;0;L;;;;;N;;;;; +1BC71;DUPLOYAN AFFIX MID HORIZONTAL SECANT;Lo;0;L;;;;;N;;;;; +1BC72;DUPLOYAN AFFIX RIGHT HORIZONTAL SECANT;Lo;0;L;;;;;N;;;;; +1BC73;DUPLOYAN AFFIX LOW VERTICAL SECANT;Lo;0;L;;;;;N;;;;; +1BC74;DUPLOYAN AFFIX MID VERTICAL SECANT;Lo;0;L;;;;;N;;;;; +1BC75;DUPLOYAN AFFIX HIGH VERTICAL SECANT;Lo;0;L;;;;;N;;;;; +1BC76;DUPLOYAN AFFIX ATTACHED SECANT;Lo;0;L;;;;;N;;;;; +1BC77;DUPLOYAN AFFIX ATTACHED LEFT-TO-RIGHT SECANT;Lo;0;L;;;;;N;;;;; +1BC78;DUPLOYAN AFFIX ATTACHED TANGENT;Lo;0;L;;;;;N;;;;; +1BC79;DUPLOYAN AFFIX ATTACHED TAIL;Lo;0;L;;;;;N;;;;; +1BC7A;DUPLOYAN AFFIX ATTACHED E HOOK;Lo;0;L;;;;;N;;;;; +1BC7B;DUPLOYAN AFFIX ATTACHED I HOOK;Lo;0;L;;;;;N;;;;; +1BC7C;DUPLOYAN AFFIX ATTACHED TANGENT HOOK;Lo;0;L;;;;;N;;;;; +1BC80;DUPLOYAN AFFIX HIGH ACUTE;Lo;0;L;;;;;N;;;;; +1BC81;DUPLOYAN AFFIX HIGH TIGHT ACUTE;Lo;0;L;;;;;N;;;;; +1BC82;DUPLOYAN AFFIX HIGH GRAVE;Lo;0;L;;;;;N;;;;; +1BC83;DUPLOYAN AFFIX HIGH LONG GRAVE;Lo;0;L;;;;;N;;;;; +1BC84;DUPLOYAN AFFIX HIGH DOT;Lo;0;L;;;;;N;;;;; +1BC85;DUPLOYAN AFFIX HIGH CIRCLE;Lo;0;L;;;;;N;;;;; +1BC86;DUPLOYAN AFFIX HIGH LINE;Lo;0;L;;;;;N;;;;; +1BC87;DUPLOYAN AFFIX HIGH WAVE;Lo;0;L;;;;;N;;;;; +1BC88;DUPLOYAN AFFIX HIGH VERTICAL;Lo;0;L;;;;;N;;;;; +1BC90;DUPLOYAN AFFIX LOW ACUTE;Lo;0;L;;;;;N;;;;; +1BC91;DUPLOYAN AFFIX LOW TIGHT ACUTE;Lo;0;L;;;;;N;;;;; +1BC92;DUPLOYAN AFFIX LOW GRAVE;Lo;0;L;;;;;N;;;;; +1BC93;DUPLOYAN AFFIX LOW LONG GRAVE;Lo;0;L;;;;;N;;;;; +1BC94;DUPLOYAN AFFIX LOW DOT;Lo;0;L;;;;;N;;;;; +1BC95;DUPLOYAN AFFIX LOW CIRCLE;Lo;0;L;;;;;N;;;;; +1BC96;DUPLOYAN AFFIX LOW LINE;Lo;0;L;;;;;N;;;;; +1BC97;DUPLOYAN AFFIX LOW WAVE;Lo;0;L;;;;;N;;;;; +1BC98;DUPLOYAN AFFIX LOW VERTICAL;Lo;0;L;;;;;N;;;;; +1BC99;DUPLOYAN AFFIX LOW ARROW;Lo;0;L;;;;;N;;;;; +1BC9C;DUPLOYAN SIGN O WITH CROSS;So;0;L;;;;;N;;;;; +1BC9D;DUPLOYAN THICK LETTER SELECTOR;Mn;0;NSM;;;;;N;;;;; +1BC9E;DUPLOYAN DOUBLE MARK;Mn;1;NSM;;;;;N;;;;; +1BC9F;DUPLOYAN PUNCTUATION CHINOOK FULL STOP;Po;0;L;;;;;N;;;;; +1BCA0;SHORTHAND FORMAT LETTER OVERLAP;Cf;0;BN;;;;;N;;;;; +1BCA1;SHORTHAND FORMAT CONTINUING OVERLAP;Cf;0;BN;;;;;N;;;;; +1BCA2;SHORTHAND FORMAT DOWN STEP;Cf;0;BN;;;;;N;;;;; +1BCA3;SHORTHAND FORMAT UP STEP;Cf;0;BN;;;;;N;;;;; 1D000;BYZANTINE MUSICAL SYMBOL PSILI;So;0;L;;;;;N;;;;; 1D001;BYZANTINE MUSICAL SYMBOL DASEIA;So;0;L;;;;;N;;;;; 1D002;BYZANTINE MUSICAL SYMBOL PERISPOMENI;So;0;L;;;;;N;;;;; @@ -21599,6 +24251,362 @@ 1D7FD;MATHEMATICAL MONOSPACE DIGIT SEVEN;Nd;0;EN;<font> 0037;7;7;7;N;;;;; 1D7FE;MATHEMATICAL MONOSPACE DIGIT EIGHT;Nd;0;EN;<font> 0038;8;8;8;N;;;;; 1D7FF;MATHEMATICAL MONOSPACE DIGIT NINE;Nd;0;EN;<font> 0039;9;9;9;N;;;;; +1E800;MENDE KIKAKUI SYLLABLE M001 KI;Lo;0;R;;;;;N;;;;; +1E801;MENDE KIKAKUI SYLLABLE M002 KA;Lo;0;R;;;;;N;;;;; +1E802;MENDE KIKAKUI SYLLABLE M003 KU;Lo;0;R;;;;;N;;;;; +1E803;MENDE KIKAKUI SYLLABLE M065 KEE;Lo;0;R;;;;;N;;;;; +1E804;MENDE KIKAKUI SYLLABLE M095 KE;Lo;0;R;;;;;N;;;;; +1E805;MENDE KIKAKUI SYLLABLE M076 KOO;Lo;0;R;;;;;N;;;;; +1E806;MENDE KIKAKUI SYLLABLE M048 KO;Lo;0;R;;;;;N;;;;; +1E807;MENDE KIKAKUI SYLLABLE M179 KUA;Lo;0;R;;;;;N;;;;; +1E808;MENDE KIKAKUI SYLLABLE M004 WI;Lo;0;R;;;;;N;;;;; +1E809;MENDE KIKAKUI SYLLABLE M005 WA;Lo;0;R;;;;;N;;;;; +1E80A;MENDE KIKAKUI SYLLABLE M006 WU;Lo;0;R;;;;;N;;;;; +1E80B;MENDE KIKAKUI SYLLABLE M126 WEE;Lo;0;R;;;;;N;;;;; +1E80C;MENDE KIKAKUI SYLLABLE M118 WE;Lo;0;R;;;;;N;;;;; +1E80D;MENDE KIKAKUI SYLLABLE M114 WOO;Lo;0;R;;;;;N;;;;; +1E80E;MENDE KIKAKUI SYLLABLE M045 WO;Lo;0;R;;;;;N;;;;; +1E80F;MENDE KIKAKUI SYLLABLE M194 WUI;Lo;0;R;;;;;N;;;;; +1E810;MENDE KIKAKUI SYLLABLE M143 WEI;Lo;0;R;;;;;N;;;;; +1E811;MENDE KIKAKUI SYLLABLE M061 WVI;Lo;0;R;;;;;N;;;;; +1E812;MENDE KIKAKUI SYLLABLE M049 WVA;Lo;0;R;;;;;N;;;;; +1E813;MENDE KIKAKUI SYLLABLE M139 WVE;Lo;0;R;;;;;N;;;;; +1E814;MENDE KIKAKUI SYLLABLE M007 MIN;Lo;0;R;;;;;N;;;;; +1E815;MENDE KIKAKUI SYLLABLE M008 MAN;Lo;0;R;;;;;N;;;;; +1E816;MENDE KIKAKUI SYLLABLE M009 MUN;Lo;0;R;;;;;N;;;;; +1E817;MENDE KIKAKUI SYLLABLE M059 MEN;Lo;0;R;;;;;N;;;;; +1E818;MENDE KIKAKUI SYLLABLE M094 MON;Lo;0;R;;;;;N;;;;; +1E819;MENDE KIKAKUI SYLLABLE M154 MUAN;Lo;0;R;;;;;N;;;;; +1E81A;MENDE KIKAKUI SYLLABLE M189 MUEN;Lo;0;R;;;;;N;;;;; +1E81B;MENDE KIKAKUI SYLLABLE M010 BI;Lo;0;R;;;;;N;;;;; +1E81C;MENDE KIKAKUI SYLLABLE M011 BA;Lo;0;R;;;;;N;;;;; +1E81D;MENDE KIKAKUI SYLLABLE M012 BU;Lo;0;R;;;;;N;;;;; +1E81E;MENDE KIKAKUI SYLLABLE M150 BEE;Lo;0;R;;;;;N;;;;; +1E81F;MENDE KIKAKUI SYLLABLE M097 BE;Lo;0;R;;;;;N;;;;; +1E820;MENDE KIKAKUI SYLLABLE M103 BOO;Lo;0;R;;;;;N;;;;; +1E821;MENDE KIKAKUI SYLLABLE M138 BO;Lo;0;R;;;;;N;;;;; +1E822;MENDE KIKAKUI SYLLABLE M013 I;Lo;0;R;;;;;N;;;;; +1E823;MENDE KIKAKUI SYLLABLE M014 A;Lo;0;R;;;;;N;;;;; +1E824;MENDE KIKAKUI SYLLABLE M015 U;Lo;0;R;;;;;N;;;;; +1E825;MENDE KIKAKUI SYLLABLE M163 EE;Lo;0;R;;;;;N;;;;; +1E826;MENDE KIKAKUI SYLLABLE M100 E;Lo;0;R;;;;;N;;;;; +1E827;MENDE KIKAKUI SYLLABLE M165 OO;Lo;0;R;;;;;N;;;;; +1E828;MENDE KIKAKUI SYLLABLE M147 O;Lo;0;R;;;;;N;;;;; +1E829;MENDE KIKAKUI SYLLABLE M137 EI;Lo;0;R;;;;;N;;;;; +1E82A;MENDE KIKAKUI SYLLABLE M131 IN;Lo;0;R;;;;;N;;;;; +1E82B;MENDE KIKAKUI SYLLABLE M135 IN;Lo;0;R;;;;;N;;;;; +1E82C;MENDE KIKAKUI SYLLABLE M195 AN;Lo;0;R;;;;;N;;;;; +1E82D;MENDE KIKAKUI SYLLABLE M178 EN;Lo;0;R;;;;;N;;;;; +1E82E;MENDE KIKAKUI SYLLABLE M019 SI;Lo;0;R;;;;;N;;;;; +1E82F;MENDE KIKAKUI SYLLABLE M020 SA;Lo;0;R;;;;;N;;;;; +1E830;MENDE KIKAKUI SYLLABLE M021 SU;Lo;0;R;;;;;N;;;;; +1E831;MENDE KIKAKUI SYLLABLE M162 SEE;Lo;0;R;;;;;N;;;;; +1E832;MENDE KIKAKUI SYLLABLE M116 SE;Lo;0;R;;;;;N;;;;; +1E833;MENDE KIKAKUI SYLLABLE M136 SOO;Lo;0;R;;;;;N;;;;; +1E834;MENDE KIKAKUI SYLLABLE M079 SO;Lo;0;R;;;;;N;;;;; +1E835;MENDE KIKAKUI SYLLABLE M196 SIA;Lo;0;R;;;;;N;;;;; +1E836;MENDE KIKAKUI SYLLABLE M025 LI;Lo;0;R;;;;;N;;;;; +1E837;MENDE KIKAKUI SYLLABLE M026 LA;Lo;0;R;;;;;N;;;;; +1E838;MENDE KIKAKUI SYLLABLE M027 LU;Lo;0;R;;;;;N;;;;; +1E839;MENDE KIKAKUI SYLLABLE M084 LEE;Lo;0;R;;;;;N;;;;; +1E83A;MENDE KIKAKUI SYLLABLE M073 LE;Lo;0;R;;;;;N;;;;; +1E83B;MENDE KIKAKUI SYLLABLE M054 LOO;Lo;0;R;;;;;N;;;;; +1E83C;MENDE KIKAKUI SYLLABLE M153 LO;Lo;0;R;;;;;N;;;;; +1E83D;MENDE KIKAKUI SYLLABLE M110 LONG LE;Lo;0;R;;;;;N;;;;; +1E83E;MENDE KIKAKUI SYLLABLE M016 DI;Lo;0;R;;;;;N;;;;; +1E83F;MENDE KIKAKUI SYLLABLE M017 DA;Lo;0;R;;;;;N;;;;; +1E840;MENDE KIKAKUI SYLLABLE M018 DU;Lo;0;R;;;;;N;;;;; +1E841;MENDE KIKAKUI SYLLABLE M089 DEE;Lo;0;R;;;;;N;;;;; +1E842;MENDE KIKAKUI SYLLABLE M180 DOO;Lo;0;R;;;;;N;;;;; +1E843;MENDE KIKAKUI SYLLABLE M181 DO;Lo;0;R;;;;;N;;;;; +1E844;MENDE KIKAKUI SYLLABLE M022 TI;Lo;0;R;;;;;N;;;;; +1E845;MENDE KIKAKUI SYLLABLE M023 TA;Lo;0;R;;;;;N;;;;; +1E846;MENDE KIKAKUI SYLLABLE M024 TU;Lo;0;R;;;;;N;;;;; +1E847;MENDE KIKAKUI SYLLABLE M091 TEE;Lo;0;R;;;;;N;;;;; +1E848;MENDE KIKAKUI SYLLABLE M055 TE;Lo;0;R;;;;;N;;;;; +1E849;MENDE KIKAKUI SYLLABLE M104 TOO;Lo;0;R;;;;;N;;;;; +1E84A;MENDE KIKAKUI SYLLABLE M069 TO;Lo;0;R;;;;;N;;;;; +1E84B;MENDE KIKAKUI SYLLABLE M028 JI;Lo;0;R;;;;;N;;;;; +1E84C;MENDE KIKAKUI SYLLABLE M029 JA;Lo;0;R;;;;;N;;;;; +1E84D;MENDE KIKAKUI SYLLABLE M030 JU;Lo;0;R;;;;;N;;;;; +1E84E;MENDE KIKAKUI SYLLABLE M157 JEE;Lo;0;R;;;;;N;;;;; +1E84F;MENDE KIKAKUI SYLLABLE M113 JE;Lo;0;R;;;;;N;;;;; +1E850;MENDE KIKAKUI SYLLABLE M160 JOO;Lo;0;R;;;;;N;;;;; +1E851;MENDE KIKAKUI SYLLABLE M063 JO;Lo;0;R;;;;;N;;;;; +1E852;MENDE KIKAKUI SYLLABLE M175 LONG JO;Lo;0;R;;;;;N;;;;; +1E853;MENDE KIKAKUI SYLLABLE M031 YI;Lo;0;R;;;;;N;;;;; +1E854;MENDE KIKAKUI SYLLABLE M032 YA;Lo;0;R;;;;;N;;;;; +1E855;MENDE KIKAKUI SYLLABLE M033 YU;Lo;0;R;;;;;N;;;;; +1E856;MENDE KIKAKUI SYLLABLE M109 YEE;Lo;0;R;;;;;N;;;;; +1E857;MENDE KIKAKUI SYLLABLE M080 YE;Lo;0;R;;;;;N;;;;; +1E858;MENDE KIKAKUI SYLLABLE M141 YOO;Lo;0;R;;;;;N;;;;; +1E859;MENDE KIKAKUI SYLLABLE M121 YO;Lo;0;R;;;;;N;;;;; +1E85A;MENDE KIKAKUI SYLLABLE M034 FI;Lo;0;R;;;;;N;;;;; +1E85B;MENDE KIKAKUI SYLLABLE M035 FA;Lo;0;R;;;;;N;;;;; +1E85C;MENDE KIKAKUI SYLLABLE M036 FU;Lo;0;R;;;;;N;;;;; +1E85D;MENDE KIKAKUI SYLLABLE M078 FEE;Lo;0;R;;;;;N;;;;; +1E85E;MENDE KIKAKUI SYLLABLE M075 FE;Lo;0;R;;;;;N;;;;; +1E85F;MENDE KIKAKUI SYLLABLE M133 FOO;Lo;0;R;;;;;N;;;;; +1E860;MENDE KIKAKUI SYLLABLE M088 FO;Lo;0;R;;;;;N;;;;; +1E861;MENDE KIKAKUI SYLLABLE M197 FUA;Lo;0;R;;;;;N;;;;; +1E862;MENDE KIKAKUI SYLLABLE M101 FAN;Lo;0;R;;;;;N;;;;; +1E863;MENDE KIKAKUI SYLLABLE M037 NIN;Lo;0;R;;;;;N;;;;; +1E864;MENDE KIKAKUI SYLLABLE M038 NAN;Lo;0;R;;;;;N;;;;; +1E865;MENDE KIKAKUI SYLLABLE M039 NUN;Lo;0;R;;;;;N;;;;; +1E866;MENDE KIKAKUI SYLLABLE M117 NEN;Lo;0;R;;;;;N;;;;; +1E867;MENDE KIKAKUI SYLLABLE M169 NON;Lo;0;R;;;;;N;;;;; +1E868;MENDE KIKAKUI SYLLABLE M176 HI;Lo;0;R;;;;;N;;;;; +1E869;MENDE KIKAKUI SYLLABLE M041 HA;Lo;0;R;;;;;N;;;;; +1E86A;MENDE KIKAKUI SYLLABLE M186 HU;Lo;0;R;;;;;N;;;;; +1E86B;MENDE KIKAKUI SYLLABLE M040 HEE;Lo;0;R;;;;;N;;;;; +1E86C;MENDE KIKAKUI SYLLABLE M096 HE;Lo;0;R;;;;;N;;;;; +1E86D;MENDE KIKAKUI SYLLABLE M042 HOO;Lo;0;R;;;;;N;;;;; +1E86E;MENDE KIKAKUI SYLLABLE M140 HO;Lo;0;R;;;;;N;;;;; +1E86F;MENDE KIKAKUI SYLLABLE M083 HEEI;Lo;0;R;;;;;N;;;;; +1E870;MENDE KIKAKUI SYLLABLE M128 HOOU;Lo;0;R;;;;;N;;;;; +1E871;MENDE KIKAKUI SYLLABLE M053 HIN;Lo;0;R;;;;;N;;;;; +1E872;MENDE KIKAKUI SYLLABLE M130 HAN;Lo;0;R;;;;;N;;;;; +1E873;MENDE KIKAKUI SYLLABLE M087 HUN;Lo;0;R;;;;;N;;;;; +1E874;MENDE KIKAKUI SYLLABLE M052 HEN;Lo;0;R;;;;;N;;;;; +1E875;MENDE KIKAKUI SYLLABLE M193 HON;Lo;0;R;;;;;N;;;;; +1E876;MENDE KIKAKUI SYLLABLE M046 HUAN;Lo;0;R;;;;;N;;;;; +1E877;MENDE KIKAKUI SYLLABLE M090 NGGI;Lo;0;R;;;;;N;;;;; +1E878;MENDE KIKAKUI SYLLABLE M043 NGGA;Lo;0;R;;;;;N;;;;; +1E879;MENDE KIKAKUI SYLLABLE M082 NGGU;Lo;0;R;;;;;N;;;;; +1E87A;MENDE KIKAKUI SYLLABLE M115 NGGEE;Lo;0;R;;;;;N;;;;; +1E87B;MENDE KIKAKUI SYLLABLE M146 NGGE;Lo;0;R;;;;;N;;;;; +1E87C;MENDE KIKAKUI SYLLABLE M156 NGGOO;Lo;0;R;;;;;N;;;;; +1E87D;MENDE KIKAKUI SYLLABLE M120 NGGO;Lo;0;R;;;;;N;;;;; +1E87E;MENDE KIKAKUI SYLLABLE M159 NGGAA;Lo;0;R;;;;;N;;;;; +1E87F;MENDE KIKAKUI SYLLABLE M127 NGGUA;Lo;0;R;;;;;N;;;;; +1E880;MENDE KIKAKUI SYLLABLE M086 LONG NGGE;Lo;0;R;;;;;N;;;;; +1E881;MENDE KIKAKUI SYLLABLE M106 LONG NGGOO;Lo;0;R;;;;;N;;;;; +1E882;MENDE KIKAKUI SYLLABLE M183 LONG NGGO;Lo;0;R;;;;;N;;;;; +1E883;MENDE KIKAKUI SYLLABLE M155 GI;Lo;0;R;;;;;N;;;;; +1E884;MENDE KIKAKUI SYLLABLE M111 GA;Lo;0;R;;;;;N;;;;; +1E885;MENDE KIKAKUI SYLLABLE M168 GU;Lo;0;R;;;;;N;;;;; +1E886;MENDE KIKAKUI SYLLABLE M190 GEE;Lo;0;R;;;;;N;;;;; +1E887;MENDE KIKAKUI SYLLABLE M166 GUEI;Lo;0;R;;;;;N;;;;; +1E888;MENDE KIKAKUI SYLLABLE M167 GUAN;Lo;0;R;;;;;N;;;;; +1E889;MENDE KIKAKUI SYLLABLE M184 NGEN;Lo;0;R;;;;;N;;;;; +1E88A;MENDE KIKAKUI SYLLABLE M057 NGON;Lo;0;R;;;;;N;;;;; +1E88B;MENDE KIKAKUI SYLLABLE M177 NGUAN;Lo;0;R;;;;;N;;;;; +1E88C;MENDE KIKAKUI SYLLABLE M068 PI;Lo;0;R;;;;;N;;;;; +1E88D;MENDE KIKAKUI SYLLABLE M099 PA;Lo;0;R;;;;;N;;;;; +1E88E;MENDE KIKAKUI SYLLABLE M050 PU;Lo;0;R;;;;;N;;;;; +1E88F;MENDE KIKAKUI SYLLABLE M081 PEE;Lo;0;R;;;;;N;;;;; +1E890;MENDE KIKAKUI SYLLABLE M051 PE;Lo;0;R;;;;;N;;;;; +1E891;MENDE KIKAKUI SYLLABLE M102 POO;Lo;0;R;;;;;N;;;;; +1E892;MENDE KIKAKUI SYLLABLE M066 PO;Lo;0;R;;;;;N;;;;; +1E893;MENDE KIKAKUI SYLLABLE M145 MBI;Lo;0;R;;;;;N;;;;; +1E894;MENDE KIKAKUI SYLLABLE M062 MBA;Lo;0;R;;;;;N;;;;; +1E895;MENDE KIKAKUI SYLLABLE M122 MBU;Lo;0;R;;;;;N;;;;; +1E896;MENDE KIKAKUI SYLLABLE M047 MBEE;Lo;0;R;;;;;N;;;;; +1E897;MENDE KIKAKUI SYLLABLE M188 MBEE;Lo;0;R;;;;;N;;;;; +1E898;MENDE KIKAKUI SYLLABLE M072 MBE;Lo;0;R;;;;;N;;;;; +1E899;MENDE KIKAKUI SYLLABLE M172 MBOO;Lo;0;R;;;;;N;;;;; +1E89A;MENDE KIKAKUI SYLLABLE M174 MBO;Lo;0;R;;;;;N;;;;; +1E89B;MENDE KIKAKUI SYLLABLE M187 MBUU;Lo;0;R;;;;;N;;;;; +1E89C;MENDE KIKAKUI SYLLABLE M161 LONG MBE;Lo;0;R;;;;;N;;;;; +1E89D;MENDE KIKAKUI SYLLABLE M105 LONG MBOO;Lo;0;R;;;;;N;;;;; +1E89E;MENDE KIKAKUI SYLLABLE M142 LONG MBO;Lo;0;R;;;;;N;;;;; +1E89F;MENDE KIKAKUI SYLLABLE M132 KPI;Lo;0;R;;;;;N;;;;; +1E8A0;MENDE KIKAKUI SYLLABLE M092 KPA;Lo;0;R;;;;;N;;;;; +1E8A1;MENDE KIKAKUI SYLLABLE M074 KPU;Lo;0;R;;;;;N;;;;; +1E8A2;MENDE KIKAKUI SYLLABLE M044 KPEE;Lo;0;R;;;;;N;;;;; +1E8A3;MENDE KIKAKUI SYLLABLE M108 KPE;Lo;0;R;;;;;N;;;;; +1E8A4;MENDE KIKAKUI SYLLABLE M112 KPOO;Lo;0;R;;;;;N;;;;; +1E8A5;MENDE KIKAKUI SYLLABLE M158 KPO;Lo;0;R;;;;;N;;;;; +1E8A6;MENDE KIKAKUI SYLLABLE M124 GBI;Lo;0;R;;;;;N;;;;; +1E8A7;MENDE KIKAKUI SYLLABLE M056 GBA;Lo;0;R;;;;;N;;;;; +1E8A8;MENDE KIKAKUI SYLLABLE M148 GBU;Lo;0;R;;;;;N;;;;; +1E8A9;MENDE KIKAKUI SYLLABLE M093 GBEE;Lo;0;R;;;;;N;;;;; +1E8AA;MENDE KIKAKUI SYLLABLE M107 GBE;Lo;0;R;;;;;N;;;;; +1E8AB;MENDE KIKAKUI SYLLABLE M071 GBOO;Lo;0;R;;;;;N;;;;; +1E8AC;MENDE KIKAKUI SYLLABLE M070 GBO;Lo;0;R;;;;;N;;;;; +1E8AD;MENDE KIKAKUI SYLLABLE M171 RA;Lo;0;R;;;;;N;;;;; +1E8AE;MENDE KIKAKUI SYLLABLE M123 NDI;Lo;0;R;;;;;N;;;;; +1E8AF;MENDE KIKAKUI SYLLABLE M129 NDA;Lo;0;R;;;;;N;;;;; +1E8B0;MENDE KIKAKUI SYLLABLE M125 NDU;Lo;0;R;;;;;N;;;;; +1E8B1;MENDE KIKAKUI SYLLABLE M191 NDEE;Lo;0;R;;;;;N;;;;; +1E8B2;MENDE KIKAKUI SYLLABLE M119 NDE;Lo;0;R;;;;;N;;;;; +1E8B3;MENDE KIKAKUI SYLLABLE M067 NDOO;Lo;0;R;;;;;N;;;;; +1E8B4;MENDE KIKAKUI SYLLABLE M064 NDO;Lo;0;R;;;;;N;;;;; +1E8B5;MENDE KIKAKUI SYLLABLE M152 NJA;Lo;0;R;;;;;N;;;;; +1E8B6;MENDE KIKAKUI SYLLABLE M192 NJU;Lo;0;R;;;;;N;;;;; +1E8B7;MENDE KIKAKUI SYLLABLE M149 NJEE;Lo;0;R;;;;;N;;;;; +1E8B8;MENDE KIKAKUI SYLLABLE M134 NJOO;Lo;0;R;;;;;N;;;;; +1E8B9;MENDE KIKAKUI SYLLABLE M182 VI;Lo;0;R;;;;;N;;;;; +1E8BA;MENDE KIKAKUI SYLLABLE M185 VA;Lo;0;R;;;;;N;;;;; +1E8BB;MENDE KIKAKUI SYLLABLE M151 VU;Lo;0;R;;;;;N;;;;; +1E8BC;MENDE KIKAKUI SYLLABLE M173 VEE;Lo;0;R;;;;;N;;;;; +1E8BD;MENDE KIKAKUI SYLLABLE M085 VE;Lo;0;R;;;;;N;;;;; +1E8BE;MENDE KIKAKUI SYLLABLE M144 VOO;Lo;0;R;;;;;N;;;;; +1E8BF;MENDE KIKAKUI SYLLABLE M077 VO;Lo;0;R;;;;;N;;;;; +1E8C0;MENDE KIKAKUI SYLLABLE M164 NYIN;Lo;0;R;;;;;N;;;;; +1E8C1;MENDE KIKAKUI SYLLABLE M058 NYAN;Lo;0;R;;;;;N;;;;; +1E8C2;MENDE KIKAKUI SYLLABLE M170 NYUN;Lo;0;R;;;;;N;;;;; +1E8C3;MENDE KIKAKUI SYLLABLE M098 NYEN;Lo;0;R;;;;;N;;;;; +1E8C4;MENDE KIKAKUI SYLLABLE M060 NYON;Lo;0;R;;;;;N;;;;; +1E8C7;MENDE KIKAKUI DIGIT ONE;No;0;R;;;;1;N;;;;; +1E8C8;MENDE KIKAKUI DIGIT TWO;No;0;R;;;;2;N;;;;; +1E8C9;MENDE KIKAKUI DIGIT THREE;No;0;R;;;;3;N;;;;; +1E8CA;MENDE KIKAKUI DIGIT FOUR;No;0;R;;;;4;N;;;;; +1E8CB;MENDE KIKAKUI DIGIT FIVE;No;0;R;;;;5;N;;;;; +1E8CC;MENDE KIKAKUI DIGIT SIX;No;0;R;;;;6;N;;;;; +1E8CD;MENDE KIKAKUI DIGIT SEVEN;No;0;R;;;;7;N;;;;; +1E8CE;MENDE KIKAKUI DIGIT EIGHT;No;0;R;;;;8;N;;;;; +1E8CF;MENDE KIKAKUI DIGIT NINE;No;0;R;;;;9;N;;;;; +1E8D0;MENDE KIKAKUI COMBINING NUMBER TEENS;Mn;220;NSM;;;;;N;;;;; +1E8D1;MENDE KIKAKUI COMBINING NUMBER TENS;Mn;220;NSM;;;;;N;;;;; +1E8D2;MENDE KIKAKUI COMBINING NUMBER HUNDREDS;Mn;220;NSM;;;;;N;;;;; +1E8D3;MENDE KIKAKUI COMBINING NUMBER THOUSANDS;Mn;220;NSM;;;;;N;;;;; +1E8D4;MENDE KIKAKUI COMBINING NUMBER TEN THOUSANDS;Mn;220;NSM;;;;;N;;;;; +1E8D5;MENDE KIKAKUI COMBINING NUMBER HUNDRED THOUSANDS;Mn;220;NSM;;;;;N;;;;; +1E8D6;MENDE KIKAKUI COMBINING NUMBER MILLIONS;Mn;220;NSM;;;;;N;;;;; +1EE00;ARABIC MATHEMATICAL ALEF;Lo;0;AL;<font> 0627;;;;N;;;;; +1EE01;ARABIC MATHEMATICAL BEH;Lo;0;AL;<font> 0628;;;;N;;;;; +1EE02;ARABIC MATHEMATICAL JEEM;Lo;0;AL;<font> 062C;;;;N;;;;; +1EE03;ARABIC MATHEMATICAL DAL;Lo;0;AL;<font> 062F;;;;N;;;;; +1EE05;ARABIC MATHEMATICAL WAW;Lo;0;AL;<font> 0648;;;;N;;;;; +1EE06;ARABIC MATHEMATICAL ZAIN;Lo;0;AL;<font> 0632;;;;N;;;;; +1EE07;ARABIC MATHEMATICAL HAH;Lo;0;AL;<font> 062D;;;;N;;;;; +1EE08;ARABIC MATHEMATICAL TAH;Lo;0;AL;<font> 0637;;;;N;;;;; +1EE09;ARABIC MATHEMATICAL YEH;Lo;0;AL;<font> 064A;;;;N;;;;; +1EE0A;ARABIC MATHEMATICAL KAF;Lo;0;AL;<font> 0643;;;;N;;;;; +1EE0B;ARABIC MATHEMATICAL LAM;Lo;0;AL;<font> 0644;;;;N;;;;; +1EE0C;ARABIC MATHEMATICAL MEEM;Lo;0;AL;<font> 0645;;;;N;;;;; +1EE0D;ARABIC MATHEMATICAL NOON;Lo;0;AL;<font> 0646;;;;N;;;;; +1EE0E;ARABIC MATHEMATICAL SEEN;Lo;0;AL;<font> 0633;;;;N;;;;; +1EE0F;ARABIC MATHEMATICAL AIN;Lo;0;AL;<font> 0639;;;;N;;;;; +1EE10;ARABIC MATHEMATICAL FEH;Lo;0;AL;<font> 0641;;;;N;;;;; +1EE11;ARABIC MATHEMATICAL SAD;Lo;0;AL;<font> 0635;;;;N;;;;; +1EE12;ARABIC MATHEMATICAL QAF;Lo;0;AL;<font> 0642;;;;N;;;;; +1EE13;ARABIC MATHEMATICAL REH;Lo;0;AL;<font> 0631;;;;N;;;;; +1EE14;ARABIC MATHEMATICAL SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;; +1EE15;ARABIC MATHEMATICAL TEH;Lo;0;AL;<font> 062A;;;;N;;;;; +1EE16;ARABIC MATHEMATICAL THEH;Lo;0;AL;<font> 062B;;;;N;;;;; +1EE17;ARABIC MATHEMATICAL KHAH;Lo;0;AL;<font> 062E;;;;N;;;;; +1EE18;ARABIC MATHEMATICAL THAL;Lo;0;AL;<font> 0630;;;;N;;;;; +1EE19;ARABIC MATHEMATICAL DAD;Lo;0;AL;<font> 0636;;;;N;;;;; +1EE1A;ARABIC MATHEMATICAL ZAH;Lo;0;AL;<font> 0638;;;;N;;;;; +1EE1B;ARABIC MATHEMATICAL GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;; +1EE1C;ARABIC MATHEMATICAL DOTLESS BEH;Lo;0;AL;<font> 066E;;;;N;;;;; +1EE1D;ARABIC MATHEMATICAL DOTLESS NOON;Lo;0;AL;<font> 06BA;;;;N;;;;; +1EE1E;ARABIC MATHEMATICAL DOTLESS FEH;Lo;0;AL;<font> 06A1;;;;N;;;;; +1EE1F;ARABIC MATHEMATICAL DOTLESS QAF;Lo;0;AL;<font> 066F;;;;N;;;;; +1EE21;ARABIC MATHEMATICAL INITIAL BEH;Lo;0;AL;<font> 0628;;;;N;;;;; +1EE22;ARABIC MATHEMATICAL INITIAL JEEM;Lo;0;AL;<font> 062C;;;;N;;;;; +1EE24;ARABIC MATHEMATICAL INITIAL HEH;Lo;0;AL;<font> 0647;;;;N;;;;; +1EE27;ARABIC MATHEMATICAL INITIAL HAH;Lo;0;AL;<font> 062D;;;;N;;;;; +1EE29;ARABIC MATHEMATICAL INITIAL YEH;Lo;0;AL;<font> 064A;;;;N;;;;; +1EE2A;ARABIC MATHEMATICAL INITIAL KAF;Lo;0;AL;<font> 0643;;;;N;;;;; +1EE2B;ARABIC MATHEMATICAL INITIAL LAM;Lo;0;AL;<font> 0644;;;;N;;;;; +1EE2C;ARABIC MATHEMATICAL INITIAL MEEM;Lo;0;AL;<font> 0645;;;;N;;;;; +1EE2D;ARABIC MATHEMATICAL INITIAL NOON;Lo;0;AL;<font> 0646;;;;N;;;;; +1EE2E;ARABIC MATHEMATICAL INITIAL SEEN;Lo;0;AL;<font> 0633;;;;N;;;;; +1EE2F;ARABIC MATHEMATICAL INITIAL AIN;Lo;0;AL;<font> 0639;;;;N;;;;; +1EE30;ARABIC MATHEMATICAL INITIAL FEH;Lo;0;AL;<font> 0641;;;;N;;;;; +1EE31;ARABIC MATHEMATICAL INITIAL SAD;Lo;0;AL;<font> 0635;;;;N;;;;; +1EE32;ARABIC MATHEMATICAL INITIAL QAF;Lo;0;AL;<font> 0642;;;;N;;;;; +1EE34;ARABIC MATHEMATICAL INITIAL SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;; +1EE35;ARABIC MATHEMATICAL INITIAL TEH;Lo;0;AL;<font> 062A;;;;N;;;;; +1EE36;ARABIC MATHEMATICAL INITIAL THEH;Lo;0;AL;<font> 062B;;;;N;;;;; +1EE37;ARABIC MATHEMATICAL INITIAL KHAH;Lo;0;AL;<font> 062E;;;;N;;;;; +1EE39;ARABIC MATHEMATICAL INITIAL DAD;Lo;0;AL;<font> 0636;;;;N;;;;; +1EE3B;ARABIC MATHEMATICAL INITIAL GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;; +1EE42;ARABIC MATHEMATICAL TAILED JEEM;Lo;0;AL;<font> 062C;;;;N;;;;; +1EE47;ARABIC MATHEMATICAL TAILED HAH;Lo;0;AL;<font> 062D;;;;N;;;;; +1EE49;ARABIC MATHEMATICAL TAILED YEH;Lo;0;AL;<font> 064A;;;;N;;;;; +1EE4B;ARABIC MATHEMATICAL TAILED LAM;Lo;0;AL;<font> 0644;;;;N;;;;; +1EE4D;ARABIC MATHEMATICAL TAILED NOON;Lo;0;AL;<font> 0646;;;;N;;;;; +1EE4E;ARABIC MATHEMATICAL TAILED SEEN;Lo;0;AL;<font> 0633;;;;N;;;;; +1EE4F;ARABIC MATHEMATICAL TAILED AIN;Lo;0;AL;<font> 0639;;;;N;;;;; +1EE51;ARABIC MATHEMATICAL TAILED SAD;Lo;0;AL;<font> 0635;;;;N;;;;; +1EE52;ARABIC MATHEMATICAL TAILED QAF;Lo;0;AL;<font> 0642;;;;N;;;;; +1EE54;ARABIC MATHEMATICAL TAILED SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;; +1EE57;ARABIC MATHEMATICAL TAILED KHAH;Lo;0;AL;<font> 062E;;;;N;;;;; +1EE59;ARABIC MATHEMATICAL TAILED DAD;Lo;0;AL;<font> 0636;;;;N;;;;; +1EE5B;ARABIC MATHEMATICAL TAILED GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;; +1EE5D;ARABIC MATHEMATICAL TAILED DOTLESS NOON;Lo;0;AL;<font> 06BA;;;;N;;;;; +1EE5F;ARABIC MATHEMATICAL TAILED DOTLESS QAF;Lo;0;AL;<font> 066F;;;;N;;;;; +1EE61;ARABIC MATHEMATICAL STRETCHED BEH;Lo;0;AL;<font> 0628;;;;N;;;;; +1EE62;ARABIC MATHEMATICAL STRETCHED JEEM;Lo;0;AL;<font> 062C;;;;N;;;;; +1EE64;ARABIC MATHEMATICAL STRETCHED HEH;Lo;0;AL;<font> 0647;;;;N;;;;; +1EE67;ARABIC MATHEMATICAL STRETCHED HAH;Lo;0;AL;<font> 062D;;;;N;;;;; +1EE68;ARABIC MATHEMATICAL STRETCHED TAH;Lo;0;AL;<font> 0637;;;;N;;;;; +1EE69;ARABIC MATHEMATICAL STRETCHED YEH;Lo;0;AL;<font> 064A;;;;N;;;;; +1EE6A;ARABIC MATHEMATICAL STRETCHED KAF;Lo;0;AL;<font> 0643;;;;N;;;;; +1EE6C;ARABIC MATHEMATICAL STRETCHED MEEM;Lo;0;AL;<font> 0645;;;;N;;;;; +1EE6D;ARABIC MATHEMATICAL STRETCHED NOON;Lo;0;AL;<font> 0646;;;;N;;;;; +1EE6E;ARABIC MATHEMATICAL STRETCHED SEEN;Lo;0;AL;<font> 0633;;;;N;;;;; +1EE6F;ARABIC MATHEMATICAL STRETCHED AIN;Lo;0;AL;<font> 0639;;;;N;;;;; +1EE70;ARABIC MATHEMATICAL STRETCHED FEH;Lo;0;AL;<font> 0641;;;;N;;;;; +1EE71;ARABIC MATHEMATICAL STRETCHED SAD;Lo;0;AL;<font> 0635;;;;N;;;;; +1EE72;ARABIC MATHEMATICAL STRETCHED QAF;Lo;0;AL;<font> 0642;;;;N;;;;; +1EE74;ARABIC MATHEMATICAL STRETCHED SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;; +1EE75;ARABIC MATHEMATICAL STRETCHED TEH;Lo;0;AL;<font> 062A;;;;N;;;;; +1EE76;ARABIC MATHEMATICAL STRETCHED THEH;Lo;0;AL;<font> 062B;;;;N;;;;; +1EE77;ARABIC MATHEMATICAL STRETCHED KHAH;Lo;0;AL;<font> 062E;;;;N;;;;; +1EE79;ARABIC MATHEMATICAL STRETCHED DAD;Lo;0;AL;<font> 0636;;;;N;;;;; +1EE7A;ARABIC MATHEMATICAL STRETCHED ZAH;Lo;0;AL;<font> 0638;;;;N;;;;; +1EE7B;ARABIC MATHEMATICAL STRETCHED GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;; +1EE7C;ARABIC MATHEMATICAL STRETCHED DOTLESS BEH;Lo;0;AL;<font> 066E;;;;N;;;;; +1EE7E;ARABIC MATHEMATICAL STRETCHED DOTLESS FEH;Lo;0;AL;<font> 06A1;;;;N;;;;; +1EE80;ARABIC MATHEMATICAL LOOPED ALEF;Lo;0;AL;<font> 0627;;;;N;;;;; +1EE81;ARABIC MATHEMATICAL LOOPED BEH;Lo;0;AL;<font> 0628;;;;N;;;;; +1EE82;ARABIC MATHEMATICAL LOOPED JEEM;Lo;0;AL;<font> 062C;;;;N;;;;; +1EE83;ARABIC MATHEMATICAL LOOPED DAL;Lo;0;AL;<font> 062F;;;;N;;;;; +1EE84;ARABIC MATHEMATICAL LOOPED HEH;Lo;0;AL;<font> 0647;;;;N;;;;; +1EE85;ARABIC MATHEMATICAL LOOPED WAW;Lo;0;AL;<font> 0648;;;;N;;;;; +1EE86;ARABIC MATHEMATICAL LOOPED ZAIN;Lo;0;AL;<font> 0632;;;;N;;;;; +1EE87;ARABIC MATHEMATICAL LOOPED HAH;Lo;0;AL;<font> 062D;;;;N;;;;; +1EE88;ARABIC MATHEMATICAL LOOPED TAH;Lo;0;AL;<font> 0637;;;;N;;;;; +1EE89;ARABIC MATHEMATICAL LOOPED YEH;Lo;0;AL;<font> 064A;;;;N;;;;; +1EE8B;ARABIC MATHEMATICAL LOOPED LAM;Lo;0;AL;<font> 0644;;;;N;;;;; +1EE8C;ARABIC MATHEMATICAL LOOPED MEEM;Lo;0;AL;<font> 0645;;;;N;;;;; +1EE8D;ARABIC MATHEMATICAL LOOPED NOON;Lo;0;AL;<font> 0646;;;;N;;;;; +1EE8E;ARABIC MATHEMATICAL LOOPED SEEN;Lo;0;AL;<font> 0633;;;;N;;;;; +1EE8F;ARABIC MATHEMATICAL LOOPED AIN;Lo;0;AL;<font> 0639;;;;N;;;;; +1EE90;ARABIC MATHEMATICAL LOOPED FEH;Lo;0;AL;<font> 0641;;;;N;;;;; +1EE91;ARABIC MATHEMATICAL LOOPED SAD;Lo;0;AL;<font> 0635;;;;N;;;;; +1EE92;ARABIC MATHEMATICAL LOOPED QAF;Lo;0;AL;<font> 0642;;;;N;;;;; +1EE93;ARABIC MATHEMATICAL LOOPED REH;Lo;0;AL;<font> 0631;;;;N;;;;; +1EE94;ARABIC MATHEMATICAL LOOPED SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;; +1EE95;ARABIC MATHEMATICAL LOOPED TEH;Lo;0;AL;<font> 062A;;;;N;;;;; +1EE96;ARABIC MATHEMATICAL LOOPED THEH;Lo;0;AL;<font> 062B;;;;N;;;;; +1EE97;ARABIC MATHEMATICAL LOOPED KHAH;Lo;0;AL;<font> 062E;;;;N;;;;; +1EE98;ARABIC MATHEMATICAL LOOPED THAL;Lo;0;AL;<font> 0630;;;;N;;;;; +1EE99;ARABIC MATHEMATICAL LOOPED DAD;Lo;0;AL;<font> 0636;;;;N;;;;; +1EE9A;ARABIC MATHEMATICAL LOOPED ZAH;Lo;0;AL;<font> 0638;;;;N;;;;; +1EE9B;ARABIC MATHEMATICAL LOOPED GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;; +1EEA1;ARABIC MATHEMATICAL DOUBLE-STRUCK BEH;Lo;0;AL;<font> 0628;;;;N;;;;; +1EEA2;ARABIC MATHEMATICAL DOUBLE-STRUCK JEEM;Lo;0;AL;<font> 062C;;;;N;;;;; +1EEA3;ARABIC MATHEMATICAL DOUBLE-STRUCK DAL;Lo;0;AL;<font> 062F;;;;N;;;;; +1EEA5;ARABIC MATHEMATICAL DOUBLE-STRUCK WAW;Lo;0;AL;<font> 0648;;;;N;;;;; +1EEA6;ARABIC MATHEMATICAL DOUBLE-STRUCK ZAIN;Lo;0;AL;<font> 0632;;;;N;;;;; +1EEA7;ARABIC MATHEMATICAL DOUBLE-STRUCK HAH;Lo;0;AL;<font> 062D;;;;N;;;;; +1EEA8;ARABIC MATHEMATICAL DOUBLE-STRUCK TAH;Lo;0;AL;<font> 0637;;;;N;;;;; +1EEA9;ARABIC MATHEMATICAL DOUBLE-STRUCK YEH;Lo;0;AL;<font> 064A;;;;N;;;;; +1EEAB;ARABIC MATHEMATICAL DOUBLE-STRUCK LAM;Lo;0;AL;<font> 0644;;;;N;;;;; +1EEAC;ARABIC MATHEMATICAL DOUBLE-STRUCK MEEM;Lo;0;AL;<font> 0645;;;;N;;;;; +1EEAD;ARABIC MATHEMATICAL DOUBLE-STRUCK NOON;Lo;0;AL;<font> 0646;;;;N;;;;; +1EEAE;ARABIC MATHEMATICAL DOUBLE-STRUCK SEEN;Lo;0;AL;<font> 0633;;;;N;;;;; +1EEAF;ARABIC MATHEMATICAL DOUBLE-STRUCK AIN;Lo;0;AL;<font> 0639;;;;N;;;;; +1EEB0;ARABIC MATHEMATICAL DOUBLE-STRUCK FEH;Lo;0;AL;<font> 0641;;;;N;;;;; +1EEB1;ARABIC MATHEMATICAL DOUBLE-STRUCK SAD;Lo;0;AL;<font> 0635;;;;N;;;;; +1EEB2;ARABIC MATHEMATICAL DOUBLE-STRUCK QAF;Lo;0;AL;<font> 0642;;;;N;;;;; +1EEB3;ARABIC MATHEMATICAL DOUBLE-STRUCK REH;Lo;0;AL;<font> 0631;;;;N;;;;; +1EEB4;ARABIC MATHEMATICAL DOUBLE-STRUCK SHEEN;Lo;0;AL;<font> 0634;;;;N;;;;; +1EEB5;ARABIC MATHEMATICAL DOUBLE-STRUCK TEH;Lo;0;AL;<font> 062A;;;;N;;;;; +1EEB6;ARABIC MATHEMATICAL DOUBLE-STRUCK THEH;Lo;0;AL;<font> 062B;;;;N;;;;; +1EEB7;ARABIC MATHEMATICAL DOUBLE-STRUCK KHAH;Lo;0;AL;<font> 062E;;;;N;;;;; +1EEB8;ARABIC MATHEMATICAL DOUBLE-STRUCK THAL;Lo;0;AL;<font> 0630;;;;N;;;;; +1EEB9;ARABIC MATHEMATICAL DOUBLE-STRUCK DAD;Lo;0;AL;<font> 0636;;;;N;;;;; +1EEBA;ARABIC MATHEMATICAL DOUBLE-STRUCK ZAH;Lo;0;AL;<font> 0638;;;;N;;;;; +1EEBB;ARABIC MATHEMATICAL DOUBLE-STRUCK GHAIN;Lo;0;AL;<font> 063A;;;;N;;;;; +1EEF0;ARABIC MATHEMATICAL OPERATOR MEEM WITH HAH WITH TATWEEL;Sm;0;ON;;;;;N;;;;; +1EEF1;ARABIC MATHEMATICAL OPERATOR HAH WITH DAL;Sm;0;ON;;;;;N;;;;; 1F000;MAHJONG TILE EAST WIND;So;0;ON;;;;;N;;;;; 1F001;MAHJONG TILE SOUTH WIND;So;0;ON;;;;;N;;;;; 1F002;MAHJONG TILE WEST WIND;So;0;ON;;;;;N;;;;; @@ -21772,6 +24780,7 @@ 1F0BC;PLAYING CARD KNIGHT OF HEARTS;So;0;ON;;;;;N;;;;; 1F0BD;PLAYING CARD QUEEN OF HEARTS;So;0;ON;;;;;N;;;;; 1F0BE;PLAYING CARD KING OF HEARTS;So;0;ON;;;;;N;;;;; +1F0BF;PLAYING CARD RED JOKER;So;0;ON;;;;;N;;;;; 1F0C1;PLAYING CARD ACE OF DIAMONDS;So;0;ON;;;;;N;;;;; 1F0C2;PLAYING CARD TWO OF DIAMONDS;So;0;ON;;;;;N;;;;; 1F0C3;PLAYING CARD THREE OF DIAMONDS;So;0;ON;;;;;N;;;;; @@ -21802,6 +24811,28 @@ 1F0DD;PLAYING CARD QUEEN OF CLUBS;So;0;ON;;;;;N;;;;; 1F0DE;PLAYING CARD KING OF CLUBS;So;0;ON;;;;;N;;;;; 1F0DF;PLAYING CARD WHITE JOKER;So;0;ON;;;;;N;;;;; +1F0E0;PLAYING CARD FOOL;So;0;ON;;;;;N;;;;; +1F0E1;PLAYING CARD TRUMP-1;So;0;ON;;;;;N;;;;; +1F0E2;PLAYING CARD TRUMP-2;So;0;ON;;;;;N;;;;; +1F0E3;PLAYING CARD TRUMP-3;So;0;ON;;;;;N;;;;; +1F0E4;PLAYING CARD TRUMP-4;So;0;ON;;;;;N;;;;; +1F0E5;PLAYING CARD TRUMP-5;So;0;ON;;;;;N;;;;; +1F0E6;PLAYING CARD TRUMP-6;So;0;ON;;;;;N;;;;; +1F0E7;PLAYING CARD TRUMP-7;So;0;ON;;;;;N;;;;; +1F0E8;PLAYING CARD TRUMP-8;So;0;ON;;;;;N;;;;; +1F0E9;PLAYING CARD TRUMP-9;So;0;ON;;;;;N;;;;; +1F0EA;PLAYING CARD TRUMP-10;So;0;ON;;;;;N;;;;; +1F0EB;PLAYING CARD TRUMP-11;So;0;ON;;;;;N;;;;; +1F0EC;PLAYING CARD TRUMP-12;So;0;ON;;;;;N;;;;; +1F0ED;PLAYING CARD TRUMP-13;So;0;ON;;;;;N;;;;; +1F0EE;PLAYING CARD TRUMP-14;So;0;ON;;;;;N;;;;; +1F0EF;PLAYING CARD TRUMP-15;So;0;ON;;;;;N;;;;; +1F0F0;PLAYING CARD TRUMP-16;So;0;ON;;;;;N;;;;; +1F0F1;PLAYING CARD TRUMP-17;So;0;ON;;;;;N;;;;; +1F0F2;PLAYING CARD TRUMP-18;So;0;ON;;;;;N;;;;; +1F0F3;PLAYING CARD TRUMP-19;So;0;ON;;;;;N;;;;; +1F0F4;PLAYING CARD TRUMP-20;So;0;ON;;;;;N;;;;; +1F0F5;PLAYING CARD TRUMP-21;So;0;ON;;;;;N;;;;; 1F100;DIGIT ZERO FULL STOP;No;0;EN;<compat> 0030 002E;;0;0;N;;;;; 1F101;DIGIT ZERO COMMA;No;0;EN;<compat> 0030 002C;;0;0;N;;;;; 1F102;DIGIT ONE COMMA;No;0;EN;<compat> 0031 002C;;1;1;N;;;;; @@ -21813,6 +24844,8 @@ 1F108;DIGIT SEVEN COMMA;No;0;EN;<compat> 0037 002C;;7;7;N;;;;; 1F109;DIGIT EIGHT COMMA;No;0;EN;<compat> 0038 002C;;8;8;N;;;;; 1F10A;DIGIT NINE COMMA;No;0;EN;<compat> 0039 002C;;9;9;N;;;;; +1F10B;DINGBAT CIRCLED SANS-SERIF DIGIT ZERO;No;0;ON;;;;0;N;;;;; +1F10C;DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ZERO;No;0;ON;;;;0;N;;;;; 1F110;PARENTHESIZED LATIN CAPITAL LETTER A;So;0;L;<compat> 0028 0041 0029;;;;N;;;;; 1F111;PARENTHESIZED LATIN CAPITAL LETTER B;So;0;L;<compat> 0028 0042 0029;;;;N;;;;; 1F112;PARENTHESIZED LATIN CAPITAL LETTER C;So;0;L;<compat> 0028 0043 0029;;;;N;;;;; @@ -21902,6 +24935,8 @@ 1F167;NEGATIVE CIRCLED LATIN CAPITAL LETTER X;So;0;L;;;;;N;;;;; 1F168;NEGATIVE CIRCLED LATIN CAPITAL LETTER Y;So;0;L;;;;;N;;;;; 1F169;NEGATIVE CIRCLED LATIN CAPITAL LETTER Z;So;0;L;;;;;N;;;;; +1F16A;RAISED MC SIGN;So;0;ON;<super> 004D 0043;;;;N;;;;; +1F16B;RAISED MD SIGN;So;0;ON;<super> 004D 0044;;;;N;;;;; 1F170;NEGATIVE SQUARED LATIN CAPITAL LETTER A;So;0;L;;;;;N;;;;; 1F171;NEGATIVE SQUARED LATIN CAPITAL LETTER B;So;0;L;;;;;N;;;;; 1F172;NEGATIVE SQUARED LATIN CAPITAL LETTER C;So;0;L;;;;;N;;;;; @@ -22061,12 +25096,25 @@ 1F31E;SUN WITH FACE;So;0;ON;;;;;N;;;;; 1F31F;GLOWING STAR;So;0;ON;;;;;N;;;;; 1F320;SHOOTING STAR;So;0;ON;;;;;N;;;;; +1F321;THERMOMETER;So;0;ON;;;;;N;;;;; +1F322;BLACK DROPLET;So;0;ON;;;;;N;;;;; +1F323;WHITE SUN;So;0;ON;;;;;N;;;;; +1F324;WHITE SUN WITH SMALL CLOUD;So;0;ON;;;;;N;;;;; +1F325;WHITE SUN BEHIND CLOUD;So;0;ON;;;;;N;;;;; +1F326;WHITE SUN BEHIND CLOUD WITH RAIN;So;0;ON;;;;;N;;;;; +1F327;CLOUD WITH RAIN;So;0;ON;;;;;N;;;;; +1F328;CLOUD WITH SNOW;So;0;ON;;;;;N;;;;; +1F329;CLOUD WITH LIGHTNING;So;0;ON;;;;;N;;;;; +1F32A;CLOUD WITH TORNADO;So;0;ON;;;;;N;;;;; +1F32B;FOG;So;0;ON;;;;;N;;;;; +1F32C;WIND BLOWING FACE;So;0;ON;;;;;N;;;;; 1F330;CHESTNUT;So;0;ON;;;;;N;;;;; 1F331;SEEDLING;So;0;ON;;;;;N;;;;; 1F332;EVERGREEN TREE;So;0;ON;;;;;N;;;;; 1F333;DECIDUOUS TREE;So;0;ON;;;;;N;;;;; 1F334;PALM TREE;So;0;ON;;;;;N;;;;; 1F335;CACTUS;So;0;ON;;;;;N;;;;; +1F336;HOT PEPPER;So;0;ON;;;;;N;;;;; 1F337;TULIP;So;0;ON;;;;;N;;;;; 1F338;CHERRY BLOSSOM;So;0;ON;;;;;N;;;;; 1F339;ROSE;So;0;ON;;;;;N;;;;; @@ -22137,6 +25185,7 @@ 1F37A;BEER MUG;So;0;ON;;;;;N;;;;; 1F37B;CLINKING BEER MUGS;So;0;ON;;;;;N;;;;; 1F37C;BABY BOTTLE;So;0;ON;;;;;N;;;;; +1F37D;FORK AND KNIFE WITH PLATE;So;0;ON;;;;;N;;;;; 1F380;RIBBON;So;0;ON;;;;;N;;;;; 1F381;WRAPPED PRESENT;So;0;ON;;;;;N;;;;; 1F382;BIRTHDAY CAKE;So;0;ON;;;;;N;;;;; @@ -22157,6 +25206,18 @@ 1F391;MOON VIEWING CEREMONY;So;0;ON;;;;;N;;;;; 1F392;SCHOOL SATCHEL;So;0;ON;;;;;N;;;;; 1F393;GRADUATION CAP;So;0;ON;;;;;N;;;;; +1F394;HEART WITH TIP ON THE LEFT;So;0;ON;;;;;N;;;;; +1F395;BOUQUET OF FLOWERS;So;0;ON;;;;;N;;;;; +1F396;MILITARY MEDAL;So;0;ON;;;;;N;;;;; +1F397;REMINDER RIBBON;So;0;ON;;;;;N;;;;; +1F398;MUSICAL KEYBOARD WITH JACKS;So;0;ON;;;;;N;;;;; +1F399;STUDIO MICROPHONE;So;0;ON;;;;;N;;;;; +1F39A;LEVEL SLIDER;So;0;ON;;;;;N;;;;; +1F39B;CONTROL KNOBS;So;0;ON;;;;;N;;;;; +1F39C;BEAMED ASCENDING MUSICAL NOTES;So;0;ON;;;;;N;;;;; +1F39D;BEAMED DESCENDING MUSICAL NOTES;So;0;ON;;;;;N;;;;; +1F39E;FILM FRAMES;So;0;ON;;;;;N;;;;; +1F39F;ADMISSION TICKETS;So;0;ON;;;;;N;;;;; 1F3A0;CAROUSEL HORSE;So;0;ON;;;;;N;;;;; 1F3A1;FERRIS WHEEL;So;0;ON;;;;;N;;;;; 1F3A2;ROLLER COASTER;So;0;ON;;;;;N;;;;; @@ -22194,11 +25255,28 @@ 1F3C2;SNOWBOARDER;So;0;ON;;;;;N;;;;; 1F3C3;RUNNER;So;0;ON;;;;;N;;;;; 1F3C4;SURFER;So;0;ON;;;;;N;;;;; +1F3C5;SPORTS MEDAL;So;0;ON;;;;;N;;;;; 1F3C6;TROPHY;So;0;ON;;;;;N;;;;; 1F3C7;HORSE RACING;So;0;ON;;;;;N;;;;; 1F3C8;AMERICAN FOOTBALL;So;0;ON;;;;;N;;;;; 1F3C9;RUGBY FOOTBALL;So;0;ON;;;;;N;;;;; 1F3CA;SWIMMER;So;0;ON;;;;;N;;;;; +1F3CB;WEIGHT LIFTER;So;0;ON;;;;;N;;;;; +1F3CC;GOLFER;So;0;ON;;;;;N;;;;; +1F3CD;RACING MOTORCYCLE;So;0;ON;;;;;N;;;;; +1F3CE;RACING CAR;So;0;ON;;;;;N;;;;; +1F3D4;SNOW CAPPED MOUNTAIN;So;0;ON;;;;;N;;;;; +1F3D5;CAMPING;So;0;ON;;;;;N;;;;; +1F3D6;BEACH WITH UMBRELLA;So;0;ON;;;;;N;;;;; +1F3D7;BUILDING CONSTRUCTION;So;0;ON;;;;;N;;;;; +1F3D8;HOUSE BUILDINGS;So;0;ON;;;;;N;;;;; +1F3D9;CITYSCAPE;So;0;ON;;;;;N;;;;; +1F3DA;DERELICT HOUSE BUILDING;So;0;ON;;;;;N;;;;; +1F3DB;CLASSICAL BUILDING;So;0;ON;;;;;N;;;;; +1F3DC;DESERT;So;0;ON;;;;;N;;;;; +1F3DD;DESERT ISLAND;So;0;ON;;;;;N;;;;; +1F3DE;NATIONAL PARK;So;0;ON;;;;;N;;;;; +1F3DF;STADIUM;So;0;ON;;;;;N;;;;; 1F3E0;HOUSE BUILDING;So;0;ON;;;;;N;;;;; 1F3E1;HOUSE WITH GARDEN;So;0;ON;;;;;N;;;;; 1F3E2;OFFICE BUILDING;So;0;ON;;;;;N;;;;; @@ -22216,6 +25294,13 @@ 1F3EE;IZAKAYA LANTERN;So;0;ON;;;;;N;;;;; 1F3EF;JAPANESE CASTLE;So;0;ON;;;;;N;;;;; 1F3F0;EUROPEAN CASTLE;So;0;ON;;;;;N;;;;; +1F3F1;WHITE PENNANT;So;0;ON;;;;;N;;;;; +1F3F2;BLACK PENNANT;So;0;ON;;;;;N;;;;; +1F3F3;WAVING WHITE FLAG;So;0;ON;;;;;N;;;;; +1F3F4;WAVING BLACK FLAG;So;0;ON;;;;;N;;;;; +1F3F5;ROSETTE;So;0;ON;;;;;N;;;;; +1F3F6;BLACK ROSETTE;So;0;ON;;;;;N;;;;; +1F3F7;LABEL;So;0;ON;;;;;N;;;;; 1F400;RAT;So;0;ON;;;;;N;;;;; 1F401;MOUSE;So;0;ON;;;;;N;;;;; 1F402;OX;So;0;ON;;;;;N;;;;; @@ -22279,7 +25364,9 @@ 1F43C;PANDA FACE;So;0;ON;;;;;N;;;;; 1F43D;PIG NOSE;So;0;ON;;;;;N;;;;; 1F43E;PAW PRINTS;So;0;ON;;;;;N;;;;; +1F43F;CHIPMUNK;So;0;ON;;;;;N;;;;; 1F440;EYES;So;0;ON;;;;;N;;;;; +1F441;EYE;So;0;ON;;;;;N;;;;; 1F442;EAR;So;0;ON;;;;;N;;;;; 1F443;NOSE;So;0;ON;;;;;N;;;;; 1F444;MOUTH;So;0;ON;;;;;N;;;;; @@ -22354,7 +25441,7 @@ 1F489;SYRINGE;So;0;ON;;;;;N;;;;; 1F48A;PILL;So;0;ON;;;;;N;;;;; 1F48B;KISS MARK;So;0;ON;;;;;N;;;;; -1F48C;LOVE LETTER;So;0;L;;;;;N;;;;; +1F48C;LOVE LETTER;So;0;ON;;;;;N;;;;; 1F48D;RING;So;0;ON;;;;;N;;;;; 1F48E;GEM STONE;So;0;ON;;;;;N;;;;; 1F48F;KISS;So;0;ON;;;;;N;;;;; @@ -22462,10 +25549,13 @@ 1F4F5;NO MOBILE PHONES;So;0;ON;;;;;N;;;;; 1F4F6;ANTENNA WITH BARS;So;0;ON;;;;;N;;;;; 1F4F7;CAMERA;So;0;ON;;;;;N;;;;; +1F4F8;CAMERA WITH FLASH;So;0;ON;;;;;N;;;;; 1F4F9;VIDEO CAMERA;So;0;ON;;;;;N;;;;; 1F4FA;TELEVISION;So;0;ON;;;;;N;;;;; 1F4FB;RADIO;So;0;ON;;;;;N;;;;; 1F4FC;VIDEOCASSETTE;So;0;ON;;;;;N;;;;; +1F4FD;FILM PROJECTOR;So;0;ON;;;;;N;;;;; +1F4FE;PORTABLE STEREO;So;0;ON;;;;;N;;;;; 1F500;TWISTED RIGHTWARDS ARROWS;So;0;ON;;;;;N;;;;; 1F501;CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS;So;0;ON;;;;;N;;;;; 1F502;CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS WITH CIRCLED ONE OVERLAY;So;0;ON;;;;;N;;;;; @@ -22502,7 +25592,7 @@ 1F521;INPUT SYMBOL FOR LATIN SMALL LETTERS;So;0;ON;;;;;N;;;;; 1F522;INPUT SYMBOL FOR NUMBERS;So;0;ON;;;;;N;;;;; 1F523;INPUT SYMBOL FOR SYMBOLS;So;0;ON;;;;;N;;;;; -1F524;INPUT SYMBOL FOR LATIN LETTERS;So;0;L;;;;;N;;;;; +1F524;INPUT SYMBOL FOR LATIN LETTERS;So;0;ON;;;;;N;;;;; 1F525;FIRE;So;0;ON;;;;;N;;;;; 1F526;ELECTRIC TORCH;So;0;ON;;;;;N;;;;; 1F527;WRENCH;So;0;ON;;;;;N;;;;; @@ -22528,6 +25618,19 @@ 1F53B;DOWN-POINTING RED TRIANGLE;So;0;ON;;;;;N;;;;; 1F53C;UP-POINTING SMALL RED TRIANGLE;So;0;ON;;;;;N;;;;; 1F53D;DOWN-POINTING SMALL RED TRIANGLE;So;0;ON;;;;;N;;;;; +1F53E;LOWER RIGHT SHADOWED WHITE CIRCLE;So;0;ON;;;;;N;;;;; +1F53F;UPPER RIGHT SHADOWED WHITE CIRCLE;So;0;ON;;;;;N;;;;; +1F540;CIRCLED CROSS POMMEE;So;0;ON;;;;;N;;;;; +1F541;CROSS POMMEE WITH HALF-CIRCLE BELOW;So;0;ON;;;;;N;;;;; +1F542;CROSS POMMEE;So;0;ON;;;;;N;;;;; +1F543;NOTCHED LEFT SEMICIRCLE WITH THREE DOTS;So;0;ON;;;;;N;;;;; +1F544;NOTCHED RIGHT SEMICIRCLE WITH THREE DOTS;So;0;ON;;;;;N;;;;; +1F545;SYMBOL FOR MARKS CHAPTER;So;0;ON;;;;;N;;;;; +1F546;WHITE LATIN CROSS;So;0;ON;;;;;N;;;;; +1F547;HEAVY LATIN CROSS;So;0;ON;;;;;N;;;;; +1F548;CELTIC CROSS;So;0;ON;;;;;N;;;;; +1F549;OM SYMBOL;So;0;ON;;;;;N;;;;; +1F54A;DOVE OF PEACE;So;0;ON;;;;;N;;;;; 1F550;CLOCK FACE ONE OCLOCK;So;0;ON;;;;;N;;;;; 1F551;CLOCK FACE TWO OCLOCK;So;0;ON;;;;;N;;;;; 1F552;CLOCK FACE THREE OCLOCK;So;0;ON;;;;;N;;;;; @@ -22552,11 +25655,157 @@ 1F565;CLOCK FACE TEN-THIRTY;So;0;ON;;;;;N;;;;; 1F566;CLOCK FACE ELEVEN-THIRTY;So;0;ON;;;;;N;;;;; 1F567;CLOCK FACE TWELVE-THIRTY;So;0;ON;;;;;N;;;;; +1F568;RIGHT SPEAKER;So;0;ON;;;;;N;;;;; +1F569;RIGHT SPEAKER WITH ONE SOUND WAVE;So;0;ON;;;;;N;;;;; +1F56A;RIGHT SPEAKER WITH THREE SOUND WAVES;So;0;ON;;;;;N;;;;; +1F56B;BULLHORN;So;0;ON;;;;;N;;;;; +1F56C;BULLHORN WITH SOUND WAVES;So;0;ON;;;;;N;;;;; +1F56D;RINGING BELL;So;0;ON;;;;;N;;;;; +1F56E;BOOK;So;0;ON;;;;;N;;;;; +1F56F;CANDLE;So;0;ON;;;;;N;;;;; +1F570;MANTELPIECE CLOCK;So;0;ON;;;;;N;;;;; +1F571;BLACK SKULL AND CROSSBONES;So;0;ON;;;;;N;;;;; +1F572;NO PIRACY;So;0;ON;;;;;N;;;;; +1F573;HOLE;So;0;ON;;;;;N;;;;; +1F574;MAN IN BUSINESS SUIT LEVITATING;So;0;ON;;;;;N;;;;; +1F575;SLEUTH OR SPY;So;0;ON;;;;;N;;;;; +1F576;DARK SUNGLASSES;So;0;ON;;;;;N;;;;; +1F577;SPIDER;So;0;ON;;;;;N;;;;; +1F578;SPIDER WEB;So;0;ON;;;;;N;;;;; +1F579;JOYSTICK;So;0;ON;;;;;N;;;;; +1F57B;LEFT HAND TELEPHONE RECEIVER;So;0;ON;;;;;N;;;;; +1F57C;TELEPHONE RECEIVER WITH PAGE;So;0;ON;;;;;N;;;;; +1F57D;RIGHT HAND TELEPHONE RECEIVER;So;0;ON;;;;;N;;;;; +1F57E;WHITE TOUCHTONE TELEPHONE;So;0;ON;;;;;N;;;;; +1F57F;BLACK TOUCHTONE TELEPHONE;So;0;ON;;;;;N;;;;; +1F580;TELEPHONE ON TOP OF MODEM;So;0;ON;;;;;N;;;;; +1F581;CLAMSHELL MOBILE PHONE;So;0;ON;;;;;N;;;;; +1F582;BACK OF ENVELOPE;So;0;ON;;;;;N;;;;; +1F583;STAMPED ENVELOPE;So;0;ON;;;;;N;;;;; +1F584;ENVELOPE WITH LIGHTNING;So;0;ON;;;;;N;;;;; +1F585;FLYING ENVELOPE;So;0;ON;;;;;N;;;;; +1F586;PEN OVER STAMPED ENVELOPE;So;0;ON;;;;;N;;;;; +1F587;LINKED PAPERCLIPS;So;0;ON;;;;;N;;;;; +1F588;BLACK PUSHPIN;So;0;ON;;;;;N;;;;; +1F589;LOWER LEFT PENCIL;So;0;ON;;;;;N;;;;; +1F58A;LOWER LEFT BALLPOINT PEN;So;0;ON;;;;;N;;;;; +1F58B;LOWER LEFT FOUNTAIN PEN;So;0;ON;;;;;N;;;;; +1F58C;LOWER LEFT PAINTBRUSH;So;0;ON;;;;;N;;;;; +1F58D;LOWER LEFT CRAYON;So;0;ON;;;;;N;;;;; +1F58E;LEFT WRITING HAND;So;0;ON;;;;;N;;;;; +1F58F;TURNED OK HAND SIGN;So;0;ON;;;;;N;;;;; +1F590;RAISED HAND WITH FINGERS SPLAYED;So;0;ON;;;;;N;;;;; +1F591;REVERSED RAISED HAND WITH FINGERS SPLAYED;So;0;ON;;;;;N;;;;; +1F592;REVERSED THUMBS UP SIGN;So;0;ON;;;;;N;;;;; +1F593;REVERSED THUMBS DOWN SIGN;So;0;ON;;;;;N;;;;; +1F594;REVERSED VICTORY HAND;So;0;ON;;;;;N;;;;; +1F595;REVERSED HAND WITH MIDDLE FINGER EXTENDED;So;0;ON;;;;;N;;;;; +1F596;RAISED HAND WITH PART BETWEEN MIDDLE AND RING FINGERS;So;0;ON;;;;;N;;;;; +1F597;WHITE DOWN POINTING LEFT HAND INDEX;So;0;ON;;;;;N;;;;; +1F598;SIDEWAYS WHITE LEFT POINTING INDEX;So;0;ON;;;;;N;;;;; +1F599;SIDEWAYS WHITE RIGHT POINTING INDEX;So;0;ON;;;;;N;;;;; +1F59A;SIDEWAYS BLACK LEFT POINTING INDEX;So;0;ON;;;;;N;;;;; +1F59B;SIDEWAYS BLACK RIGHT POINTING INDEX;So;0;ON;;;;;N;;;;; +1F59C;BLACK LEFT POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;; +1F59D;BLACK RIGHT POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;; +1F59E;SIDEWAYS WHITE UP POINTING INDEX;So;0;ON;;;;;N;;;;; +1F59F;SIDEWAYS WHITE DOWN POINTING INDEX;So;0;ON;;;;;N;;;;; +1F5A0;SIDEWAYS BLACK UP POINTING INDEX;So;0;ON;;;;;N;;;;; +1F5A1;SIDEWAYS BLACK DOWN POINTING INDEX;So;0;ON;;;;;N;;;;; +1F5A2;BLACK UP POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;; +1F5A3;BLACK DOWN POINTING BACKHAND INDEX;So;0;ON;;;;;N;;;;; +1F5A5;DESKTOP COMPUTER;So;0;ON;;;;;N;;;;; +1F5A6;KEYBOARD AND MOUSE;So;0;ON;;;;;N;;;;; +1F5A7;THREE NETWORKED COMPUTERS;So;0;ON;;;;;N;;;;; +1F5A8;PRINTER;So;0;ON;;;;;N;;;;; +1F5A9;POCKET CALCULATOR;So;0;ON;;;;;N;;;;; +1F5AA;BLACK HARD SHELL FLOPPY DISK;So;0;ON;;;;;N;;;;; +1F5AB;WHITE HARD SHELL FLOPPY DISK;So;0;ON;;;;;N;;;;; +1F5AC;SOFT SHELL FLOPPY DISK;So;0;ON;;;;;N;;;;; +1F5AD;TAPE CARTRIDGE;So;0;ON;;;;;N;;;;; +1F5AE;WIRED KEYBOARD;So;0;ON;;;;;N;;;;; +1F5AF;ONE BUTTON MOUSE;So;0;ON;;;;;N;;;;; +1F5B0;TWO BUTTON MOUSE;So;0;ON;;;;;N;;;;; +1F5B1;THREE BUTTON MOUSE;So;0;ON;;;;;N;;;;; +1F5B2;TRACKBALL;So;0;ON;;;;;N;;;;; +1F5B3;OLD PERSONAL COMPUTER;So;0;ON;;;;;N;;;;; +1F5B4;HARD DISK;So;0;ON;;;;;N;;;;; +1F5B5;SCREEN;So;0;ON;;;;;N;;;;; +1F5B6;PRINTER ICON;So;0;ON;;;;;N;;;;; +1F5B7;FAX ICON;So;0;ON;;;;;N;;;;; +1F5B8;OPTICAL DISC ICON;So;0;ON;;;;;N;;;;; +1F5B9;DOCUMENT WITH TEXT;So;0;ON;;;;;N;;;;; +1F5BA;DOCUMENT WITH TEXT AND PICTURE;So;0;ON;;;;;N;;;;; +1F5BB;DOCUMENT WITH PICTURE;So;0;ON;;;;;N;;;;; +1F5BC;FRAME WITH PICTURE;So;0;ON;;;;;N;;;;; +1F5BD;FRAME WITH TILES;So;0;ON;;;;;N;;;;; +1F5BE;FRAME WITH AN X;So;0;ON;;;;;N;;;;; +1F5BF;BLACK FOLDER;So;0;ON;;;;;N;;;;; +1F5C0;FOLDER;So;0;ON;;;;;N;;;;; +1F5C1;OPEN FOLDER;So;0;ON;;;;;N;;;;; +1F5C2;CARD INDEX DIVIDERS;So;0;ON;;;;;N;;;;; +1F5C3;CARD FILE BOX;So;0;ON;;;;;N;;;;; +1F5C4;FILE CABINET;So;0;ON;;;;;N;;;;; +1F5C5;EMPTY NOTE;So;0;ON;;;;;N;;;;; +1F5C6;EMPTY NOTE PAGE;So;0;ON;;;;;N;;;;; +1F5C7;EMPTY NOTE PAD;So;0;ON;;;;;N;;;;; +1F5C8;NOTE;So;0;ON;;;;;N;;;;; +1F5C9;NOTE PAGE;So;0;ON;;;;;N;;;;; +1F5CA;NOTE PAD;So;0;ON;;;;;N;;;;; +1F5CB;EMPTY DOCUMENT;So;0;ON;;;;;N;;;;; +1F5CC;EMPTY PAGE;So;0;ON;;;;;N;;;;; +1F5CD;EMPTY PAGES;So;0;ON;;;;;N;;;;; +1F5CE;DOCUMENT;So;0;ON;;;;;N;;;;; +1F5CF;PAGE;So;0;ON;;;;;N;;;;; +1F5D0;PAGES;So;0;ON;;;;;N;;;;; +1F5D1;WASTEBASKET;So;0;ON;;;;;N;;;;; +1F5D2;SPIRAL NOTE PAD;So;0;ON;;;;;N;;;;; +1F5D3;SPIRAL CALENDAR PAD;So;0;ON;;;;;N;;;;; +1F5D4;DESKTOP WINDOW;So;0;ON;;;;;N;;;;; +1F5D5;MINIMIZE;So;0;ON;;;;;N;;;;; +1F5D6;MAXIMIZE;So;0;ON;;;;;N;;;;; +1F5D7;OVERLAP;So;0;ON;;;;;N;;;;; +1F5D8;CLOCKWISE RIGHT AND LEFT SEMICIRCLE ARROWS;So;0;ON;;;;;N;;;;; +1F5D9;CANCELLATION X;So;0;ON;;;;;N;;;;; +1F5DA;INCREASE FONT SIZE SYMBOL;So;0;ON;;;;;N;;;;; +1F5DB;DECREASE FONT SIZE SYMBOL;So;0;ON;;;;;N;;;;; +1F5DC;COMPRESSION;So;0;ON;;;;;N;;;;; +1F5DD;OLD KEY;So;0;ON;;;;;N;;;;; +1F5DE;ROLLED-UP NEWSPAPER;So;0;ON;;;;;N;;;;; +1F5DF;PAGE WITH CIRCLED TEXT;So;0;ON;;;;;N;;;;; +1F5E0;STOCK CHART;So;0;ON;;;;;N;;;;; +1F5E1;DAGGER KNIFE;So;0;ON;;;;;N;;;;; +1F5E2;LIPS;So;0;ON;;;;;N;;;;; +1F5E3;SPEAKING HEAD IN SILHOUETTE;So;0;ON;;;;;N;;;;; +1F5E4;THREE RAYS ABOVE;So;0;ON;;;;;N;;;;; +1F5E5;THREE RAYS BELOW;So;0;ON;;;;;N;;;;; +1F5E6;THREE RAYS LEFT;So;0;ON;;;;;N;;;;; +1F5E7;THREE RAYS RIGHT;So;0;ON;;;;;N;;;;; +1F5E8;LEFT SPEECH BUBBLE;So;0;ON;;;;;N;;;;; +1F5E9;RIGHT SPEECH BUBBLE;So;0;ON;;;;;N;;;;; +1F5EA;TWO SPEECH BUBBLES;So;0;ON;;;;;N;;;;; +1F5EB;THREE SPEECH BUBBLES;So;0;ON;;;;;N;;;;; +1F5EC;LEFT THOUGHT BUBBLE;So;0;ON;;;;;N;;;;; +1F5ED;RIGHT THOUGHT BUBBLE;So;0;ON;;;;;N;;;;; +1F5EE;LEFT ANGER BUBBLE;So;0;ON;;;;;N;;;;; +1F5EF;RIGHT ANGER BUBBLE;So;0;ON;;;;;N;;;;; +1F5F0;MOOD BUBBLE;So;0;ON;;;;;N;;;;; +1F5F1;LIGHTNING MOOD BUBBLE;So;0;ON;;;;;N;;;;; +1F5F2;LIGHTNING MOOD;So;0;ON;;;;;N;;;;; +1F5F3;BALLOT BOX WITH BALLOT;So;0;ON;;;;;N;;;;; +1F5F4;BALLOT SCRIPT X;So;0;ON;;;;;N;;;;; +1F5F5;BALLOT BOX WITH SCRIPT X;So;0;ON;;;;;N;;;;; +1F5F6;BALLOT BOLD SCRIPT X;So;0;ON;;;;;N;;;;; +1F5F7;BALLOT BOX WITH BOLD SCRIPT X;So;0;ON;;;;;N;;;;; +1F5F8;LIGHT CHECK MARK;So;0;ON;;;;;N;;;;; +1F5F9;BALLOT BOX WITH BOLD CHECK;So;0;ON;;;;;N;;;;; +1F5FA;WORLD MAP;So;0;ON;;;;;N;;;;; 1F5FB;MOUNT FUJI;So;0;ON;;;;;N;;;;; 1F5FC;TOKYO TOWER;So;0;ON;;;;;N;;;;; 1F5FD;STATUE OF LIBERTY;So;0;ON;;;;;N;;;;; 1F5FE;SILHOUETTE OF JAPAN;So;0;ON;;;;;N;;;;; 1F5FF;MOYAI;So;0;ON;;;;;N;;;;; +1F600;GRINNING FACE;So;0;ON;;;;;N;;;;; 1F601;GRINNING FACE WITH SMILING EYES;So;0;ON;;;;;N;;;;; 1F602;FACE WITH TEARS OF JOY;So;0;ON;;;;;N;;;;; 1F603;SMILING FACE WITH OPEN MOUTH;So;0;ON;;;;;N;;;;; @@ -22573,30 +25822,42 @@ 1F60E;SMILING FACE WITH SUNGLASSES;So;0;ON;;;;;N;;;;; 1F60F;SMIRKING FACE;So;0;ON;;;;;N;;;;; 1F610;NEUTRAL FACE;So;0;ON;;;;;N;;;;; +1F611;EXPRESSIONLESS FACE;So;0;ON;;;;;N;;;;; 1F612;UNAMUSED FACE;So;0;ON;;;;;N;;;;; 1F613;FACE WITH COLD SWEAT;So;0;ON;;;;;N;;;;; 1F614;PENSIVE FACE;So;0;ON;;;;;N;;;;; +1F615;CONFUSED FACE;So;0;ON;;;;;N;;;;; 1F616;CONFOUNDED FACE;So;0;ON;;;;;N;;;;; +1F617;KISSING FACE;So;0;ON;;;;;N;;;;; 1F618;FACE THROWING A KISS;So;0;ON;;;;;N;;;;; +1F619;KISSING FACE WITH SMILING EYES;So;0;ON;;;;;N;;;;; 1F61A;KISSING FACE WITH CLOSED EYES;So;0;ON;;;;;N;;;;; +1F61B;FACE WITH STUCK-OUT TONGUE;So;0;ON;;;;;N;;;;; 1F61C;FACE WITH STUCK-OUT TONGUE AND WINKING EYE;So;0;ON;;;;;N;;;;; 1F61D;FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES;So;0;ON;;;;;N;;;;; 1F61E;DISAPPOINTED FACE;So;0;ON;;;;;N;;;;; +1F61F;WORRIED FACE;So;0;ON;;;;;N;;;;; 1F620;ANGRY FACE;So;0;ON;;;;;N;;;;; 1F621;POUTING FACE;So;0;ON;;;;;N;;;;; 1F622;CRYING FACE;So;0;ON;;;;;N;;;;; 1F623;PERSEVERING FACE;So;0;ON;;;;;N;;;;; 1F624;FACE WITH LOOK OF TRIUMPH;So;0;ON;;;;;N;;;;; 1F625;DISAPPOINTED BUT RELIEVED FACE;So;0;ON;;;;;N;;;;; +1F626;FROWNING FACE WITH OPEN MOUTH;So;0;ON;;;;;N;;;;; +1F627;ANGUISHED FACE;So;0;ON;;;;;N;;;;; 1F628;FEARFUL FACE;So;0;ON;;;;;N;;;;; 1F629;WEARY FACE;So;0;ON;;;;;N;;;;; 1F62A;SLEEPY FACE;So;0;ON;;;;;N;;;;; 1F62B;TIRED FACE;So;0;ON;;;;;N;;;;; +1F62C;GRIMACING FACE;So;0;ON;;;;;N;;;;; 1F62D;LOUDLY CRYING FACE;So;0;ON;;;;;N;;;;; +1F62E;FACE WITH OPEN MOUTH;So;0;ON;;;;;N;;;;; +1F62F;HUSHED FACE;So;0;ON;;;;;N;;;;; 1F630;FACE WITH OPEN MOUTH AND COLD SWEAT;So;0;ON;;;;;N;;;;; 1F631;FACE SCREAMING IN FEAR;So;0;ON;;;;;N;;;;; 1F632;ASTONISHED FACE;So;0;ON;;;;;N;;;;; 1F633;FLUSHED FACE;So;0;ON;;;;;N;;;;; +1F634;SLEEPING FACE;So;0;ON;;;;;N;;;;; 1F635;DIZZY FACE;So;0;ON;;;;;N;;;;; 1F636;FACE WITHOUT MOUTH;So;0;ON;;;;;N;;;;; 1F637;FACE WITH MEDICAL MASK;So;0;ON;;;;;N;;;;; @@ -22609,6 +25870,8 @@ 1F63E;POUTING CAT FACE;So;0;ON;;;;;N;;;;; 1F63F;CRYING CAT FACE;So;0;ON;;;;;N;;;;; 1F640;WEARY CAT FACE;So;0;ON;;;;;N;;;;; +1F641;SLIGHTLY FROWNING FACE;So;0;ON;;;;;N;;;;; +1F642;SLIGHTLY SMILING FACE;So;0;ON;;;;;N;;;;; 1F645;FACE WITH NO GOOD GESTURE;So;0;ON;;;;;N;;;;; 1F646;FACE WITH OK GESTURE;So;0;ON;;;;;N;;;;; 1F647;PERSON BOWING DEEPLY;So;0;ON;;;;;N;;;;; @@ -22620,6 +25883,54 @@ 1F64D;PERSON FROWNING;So;0;ON;;;;;N;;;;; 1F64E;PERSON WITH POUTING FACE;So;0;ON;;;;;N;;;;; 1F64F;PERSON WITH FOLDED HANDS;So;0;ON;;;;;N;;;;; +1F650;NORTH WEST POINTING LEAF;So;0;ON;;;;;N;;;;; +1F651;SOUTH WEST POINTING LEAF;So;0;ON;;;;;N;;;;; +1F652;NORTH EAST POINTING LEAF;So;0;ON;;;;;N;;;;; +1F653;SOUTH EAST POINTING LEAF;So;0;ON;;;;;N;;;;; +1F654;TURNED NORTH WEST POINTING LEAF;So;0;ON;;;;;N;;;;; +1F655;TURNED SOUTH WEST POINTING LEAF;So;0;ON;;;;;N;;;;; +1F656;TURNED NORTH EAST POINTING LEAF;So;0;ON;;;;;N;;;;; +1F657;TURNED SOUTH EAST POINTING LEAF;So;0;ON;;;;;N;;;;; +1F658;NORTH WEST POINTING VINE LEAF;So;0;ON;;;;;N;;;;; +1F659;SOUTH WEST POINTING VINE LEAF;So;0;ON;;;;;N;;;;; +1F65A;NORTH EAST POINTING VINE LEAF;So;0;ON;;;;;N;;;;; +1F65B;SOUTH EAST POINTING VINE LEAF;So;0;ON;;;;;N;;;;; +1F65C;HEAVY NORTH WEST POINTING VINE LEAF;So;0;ON;;;;;N;;;;; +1F65D;HEAVY SOUTH WEST POINTING VINE LEAF;So;0;ON;;;;;N;;;;; +1F65E;HEAVY NORTH EAST POINTING VINE LEAF;So;0;ON;;;;;N;;;;; +1F65F;HEAVY SOUTH EAST POINTING VINE LEAF;So;0;ON;;;;;N;;;;; +1F660;NORTH WEST POINTING BUD;So;0;ON;;;;;N;;;;; +1F661;SOUTH WEST POINTING BUD;So;0;ON;;;;;N;;;;; +1F662;NORTH EAST POINTING BUD;So;0;ON;;;;;N;;;;; +1F663;SOUTH EAST POINTING BUD;So;0;ON;;;;;N;;;;; +1F664;HEAVY NORTH WEST POINTING BUD;So;0;ON;;;;;N;;;;; +1F665;HEAVY SOUTH WEST POINTING BUD;So;0;ON;;;;;N;;;;; +1F666;HEAVY NORTH EAST POINTING BUD;So;0;ON;;;;;N;;;;; +1F667;HEAVY SOUTH EAST POINTING BUD;So;0;ON;;;;;N;;;;; +1F668;HOLLOW QUILT SQUARE ORNAMENT;So;0;ON;;;;;N;;;;; +1F669;HOLLOW QUILT SQUARE ORNAMENT IN BLACK SQUARE;So;0;ON;;;;;N;;;;; +1F66A;SOLID QUILT SQUARE ORNAMENT;So;0;ON;;;;;N;;;;; +1F66B;SOLID QUILT SQUARE ORNAMENT IN BLACK SQUARE;So;0;ON;;;;;N;;;;; +1F66C;LEFTWARDS ROCKET;So;0;ON;;;;;N;;;;; +1F66D;UPWARDS ROCKET;So;0;ON;;;;;N;;;;; +1F66E;RIGHTWARDS ROCKET;So;0;ON;;;;;N;;;;; +1F66F;DOWNWARDS ROCKET;So;0;ON;;;;;N;;;;; +1F670;SCRIPT LIGATURE ET ORNAMENT;So;0;ON;;;;;N;;;;; +1F671;HEAVY SCRIPT LIGATURE ET ORNAMENT;So;0;ON;;;;;N;;;;; +1F672;LIGATURE OPEN ET ORNAMENT;So;0;ON;;;;;N;;;;; +1F673;HEAVY LIGATURE OPEN ET ORNAMENT;So;0;ON;;;;;N;;;;; +1F674;HEAVY AMPERSAND ORNAMENT;So;0;ON;;;;;N;;;;; +1F675;SWASH AMPERSAND ORNAMENT;So;0;ON;;;;;N;;;;; +1F676;SANS-SERIF HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;; +1F677;SANS-SERIF HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;; +1F678;SANS-SERIF HEAVY LOW DOUBLE COMMA QUOTATION MARK ORNAMENT;So;0;ON;;;;;N;;;;; +1F679;HEAVY INTERROBANG ORNAMENT;So;0;ON;;;;;N;;;;; +1F67A;SANS-SERIF INTERROBANG ORNAMENT;So;0;ON;;;;;N;;;;; +1F67B;HEAVY SANS-SERIF INTERROBANG ORNAMENT;So;0;ON;;;;;N;;;;; +1F67C;VERY HEAVY SOLIDUS;So;0;ON;;;;;N;;;;; +1F67D;VERY HEAVY REVERSE SOLIDUS;So;0;ON;;;;;N;;;;; +1F67E;CHECKER BOARD;So;0;ON;;;;;N;;;;; +1F67F;REVERSE CHECKER BOARD;So;0;ON;;;;;N;;;;; 1F680;ROCKET;So;0;ON;;;;;N;;;;; 1F681;HELICOPTER;So;0;ON;;;;;N;;;;; 1F682;STEAM LOCOMOTIVE;So;0;ON;;;;;N;;;;; @@ -22690,6 +26001,33 @@ 1F6C3;CUSTOMS;So;0;ON;;;;;N;;;;; 1F6C4;BAGGAGE CLAIM;So;0;ON;;;;;N;;;;; 1F6C5;LEFT LUGGAGE;So;0;ON;;;;;N;;;;; +1F6C6;TRIANGLE WITH ROUNDED CORNERS;So;0;ON;;;;;N;;;;; +1F6C7;PROHIBITED SIGN;So;0;ON;;;;;N;;;;; +1F6C8;CIRCLED INFORMATION SOURCE;So;0;ON;;;;;N;;;;; +1F6C9;BOYS SYMBOL;So;0;ON;;;;;N;;;;; +1F6CA;GIRLS SYMBOL;So;0;ON;;;;;N;;;;; +1F6CB;COUCH AND LAMP;So;0;ON;;;;;N;;;;; +1F6CC;SLEEPING ACCOMMODATION;So;0;ON;;;;;N;;;;; +1F6CD;SHOPPING BAGS;So;0;ON;;;;;N;;;;; +1F6CE;BELLHOP BELL;So;0;ON;;;;;N;;;;; +1F6CF;BED;So;0;ON;;;;;N;;;;; +1F6E0;HAMMER AND WRENCH;So;0;ON;;;;;N;;;;; +1F6E1;SHIELD;So;0;ON;;;;;N;;;;; +1F6E2;OIL DRUM;So;0;ON;;;;;N;;;;; +1F6E3;MOTORWAY;So;0;ON;;;;;N;;;;; +1F6E4;RAILWAY TRACK;So;0;ON;;;;;N;;;;; +1F6E5;MOTOR BOAT;So;0;ON;;;;;N;;;;; +1F6E6;UP-POINTING MILITARY AIRPLANE;So;0;ON;;;;;N;;;;; +1F6E7;UP-POINTING AIRPLANE;So;0;ON;;;;;N;;;;; +1F6E8;UP-POINTING SMALL AIRPLANE;So;0;ON;;;;;N;;;;; +1F6E9;SMALL AIRPLANE;So;0;ON;;;;;N;;;;; +1F6EA;NORTHEAST-POINTING AIRPLANE;So;0;ON;;;;;N;;;;; +1F6EB;AIRPLANE DEPARTURE;So;0;ON;;;;;N;;;;; +1F6EC;AIRPLANE ARRIVING;So;0;ON;;;;;N;;;;; +1F6F0;SATELLITE;So;0;ON;;;;;N;;;;; +1F6F1;ONCOMING FIRE ENGINE;So;0;ON;;;;;N;;;;; +1F6F2;DIESEL LOCOMOTIVE;So;0;ON;;;;;N;;;;; +1F6F3;PASSENGER SHIP;So;0;ON;;;;;N;;;;; 1F700;ALCHEMICAL SYMBOL FOR QUINTESSENCE;So;0;ON;;;;;N;;;;; 1F701;ALCHEMICAL SYMBOL FOR AIR;So;0;ON;;;;;N;;;;; 1F702;ALCHEMICAL SYMBOL FOR FIRE;So;0;ON;;;;;N;;;;; @@ -22806,6 +26144,239 @@ 1F771;ALCHEMICAL SYMBOL FOR MONTH;So;0;ON;;;;;N;;;;; 1F772;ALCHEMICAL SYMBOL FOR HALF DRAM;So;0;ON;;;;;N;;;;; 1F773;ALCHEMICAL SYMBOL FOR HALF OUNCE;So;0;ON;;;;;N;;;;; +1F780;BLACK LEFT-POINTING ISOSCELES RIGHT TRIANGLE;So;0;ON;;;;;N;;;;; +1F781;BLACK UP-POINTING ISOSCELES RIGHT TRIANGLE;So;0;ON;;;;;N;;;;; +1F782;BLACK RIGHT-POINTING ISOSCELES RIGHT TRIANGLE;So;0;ON;;;;;N;;;;; +1F783;BLACK DOWN-POINTING ISOSCELES RIGHT TRIANGLE;So;0;ON;;;;;N;;;;; +1F784;BLACK SLIGHTLY SMALL CIRCLE;So;0;ON;;;;;N;;;;; +1F785;MEDIUM BOLD WHITE CIRCLE;So;0;ON;;;;;N;;;;; +1F786;BOLD WHITE CIRCLE;So;0;ON;;;;;N;;;;; +1F787;HEAVY WHITE CIRCLE;So;0;ON;;;;;N;;;;; +1F788;VERY HEAVY WHITE CIRCLE;So;0;ON;;;;;N;;;;; +1F789;EXTREMELY HEAVY WHITE CIRCLE;So;0;ON;;;;;N;;;;; +1F78A;WHITE CIRCLE CONTAINING BLACK SMALL CIRCLE;So;0;ON;;;;;N;;;;; +1F78B;ROUND TARGET;So;0;ON;;;;;N;;;;; +1F78C;BLACK TINY SQUARE;So;0;ON;;;;;N;;;;; +1F78D;BLACK SLIGHTLY SMALL SQUARE;So;0;ON;;;;;N;;;;; +1F78E;LIGHT WHITE SQUARE;So;0;ON;;;;;N;;;;; +1F78F;MEDIUM WHITE SQUARE;So;0;ON;;;;;N;;;;; +1F790;BOLD WHITE SQUARE;So;0;ON;;;;;N;;;;; +1F791;HEAVY WHITE SQUARE;So;0;ON;;;;;N;;;;; +1F792;VERY HEAVY WHITE SQUARE;So;0;ON;;;;;N;;;;; +1F793;EXTREMELY HEAVY WHITE SQUARE;So;0;ON;;;;;N;;;;; +1F794;WHITE SQUARE CONTAINING BLACK VERY SMALL SQUARE;So;0;ON;;;;;N;;;;; +1F795;WHITE SQUARE CONTAINING BLACK MEDIUM SQUARE;So;0;ON;;;;;N;;;;; +1F796;SQUARE TARGET;So;0;ON;;;;;N;;;;; +1F797;BLACK TINY DIAMOND;So;0;ON;;;;;N;;;;; +1F798;BLACK VERY SMALL DIAMOND;So;0;ON;;;;;N;;;;; +1F799;BLACK MEDIUM SMALL DIAMOND;So;0;ON;;;;;N;;;;; +1F79A;WHITE DIAMOND CONTAINING BLACK VERY SMALL DIAMOND;So;0;ON;;;;;N;;;;; +1F79B;WHITE DIAMOND CONTAINING BLACK MEDIUM DIAMOND;So;0;ON;;;;;N;;;;; +1F79C;DIAMOND TARGET;So;0;ON;;;;;N;;;;; +1F79D;BLACK TINY LOZENGE;So;0;ON;;;;;N;;;;; +1F79E;BLACK VERY SMALL LOZENGE;So;0;ON;;;;;N;;;;; +1F79F;BLACK MEDIUM SMALL LOZENGE;So;0;ON;;;;;N;;;;; +1F7A0;WHITE LOZENGE CONTAINING BLACK SMALL LOZENGE;So;0;ON;;;;;N;;;;; +1F7A1;THIN GREEK CROSS;So;0;ON;;;;;N;;;;; +1F7A2;LIGHT GREEK CROSS;So;0;ON;;;;;N;;;;; +1F7A3;MEDIUM GREEK CROSS;So;0;ON;;;;;N;;;;; +1F7A4;BOLD GREEK CROSS;So;0;ON;;;;;N;;;;; +1F7A5;VERY BOLD GREEK CROSS;So;0;ON;;;;;N;;;;; +1F7A6;VERY HEAVY GREEK CROSS;So;0;ON;;;;;N;;;;; +1F7A7;EXTREMELY HEAVY GREEK CROSS;So;0;ON;;;;;N;;;;; +1F7A8;THIN SALTIRE;So;0;ON;;;;;N;;;;; +1F7A9;LIGHT SALTIRE;So;0;ON;;;;;N;;;;; +1F7AA;MEDIUM SALTIRE;So;0;ON;;;;;N;;;;; +1F7AB;BOLD SALTIRE;So;0;ON;;;;;N;;;;; +1F7AC;HEAVY SALTIRE;So;0;ON;;;;;N;;;;; +1F7AD;VERY HEAVY SALTIRE;So;0;ON;;;;;N;;;;; +1F7AE;EXTREMELY HEAVY SALTIRE;So;0;ON;;;;;N;;;;; +1F7AF;LIGHT FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7B0;MEDIUM FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7B1;BOLD FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7B2;HEAVY FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7B3;VERY HEAVY FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7B4;EXTREMELY HEAVY FIVE SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7B5;LIGHT SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7B6;MEDIUM SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7B7;BOLD SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7B8;HEAVY SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7B9;VERY HEAVY SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7BA;EXTREMELY HEAVY SIX SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7BB;LIGHT EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7BC;MEDIUM EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7BD;BOLD EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7BE;HEAVY EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7BF;VERY HEAVY EIGHT SPOKED ASTERISK;So;0;ON;;;;;N;;;;; +1F7C0;LIGHT THREE POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7C1;MEDIUM THREE POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7C2;THREE POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7C3;MEDIUM THREE POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;; +1F7C4;LIGHT FOUR POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7C5;MEDIUM FOUR POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7C6;FOUR POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7C7;MEDIUM FOUR POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;; +1F7C8;REVERSE LIGHT FOUR POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;; +1F7C9;LIGHT FIVE POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7CA;HEAVY FIVE POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7CB;MEDIUM SIX POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7CC;HEAVY SIX POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7CD;SIX POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;; +1F7CE;MEDIUM EIGHT POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7CF;HEAVY EIGHT POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7D0;VERY HEAVY EIGHT POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7D1;HEAVY EIGHT POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;; +1F7D2;LIGHT TWELVE POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7D3;HEAVY TWELVE POINTED BLACK STAR;So;0;ON;;;;;N;;;;; +1F7D4;HEAVY TWELVE POINTED PINWHEEL STAR;So;0;ON;;;;;N;;;;; +1F800;LEFTWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F801;UPWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F802;RIGHTWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F803;DOWNWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F804;LEFTWARDS ARROW WITH MEDIUM TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F805;UPWARDS ARROW WITH MEDIUM TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F806;RIGHTWARDS ARROW WITH MEDIUM TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F807;DOWNWARDS ARROW WITH MEDIUM TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F808;LEFTWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F809;UPWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F80A;RIGHTWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F80B;DOWNWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F810;LEFTWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F811;UPWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F812;RIGHTWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F813;DOWNWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F814;LEFTWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F815;UPWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F816;RIGHTWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F817;DOWNWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F818;HEAVY LEFTWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F819;HEAVY UPWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F81A;HEAVY RIGHTWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F81B;HEAVY DOWNWARDS ARROW WITH EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F81C;HEAVY LEFTWARDS ARROW WITH LARGE EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F81D;HEAVY UPWARDS ARROW WITH LARGE EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F81E;HEAVY RIGHTWARDS ARROW WITH LARGE EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F81F;HEAVY DOWNWARDS ARROW WITH LARGE EQUILATERAL ARROWHEAD;So;0;ON;;;;;N;;;;; +1F820;LEFTWARDS TRIANGLE-HEADED ARROW WITH NARROW SHAFT;So;0;ON;;;;;N;;;;; +1F821;UPWARDS TRIANGLE-HEADED ARROW WITH NARROW SHAFT;So;0;ON;;;;;N;;;;; +1F822;RIGHTWARDS TRIANGLE-HEADED ARROW WITH NARROW SHAFT;So;0;ON;;;;;N;;;;; +1F823;DOWNWARDS TRIANGLE-HEADED ARROW WITH NARROW SHAFT;So;0;ON;;;;;N;;;;; +1F824;LEFTWARDS TRIANGLE-HEADED ARROW WITH MEDIUM SHAFT;So;0;ON;;;;;N;;;;; +1F825;UPWARDS TRIANGLE-HEADED ARROW WITH MEDIUM SHAFT;So;0;ON;;;;;N;;;;; +1F826;RIGHTWARDS TRIANGLE-HEADED ARROW WITH MEDIUM SHAFT;So;0;ON;;;;;N;;;;; +1F827;DOWNWARDS TRIANGLE-HEADED ARROW WITH MEDIUM SHAFT;So;0;ON;;;;;N;;;;; +1F828;LEFTWARDS TRIANGLE-HEADED ARROW WITH BOLD SHAFT;So;0;ON;;;;;N;;;;; +1F829;UPWARDS TRIANGLE-HEADED ARROW WITH BOLD SHAFT;So;0;ON;;;;;N;;;;; +1F82A;RIGHTWARDS TRIANGLE-HEADED ARROW WITH BOLD SHAFT;So;0;ON;;;;;N;;;;; +1F82B;DOWNWARDS TRIANGLE-HEADED ARROW WITH BOLD SHAFT;So;0;ON;;;;;N;;;;; +1F82C;LEFTWARDS TRIANGLE-HEADED ARROW WITH HEAVY SHAFT;So;0;ON;;;;;N;;;;; +1F82D;UPWARDS TRIANGLE-HEADED ARROW WITH HEAVY SHAFT;So;0;ON;;;;;N;;;;; +1F82E;RIGHTWARDS TRIANGLE-HEADED ARROW WITH HEAVY SHAFT;So;0;ON;;;;;N;;;;; +1F82F;DOWNWARDS TRIANGLE-HEADED ARROW WITH HEAVY SHAFT;So;0;ON;;;;;N;;;;; +1F830;LEFTWARDS TRIANGLE-HEADED ARROW WITH VERY HEAVY SHAFT;So;0;ON;;;;;N;;;;; +1F831;UPWARDS TRIANGLE-HEADED ARROW WITH VERY HEAVY SHAFT;So;0;ON;;;;;N;;;;; +1F832;RIGHTWARDS TRIANGLE-HEADED ARROW WITH VERY HEAVY SHAFT;So;0;ON;;;;;N;;;;; +1F833;DOWNWARDS TRIANGLE-HEADED ARROW WITH VERY HEAVY SHAFT;So;0;ON;;;;;N;;;;; +1F834;LEFTWARDS FINGER-POST ARROW;So;0;ON;;;;;N;;;;; +1F835;UPWARDS FINGER-POST ARROW;So;0;ON;;;;;N;;;;; +1F836;RIGHTWARDS FINGER-POST ARROW;So;0;ON;;;;;N;;;;; +1F837;DOWNWARDS FINGER-POST ARROW;So;0;ON;;;;;N;;;;; +1F838;LEFTWARDS SQUARED ARROW;So;0;ON;;;;;N;;;;; +1F839;UPWARDS SQUARED ARROW;So;0;ON;;;;;N;;;;; +1F83A;RIGHTWARDS SQUARED ARROW;So;0;ON;;;;;N;;;;; +1F83B;DOWNWARDS SQUARED ARROW;So;0;ON;;;;;N;;;;; +1F83C;LEFTWARDS COMPRESSED ARROW;So;0;ON;;;;;N;;;;; +1F83D;UPWARDS COMPRESSED ARROW;So;0;ON;;;;;N;;;;; +1F83E;RIGHTWARDS COMPRESSED ARROW;So;0;ON;;;;;N;;;;; +1F83F;DOWNWARDS COMPRESSED ARROW;So;0;ON;;;;;N;;;;; +1F840;LEFTWARDS HEAVY COMPRESSED ARROW;So;0;ON;;;;;N;;;;; +1F841;UPWARDS HEAVY COMPRESSED ARROW;So;0;ON;;;;;N;;;;; +1F842;RIGHTWARDS HEAVY COMPRESSED ARROW;So;0;ON;;;;;N;;;;; +1F843;DOWNWARDS HEAVY COMPRESSED ARROW;So;0;ON;;;;;N;;;;; +1F844;LEFTWARDS HEAVY ARROW;So;0;ON;;;;;N;;;;; +1F845;UPWARDS HEAVY ARROW;So;0;ON;;;;;N;;;;; +1F846;RIGHTWARDS HEAVY ARROW;So;0;ON;;;;;N;;;;; +1F847;DOWNWARDS HEAVY ARROW;So;0;ON;;;;;N;;;;; +1F850;LEFTWARDS SANS-SERIF ARROW;So;0;ON;;;;;N;;;;; +1F851;UPWARDS SANS-SERIF ARROW;So;0;ON;;;;;N;;;;; +1F852;RIGHTWARDS SANS-SERIF ARROW;So;0;ON;;;;;N;;;;; +1F853;DOWNWARDS SANS-SERIF ARROW;So;0;ON;;;;;N;;;;; +1F854;NORTH WEST SANS-SERIF ARROW;So;0;ON;;;;;N;;;;; +1F855;NORTH EAST SANS-SERIF ARROW;So;0;ON;;;;;N;;;;; +1F856;SOUTH EAST SANS-SERIF ARROW;So;0;ON;;;;;N;;;;; +1F857;SOUTH WEST SANS-SERIF ARROW;So;0;ON;;;;;N;;;;; +1F858;LEFT RIGHT SANS-SERIF ARROW;So;0;ON;;;;;N;;;;; +1F859;UP DOWN SANS-SERIF ARROW;So;0;ON;;;;;N;;;;; +1F860;WIDE-HEADED LEFTWARDS LIGHT BARB ARROW;So;0;ON;;;;;N;;;;; +1F861;WIDE-HEADED UPWARDS LIGHT BARB ARROW;So;0;ON;;;;;N;;;;; +1F862;WIDE-HEADED RIGHTWARDS LIGHT BARB ARROW;So;0;ON;;;;;N;;;;; +1F863;WIDE-HEADED DOWNWARDS LIGHT BARB ARROW;So;0;ON;;;;;N;;;;; +1F864;WIDE-HEADED NORTH WEST LIGHT BARB ARROW;So;0;ON;;;;;N;;;;; +1F865;WIDE-HEADED NORTH EAST LIGHT BARB ARROW;So;0;ON;;;;;N;;;;; +1F866;WIDE-HEADED SOUTH EAST LIGHT BARB ARROW;So;0;ON;;;;;N;;;;; +1F867;WIDE-HEADED SOUTH WEST LIGHT BARB ARROW;So;0;ON;;;;;N;;;;; +1F868;WIDE-HEADED LEFTWARDS BARB ARROW;So;0;ON;;;;;N;;;;; +1F869;WIDE-HEADED UPWARDS BARB ARROW;So;0;ON;;;;;N;;;;; +1F86A;WIDE-HEADED RIGHTWARDS BARB ARROW;So;0;ON;;;;;N;;;;; +1F86B;WIDE-HEADED DOWNWARDS BARB ARROW;So;0;ON;;;;;N;;;;; +1F86C;WIDE-HEADED NORTH WEST BARB ARROW;So;0;ON;;;;;N;;;;; +1F86D;WIDE-HEADED NORTH EAST BARB ARROW;So;0;ON;;;;;N;;;;; +1F86E;WIDE-HEADED SOUTH EAST BARB ARROW;So;0;ON;;;;;N;;;;; +1F86F;WIDE-HEADED SOUTH WEST BARB ARROW;So;0;ON;;;;;N;;;;; +1F870;WIDE-HEADED LEFTWARDS MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;; +1F871;WIDE-HEADED UPWARDS MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;; +1F872;WIDE-HEADED RIGHTWARDS MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;; +1F873;WIDE-HEADED DOWNWARDS MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;; +1F874;WIDE-HEADED NORTH WEST MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;; +1F875;WIDE-HEADED NORTH EAST MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;; +1F876;WIDE-HEADED SOUTH EAST MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;; +1F877;WIDE-HEADED SOUTH WEST MEDIUM BARB ARROW;So;0;ON;;;;;N;;;;; +1F878;WIDE-HEADED LEFTWARDS HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F879;WIDE-HEADED UPWARDS HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F87A;WIDE-HEADED RIGHTWARDS HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F87B;WIDE-HEADED DOWNWARDS HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F87C;WIDE-HEADED NORTH WEST HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F87D;WIDE-HEADED NORTH EAST HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F87E;WIDE-HEADED SOUTH EAST HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F87F;WIDE-HEADED SOUTH WEST HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F880;WIDE-HEADED LEFTWARDS VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F881;WIDE-HEADED UPWARDS VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F882;WIDE-HEADED RIGHTWARDS VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F883;WIDE-HEADED DOWNWARDS VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F884;WIDE-HEADED NORTH WEST VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F885;WIDE-HEADED NORTH EAST VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F886;WIDE-HEADED SOUTH EAST VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F887;WIDE-HEADED SOUTH WEST VERY HEAVY BARB ARROW;So;0;ON;;;;;N;;;;; +1F890;LEFTWARDS TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F891;UPWARDS TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F892;RIGHTWARDS TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F893;DOWNWARDS TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F894;LEFTWARDS WHITE ARROW WITHIN TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F895;UPWARDS WHITE ARROW WITHIN TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F896;RIGHTWARDS WHITE ARROW WITHIN TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F897;DOWNWARDS WHITE ARROW WITHIN TRIANGLE ARROWHEAD;So;0;ON;;;;;N;;;;; +1F898;LEFTWARDS ARROW WITH NOTCHED TAIL;So;0;ON;;;;;N;;;;; +1F899;UPWARDS ARROW WITH NOTCHED TAIL;So;0;ON;;;;;N;;;;; +1F89A;RIGHTWARDS ARROW WITH NOTCHED TAIL;So;0;ON;;;;;N;;;;; +1F89B;DOWNWARDS ARROW WITH NOTCHED TAIL;So;0;ON;;;;;N;;;;; +1F89C;HEAVY ARROW SHAFT WIDTH ONE;So;0;ON;;;;;N;;;;; +1F89D;HEAVY ARROW SHAFT WIDTH TWO THIRDS;So;0;ON;;;;;N;;;;; +1F89E;HEAVY ARROW SHAFT WIDTH ONE HALF;So;0;ON;;;;;N;;;;; +1F89F;HEAVY ARROW SHAFT WIDTH ONE THIRD;So;0;ON;;;;;N;;;;; +1F8A0;LEFTWARDS BOTTOM-SHADED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8A1;RIGHTWARDS BOTTOM SHADED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8A2;LEFTWARDS TOP SHADED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8A3;RIGHTWARDS TOP SHADED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8A4;LEFTWARDS LEFT-SHADED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8A5;RIGHTWARDS RIGHT-SHADED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8A6;LEFTWARDS RIGHT-SHADED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8A7;RIGHTWARDS LEFT-SHADED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8A8;LEFTWARDS BACK-TILTED SHADOWED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8A9;RIGHTWARDS BACK-TILTED SHADOWED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8AA;LEFTWARDS FRONT-TILTED SHADOWED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8AB;RIGHTWARDS FRONT-TILTED SHADOWED WHITE ARROW;So;0;ON;;;;;N;;;;; +1F8AC;WHITE ARROW SHAFT WIDTH ONE;So;0;ON;;;;;N;;;;; +1F8AD;WHITE ARROW SHAFT WIDTH TWO THIRDS;So;0;ON;;;;;N;;;;; 20000;<CJK Ideograph Extension B, First>;Lo;0;L;;;;;N;;;;; 2A6D6;<CJK Ideograph Extension B, Last>;Lo;0;L;;;;;N;;;;; 2A700;<CJK Ideograph Extension C, First>;Lo;0;L;;;;;N;;;;;
View file
cyrus-imapd-2.5.tar.gz/lib/charset/aliases.txt
Changed
@@ -1,7 +1,11 @@ # funky names for UTF charsets utf8 utf-8 utf7 utf-7 -unicode-1-1-utf-8 utf8 +unicode-1-1-utf-8 utf-8 + +# even ASCII is not exempt +ascii us-ascii +ANSI_X3.4-1968 us-ascii # Yes, the world does contain a lot of rubbish # ways of saying iso-8859-1 @@ -21,8 +25,14 @@ x-user-defined iso-8859-1 none iso-8859-1 latin1 iso-8859-1 +ISO8859-1 iso-8859-1 +8859-1 iso-8859-1 +DISPLAY iso-8859-1 +Any iso-8859-1 + # sort-of - see Filter::Trigraph 646 iso-8859-1 +ISO646-US iso-8859-1 # other aliases 8859_2 iso-8859-2 @@ -30,6 +40,7 @@ greek-ccitt iso-8859-7 iso-8859-8-i iso-8859-8 ISO8859_15_FDIS iso-8859-15 +ISO8859-15 iso-8859-15 tis-620 iso-8859-11 ASMO-708 iso-8859-6 WIN-KOI8 koi8-r @@ -47,6 +58,10 @@ cp1252 windows-1252 win-1252 windows-1252 +1254 windows-1254 +cp1254 windows-1254 +win-1254 windows-1254 + 1255 windows-1255 cp1255 windows-1255 win-1255 windows-1255 @@ -55,6 +70,10 @@ cp1256 windows-1256 win-1256 windows-1256 +1258 windows-1258 +cp1258 windows-1258 +win-1258 windows-1258 + # stuff from the FastMail Perl mapping that we # don't have support for in Cyrus #x-gbk gbk
View file
cyrus-imapd-2.5.tar.gz/lib/charset/windows-1254.t
Added
@@ -0,0 +1,292 @@ +# +# Copyright (c) 1994-2014 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. +# +# Charset put together by Bron Gondwana <brong@fastmail.fm> +# Reference: http://msdn.microsoft.com/en-us/goglobal/cc305147 + +00 0000 NULL +01 0001 START OF HEADING +02 0002 START OF TEXT +03 0003 END OF TEXT +04 0004 END OF TRANSMISSION +05 0005 ENQUIRY +06 0006 ACKNOWLEDGE +07 0007 BELL +08 0008 BACKSPACE +09 0009 HORIZONTAL TABULATION +0A 000A LINE FEED +0B 000B VERTICAL TABULATION +0C 000C FORM FEED +0D 000D CARRIAGE RETURN +0E 000E SHIFT OUT +0F 000F SHIFT IN +10 0010 DATA LINK ESCAPE +11 0011 DEVICE CONTROL ONE +12 0012 DEVICE CONTROL TWO +13 0013 DEVICE CONTROL THREE +14 0014 DEVICE CONTROL FOUR +15 0015 NEGATIVE ACKNOWLEDGE +16 0016 SYNCHRONOUS IDLE +17 0017 END OF TRANSMISSION BLOCK +18 0018 CANCEL +19 0019 END OF MEDIUM +1A 001A SUBSTITUTE +1B 001B ESCAPE +1C 001C FILE SEPARATOR +1D 001D GROUP SEPARATOR +1E 001E RECORD SEPARATOR +1F 001F UNIT SEPARATOR +20 0020 SPACE +21 0021 EXCLAMATION MARK +22 0022 QUOTATION MARK +23 0023 NUMBER SIGN +24 0024 DOLLAR SIGN +25 0025 PERCENT SIGN +26 0026 AMPERSAND +27 0027 APOSTROPHE +28 0028 LEFT PARENTHESIS +29 0029 RIGHT PARENTHESIS +2A 002A ASTERISK +2B 002B PLUS SIGN +2C 002C COMMA +2D 002D HYPHEN-MINUS +2E 002E FULL STOP +2F 002F SOLIDUS +30 0030 DIGIT ZERO +31 0031 DIGIT ONE +32 0032 DIGIT TWO +33 0033 DIGIT THREE +34 0034 DIGIT FOUR +35 0035 DIGIT FIVE +36 0036 DIGIT SIX +37 0037 DIGIT SEVEN +38 0038 DIGIT EIGHT +39 0039 DIGIT NINE +3A 003A COLON +3B 003B SEMICOLON +3C 003C LESS-THAN SIGN +3D 003D EQUALS SIGN +3E 003E GREATER-THAN SIGN +3F 003F QUESTION MARK +40 0040 COMMERCIAL AT +41 0041 LATIN CAPITAL LETTER A +42 0042 LATIN CAPITAL LETTER B +43 0043 LATIN CAPITAL LETTER C +44 0044 LATIN CAPITAL LETTER D +45 0045 LATIN CAPITAL LETTER E +46 0046 LATIN CAPITAL LETTER F +47 0047 LATIN CAPITAL LETTER G +48 0048 LATIN CAPITAL LETTER H +49 0049 LATIN CAPITAL LETTER I +4A 004A LATIN CAPITAL LETTER J +4B 004B LATIN CAPITAL LETTER K +4C 004C LATIN CAPITAL LETTER L +4D 004D LATIN CAPITAL LETTER M +4E 004E LATIN CAPITAL LETTER N +4F 004F LATIN CAPITAL LETTER O +50 0050 LATIN CAPITAL LETTER P +51 0051 LATIN CAPITAL LETTER Q +52 0052 LATIN CAPITAL LETTER R +53 0053 LATIN CAPITAL LETTER S +54 0054 LATIN CAPITAL LETTER T +55 0055 LATIN CAPITAL LETTER U +56 0056 LATIN CAPITAL LETTER V +57 0057 LATIN CAPITAL LETTER W +58 0058 LATIN CAPITAL LETTER X +59 0059 LATIN CAPITAL LETTER Y +5A 005A LATIN CAPITAL LETTER Z +5B 005B LEFT SQUARE BRACKET +5C 005C REVERSE SOLIDUS +5D 005D RIGHT SQUARE BRACKET +5E 005E CIRCUMFLEX ACCENT +5F 005F LOW LINE +60 0060 GRAVE ACCENT +61 0061 LATIN SMALL LETTER A +62 0062 LATIN SMALL LETTER B +63 0063 LATIN SMALL LETTER C +64 0064 LATIN SMALL LETTER D +65 0065 LATIN SMALL LETTER E +66 0066 LATIN SMALL LETTER F +67 0067 LATIN SMALL LETTER G +68 0068 LATIN SMALL LETTER H +69 0069 LATIN SMALL LETTER I +6A 006A LATIN SMALL LETTER J +6B 006B LATIN SMALL LETTER K +6C 006C LATIN SMALL LETTER L +6D 006D LATIN SMALL LETTER M +6E 006E LATIN SMALL LETTER N +6F 006F LATIN SMALL LETTER O +70 0070 LATIN SMALL LETTER P +71 0071 LATIN SMALL LETTER Q +72 0072 LATIN SMALL LETTER R +73 0073 LATIN SMALL LETTER S +74 0074 LATIN SMALL LETTER T +75 0075 LATIN SMALL LETTER U +76 0076 LATIN SMALL LETTER V +77 0077 LATIN SMALL LETTER W +78 0078 LATIN SMALL LETTER X +79 0079 LATIN SMALL LETTER Y +7A 007A LATIN SMALL LETTER Z +7B 007B LEFT CURLY BRACKET +7C 007C VERTICAL LINE +7D 007D RIGHT CURLY BRACKET +7E 007E TILDE +7F 007F DELETE +80 20AC EURO SIGN +82 201A SINGLE LOW-9 QUOTATION MARK +83 0192 LATIN SMALL LETTER F WITH HOOK +84 201E DOUBLE LOW-9 QUOTATION MARK +85 2026 HORIZONTAL ELLIPSIS +86 2020 DAGGER +87 2021 DOUBLE DAGGER +88 02C6 MODIFIER LETTER CIRCUMFLEX ACCENT +89 2030 PER MILLE SIGN +8A 0160 LATIN CAPITAL LETTER S WITH CARON +8B 2039 SINGLE LEFT-POINTING ANGLE QUOTATION MARK +8C 0152 LATIN CAPITAL LIGATURE OE +91 2018 LEFT SINGLE QUOTATION MARK +92 2019 RIGHT SINGLE QUOTATION MARK +93 201C LEFT DOUBLE QUOTATION MARK +94 201D RIGHT DOUBLE QUOTATION MARK +95 2022 BULLET +96 2013 EN DASH +97 2014 EM DASH +98 02DC SMALL TILDE +99 2122 TRADE MARK SIGN +9A 0161 LATIN SMALL LETTER S WITH CARON +9B 203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK +9C 0153 LATIN SMALL LIGATURE OE +9F 0178 LATIN CAPITAL LETTER Y WITH DIAERESIS +A0 00A0 NO-BREAK SPACE +A1 00A1 INVERTED EXCLAMATION MARK +A2 00A2 CENT SIGN +A3 00A3 POUND SIGN +A4 00A4 CURRENCY SIGN +A5 00A5 YEN SIGN +A6 00A6 BROKEN BAR +A7 00A7 SECTION SIGN +A8 00A8 DIAERESIS +A9 00A9 COPYRIGHT SIGN +AA 00AA FEMININE ORDINAL INDICATOR +AB 00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK +AC 00AC NOT SIGN +AD 00AD SOFT HYPHEN +AE 00AE REGISTERED SIGN +AF 00AF MACRON +B0 00B0 DEGREE SIGN +B1 00B1 PLUS-MINUS SIGN +B2 00B2 SUPERSCRIPT TWO +B3 00B3 SUPERSCRIPT THREE +B4 00B4 ACUTE ACCENT +B5 00B5 MICRO SIGN +B6 00B6 PILCROW SIGN +B7 00B7 MIDDLE DOT +B8 00B8 CEDILLA +B9 00B9 SUPERSCRIPT ONE +BA 00BA MASCULINE ORDINAL INDICATOR +BB 00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK +BC 00BC VULGAR FRACTION ONE QUARTER +BD 00BD VULGAR FRACTION ONE HALF +BE 00BE VULGAR FRACTION THREE QUARTERS +BF 00BF INVERTED QUESTION MARK +C0 00C0 LATIN CAPITAL LETTER A WITH GRAVE +C1 00C1 LATIN CAPITAL LETTER A WITH ACUTE +C2 00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX +C3 00C3 LATIN CAPITAL LETTER A WITH TILDE +C4 00C4 LATIN CAPITAL LETTER A WITH DIAERESIS +C5 00C5 LATIN CAPITAL LETTER A WITH RING ABOVE +C6 00C6 LATIN CAPITAL LETTER AE +C7 00C7 LATIN CAPITAL LETTER C WITH CEDILLA +C8 00C8 LATIN CAPITAL LETTER E WITH GRAVE +C9 00C9 LATIN CAPITAL LETTER E WITH ACUTE +CA 00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX +CB 00CB LATIN CAPITAL LETTER E WITH DIAERESIS +CC 00CC LATIN CAPITAL LETTER I WITH GRAVE +CD 00CD LATIN CAPITAL LETTER I WITH ACUTE +CE 00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX +CF 00CF LATIN CAPITAL LETTER I WITH DIAERESIS +D0 011E LATIN CAPITAL LETTER G WITH BREVE +D1 00D1 LATIN CAPITAL LETTER N WITH TILDE +D2 00D2 LATIN CAPITAL LETTER O WITH GRAVE +D3 00D3 LATIN CAPITAL LETTER O WITH ACUTE +D4 00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX +D5 00D5 LATIN CAPITAL LETTER O WITH TILDE +D6 00D6 LATIN CAPITAL LETTER O WITH DIAERESIS +D7 00D7 MULTIPLICATION SIGN +D8 00D8 LATIN CAPITAL LETTER O WITH STROKE +D9 00D9 LATIN CAPITAL LETTER U WITH GRAVE +DA 00DA LATIN CAPITAL LETTER U WITH ACUTE +DB 00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX +DC 00DC LATIN CAPITAL LETTER U WITH DIAERESIS +DD 0130 LATIN CAPITAL LETTER I WITH DOT ABOVE +DE 015E LATIN CAPITAL LETTER S WITH CEDILLA +DF 00DF LATIN SMALL LETTER SHARP S +E0 00E0 LATIN SMALL LETTER A WITH GRAVE +E1 00E1 LATIN SMALL LETTER A WITH ACUTE +E2 00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX +E3 00E3 LATIN SMALL LETTER A WITH TILDE +E4 00E4 LATIN SMALL LETTER A WITH DIAERESIS +E5 00E5 LATIN SMALL LETTER A WITH RING ABOVE +E6 00E6 LATIN SMALL LETTER AE +E7 00E7 LATIN SMALL LETTER C WITH CEDILLA +E8 00E8 LATIN SMALL LETTER E WITH GRAVE +E9 00E9 LATIN SMALL LETTER E WITH ACUTE +EA 00EA LATIN SMALL LETTER E WITH CIRCUMFLEX +EB 00EB LATIN SMALL LETTER E WITH DIAERESIS +EC 00EC LATIN SMALL LETTER I WITH GRAVE +ED 00ED LATIN SMALL LETTER I WITH ACUTE +EE 00EE LATIN SMALL LETTER I WITH CIRCUMFLEX +EF 00EF LATIN SMALL LETTER I WITH DIAERESIS +F0 011F LATIN SMALL LETTER G WITH BREVE +F1 00F1 LATIN SMALL LETTER N WITH TILDE +F2 00F2 LATIN SMALL LETTER O WITH GRAVE +F3 00F3 LATIN SMALL LETTER O WITH ACUTE +F4 00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX +F5 00F5 LATIN SMALL LETTER O WITH TILDE +F6 00F6 LATIN SMALL LETTER O WITH DIAERESIS +F7 00F7 DIVISION SIGN +F8 00F8 LATIN SMALL LETTER O WITH STROKE +F9 00F9 LATIN SMALL LETTER U WITH GRAVE +FA 00FA LATIN SMALL LETTER U WITH ACUTE +FB 00FB LATIN SMALL LETTER U WITH CIRCUMFLEX +FC 00FC LATIN SMALL LETTER U WITH DIAERESIS +FD 0131 LATIN SMALL LETTER DOTLESS I +FE 015F LATIN SMALL LETTER S WITH CEDILLA +FF 00FF LATIN SMALL LETTER Y WITH DIAERESIS
View file
cyrus-imapd-2.5.tar.gz/lib/charset/windows-1258.t
Added
@@ -0,0 +1,290 @@ +# +# Copyright (c) 1994-2014 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. +# +# Charset put together by Bron Gondwana <brong@fastmail.fm> +# Reference: http://msdn.microsoft.com/en-us/goglobal/cc305151 + +00 0000 NULL +01 0001 START OF HEADING +02 0002 START OF TEXT +03 0003 END OF TEXT +04 0004 END OF TRANSMISSION +05 0005 ENQUIRY +06 0006 ACKNOWLEDGE +07 0007 BELL +08 0008 BACKSPACE +09 0009 HORIZONTAL TABULATION +0A 000A LINE FEED +0B 000B VERTICAL TABULATION +0C 000C FORM FEED +0D 000D CARRIAGE RETURN +0E 000E SHIFT OUT +0F 000F SHIFT IN +10 0010 DATA LINK ESCAPE +11 0011 DEVICE CONTROL ONE +12 0012 DEVICE CONTROL TWO +13 0013 DEVICE CONTROL THREE +14 0014 DEVICE CONTROL FOUR +15 0015 NEGATIVE ACKNOWLEDGE +16 0016 SYNCHRONOUS IDLE +17 0017 END OF TRANSMISSION BLOCK +18 0018 CANCEL +19 0019 END OF MEDIUM +1A 001A SUBSTITUTE +1B 001B ESCAPE +1C 001C FILE SEPARATOR +1D 001D GROUP SEPARATOR +1E 001E RECORD SEPARATOR +1F 001F UNIT SEPARATOR +20 0020 SPACE +21 0021 EXCLAMATION MARK +22 0022 QUOTATION MARK +23 0023 NUMBER SIGN +24 0024 DOLLAR SIGN +25 0025 PERCENT SIGN +26 0026 AMPERSAND +27 0027 APOSTROPHE +28 0028 LEFT PARENTHESIS +29 0029 RIGHT PARENTHESIS +2A 002A ASTERISK +2B 002B PLUS SIGN +2C 002C COMMA +2D 002D HYPHEN-MINUS +2E 002E FULL STOP +2F 002F SOLIDUS +30 0030 DIGIT ZERO +31 0031 DIGIT ONE +32 0032 DIGIT TWO +33 0033 DIGIT THREE +34 0034 DIGIT FOUR +35 0035 DIGIT FIVE +36 0036 DIGIT SIX +37 0037 DIGIT SEVEN +38 0038 DIGIT EIGHT +39 0039 DIGIT NINE +3A 003A COLON +3B 003B SEMICOLON +3C 003C LESS-THAN SIGN +3D 003D EQUALS SIGN +3E 003E GREATER-THAN SIGN +3F 003F QUESTION MARK +40 0040 COMMERCIAL AT +41 0041 LATIN CAPITAL LETTER A +42 0042 LATIN CAPITAL LETTER B +43 0043 LATIN CAPITAL LETTER C +44 0044 LATIN CAPITAL LETTER D +45 0045 LATIN CAPITAL LETTER E +46 0046 LATIN CAPITAL LETTER F +47 0047 LATIN CAPITAL LETTER G +48 0048 LATIN CAPITAL LETTER H +49 0049 LATIN CAPITAL LETTER I +4A 004A LATIN CAPITAL LETTER J +4B 004B LATIN CAPITAL LETTER K +4C 004C LATIN CAPITAL LETTER L +4D 004D LATIN CAPITAL LETTER M +4E 004E LATIN CAPITAL LETTER N +4F 004F LATIN CAPITAL LETTER O +50 0050 LATIN CAPITAL LETTER P +51 0051 LATIN CAPITAL LETTER Q +52 0052 LATIN CAPITAL LETTER R +53 0053 LATIN CAPITAL LETTER S +54 0054 LATIN CAPITAL LETTER T +55 0055 LATIN CAPITAL LETTER U +56 0056 LATIN CAPITAL LETTER V +57 0057 LATIN CAPITAL LETTER W +58 0058 LATIN CAPITAL LETTER X +59 0059 LATIN CAPITAL LETTER Y +5A 005A LATIN CAPITAL LETTER Z +5B 005B LEFT SQUARE BRACKET +5C 005C REVERSE SOLIDUS +5D 005D RIGHT SQUARE BRACKET +5E 005E CIRCUMFLEX ACCENT +5F 005F LOW LINE +60 0060 GRAVE ACCENT +61 0061 LATIN SMALL LETTER A +62 0062 LATIN SMALL LETTER B +63 0063 LATIN SMALL LETTER C +64 0064 LATIN SMALL LETTER D +65 0065 LATIN SMALL LETTER E +66 0066 LATIN SMALL LETTER F +67 0067 LATIN SMALL LETTER G +68 0068 LATIN SMALL LETTER H +69 0069 LATIN SMALL LETTER I +6A 006A LATIN SMALL LETTER J +6B 006B LATIN SMALL LETTER K +6C 006C LATIN SMALL LETTER L +6D 006D LATIN SMALL LETTER M +6E 006E LATIN SMALL LETTER N +6F 006F LATIN SMALL LETTER O +70 0070 LATIN SMALL LETTER P +71 0071 LATIN SMALL LETTER Q +72 0072 LATIN SMALL LETTER R +73 0073 LATIN SMALL LETTER S +74 0074 LATIN SMALL LETTER T +75 0075 LATIN SMALL LETTER U +76 0076 LATIN SMALL LETTER V +77 0077 LATIN SMALL LETTER W +78 0078 LATIN SMALL LETTER X +79 0079 LATIN SMALL LETTER Y +7A 007A LATIN SMALL LETTER Z +7B 007B LEFT CURLY BRACKET +7C 007C VERTICAL LINE +7D 007D RIGHT CURLY BRACKET +7E 007E TILDE +7F 007F DELETE +80 20AC EURO SIGN +82 201A SINGLE LOW-9 QUOTATION MARK +83 0192 LATIN SMALL LETTER F WITH HOOK +84 201E DOUBLE LOW-9 QUOTATION MARK +85 2026 HORIZONTAL ELLIPSIS +86 2020 DAGGER +87 2021 DOUBLE DAGGER +88 02C6 MODIFIER LETTER CIRCUMFLEX ACCENT +89 2030 PER MILLE SIGN +8B 2039 SINGLE LEFT-POINTING ANGLE QUOTATION MARK +8C 0152 LATIN CAPITAL LIGATURE OE +91 2018 LEFT SINGLE QUOTATION MARK +92 2019 RIGHT SINGLE QUOTATION MARK +93 201C LEFT DOUBLE QUOTATION MARK +94 201D RIGHT DOUBLE QUOTATION MARK +95 2022 BULLET +96 2013 EN DASH +97 2014 EM DASH +98 02DC SMALL TILDE +99 2122 TRADE MARK SIGN +9B 203A SINGLE RIGHT-POINTING ANGLE QUOTATION MARK +9C 0153 LATIN SMALL LIGATURE OE +9F 0178 LATIN CAPITAL LETTER Y WITH DIAERESIS +A0 00A0 NO-BREAK SPACE +A1 00A1 INVERTED EXCLAMATION MARK +A2 00A2 CENT SIGN +A3 00A3 POUND SIGN +A4 00A4 CURRENCY SIGN +A5 00A5 YEN SIGN +A6 00A6 BROKEN BAR +A7 00A7 SECTION SIGN +A8 00A8 DIAERESIS +A9 00A9 COPYRIGHT SIGN +AA 00AA FEMININE ORDINAL INDICATOR +AB 00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK +AC 00AC NOT SIGN +AD 00AD SOFT HYPHEN +AE 00AE REGISTERED SIGN +AF 00AF MACRON +B0 00B0 DEGREE SIGN +B1 00B1 PLUS-MINUS SIGN +B2 00B2 SUPERSCRIPT TWO +B3 00B3 SUPERSCRIPT THREE +B4 00B4 ACUTE ACCENT +B5 00B5 MICRO SIGN +B6 00B6 PILCROW SIGN +B7 00B7 MIDDLE DOT +B8 00B8 CEDILLA +B9 00B9 SUPERSCRIPT ONE +BA 00BA MASCULINE ORDINAL INDICATOR +BB 00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK +BC 00BC VULGAR FRACTION ONE QUARTER +BD 00BD VULGAR FRACTION ONE HALF +BE 00BE VULGAR FRACTION THREE QUARTERS +BF 00BF INVERTED QUESTION MARK +C0 00C0 LATIN CAPITAL LETTER A WITH GRAVE +C1 00C1 LATIN CAPITAL LETTER A WITH ACUTE +C2 00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX +C3 0102 LATIN CAPITAL LETTER A WITH BREVE +C4 00C4 LATIN CAPITAL LETTER A WITH DIAERESIS +C5 00C5 LATIN CAPITAL LETTER A WITH RING ABOVE +C6 00C6 LATIN CAPITAL LETTER AE +C7 00C7 LATIN CAPITAL LETTER C WITH CEDILLA +C8 00C8 LATIN CAPITAL LETTER E WITH GRAVE +C9 00C9 LATIN CAPITAL LETTER E WITH ACUTE +CA 00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX +CB 00CB LATIN CAPITAL LETTER E WITH DIAERESIS +CC 0300 COMBINING GRAVE ACCENT +CD 00CD LATIN CAPITAL LETTER I WITH ACUTE +CE 00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX +CF 00CF LATIN CAPITAL LETTER I WITH DIAERESIS +D0 0110 LATIN CAPITAL LETTER D WITH STROKE +D1 00D1 LATIN CAPITAL LETTER N WITH TILDE +D2 0309 COMBINING HOOK ABOVE +D3 00D3 LATIN CAPITAL LETTER O WITH ACUTE +D4 00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX +D5 01A0 LATIN CAPITAL LETTER O WITH HORN +D6 00D6 LATIN CAPITAL LETTER O WITH DIAERESIS +D7 00D7 MULTIPLICATION SIGN +D8 00D8 LATIN CAPITAL LETTER O WITH STROKE +D9 00D9 LATIN CAPITAL LETTER U WITH GRAVE +DA 00DA LATIN CAPITAL LETTER U WITH ACUTE +DB 00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX +DC 00DC LATIN CAPITAL LETTER U WITH DIAERESIS +DD 01AF LATIN CAPITAL LETTER U WITH HORN +DE 0303 COMBINING TILDE +DF 00DF LATIN SMALL LETTER SHARP S +E0 00E0 LATIN SMALL LETTER A WITH GRAVE +E1 00E1 LATIN SMALL LETTER A WITH ACUTE +E2 00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX +E3 0103 LATIN SMALL LETTER A WITH BREVE +E4 00E4 LATIN SMALL LETTER A WITH DIAERESIS +E5 00E5 LATIN SMALL LETTER A WITH RING ABOVE +E6 00E6 LATIN SMALL LETTER AE +E7 00E7 LATIN SMALL LETTER C WITH CEDILLA +E8 00E8 LATIN SMALL LETTER E WITH GRAVE +E9 00E9 LATIN SMALL LETTER E WITH ACUTE +EA 00EA LATIN SMALL LETTER E WITH CIRCUMFLEX +EB 00EB LATIN SMALL LETTER E WITH DIAERESIS +EC 0301 COMBINING ACUTE ACCENT +ED 00ED LATIN SMALL LETTER I WITH ACUTE +EE 00EE LATIN SMALL LETTER I WITH CIRCUMFLEX +EF 00EF LATIN SMALL LETTER I WITH DIAERESIS +F0 0111 LATIN SMALL LETTER D WITH STROKE +F1 00F1 LATIN SMALL LETTER N WITH TILDE +F2 0323 COMBINING DOT BELOW +F3 00F3 LATIN SMALL LETTER O WITH ACUTE +F4 00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX +F5 01A1 LATIN SMALL LETTER O WITH HORN +F6 00F6 LATIN SMALL LETTER O WITH DIAERESIS +F7 00F7 DIVISION SIGN +F8 00F8 LATIN SMALL LETTER O WITH STROKE +F9 00F9 LATIN SMALL LETTER U WITH GRAVE +FA 00FA LATIN SMALL LETTER U WITH ACUTE +FB 00FB LATIN SMALL LETTER U WITH CIRCUMFLEX +FC 00FC LATIN SMALL LETTER U WITH DIAERESIS +FD 01B0 LATIN SMALL LETTER U WITH HORN +FE 20AB DONG SIGN +FF 00FF LATIN SMALL LETTER Y WITH DIAERESIS
View file
cyrus-imapd-2.5.tar.gz/lib/charset/windows-874.t
Added
@@ -0,0 +1,270 @@ +# +# Copyright (c) 1994-2014 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. +# +# Charset put together by Bron Gondwana <brong@fastmail.fm> +# Reference: http://msdn.microsoft.com/en-au/goglobal/cc305142.aspx + +00 0000 NULL +01 0001 START OF HEADING +02 0002 START OF TEXT +03 0003 END OF TEXT +04 0004 END OF TRANSMISSION +05 0005 ENQUIRY +06 0006 ACKNOWLEDGE +07 0007 BELL +08 0008 BACKSPACE +09 0009 HORIZONTAL TABULATION +0A 000A LINE FEED +0B 000B VERTICAL TABULATION +0C 000C FORM FEED +0D 000D CARRIAGE RETURN +0E 000E SHIFT OUT +0F 000F SHIFT IN +10 0010 DATA LINK ESCAPE +11 0011 DEVICE CONTROL ONE +12 0012 DEVICE CONTROL TWO +13 0013 DEVICE CONTROL THREE +14 0014 DEVICE CONTROL FOUR +15 0015 NEGATIVE ACKNOWLEDGE +16 0016 SYNCHRONOUS IDLE +17 0017 END OF TRANSMISSION BLOCK +18 0018 CANCEL +19 0019 END OF MEDIUM +1A 001A SUBSTITUTE +1B 001B ESCAPE +1C 001C FILE SEPARATOR +1D 001D GROUP SEPARATOR +1E 001E RECORD SEPARATOR +1F 001F UNIT SEPARATOR +20 0020 SPACE +21 0021 EXCLAMATION MARK +22 0022 QUOTATION MARK +23 0023 NUMBER SIGN +24 0024 DOLLAR SIGN +25 0025 PERCENT SIGN +26 0026 AMPERSAND +27 0027 APOSTROPHE +28 0028 LEFT PARENTHESIS +29 0029 RIGHT PARENTHESIS +2A 002A ASTERISK +2B 002B PLUS SIGN +2C 002C COMMA +2D 002D HYPHEN-MINUS +2E 002E FULL STOP +2F 002F SOLIDUS +30 0030 DIGIT ZERO +31 0031 DIGIT ONE +32 0032 DIGIT TWO +33 0033 DIGIT THREE +34 0034 DIGIT FOUR +35 0035 DIGIT FIVE +36 0036 DIGIT SIX +37 0037 DIGIT SEVEN +38 0038 DIGIT EIGHT +39 0039 DIGIT NINE +3A 003A COLON +3B 003B SEMICOLON +3C 003C LESS-THAN SIGN +3D 003D EQUALS SIGN +3E 003E GREATER-THAN SIGN +3F 003F QUESTION MARK +40 0040 COMMERCIAL AT +41 0041 LATIN CAPITAL LETTER A +42 0042 LATIN CAPITAL LETTER B +43 0043 LATIN CAPITAL LETTER C +44 0044 LATIN CAPITAL LETTER D +45 0045 LATIN CAPITAL LETTER E +46 0046 LATIN CAPITAL LETTER F +47 0047 LATIN CAPITAL LETTER G +48 0048 LATIN CAPITAL LETTER H +49 0049 LATIN CAPITAL LETTER I +4A 004A LATIN CAPITAL LETTER J +4B 004B LATIN CAPITAL LETTER K +4C 004C LATIN CAPITAL LETTER L +4D 004D LATIN CAPITAL LETTER M +4E 004E LATIN CAPITAL LETTER N +4F 004F LATIN CAPITAL LETTER O +50 0050 LATIN CAPITAL LETTER P +51 0051 LATIN CAPITAL LETTER Q +52 0052 LATIN CAPITAL LETTER R +53 0053 LATIN CAPITAL LETTER S +54 0054 LATIN CAPITAL LETTER T +55 0055 LATIN CAPITAL LETTER U +56 0056 LATIN CAPITAL LETTER V +57 0057 LATIN CAPITAL LETTER W +58 0058 LATIN CAPITAL LETTER X +59 0059 LATIN CAPITAL LETTER Y +5A 005A LATIN CAPITAL LETTER Z +5B 005B LEFT SQUARE BRACKET +5C 005C REVERSE SOLIDUS +5D 005D RIGHT SQUARE BRACKET +5E 005E CIRCUMFLEX ACCENT +5F 005F LOW LINE +60 0060 GRAVE ACCENT +61 0061 LATIN SMALL LETTER A +62 0062 LATIN SMALL LETTER B +63 0063 LATIN SMALL LETTER C +64 0064 LATIN SMALL LETTER D +65 0065 LATIN SMALL LETTER E +66 0066 LATIN SMALL LETTER F +67 0067 LATIN SMALL LETTER G +68 0068 LATIN SMALL LETTER H +69 0069 LATIN SMALL LETTER I +6A 006A LATIN SMALL LETTER J +6B 006B LATIN SMALL LETTER K +6C 006C LATIN SMALL LETTER L +6D 006D LATIN SMALL LETTER M +6E 006E LATIN SMALL LETTER N +6F 006F LATIN SMALL LETTER O +70 0070 LATIN SMALL LETTER P +71 0071 LATIN SMALL LETTER Q +72 0072 LATIN SMALL LETTER R +73 0073 LATIN SMALL LETTER S +74 0074 LATIN SMALL LETTER T +75 0075 LATIN SMALL LETTER U +76 0076 LATIN SMALL LETTER V +77 0077 LATIN SMALL LETTER W +78 0078 LATIN SMALL LETTER X +79 0079 LATIN SMALL LETTER Y +7A 007A LATIN SMALL LETTER Z +7B 007B LEFT CURLY BRACKET +7C 007C VERTICAL LINE +7D 007D RIGHT CURLY BRACKET +7E 007E TILDE +7F 007F DELETE +80 20AC EURO SIGN +85 2026 HORIZONTAL ELLIPSIS +91 2018 LEFT SINGLE QUOTATION MARK +92 2019 RIGHT SINGLE QUOTATION MARK +93 201C LEFT DOUBLE QUOTATION MARK +94 201D RIGHT DOUBLE QUOTATION MARK +95 2022 BULLET +96 2013 EN DASH +97 2014 EM DASH +A0 00A0 NO-BREAK SPACE +A1 0E01 THAI CHARACTER KO KAI +A2 0E02 THAI CHARACTER KHO KHAI +A3 0E03 THAI CHARACTER KHO KHUAT +A4 0E04 THAI CHARACTER KHO KHWAI +A5 0E05 THAI CHARACTER KHO KHON +A6 0E06 THAI CHARACTER KHO RAKHANG +A7 0E07 THAI CHARACTER NGO NGU +A8 0E08 THAI CHARACTER CHO CHAN +A9 0E09 THAI CHARACTER CHO CHING +AA 0E0A THAI CHARACTER CHO CHANG +AB 0E0B THAI CHARACTER SO SO +AC 0E0C THAI CHARACTER CHO CHOE +AD 0E0D THAI CHARACTER YO YING +AE 0E0E THAI CHARACTER DO CHADA +AF 0E0F THAI CHARACTER TO PATAK +B0 0E10 THAI CHARACTER THO THAN +B1 0E11 THAI CHARACTER THO NANGMONTHO +B2 0E12 THAI CHARACTER THO PHUTHAO +B3 0E13 THAI CHARACTER NO NEN +B4 0E14 THAI CHARACTER DO DEK +B5 0E15 THAI CHARACTER TO TAO +B6 0E16 THAI CHARACTER THO THUNG +B7 0E17 THAI CHARACTER THO THAHAN +B8 0E18 THAI CHARACTER THO THONG +B9 0E19 THAI CHARACTER NO NU +BA 0E1A THAI CHARACTER BO BAIMAI +BB 0E1B THAI CHARACTER PO PLA +BC 0E1C THAI CHARACTER PHO PHUNG +BD 0E1D THAI CHARACTER FO FA +BE 0E1E THAI CHARACTER PHO PHAN +BF 0E1F THAI CHARACTER FO FAN +C0 0E20 THAI CHARACTER PHO SAMPHAO +C1 0E21 THAI CHARACTER MO MA +C2 0E22 THAI CHARACTER YO YAK +C3 0E23 THAI CHARACTER RO RUA +C4 0E24 THAI CHARACTER RU +C5 0E25 THAI CHARACTER LO LING +C6 0E26 THAI CHARACTER LU +C7 0E27 THAI CHARACTER WO WAEN +C8 0E28 THAI CHARACTER SO SALA +C9 0E29 THAI CHARACTER SO RUSI +CA 0E2A THAI CHARACTER SO SUA +CB 0E2B THAI CHARACTER HO HIP +CC 0E2C THAI CHARACTER LO CHULA +CD 0E2D THAI CHARACTER O ANG +CE 0E2E THAI CHARACTER HO NOKHUK +CF 0E2F THAI CHARACTER PAIYANNOI +D0 0E30 THAI CHARACTER SARA A +D1 0E31 THAI CHARACTER MAI HAN-AKAT +D2 0E32 THAI CHARACTER SARA AA +D3 0E33 THAI CHARACTER SARA AM +D4 0E34 THAI CHARACTER SARA I +D5 0E35 THAI CHARACTER SARA II +D6 0E36 THAI CHARACTER SARA UE +D7 0E37 THAI CHARACTER SARA UEE +D8 0E38 THAI CHARACTER SARA U +D9 0E39 THAI CHARACTER SARA UU +DA 0E3A THAI CHARACTER PHINTHU +DF 0E3F THAI CURRENCY SYMBOL BAHT +E0 0E40 THAI CHARACTER SARA E +E1 0E41 THAI CHARACTER SARA AE +E2 0E42 THAI CHARACTER SARA O +E3 0E43 THAI CHARACTER SARA AI MAIMUAN +E4 0E44 THAI CHARACTER SARA AI MAIMALAI +E5 0E45 THAI CHARACTER LAKKHANGYAO +E6 0E46 THAI CHARACTER MAIYAMOK +E7 0E47 THAI CHARACTER MAITAIKHU +E8 0E48 THAI CHARACTER MAI EK +E9 0E49 THAI CHARACTER MAI THO +EA 0E4A THAI CHARACTER MAI TRI +EB 0E4B THAI CHARACTER MAI CHATTAWA +EC 0E4C THAI CHARACTER THANTHAKHAT +ED 0E4D THAI CHARACTER NIKHAHIT +EE 0E4E THAI CHARACTER YAMAKKAN +EF 0E4F THAI CHARACTER FONGMAN +F0 0E50 THAI DIGIT ZERO +F1 0E51 THAI DIGIT ONE +F2 0E52 THAI DIGIT TWO +F3 0E53 THAI DIGIT THREE +F4 0E54 THAI DIGIT FOUR +F5 0E55 THAI DIGIT FIVE +F6 0E56 THAI DIGIT SIX +F7 0E57 THAI DIGIT SEVEN +F8 0E58 THAI DIGIT EIGHT +F9 0E59 THAI DIGIT NINE +FA 0E5A THAI CHARACTER ANGKHANKHU +FB 0E5B THAI CHARACTER KHOMUT + +
View file
cyrus-imapd-2.5.tar.gz/lib/cyrusdb_flat.c
Changed
@@ -475,7 +475,7 @@ static int foreach(struct dbengine *db, const char *prefix, size_t prefixlen, foreach_p *goodp, - foreach_cb *cb, void *rock, + foreach_cb *cb, void *rock, struct txn **mytid) { int r = CYRUSDB_OK; @@ -502,19 +502,20 @@ r = starttxn_or_refetch(db, mytid); if (r) return r; - if(!mytid) { + if (!mytid) { /* No transaction, use the fast method to avoid stomping on our * memory map if changes happen */ dbfd = dup(db->fd); if(dbfd == -1) return CYRUSDB_IOERROR; - + map_refresh(dbfd, 1, &dbbase, &dblen, db->size, db->fname, 0); /* drop our read lock on the file, since we don't really care * if it gets replaced out from under us, our mmap stays on the * old version */ lock_unlock(db->fd, db->fname); - } else { + } + else { /* use the same variables as in the no transaction case, just to * get things set up */ dbbase = db->base; @@ -524,15 +525,16 @@ if (prefix) { encode(prefix, prefixlen, &prefixbuf); offset = bsearch_mem_mbox(prefixbuf.s, dbbase, db->size, 0, &len); - } else { + } + else { offset = 0; } - + p = dbbase + offset; pend = dbbase + db->size; while (p < pend) { - if(!dontmove) { + if (!dontmove) { GETENTRY(p) } else dontmove = 0; @@ -543,7 +545,7 @@ if (!goodp || goodp(rock, keybuf.s, keybuf.len, DATA(db), DATALEN(db))) { unsigned long ino = db->ino; - unsigned long sz = db->size; + unsigned long sz = db->size; if(mytid) { /* transaction present, this means we do the slow way */ @@ -554,7 +556,7 @@ r = cb(rock, keybuf.s, keybuf.len, DATA(db), DATALEN(db)); if (r) break; - if(mytid) { + if (mytid) { /* reposition? (we made a change) */ if (!(ino == db->ino && sz == db->size)) { /* something changed in the file; reseek */ @@ -562,27 +564,28 @@ offset = bsearch_mem_mbox(savebuf.s, db->base, db->size, 0, &len); p = db->base + offset; - + GETENTRY(p); - + /* 'key' might not equal 'savebuf'. if it's different, we want to stay where we are. if it's the same, we should move on to the next one */ if (!buf_cmp(&savebuf, &keybuf)) { p = dataend + 1; - } else { + } + else { /* 'savebuf' got deleted, so we're now pointing at the right thing */ dontmove = 1; } - } + } } } p = dataend + 1; } - if(!mytid) { + if (!mytid) { /* cleanup the fast method */ map_free(&dbbase, &dblen); close(dbfd);
View file
cyrus-imapd-2.5.tar.gz/lib/cyrusdb_twoskip.c
Changed
@@ -1846,6 +1846,7 @@ static int dump(struct dbengine *db, int detail __attribute__((unused))) { struct skiprecord record; + struct buf scratch = BUF_INITIALIZER; size_t offset = DUMMY_OFFSET; int r = 0; int i; @@ -1878,10 +1879,12 @@ case RECORD: case DUMMY: - printf("%s kl=%llu dl=%llu lvl=%d (%.*s)\n", + buf_setmap(&scratch, KEY(db, &record), record.keylen); + buf_replace_char(&scratch, '\0', '-'); + printf("%s kl=%llu dl=%llu lvl=%d (%s)\n", (record.type == RECORD ? "RECORD" : "DUMMY"), (LLU)record.keylen, (LLU)record.vallen, - record.level, (int)record.keylen, KEY(db, &record)); + record.level, buf_cstring(&scratch)); printf("\t"); for (i = 0; i <= record.level; i++) { printf("%08llX ", (LLU)record.nextloc[i]); @@ -1895,6 +1898,8 @@ offset += record.len; } + buf_free(&scratch); + return r; }
View file
cyrus-imapd-2.5.tar.gz/lib/imapoptions
Changed
@@ -93,6 +93,12 @@ # OPTIONS +{ "addressbookprefix", "#addressbooks", STRING } +/* The prefix for the addressbook mailboxes hierarchies. The hierarchy + delimiter will be automatically appended. The public addressbook + hierarchy will be at the toplevel of the shared namespace. A + user's personal calendar hierarchy will be a child of their Inbox. */ + { "admins", "", STRING } /* The list of userids with administrative rights. Separate each userid with a space. Sites using Kerberos authentication may use @@ -263,6 +269,33 @@ { "auth_mech", "unix", STRINGLIST("unix", "pts", "krb", "krb5")} /* The authorization mechanism to use. */ +{ "autocreateinboxfolders", NULL, STRING } +/* Deprecated in favor of \fIautocreate_inbox_folders\fR. */ + +{ "autocreatequota", 0, INT } +/* Deprecated in favor of \fIautocreate_quota\fR. */ + +{ "autocreatequotamsg", -1, INT } +/* Deprecated in favor of \fIautocreate_quota_messages\fR. */ + +{ "autosievefolders", NULL, STRING } +/* Deprecated in favor of \fIautocreate_sieve_folders\fR. */ + +{ "generate_compiled_sieve_script", NULL, SWITCH } +/* Deprecated in favor of \fIautocreate_sieve_script_compile\fR. */ + +{ "autocreate_sieve_compiled_script", NULL, STRING } +/* Deprecated in favor of \fIautocreate_sieve_script_compiled\fR. */ + +{ "autosubscribeinboxfolders", NULL, STRING } +/* Deprecated in favor of \fIautocreate_subscribe_folders\fR. */ + +{ "autosubscribesharedfolders", NULL, STRING } +/* Deprecated in favor of \fIautocreate_subscribe_sharedfolders\fR. */ + +{ "autosubscribe_all_sharedfolders", 0, SWITCH } +/* Deprecated in favor of \fIautocreate_subscribe_sharedfolders_all\fR. */ + { "autocreate_inbox_folders", NULL, STRING } /* If a user does not have an INBOX created then the INBOX as well as some INBOX subfolders are created under two conditions. @@ -345,6 +378,44 @@ layers of MIME structure. The default of 1000 is much higher than any sane message should have. */ +{ "caldav_allowscheduling", "on", ENUM("off", "on", "apple") } +/* Enable calendar scheduling operations. If set to "apple", the + server will emulate Apple CalendarServer behavior as closely as + possible. */ + +{ "caldav_maxdatetime", "20380119T031407Z", STRING } +/* The latest date and time accepted by the server (ISO format). This + value is also used for expanding non-terminating recurrence rules. +.PP + Note that increasing this value will require the DAV databases for + calendars to be reconstructed with the \fBdav_reconstruct\fR + utility in order to see its effect on serer-side time-based + queries. */ + +{ "caldav_mindatetime", "19011213T204552Z", STRING } +/* The earliest date and time accepted by the server (ISO format). */ + +{ "caldav_realm", NULL, STRING } +/* The realm to present for HTTP authentication of CalDAV resources. + If not set (the default), the value of the "servername" option will + be used.*/ + +{ "calendarprefix", "#calendars", STRING } +/* The prefix for the calendar mailboxes hierarchies. The hierarchy + delimiter will be automatically appended. The public calendar + hierarchy will be at the toplevel of the shared namespace. A + user's personal calendar hierarchy will be a child of their Inbox. */ + +{ "calendar_user_address_set", NULL, STRING } +/* Space-separated list of domains corresponding to calendar user + addresses for which the server is responsible. If not set (the + default), the value of the "servername" option will be used. */ + +{ "carddav_realm", NULL, STRING } +/* The realm to present for HTTP authentication of CardDAV resources. + If not set (the default), the value of the "servername" option will + be used.*/ + { "chatty", 0, SWITCH } /* If yes, syslog tags and commands for every IMAP command, mailboxes for every lmtp connection, every POP3 command, etc */ @@ -361,6 +432,14 @@ /* The pathname of the IMAP configuration directory. This field is required. */ +{ "createonpost", 0, SWITCH } +/* Deprecated in favor of \fIautocreate_post\fR. */ + +{ "dav_realm", NULL, STRING } +/* The realm to present for HTTP authentication of generic DAV + resources (principals). If not set (the default), the value of the + "servername" option will be used.*/ + { "debug_command", NULL, STRING } /* Debug command to be used by processes started with -D option. The string is a C format string that gets 3 options: the first is the name of the @@ -418,6 +497,17 @@ session. Otherwise, the missing mailbox is treated as empty while in use by the client.*/ +{ "ischedule_dkim_domain", NULL, STRING } +/* The domain to be reported as doing iSchedule DKIM signing. */ + +{ "ischedule_dkim_key_file", NULL, STRING } +/* File containing the private key for iSchedule DKIM signing. */ + +{ "ischedule_dkim_selector", NULL, STRING } +/* Name of the selector subdividing the domain namespace. This + specifies the actual key used for iSchedule DKIM signing within the + domain. */ + { "duplicate_db", "skiplist", STRINGLIST("berkeley", "berkeley-nosync", "berkeley-hash", "berkeley-hash-nosync", "skiplist", "sql", "twoskip")} /* The cyrusdb backend to use for the duplicate delivery suppression and sieve. */ @@ -452,7 +542,7 @@ /* Don't send event notification for folder with given special-use attributes. Set ALL for any folder */ -{ "event_extra_params", "timestamp", BITFIELD("bodyStructure", "clientAddress", "diskUsed", "flagNames", "messageContent", "messageSize", "messages", "modseq", "service", "timestamp", "uidnext", "vnd.cmu.midset", "vnd.cmu.unseenMessages", "vnd.cmu.envelope", "vnd.cmu.sessionId") } +{ "event_extra_params", "timestamp", BITFIELD("bodyStructure", "clientAddress", "diskUsed", "flagNames", "messageContent", "messageSize", "messages", "modseq", "service", "timestamp", "uidnext", "vnd.cmu.midset", "vnd.cmu.unseenMessages", "vnd.cmu.envelope", "vnd.cmu.sessionId", "vnd.cmu.mbtype", "vnd.cmu.davFilename", "vnd.cmu.davUid") } /* Space-separated list of extra parameters to add to any appropriated event. */ { "event_groups", "message mailbox", BITFIELD("message", "quota", "flags", "access", "mailbox", "subscription") } @@ -478,22 +568,9 @@ EXPUNGE and should result in greater responsiveness for the client, especially when expunging a large number of messages. */ -{ "expunge_days", 7, INT } -/* Number of days to retain expunged messages before cleaning up their - index records. The default is 7. This is necessary for QRESYNC - to work correctly. If combined with delayed expunge (above) you - will also be able to unexpunge messages during this time. */ - { "failedloginpause", 3, INT } /* Number of seconds to pause after a failed login. */ -{ "flushseenstate", 0, SWITCH } -/* If enabled, changes to the seen state will be flushed to disk - immediately, otherwise changes will be cached and flushed when the - mailbox is closed. This option may be used to fix the problem of - previously read messages being marked as unread in Microsoft - Outlook, at the expense of a loss of performance/scalability. */ - { "foolstupidclients", 0, SWITCH } /* If enabled, only list the personal namespace when a LIST "*" is performed (it changes the request to a LIST "INBOX*"). */ @@ -537,6 +614,75 @@ /* The password to use for authentication to the backend server hostname (where hostname is the short hostname of the server) - Cyrus Murder */ +{ "httpallowcompress", 1, SWITCH } +/* If enabled, the server will compress response payloads if the client + indicates that it can accept them. Note that the compressed data + will appear in telemetry logs, leaving only the response headers as + human-readable.*/ + +{ "httpallowcors", NULL, STRING } +/* A wildmat pattern specifying a list of origin URIs ( scheme "://" + host [ ":" port ] ) that are allowed to make Cross-Origin Resource + Sharing (CORS) requests on the server. By default, CORS requests + are disabled. +.PP + Note that the scheme and host should both be lowercase, the port + should be omitted if using the default for the scheme (80 for http, + 443 for https), and there should be no trailing '/' (e.g.: + "http://www.example.com:8080", "https://example.org"). */ + +{ "httpallowtrace", 0, SWITCH } +/* Allow use of the TRACE method. +.PP + Note that sensitive data might be disclosed by the response. */ + +{ "httpallowedurls", NULL, STRING } +/* Space-separated list of relative URLs (paths) rooted at + "httpdocroot" (see below) to be served by httpd. If set, this + option will limit served static content to only those paths specified + (returning "404 Not Found" to any other client requested URLs). + Otherwise, httpd will serve any content found in "httpdocroot". +.PP + Note that any path specified by "rss_feedlist_template" is an + exception to this rule.*/ + +{ "httpcontentmd5", 0, SWITCH } +/* If enabled, HTTP responses will include a Content-MD5 header for + the purpose of providing an end-to-end message integrity check + (MIC) of the payload body. Note that enabling this option will + use additional CPU to generate the MD5 digest, which may be ignored + by clients anyways. */ + +{ "httpdocroot", NULL, STRING } +/* If set, http will serve the static content (html/text/jpeg/gif + files, etc) rooted at this directory. Otherwise, httpd will not + serve any static content. */ + +{ "httpkeepalive", 20, INT } +/* Set the length of the HTTP server's keepalive heartbeat in seconds. + The default is 20. The minimum value is 0, which will disable the + keepalive heartbeat. When enabled, if a request takes longer than + \fIhttpkeepalive\fR seconds to process, the server will send the client + provisional responses every \fIhttpkeepalive\fR seconds until the + final response can be sent */ + +{ "httpmodules", "", BITFIELD("caldav", "carddav", "domainkey", "ischedule", "rss", "timezone") } +/* Space-separated list of HTTP modules that will be enabled in + httpd(8). This option has no effect on modules that are disabled + at compile time due to missing dependencies (e.g. libical). */ + +{ "httpprettytelemetry", 0, SWITCH } +/* If enabled, HTTP response payloads including server-generated + markup languages (HTML, XML) will utilize line breaks and + indentation to promote better human-readability in telemetry logs. + Note that enabling this option will increase the amount of data + sent across the wire. */ + +{ "httptimeout", 5, INT } +/* Set the length of the HTTP server's inactivity autologout timer, + in minutes. The default is 5. The minimum value is 0, which will + disable persistent connections. */ + { "idlesocket", "{configdirectory}/socket/idle", STRING } /* Unix domain socket that idled listens on. */ @@ -743,6 +889,24 @@ { "ldap_client_key", NULL, STRING } /* File containing the private client key. */ +{ "ldap_tls_cacert_dir", NULL, STRING } +/* Deprecated in favor of \fIldap_ca_dir\fR. */ + +{ "ldap_tls_cacert_file", NULL, STRING } +/* Deprecated in favor of \fIldap_ca_file\fR. */ + +{ "ldap_tls_cert", NULL, STRING } +/* Deprecated in favor of \fIldap_client_cert\fR. */ + +{ "ldap_tls_key", NULL, STRING } +/* Deprecated in favor of \fIldap_client_key\fR. */ + +{ "ldap_tls_check_peer", NULL, STRING } +/* Deprecated in favor of \fIldap_verify_peer\fR. */ + +{ "ldap_tls_ciphers", NULL, STRING } +/* Deprecated in favor of \fIldap_ciphers\fR. */ + { "ldap_uri", NULL, STRING } /* Contains a list of the URLs of all the LDAP servers when using the LDAP PTS module. */ @@ -859,7 +1023,7 @@ { "mboxname_lockpath", NULL, STRING } /* Path to mailbox name lock files (default $conf/lock) */ -{ "metapartition_files", "", BITFIELD("header", "index", "cache", "expunge", "squat", "annotations") } +{ "metapartition_files", "", BITFIELD("header", "index", "cache", "expunge", "squat", "annotations", "lock", "dav") } /* Space-separated list of metadata files to be stored on a \fImetapartition\fR rather than in the mailbox directory on a spool partition. */ @@ -1158,6 +1322,11 @@ the "shared.blah" folder. By default, an email address of "+shared.blah" would be used. */ +{ "proc_path", NULL, STRING } +/* Path to proc directory. Default is NULL - must be an absolute path + if specified. If not specified, the path $confdir/proc/ will be + used. */ + { "proxy_authname", "proxy", STRING } /* The authentication name to use when authenticating to a backend server in the Cyrus Murder. */ @@ -1271,6 +1440,45 @@ allowed in envelope tests. When disabled, ANY grammatically correct header will be allowed. */ +{ "rss_feedlist_template", NULL, STRING } +/* File containing HTML that will be used as a template for displaying + the list of available RSS feeds. A single instance of the variable + %RSS_FEEDLIST% should appear in the file, which will be replaced by + a nested unordered list of feeds. The toplevel unordered list will + be tagged with an id of "feed" (<ul id='feed'>) which can be used + by stylesheet(s) in your template. The dynamically created list of + feeds based on the HTML template will be accessible at the "/rss" + URL on the server. */ + +{ "rss_feeds", "*", STRING } +/* A wildmat pattern specifying which mailbox hierarchies should be + treated as RSS feeds. Only mailboxes matching the wildmat will + have their messages available via RSS. If not set, a default + wildmat of "*" (ALL mailboxes) will be used. */ + +{ "rss_maxage", 0, INT } +/* Maximum age (in days) of items to display in an RSS channel. If + non-zero, httpd will only display items received within the last + \fIrss_maxage\fR days. If set to 0, all available items will be + displayed (the default). */ + +{ "rss_maxitems", 0, INT } +/* Maximum number of items to display in an RSS channel. If non-zero, + httpd will display no more than the \fIrss_maxitems\fR most recent + items. If set to 0, all available items will be displayed (the + default). */ + +{ "rss_maxsynopsis", 0, INT } +/* Maximum RSS item synopsis length. If non-zero, httpd will display + no more than the first \fIrss_maxsynopsis\fR characters of an + item's synopsis. If set to 0, the entire synopsis will be + displayed (the default). */ + +{ "rss_realm", NULL, STRING } +/* The realm to present for HTTP authentication of RSS feeds. If not + set (the default), the value of the "servername" option will be + used.*/ + # Commented out - used by libsasl # { "sasl_auto_transition", 0, SWITCH } /* If enabled, the SASL library will automatically create authentication @@ -1380,7 +1588,7 @@ user's scripts reside on a remote server (in a Murder). Otherwise, timsieved will proxy traffic to the remote server. */ -{ "sieve_extensions", "fileinto reject vacation vacation-seconds imapflags notify envelope relational regex subaddress copy date index", BITFIELD("fileinto", "reject", "vacation", "vacation-seconds", "imapflags", "notify", "include", "envelope", "body", "relational", "regex", "subaddress", "copy", "date", "index") } +{ "sieve_extensions", "fileinto reject vacation vacation-seconds imapflags notify envelope relational regex subaddress copy date index imap4flags", BITFIELD("fileinto", "reject", "vacation", "vacation-seconds", "imapflags", "notify", "include", "envelope", "body", "relational", "regex", "subaddress", "copy", "date", "index", "imap4flags") } /* Space-separated list of Sieve extensions allowed to be used in sieve scripts, enforced at submission by timsieved(8). Any previously installed script will be unaffected by this option and @@ -1601,6 +1809,18 @@ /* The length of the IMAP server's inactivity autologout timer, in minutes. The minimum value is 30, the default. */ +{ "tls_ca_file", "DEFAULT", STRING } +/* Deprecated in favor of \fItls_client_ca_file\fR. */ + +{ "tls_ca_path", "DEFAULT", STRING } +/* Deprecated in favor of \fItls_client_ca_dir\fR. */ + +{ "tls_cert_file", "DEFAULT", STRING } +/* Deprecated in favor of \fItls_server_cert\fR. */ + +{ "tls_cipher_list", "DEFAULT", STRING } +/* Deprecated in favor of \fItls_ciphers\fR. */ + { "tls_ciphers", "DEFAULT", STRING } /* The list of SSL/TLS ciphers to allow. The format of the string is described in ciphers(1). */ @@ -1634,6 +1854,9 @@ /* The elliptic curve used for ECDHE. Default is NIST Suite B prime256. See 'openssl ecparam -list_curves' for possible values. */ +{ "tls_key_file", "DEFAULT", STRING } +/* Deprecated in favor of \fItls_server_key\fR. */ + { "tls_prefer_server_ciphers", 0, SWITCH } /* Prefer the ciphers on the server side instead of client side. */ @@ -1714,13 +1937,21 @@ domain (if set). */ { "lmtp_catchall_mailbox", NULL, STRING } -/* Send mail to mailboxes, which do not exists, to this user. NOTE: This must - be an existing local mailbox name. NOT an email address! */ +/* Mail sent to mailboxes which do not exist, will be delivered to + this user. NOTE: This must be an existing local user name with an + INBOX, NOT an email address! */ + +{ "zoneinfo_db", "skiplist", STRINGLIST("flat", "berkeley", "berkeley-hash", "skiplist")} +/* The cyrusdb backend to use for zoneinfo. */ + +{ "zoneinfo_db_path", NULL, STRING } +/* The absolute path to the zoneinfo db file. If not specified, + will be confdir/zoneinfo.db */ /* .SH SEE ALSO .PP \fBimapd(8)\fR, \fBpop3d(8)\fR, \fBnntpd(8)\fR, \fBlmtpd(8)\fR, -\fBtimsieved(8)\fR, \fBidled(8)\fR, \fBnotifyd(8)\fR, +\fBhttpd(8)\fR, \fBtimsieved(8)\fR, \fBidled(8)\fR, \fBnotifyd(8)\fR, \fBdeliver(8)\fR, \fBmaster(8)\fR, \fBciphers(1)\fR */
View file
cyrus-imapd-2.5.tar.gz/lib/imparse.c
Changed
@@ -142,22 +142,25 @@ } /* - * Return nonzero if 's' matches the grammar for an atom + * Return nonzero if 's' matches the grammar for an atom. If 'len' is + * zero then treat as a c string, \0 delimited. Otherwise check the + * entire map, and consider not an natom if there's a NULL byte in the + * mapped space. */ -EXPORTED int imparse_isnatom(const char *s, int maxlen) +EXPORTED int imparse_isnatom(const char *s, int len) { - int len = 0; + int count = 0; if (!*s) return 0; - for (; *s; s++) { - len++; - if (maxlen && len > maxlen) break; + for (; len || *s; s++) { + count++; + if (len && count > len) break; if (*s & 0x80 || *s < 0x1f || *s == 0x7f || *s == ' ' || *s == '{' || *s == '(' || *s == ')' || *s == '\"' || *s == '%' || *s == '*' || *s == '\\') return 0; } - if (len >= 1024) return 0; - return 1; + if (count >= 1024) return 0; + return count; } EXPORTED int imparse_isatom(const char *s)
View file
cyrus-imapd-2.5.tar.gz/lib/libconfig.c
Changed
@@ -65,6 +65,7 @@ * because it is for overflow only */ #define INCLUDEHASHSIZE 5 /* relatively small, * but how many includes are reasonable? */ + static struct hash_table confighash, includehash; /* cached configuration variables accessible to the external world */ @@ -155,8 +156,8 @@ /* First lookup <ident>_key, to see if we have a service-specific * override */ - if(config_ident) { - if(snprintf(buf,sizeof(buf),"%s_%s",config_ident,key) == -1) + if (config_ident) { + if (snprintf(buf,sizeof(buf),"%s_%s",config_ident,key) == -1) fatal("key too long in config_getoverflowstring", EC_TEMPFAIL); lcase(buf); @@ -164,7 +165,7 @@ } /* No service-specific override, check the actual key */ - if(!ret) + if (!ret) ret = hash_lookup(key, &confighash); /* Return what we got or the default */ @@ -183,9 +184,9 @@ { char buf[80]; - if(strlcpy(buf, "partition-", sizeof(buf)) >= sizeof(buf)) + if (strlcpy(buf, "partition-", sizeof(buf)) >= sizeof(buf)) return 0; - if(strlcat(buf, partition, sizeof(buf)) >= sizeof(buf)) + if (strlcat(buf, partition, sizeof(buf)) >= sizeof(buf)) return 0; return config_getoverflowstring(buf, NULL); @@ -195,9 +196,9 @@ { char buf[80]; - if(strlcpy(buf, "metapartition-", sizeof(buf)) >= sizeof(buf)) + if (strlcpy(buf, "metapartition-", sizeof(buf)) >= sizeof(buf)) return 0; - if(strlcat(buf, partition, sizeof(buf)) >= sizeof(buf)) + if (strlcat(buf, partition, sizeof(buf)) >= sizeof(buf)) return 0; return config_getoverflowstring(buf, NULL); @@ -212,6 +213,135 @@ if (!strncmp("partition-", key, 10)) *found = 1; } +static void config_option_deprecate(const int dopt, const int opt, char *since) +{ + syslog( + LOG_WARNING, + "Option '%s' is deprecated in favor of '%s' since version %s.", + imapopts[dopt].optname, + imapopts[opt].optname, + since + ); + + switch (imapopts[dopt].t) { + case OPT_BITFIELD: { + imapopts[opt].val.x = imapopts[dopt].val.x; + break; + } + case OPT_ENUM: { + imapopts[opt].val.e = imapopts[dopt].val.e; + break; + } + case OPT_SWITCH: { + imapopts[opt].val.b = imapopts[dopt].val.b; + break; + } + case OPT_INT: { + imapopts[opt].val.i = imapopts[dopt].val.i; + break; + } + case OPT_STRINGLIST: + case OPT_STRING: { + imapopts[opt].val.s = xstrdup(imapopts[dopt].val.s); + free((char *)imapopts[dopt].val.s); + break; + } + default: { + break; + } + } +} + +static void config_option_deprecated(const int opt) +{ + switch (opt) { + case IMAPOPT_AUTOCREATEINBOXFOLDERS: { + config_option_deprecate(opt, IMAPOPT_AUTOCREATE_INBOX_FOLDERS, "2.5.0"); + break; + } + case IMAPOPT_AUTOCREATEQUOTA: { + config_option_deprecate(opt, IMAPOPT_AUTOCREATE_QUOTA, "2.5.0"); + break; + } + case IMAPOPT_AUTOCREATEQUOTAMSG: { + config_option_deprecate(opt, IMAPOPT_AUTOCREATE_QUOTA_MESSAGES, "2.5.0"); + break; + } + case IMAPOPT_AUTOSIEVEFOLDERS: { + config_option_deprecate(opt, IMAPOPT_AUTOCREATE_SIEVE_FOLDERS, "2.5.0"); + break; + } + case IMAPOPT_AUTOCREATE_SIEVE_COMPILED_SCRIPT: { + config_option_deprecate(opt, IMAPOPT_AUTOCREATE_SIEVE_SCRIPT_COMPILED, "2.5.0"); + break; + } + case IMAPOPT_AUTOSUBSCRIBEINBOXFOLDERS: { + config_option_deprecate(opt, IMAPOPT_AUTOCREATE_SUBSCRIBE_FOLDERS, "2.5.0"); + break; + } + case IMAPOPT_AUTOSUBSCRIBESHAREDFOLDERS: { + config_option_deprecate(opt, IMAPOPT_AUTOCREATE_SUBSCRIBE_SHAREDFOLDERS, "2.5.0"); + break; + } + case IMAPOPT_AUTOSUBSCRIBE_ALL_SHAREDFOLDERS: { + config_option_deprecate(opt, IMAPOPT_AUTOCREATE_SUBSCRIBE_SHAREDFOLDERS_ALL, "2.5.0"); + break; + } + case IMAPOPT_CREATEONPOST: { + config_option_deprecate(opt, IMAPOPT_AUTOCREATE_POST, "2.5.0"); + break; + } + case IMAPOPT_GENERATE_COMPILED_SIEVE_SCRIPT: { + config_option_deprecate(opt, IMAPOPT_AUTOCREATE_SIEVE_SCRIPT_COMPILE, "2.5.0"); + break; + } + case IMAPOPT_LDAP_TLS_CACERT_DIR: { + config_option_deprecate(opt, IMAPOPT_LDAP_CA_DIR, "2.5.0"); + break; + } + case IMAPOPT_LDAP_TLS_CACERT_FILE: { + config_option_deprecate(opt, IMAPOPT_LDAP_CA_FILE, "2.5.0"); + break; + } + case IMAPOPT_LDAP_TLS_CERT: { + config_option_deprecate(opt, IMAPOPT_LDAP_CLIENT_CERT, "2.5.0"); + break; + } + case IMAPOPT_LDAP_TLS_CHECK_PEER: { + config_option_deprecate(opt, IMAPOPT_LDAP_VERIFY_PEER, "2.5.0"); + break; + } + case IMAPOPT_LDAP_TLS_CIPHERS: { + config_option_deprecate(opt, IMAPOPT_LDAP_CIPHERS, "2.5.0"); + break; + } + case IMAPOPT_LDAP_TLS_KEY: { + config_option_deprecate(opt, IMAPOPT_LDAP_CLIENT_KEY, "2.5.0"); + break; + } + case IMAPOPT_TLS_CA_FILE: { + config_option_deprecate(opt, IMAPOPT_TLS_CLIENT_CA_FILE, "2.5.0"); + break; + } + case IMAPOPT_TLS_CA_PATH: { + config_option_deprecate(opt, IMAPOPT_TLS_CLIENT_CA_DIR, "2.5.0"); + break; + } + case IMAPOPT_TLS_CERT_FILE: { + config_option_deprecate(opt, IMAPOPT_TLS_SERVER_CERT, "2.5.0"); + break; + } + case IMAPOPT_TLS_CIPHER_LIST: { + config_option_deprecate(opt, IMAPOPT_TLS_CIPHERS, "2.5.0"); + break; + } + case IMAPOPT_TLS_KEY_FILE: { + config_option_deprecate(opt, IMAPOPT_TLS_SERVER_KEY, "2.5.0"); + break; + } + } +} + /* * Reset the global configuration to a virginal state. This is * only useful for unit tests. @@ -277,14 +407,14 @@ int ival; /* xxx this is leaked, this may be able to be better in 2.2 (cyrus_done) */ - if(alt_config) config_filename = xstrdup(alt_config); + if (alt_config) config_filename = xstrdup(alt_config); else config_filename = xstrdup(CONFIG_FILENAME); - if(!construct_hash_table(&confighash, CONFIGHASHSIZE, 1)) { + if (!construct_hash_table(&confighash, CONFIGHASHSIZE, 1)) { fatal("could not construct configuration hash table", EC_CONFIG); } - if(!construct_hash_table(&includehash, INCLUDEHASHSIZE, 1)) { + if (!construct_hash_table(&includehash, INCLUDEHASHSIZE, 1)) { fatal("could not construct include file hash table", EC_CONFIG); } @@ -298,22 +428,26 @@ EC_CONFIG); } - /* Scan options to see if we need to replace {configdirectory} */ - /* xxx need to scan overflow options as well! */ - for(opt = IMAPOPT_ZERO; opt < IMAPOPT_LAST; opt++) { - if(!imapopts[opt].val.s || - imapopts[opt].t != OPT_STRING || - opt == IMAPOPT_CONFIGDIRECTORY) { - /* Skip options that have a NULL value, aren't strings, or - * are the configdirectory option */ + for (opt = IMAPOPT_ZERO; opt < IMAPOPT_LAST; opt++) { + /* Scan options to see if we need to replace {configdirectory} */ + /* xxx need to scan overflow options as well! */ + + /* Skip options that have a NULL value, aren't strings, or + * are the configdirectory option */ + if ( + !imapopts[opt].val.s || + imapopts[opt].t != OPT_STRING || + opt == IMAPOPT_CONFIGDIRECTORY + ) { + continue; } - + /* We use some magic numbers here, * 17 is the length of "{configdirectory}", * 16 is one less than that length, so that the replacement string * that is malloced has room for the '\0' */ - if(!strncasecmp(imapopts[opt].val.s,"{configdirectory}",17)) { + if (!strncasecmp(imapopts[opt].val.s,"{configdirectory}",17)) { const char *str = imapopts[opt].val.s; char *newstring = xmalloc(strlen(config_dir) + strlen(str) - 16); @@ -322,7 +456,7 @@ /* we need to replace this string, will we need to free * the current value? -- only if we've actually seen it in * the config file. */ - if(imapopts[opt].seen) + if (imapopts[opt].seen) freeme = (char *)str; /* Build replacement string from configdirectory option */ @@ -331,7 +465,15 @@ imapopts[opt].val.s = newstring; - if(freeme) free(freeme); + if (freeme) free(freeme); + } + } + + for (opt = IMAPOPT_ZERO; opt < IMAPOPT_LAST; opt++) { + /* See if the option configured is a part of the deprecated hash. + */ + if (imapopts[opt].seen) { + config_option_deprecated(opt); } } @@ -539,14 +681,14 @@ } /* Find if there is a <service>_ prefix */ - if(config_ident && !strncasecmp(key, config_ident, idlen) + if (config_ident && !strncasecmp(key, config_ident, idlen) && key[idlen] == '_') { /* skip service_ prefix */ srvkey = key + idlen + 1; } /* look for a service_ prefix match in imapopts */ - if(srvkey) { + if (srvkey) { for (opt = IMAPOPT_ZERO; opt < IMAPOPT_LAST; opt++) { if (!strcasecmp(imapopts[opt].optname, srvkey)) { key = srvkey; @@ -558,7 +700,7 @@ /* Did not find a service_ specific match, try looking for an * exact match */ - if(!service_specific) { + if (!service_specific) { for (opt = IMAPOPT_ZERO; opt < IMAPOPT_LAST; opt++) { if (!strcasecmp(imapopts[opt].optname, key)) { break; @@ -578,22 +720,26 @@ * If we have already seen a service-specific form, and this is * a generic form, just skip it and don't moan. */ - if((imapopts[opt].seen == 1 && !service_specific) - ||(imapopts[opt].seen == 2 && service_specific)) { + if ( + (imapopts[opt].seen == 1 && !service_specific) || + (imapopts[opt].seen == 2 && service_specific) + ) { + sprintf(errbuf, "option '%s' was specified twice in config file (second occurance on line %d)", fullkey, lineno); fatal(errbuf, EC_CONFIG); - } else if(imapopts[opt].seen == 2 && !service_specific) { + + } else if (imapopts[opt].seen == 2 && !service_specific) { continue; } /* If we've seen it already, we're replacing it, so we need * to free the current string if there is one */ - if(imapopts[opt].seen && imapopts[opt].t == OPT_STRING) + if (imapopts[opt].seen && imapopts[opt].t == OPT_STRING) free((char *)imapopts[opt].val.s); - if(service_specific) + if (service_specific) imapopts[opt].seen = 2; else imapopts[opt].seen = 1; @@ -604,7 +750,7 @@ { imapopts[opt].val.s = xstrdup(p); - if(opt == IMAPOPT_CONFIGDIRECTORY) + if (opt == IMAPOPT_CONFIGDIRECTORY) config_dir = imapopts[opt].val.s; break; @@ -710,7 +856,7 @@ xxx this would be nice if it wasn't for other services who might be sharing this config file and whose names we cannot predict - if(strncasecmp(key,"sasl_",5) + if (strncasecmp(key,"sasl_",5) && strncasecmp(key,"partition-",10)) { sprintf(errbuf, "option '%s' is unknown on line %d of config file", @@ -722,7 +868,7 @@ /* Put it in the overflow hash table */ newval = xstrdup(p); val = hash_insert(key, newval, &confighash); - if(val != newval) { + if (val != newval) { snprintf(errbuf, sizeof(errbuf), "option '%s' was specified twice in config file (second occurance on line %d)", fullkey, lineno); @@ -730,6 +876,7 @@ } } } + fclose(infile); free(buf); }
View file
cyrus-imapd-2.5.tar.gz/lib/mappedfile.c
Changed
@@ -100,16 +100,18 @@ int is_rw; }; -static void _ensure_mapped(struct mappedfile *mf, size_t offset) +static void _ensure_mapped(struct mappedfile *mf, size_t offset, int update) { const char *base = NULL; size_t len = 0; /* we may be rewriting inside a file, so don't shrink, only extend */ - if (offset > mf->map_size) - mf->was_resized = 1; - else - offset = mf->map_size; + if (update) { + if (offset > mf->map_size) + mf->was_resized = 1; + else + offset = mf->map_size; + } /* always give refresh another go, we may be map_nommap */ map_refresh(mf->fd, 0, &base, &len, offset, mf->fname, 0); @@ -166,8 +168,7 @@ goto err; } - _ensure_mapped(mf, sbuf.st_size); - mf->was_resized = 0; /* not actually resized */ + _ensure_mapped(mf, sbuf.st_size, /*update*/0); *mfp = mf; @@ -247,7 +248,7 @@ buf_free(&mf->map_buf); } - _ensure_mapped(mf, sbuf.st_size); + _ensure_mapped(mf, sbuf.st_size, /*update*/0); return 0; } @@ -271,11 +272,11 @@ mf->lock_status = MF_WRITELOCKED; /* XXX - can we guarantee the fd isn't reused? */ - if (mf->map_ino != sbuf.st_ino) + if (mf->map_ino != sbuf.st_ino) { buf_free(&mf->map_buf); + } - _ensure_mapped(mf, sbuf.st_size); - mf->was_resized = 0; + _ensure_mapped(mf, sbuf.st_size, /*update*/0); return 0; } @@ -364,7 +365,7 @@ return -1; } - _ensure_mapped(mf, pos+written); + _ensure_mapped(mf, pos+written, /*update*/1); return written; } @@ -415,7 +416,7 @@ return -1; } - _ensure_mapped(mf, pos+written); + _ensure_mapped(mf, pos+written, /*update*/1); return written; } @@ -429,18 +430,14 @@ mf->dirty++; - /* make sure we don't think the future is valid any more */ - if (offset < (off_t)mf->map_size) - mf->map_size = offset; - r = ftruncate(mf->fd, offset); if (r < 0) { syslog(LOG_ERR, "IOERROR: ftruncate %s: %m", mf->fname); return r; } - _ensure_mapped(mf, offset); - mf->was_resized = 1; + _ensure_mapped(mf, offset, /*update*/0); + mf->was_resized = 1; /* force the fsync */ return 0; }
View file
cyrus-imapd-2.5.tar.gz/lib/prot.c
Changed
@@ -820,7 +820,7 @@ int n; time_t newtime; char timebuf[20]; - + time(&newtime); snprintf(timebuf, sizeof(timebuf), ">%ld>", newtime); n = write(s->logfd, timebuf, strlen(timebuf)); @@ -836,7 +836,9 @@ } } while (left); - (void)fsync(s->logfd); + /* we don't care THAT much about logs + * (void)fsync(s->logfd); + */ } }
View file
cyrus-imapd-2.5.tar.gz/lib/ptrarray.c
Changed
@@ -204,3 +204,10 @@ return i; return -1; } + +EXPORTED void ptrarray_sort(ptrarray_t *pa, + int (*compare)(const void **, const void **)) +{ + qsort(pa->data, pa->count, sizeof(void*), + (int (*)(const void *, const void *))compare); +}
View file
cyrus-imapd-2.5.tar.gz/lib/ptrarray.h
Changed
@@ -82,4 +82,6 @@ int ptrarray_find(const ptrarray_t *pa, void *match, int starting); +void ptrarray_sort(ptrarray_t *pa, int (*compare)(const void **, const void **)); + #endif /* __CYRUS_PTRARRAY_H__ */
View file
cyrus-imapd-2.5.tar.gz/lib/retry.c
Changed
@@ -48,6 +48,7 @@ #include <unistd.h> #endif +#include "exitcodes.h" #include "retry.h" #include "xmalloc.h" @@ -114,7 +115,8 @@ int i; ssize_t n; size_t written = 0; - struct iovec *iov, *baseiov; + size_t len = 0; + struct iovec *iov, *baseiov = NULL; static int iov_max = #ifdef MAXIOV MAXIOV @@ -130,6 +132,17 @@ if (!iovcnt) return 0; + for (i = 0; i < iovcnt; i++) { + len += srciov[i].iov_len; + } + + n = written = writev(fd, srciov, iovcnt > iov_max ? iov_max : iovcnt); + + /* did we get lucky and write it all? */ + if (written == len) + return written; + + /* oh well, welcome to the slow path - we have copies */ baseiov = iov = (struct iovec *)xmalloc(iovcnt * sizeof(struct iovec)); for (i = 0; i < iovcnt; i++) { iov[i].iov_base = srciov[i].iov_base; @@ -137,13 +150,18 @@ } for (;;) { - while (iovcnt && iov[0].iov_len == 0) { + for (i = 0; i < iovcnt; i++) { + if (iov[i].iov_len > (size_t)n) { + iov[i].iov_base += n; + iov[i].iov_len -= n; + break; + } + n -= iov[i].iov_len; iov++; iovcnt--; + if (!iovcnt) fatal("ran out of iov", EC_SOFTWARE); } - if (!iovcnt) break; - n = writev(fd, iov, iovcnt > iov_max ? iov_max : iovcnt); if (n == -1) { if (errno == EINVAL && iov_max > 10) { @@ -157,17 +175,7 @@ written += n; - for (i = 0; i < iovcnt; i++) { - if (iov[i].iov_len > (size_t)n) { - iov[i].iov_base = (char *)iov[i].iov_base + n; - iov[i].iov_len -= n; - break; - } - n -= iov[i].iov_len; - iov[i].iov_len = 0; - } - - if (i == iovcnt) break; + if (written == len) break; } free(baseiov);
View file
cyrus-imapd-2.5.tar.gz/lib/util.c
Changed
@@ -448,14 +448,6 @@ } } - destfd = open(to, O_RDWR|O_TRUNC|O_CREAT, 0666); - if (destfd == -1) { - if (!(flags & COPYFILE_MKDIR)) - syslog(LOG_ERR, "IOERROR: creating %s: %m", to); - r = -1; - goto done; - } - srcfd = open(from, O_RDONLY, 0666); if (srcfd == -1) { syslog(LOG_ERR, "IOERROR: opening %s: %m", from); @@ -469,6 +461,20 @@ goto done; } + if (!sbuf.st_size) { + syslog(LOG_ERR, "IOERROR: zero byte file %s: %m", from); + r = -1; + goto done; + } + + destfd = open(to, O_RDWR|O_TRUNC|O_CREAT, 0666); + if (destfd == -1) { + if (!(flags & COPYFILE_MKDIR)) + syslog(LOG_ERR, "IOERROR: creating %s: %m", to); + r = -1; + goto done; + } + map_refresh(srcfd, 1, &src_base, &src_size, sbuf.st_size, from, 0); n = retry_write(destfd, src_base, src_size); @@ -1073,7 +1079,7 @@ if (replace->len > length) { /* string will need to expand */ - buf_ensure(buf, replace->len - length); + buf_ensure(buf, replace->len - length + 1); } if (length != replace->len) { /* +1 to copy the NULL to keep cstring semantics */
View file
cyrus-imapd-2.5.tar.gz/man/ctl_zoneinfo.8
Added
@@ -0,0 +1,86 @@ +.\" -*- nroff -*- +.TH CTL_ZONEINFO 8 "Project Cyrus" CMU +.\" +.\" Copyright (c) 1994-2013 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. +.\" +.SH NAME +ctl_zoneinfo \- perform operations on the zoneinfo database +.SH SYNOPSIS +.B ctl_zoneinfo +[ +.B \-C +.I config-file +] +[ +.B \-v +] +.B \-r +.I version-string +.SH DESCRIPTION +.I Ctl_zoneinfo +is used to perform various administrative operations on the zoneinfo +database. +.PP +.I Ctl_zoneinfo +reads its configuration options out of the +.IR imapd.conf (5) +file unless specified otherwise by \fB-C\fR. +.SH OPTIONS +.TP +.BI \-C " config-file" +Read configuration options from \fIconfig-file\fR. +.TP +.B \-v +Enable verbose output. +.TP +.BI \-r " version-string" +Rebuild the zoneinfo database based on the directory structure of +\fIconfigdirectory\fB/zoneinfo\fR. The database to be rebuilt will be +in the default location of \fIconfigdirectory\fB/zoneinfo.db\fR unless +otherwise specified by the \fIzoneinfo_db_path\fR option in +\fBimapd.conf\fR. The \fIversion-string\fR should describe the source +of the timezone data (e.g. "Olson 2013h") and will be used by the +\fItimezone\fR module of \fBhttpd\fR. +.SH FILES +.TP +.B /etc/imapd.conf +.SH SEE ALSO +.PP +\fBimapd.conf(5)\fR, \fBmaster(8)\fR, \fBhttpd(8)\fR
View file
cyrus-imapd-2.5.tar.gz/man/cyr_dbtool.8
Changed
@@ -50,6 +50,12 @@ [ .B \-n ] +[ +.B \-o +] +[ +.B \-T +] <db file> <db backend> <action> [ <key> @@ -98,6 +104,14 @@ .TP .BI \-n Create the database file if it doesn't already exist. +.TP +.BI \-o +Store all the output in memory and only print it once the transaction +is completed. +.TP +.BI \-T +Use a transaction to do the action (most especially for 'show') - the +default used to be transactions. .SH FILES .TP .B /etc/imapd.conf
View file
cyrus-imapd-2.5.tar.gz/man/httpd.8
Added
@@ -0,0 +1,129 @@ +.\" -*- nroff -*- +.TH HTTPD 8 "Project Cyrus" CMU +.\" +.\" Copyright (c) 1994-2011 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. +.\" +.SH NAME +httpd \- HTTP server process +.SH SYNOPSIS +.B httpd +[ +.B \-C +.I config-file +] +[ +.B \-U +.I uses +] +[ +.B \-T +.I timeout +] +[ +.B \-D +] +.br + [ +.B \-s +] +[ +.B \-p +.I ssf +] +.SH DESCRIPTION +.I Httpd +is a HTTP server. +It accepts commands on its standard input and responds on its standard output. +It MUST invoked by +.IR master (8) +with those descriptors attached to a remote client connection. +.PP +.I Httpd +reads its configuration options out of the +.IR imapd.conf (5) +file unless specified otherwise by \fB-C\fR. +.PP +If the directory +.RI log/ user +exists under the directory specified in the +.I configdirectory +configuration option, then +.I httpd +will create protocol telemetry logs for sessions authenticating as +.IR user . +The telemetry logs will be stored in the +.RI log/ user +directory with a filename of the +.I httpd +process-id. +.SH OPTIONS +.TP +.BI \-C " config-file" +Read configuration options from \fIconfig-file\fR. +.TP +.BI \-U " uses" +The maximum number of times that the process should be used for new +connections before shutting down. The default is 250. +.TP +.BI \-T " timeout" +The number of seconds that the process will wait for a new connection +before shutting down. Note that a value of 0 (zero) will disable the +timeout. The default is 60. +.TP +.BI \-D +Run external debugger specified in debug_command. +.TP +.BI \-s +Serve HTTP over SSL (https). All data to and from +.I httpd +is encrypted using the Secure Sockets Layer. +.TP +.BI \-p " ssf" +Tell +.I httpd +that an external layer exists. An SSF (security strength factor) of 1 +means an integrity protection layer exists. Any higher SSF implies +some form of privacy protection. +.SH FILES +.TP +.B /etc/imapd.conf +.SH SEE ALSO +.PP +\fBimapd.conf(5)\fR, \fBmaster(8)\fR
View file
cyrus-imapd-2.5.tar.gz/man/reconstruct.8
Changed
@@ -191,6 +191,10 @@ .B -O Delete odd files. This is the opposite of '-o'. .TP +.B -V version +Change the cyrus.index minor version to a specific version. This can be +useful for upgrades or downgrades. +.TP .B \-m .B NOTE: CURRENTLY UNAVAILABLE .br
View file
cyrus-imapd-2.5.tar.gz/man/sync_client.8
Changed
@@ -91,6 +91,9 @@ [ .B \-m ] +[ +.B \-A +] .br [ .B \-s @@ -185,6 +188,12 @@ User mode. Remaining arguments are list of users who should be replicated. .TP +.BI \-A +All users mode. +Sync every user on the server to the replica (doesn't do non-user mailboxes +at all... this could be considered a bug and maybe it should do those mailboxes +independently) +.TP .BI \-m Mailbox mode. Remaining arguments are list of mailboxes which should be replicated.
View file
cyrus-imapd-2.5.tar.gz/man/unexpunge.8
Changed
@@ -62,6 +62,10 @@ [ .B \-v ] +[ +.B \-f +.I flagname +] .I mailbox .br .B unexpunge @@ -76,6 +80,10 @@ [ .B \-v ] +[ +.B \-f +.I flagname +] .I mailbox .br .B unexpunge @@ -90,6 +98,10 @@ [ .B \-v ] +[ +.B \-f +.I flagname +] .IR "mailbox uid" ... .SH DESCRIPTION .I Unexpunge @@ -124,6 +136,12 @@ .B \-d Unset the \fI\\Deleted\fR flag on any restored messages. .TP +.BI \-f " flagname" +Set the user flag +.I flagname +on any restored messages. This can make it easy to batch operate on +just the restored messages afterwards. +.TP .B \-v Enable verbose output/logging. .SH FILES
View file
cyrus-imapd-2.5.tar.gz/master/conf/normal.conf
Changed
@@ -21,6 +21,10 @@ # nntp cmd="nntpd" listen="nntp" prefork=0 # nntps cmd="nntpd -s" listen="nntps" prefork=0 + # these are only necessary if using HTTP for CalDAV, CardDAV, or RSS +# http cmd="httpd" listen="http" prefork=0 +# https cmd="httpd -s" listen="https" prefork=0 + # at least one LMTP is required for delivery # lmtp cmd="lmtpd" listen="lmtp" prefork=0 lmtpunix cmd="lmtpd" listen="/var/imap/socket/lmtp" prefork=0
View file
cyrus-imapd-2.5.tar.gz/master/conf/prefork.conf
Changed
@@ -21,6 +21,10 @@ # nntp cmd="nntpd" listen="nntp" prefork=3 # nntps cmd="nntpd -s" listen="nntps" prefork=1 + # these are only necessary if using HTTP for CalDAV, CardDAV, or RSS +# http cmd="httpd" listen="http" prefork=3 +# https cmd="httpd -s" listen="https" prefork=1 + # at least one LMTP is required for delivery # lmtp cmd="lmtpd" listen="lmtp" prefork=0 lmtpunix cmd="lmtpd" listen="/var/imap/socket/lmtp" prefork=1
View file
cyrus-imapd-2.5.tar.gz/master/master.c
Changed
@@ -829,13 +829,6 @@ fcntl_unset(STATUS_FD, FD_CLOEXEC); fcntl_unset(LISTEN_FD, FD_CLOEXEC); - - /* close all listeners */ - for (i = 0; i < nservices; i++) { - xclose(Services[i].socket); - xclose(Services[i].stat[0]); - xclose(Services[i].stat[1]); - } } else { snprintf(name_env3, sizeof(name_env3), "CYRUS_ISDAEMON=1"); @@ -843,6 +836,13 @@ } limit_fds(s->maxfds); + /* close all listeners */ + for (i = 0; i < nservices; i++) { + xclose(Services[i].socket); + xclose(Services[i].stat[0]); + xclose(Services[i].stat[1]); + } + syslog(LOG_DEBUG, "about to exec %s", path); /* add service name to environment */
View file
cyrus-imapd-2.5.tar.gz/sieve/README
Changed
@@ -58,3 +58,6 @@ [SUBADDR] Murchison, K., "Sieve Email Filtering -- Subaddress Extension", RFC 3598, September 2003. +[IMAP4FLAGS] Melnikov, A., "Sieve Email Filtering: Imap4flags Extension", +RFC 5232, January 2008. +
View file
cyrus-imapd-2.5.tar.gz/sieve/bc_dump.c
Changed
@@ -1,4 +1,6 @@ -/* bc_generate.c -- sieve bytecode- almost flattened bytecode +/* bc_dump.c -- sieve bytecode- dump almost flattened bytecode that was + * generated by bc_generate.c + * * Rob Siemborski * * Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved. @@ -98,9 +100,12 @@ /* Dump a test, return the last address used by the test */ static int dump_test(bytecode_info_t *d, int ip, int level ) { + int has_index=0; + + const int opcode = d->data[ip].op; print_spaces(level); - switch(d->data[ip].op) { + switch(opcode) { case BC_TRUE: printf("%d: TRUE\n",ip); break; @@ -138,9 +143,15 @@ break; case BC_HEADER: + has_index=1; + case BC_HEADER_PRE_INDEX: printf("%d: HEADER (\n",ip++); - print_spaces(level); - printf(" INDEX %d\n" , d->data[ip++].value); + + if (has_index) { + print_spaces(level); + printf(" INDEX:%d\n", d->data[ip++].value); + } + print_spaces(level); if (d->data[ip].value == B_COUNT || d->data[ip].value == B_VALUE) { @@ -157,17 +168,40 @@ ip = dump_sl(d,ip,level); break; + case BC_HASFLAG: + printf("%d: HASFLAG (\n",ip++); + print_spaces(level); + if (d->data[ip].value == B_COUNT || d->data[ip].value == B_VALUE) + { + printf(" MATCH:%d RELATION:%d COMP:%d FLAG-VARS:\n", + d->data[ip].value, d->data[ip+1].value,d->data[ip+2].value); + } else { + printf(" MATCH:%d COMP:%d FLAG-VARS:\n",d->data[ip].value, d->data[ip+2].value); + } + ip+=3; + ip = dump_sl(d,ip,level); + ip++; + print_spaces(level); + printf(" DATA:\n"); + ip = dump_sl(d,ip,level); + break; + case BC_ADDRESS: + has_index = 1; + case BC_ADDRESS_PRE_INDEX: case BC_ENVELOPE: if (d->data[ip].op == BC_ADDRESS) { printf("%d: ADDRESS (\n",ip++); - print_spaces(level); - printf(" INDEX %d\n" , d->data[ip++].value); } else { printf("%d: ENVELOPE (\n",ip++); } + if (has_index) { + print_spaces(level); + printf(" INDEX:%d\n", d->data[ip++].value); + } + print_spaces(level); if (d->data[ip].value == B_COUNT || d->data[ip].value == B_VALUE) { @@ -204,6 +238,64 @@ ip = dump_sl(d,ip,level); break; + case BC_DATE: + has_index=1; + case BC_CURRENTDATE: + if (BC_DATE == opcode) { + printf("%d: DATE (\n", ip++); + } + else { + printf("%d: CURRENTDATE (\n", ip++); + } + + /* index */ + if (has_index) { + print_spaces(level); + printf(" INDEX:%d\n", d->data[ip++].value); + } + + /* zone tag */ + print_spaces(level); + printf(" ZONE-TAG:%d ", d->data[ip].value); + + switch (d->data[ip++].value) { + case B_TIMEZONE: + printf("TIMEZONE:%d\n", d->data[ip++].value); + break; + case B_ORIGINALZONE: + printf("ORIGINALZONE\n"); + break; + } + + /* comparison */ + print_spaces(level); + if (d->data[ip].value == B_COUNT || d->data[ip].value == B_VALUE) + { + printf(" MATCH:%d RELATION:%d COMP:%d\n", + d->data[ip].value, d->data[ip+1].value, d->data[ip+2].value); + } else { + printf(" MATCH:%d COMP:%d\n", d->data[ip].value, d->data[ip+2].value); + } + ip+=3; + + /* date type */ + print_spaces(level); + printf(" DATE-TYPE:%d\n", d->data[ip++].value); + + /* header name */ + if (BC_DATE == opcode) { + print_spaces(level); + printf(" HEADER NAME: {%d}", d->data[ip++].len); + printf("%s\n",d->data[ip++].str); + } + + print_spaces(level); + printf(" KEY LIST:\n"); + ip = dump_sl(d,ip,level); + break; + + break; + default: printf("%d: TEST(%d)\n",ip,d->data[ip].op); break; @@ -252,7 +344,11 @@ break; case B_KEEP: - printf("%d: KEEP\n",i); + printf("%d: KEEP FLAGS:\n",i++); + i = dump_sl(d,i,level+1); + i++; + print_spaces(level+1); + printf("COPY(%d)\n",i, d->data[i].value); break; case B_MARK: @@ -264,7 +360,10 @@ break; case B_FILEINTO: - printf("%d: FILEINTO COPY(%d) FOLDER({%d}%s)\n",i, + printf("%d: FILEINTO FLAGS:\n",i++); + i = dump_sl(d,i,level+1); + print_spaces(level+1); + printf("COPY(%d) FOLDER({%d}%s)\n", d->data[i+1].value,d->data[i+2].len,d->data[i+3].str); i+=3; break;
View file
cyrus-imapd-2.5.tar.gz/sieve/bc_emit.c
Changed
@@ -246,33 +246,36 @@ } case BC_HEADER: + case BC_HASFLAG: { int ret; + if (BC_HEADER == opcode) { /* drop index */ if(write_int(fd, bc->data[(*codep)].value) == -1) return -1; wrote += sizeof(int); (*codep)++; + } /* Drop match type */ if(write_int(fd, bc->data[(*codep)].value) == -1) return -1; wrote += sizeof(int); (*codep)++; - /*drop comparator */ + /*now drop relation*/ if(write_int(fd, bc->data[(*codep)].value) == -1) return -1; wrote += sizeof(int); (*codep)++; - /*now drop relation*/ + /*drop comparator */ if(write_int(fd, bc->data[(*codep)].value) == -1) return -1; wrote += sizeof(int); (*codep)++; - /* Now drop headers */ + /* Now drop haystacks */ ret = bc_stringlist_emit(fd, codep, bc); if(ret < 0) return -1; wrote+=ret; - /* Now drop data */ + /* Now drop needles */ ret = bc_stringlist_emit(fd, codep, bc); if(ret < 0) return -1; wrote+=ret; @@ -368,10 +371,12 @@ int tmp; /* drop index */ - if(write_int(fd, bc->data[(*codep)].value) == -1) - return -1; - wrote += sizeof(int); - (*codep)++; + if(BC_DATE == opcode) { + if(write_int(fd, bc->data[(*codep)].value) == -1) + return -1; + wrote += sizeof(int); + (*codep)++; + } /* drop zone tag */ tmp = bc->data[(*codep)].value; @@ -590,9 +595,57 @@ break; } + case B_KEEP: + /* Flags Stringlist, Copy (word) */ + + /* Dump a stringlist of flags */ + ret = bc_stringlist_emit(fd, &codep, bc); + if(ret < 0) + return -1; + filelen += ret; + + if(write_int(fd,bc->data[codep++].value) == -1) + return -1; + + filelen += sizeof(int); + break; + case B_FILEINTO: + /* Flags Stringlist, Copy (word), Folder String */ + + /* Dump a stringlist of flags */ + ret = bc_stringlist_emit(fd, &codep, bc); + if(ret < 0) + return -1; + filelen += ret; + + /* Write Copy */ + if(write_int(fd,bc->data[codep++].value) == -1) + return -1; + + filelen += sizeof(int); + + /* Write string length of Folder */ + len = bc->data[codep++].len; + if(write_int(fd,len) == -1) + return -1; + + filelen+=sizeof(int); + + /* Write Folder */ + if(write(fd,bc->data[codep++].str,len) == -1) + return -1; + + ret = align_string(fd, len); + if(ret == -1) + return -1; + + filelen += len + ret; + + break; + case B_REDIRECT: - /* Copy (word), Folder/Address String */ + /* Copy (word), Address String */ if(write_int(fd,bc->data[codep++].value) == -1) return -1; @@ -850,7 +903,6 @@ case B_NULL: case B_STOP: case B_DISCARD: - case B_KEEP: case B_MARK: case B_UNMARK: case B_RETURN:
View file
cyrus-imapd-2.5.tar.gz/sieve/bc_eval.c
Changed
@@ -344,7 +344,8 @@ /* Evaluate a bytecode test */ static int eval_bc_test(sieve_interp_t *interp, void* m, - bytecode_input_t * bc, int * ip) + bytecode_input_t * bc, int * ip, + strarray_t *workingflags) { int res=0; int i=*ip; @@ -352,6 +353,7 @@ int list_len; /* for allof/anyof/exists */ int list_end; /* for allof/anyof/exists */ int address=0;/*to differentiate between address and envelope*/ + int has_index=0;/* used to differentiate between pre and post index tests */ comparator_t * comp=NULL; void * comprock=NULL; int op= ntohl(bc[i].op); @@ -368,7 +370,7 @@ case BC_NOT:/*2*/ i+=1; - res = eval_bc_test(interp, m, bc, &i); + res = eval_bc_test(interp, m, bc, &i, workingflags); if(res >= 0) res = !res; /* Only invert in non-error case */ break; @@ -427,7 +429,7 @@ * in the right place */ for (x=0; x<list_len && !res; x++) { int tmp; - tmp = eval_bc_test(interp, m, bc, &i); + tmp = eval_bc_test(interp, m, bc, &i, workingflags); if(tmp < 0) { res = tmp; break; @@ -447,7 +449,7 @@ /* return 1 unless you find one that isn't true, then return 0 */ for (x=0; x<list_len && res; x++) { int tmp; - tmp = eval_bc_test(interp, m, bc, &i); + tmp = eval_bc_test(interp, m, bc, &i, workingflags); if(tmp < 0) { res = tmp; break; @@ -458,8 +460,30 @@ i = list_end; /* handle short-circuiting */ break; - case BC_ADDRESS:/*7*/ + case BC_ADDRESS:/*13*/ + has_index=1; + /* fall through */ + case BC_ADDRESS_PRE_INDEX:/*7*/ address=1; + if (BC_ADDRESS_PRE_INDEX == op) { + /* There was a version of the bytecode that had the index extension + * but did not update the bytecode codepoints, nor did it increment + * the bytecode version number. This tests if the index extension + * was in the bytecode based on the position of the match-type + * argument. + */ + switch (bc[i+2].value) { + case B_IS: + case B_CONTAINS: + case B_MATCHES: + case B_REGEX: + case B_COUNT: + case B_VALUE: + has_index = 1; + default: + has_index = 0; + } + } /* fall through */ case BC_ENVELOPE:/*8*/ { @@ -468,7 +492,7 @@ const struct address *a; char *addr; - int headersi=address+i+5;/* the i value for the beginning of the headers */ + int headersi=has_index+i+5;/* the i value for the beginning of the headers */ int datai=(ntohl(bc[headersi+1].value)/4); int numheaders=ntohl(bc[headersi].len); @@ -477,11 +501,11 @@ int currh, currd; /* current header, current data */ int header_count; - int index=address ? ntohl(bc[i+1].value) : 0; // used for address only - int match=ntohl(bc[address+i+1].value); - int relation=ntohl(bc[address+i+2].value); - int comparator=ntohl(bc[address+i+3].value); - int apart=ntohl(bc[address+i+4].value); + int index=has_index ? ntohl(bc[i+1].value) : 0; // used for address only + int match=ntohl(bc[has_index+i+1].value); + int relation=ntohl(bc[has_index+i+2].value); + int comparator=ntohl(bc[has_index+i+3].value); + int apart=ntohl(bc[has_index+i+4].value); int count=0; int isReg = (match==B_REGEX); int ctag = 0; @@ -544,16 +568,16 @@ if (index > 0) { --index; if (index >= header_count) { - res = SIEVE_FAIL; - goto alldone; + res = 0; + break; } header_count = index + 1; } else if (index < 0) { index += header_count; if (index < 0) { - res = SIEVE_FAIL; - goto alldone; + res = 0; + break; } header_count = index + 1; } @@ -664,11 +688,33 @@ envelope_err: break; } - case BC_HEADER:/*9*/ + case BC_HEADER:/*14*/ + has_index=1; + /* fall through */ + case BC_HEADER_PRE_INDEX:/*9*/ + if (BC_HEADER_PRE_INDEX == op) { + /* There was a version of the bytecode that had the index extension + * but did not update the bytecode codepoints, nor did it increment + * the bytecode version number. This tests if the index extension + * was in the bytecode based on the position of the match-type + * argument. + */ + switch (ntohl(bc[i+2].value)) { + case B_IS: + case B_CONTAINS: + case B_MATCHES: + case B_REGEX: + case B_COUNT: + case B_VALUE: + has_index = 1; + default: + has_index = 0; + } + } { const char** val; - int headersi=i+5;/*the i value for the beginning of the headers*/ + int headersi=has_index+i+4;/*the i value for the beginning of the headers*/ int datai=(ntohl(bc[headersi+1].value)/4); int numheaders=ntohl(bc[headersi].len); @@ -677,10 +723,10 @@ int currh, currd; /*current header, current data*/ int header_count; - int index=ntohl(bc[i+1].value); - int match=ntohl(bc[i+2].value); - int relation=ntohl(bc[i+3].value); - int comparator=ntohl(bc[i+4].value); + int index=has_index ? ntohl(bc[i+1].value) : 0; + int match=ntohl(bc[has_index+i+1].value); + int relation=ntohl(bc[has_index+i+2].value); + int comparator=ntohl(bc[has_index+i+3].value); int count=0; int isReg = (match==B_REGEX); int ctag = 0; @@ -735,16 +781,16 @@ if (index > 0) { --index; if (index >= header_count) { - res = SIEVE_FAIL; - goto alldone; + res = 0; + break; } header_count = index + 1; } else if (index < 0) { index += header_count; if (index < 0) { - res = SIEVE_FAIL; - goto alldone; + res = 0; + break; } header_count = index + 1; } @@ -811,6 +857,110 @@ break; } + case BC_HASFLAG:/*15*/ + { + int haystacksi=i+4;/*the i value for the beginning of the variables*/ + int needlesi=(ntohl(bc[haystacksi+1].value)/4); + + int numneedles=ntohl(bc[needlesi].len); // number of search flags + + int currneedle; /* current needle */ + + int match=ntohl(bc[i+1].value); + int relation=ntohl(bc[i+2].value); + int comparator=ntohl(bc[i+3].value); + int count=0; + int isReg = (match==B_REGEX); + int ctag = 0; + regex_t *reg; + char errbuf[100]; /* Basically unused, regexps tested at compile */ + + /* set up variables needed for compiling regex */ + if (isReg) + { + if (comparator== B_ASCIICASEMAP) + { + ctag= REG_EXTENDED | REG_NOSUB | REG_ICASE; + } + else + { + ctag= REG_EXTENDED | REG_NOSUB; + } + + } + + /*find the correct comparator fcn*/ + comp=lookup_comp(comparator, match, relation, &comprock); + + if(!comp) { + res = SIEVE_RUN_ERROR; + break; + } + + if (match == B_COUNT ) + { + count = workingflags->count; + snprintf(scount, SCOUNT_SIZE, "%u", count); + /*search through all the data*/ + currneedle=needlesi+2; + for (z=0; z<numneedles && !res; z++) + { + const char *this_needle; + + currneedle = unwrap_string(bc, currneedle, &this_needle, NULL); +#if VERBOSE + printf("%d, %s \n", count, data_val); +#endif + res |= comp(scount, strlen(scount), this_needle, comprock); + } + } else { + + /* search through the haystack for the needles */ + currneedle=needlesi+2; + for(x=0; x<numneedles && !res; x++) + { + const char *this_needle; + + currneedle = unwrap_string(bc, currneedle, &this_needle, NULL); + +#if VERBOSE + printf ("val %s %s %s\n", val[0], val[1], val[2]); +#endif + + /* search through all the flags */ + + for (y=0; y < workingflags->count && !res; y++) + { + const char *active_flag; + + active_flag = workingflags->data[y]; + + if (isReg) { + reg= bc_compile_regex(this_needle, ctag, errbuf, + sizeof(errbuf)); + if (!reg) + { + /* Oops */ + res=-1; + goto alldone; + } + + res |= comp(active_flag, strlen(active_flag), + (const char *)reg, comprock); + free(reg); + } else { + res |= comp(active_flag, strlen(active_flag), + this_needle, comprock); + } + } + } + } + + /* Update IP */ + i=(ntohl(bc[needlesi+1].value)/4); + + break; + } case BC_BODY:/*10*/ { sieve_bodypart_t ** val; @@ -935,7 +1085,47 @@ break; } case BC_DATE:/*11*/ + has_index=1; case BC_CURRENTDATE:/*12*/ + if (BC_CURRENTDATE/*12*/ == op || BC_DATE/*11*/ == op) { + /* There was a version of the bytecode that had the index extension + * but did not update the bytecode codepoints, nor did it increment + * the bytecode version number. This tests if the index extension + * was in the bytecode based on the position of the match-type + * or comparator argument. This will correctly identify whether + * the index extension was supported in every case except the case + * of a timezone that is 61 minutes offset (since 61 corresponds to + * B_ORIGINALZONE). + * There was also an unnumbered version of BC_CURRENTDATE that did + * allow :index. This also covers that case. + */ + switch (ntohl(bc[i+4].value)) { + case B_ASCIICASEMAP: + case B_OCTET: + case B_ASCIINUMERIC: + has_index = 0; + break; + default: + if (B_TIMEZONE == ntohl(bc[i+1].value) && + B_ORIGINALZONE != ntohl(bc[i+2].value)) { + switch (ntohl(bc[i+3].value)) { + case B_IS: + case B_CONTAINS: + case B_MATCHES: + case B_REGEX: + case B_COUNT: + case B_VALUE: + has_index = 0; + break; + default: + has_index = 1; + + } + } else { + has_index = 1; + } + } + } { char buffer[64]; const char **headers = NULL; @@ -958,7 +1148,7 @@ ++i; /* BC_DATE | BC_CURRENTDATE */ /* index */ - index = ntohl(bc[i++].value); + index = has_index ? ntohl(bc[i++].value) : 0; /* zone tag */ zone = ntohl(bc[i++].value); @@ -992,8 +1182,9 @@ */ if (interp->getheader(m, header_name, &headers) != SIEVE_OK) { - res = SIEVE_FAIL; - goto alldone; + res = 0; + free(bc_makeArray(bc, &i)); + break; } /* count results */ @@ -1005,15 +1196,28 @@ /* convert index argument value to array index */ if (index > 0) { --index; + if (index >= header_count) { + res = 0; + free(bc_makeArray(bc, &i)); + break; + } + header_count = index + 1; } - else { + else if (index < 0) { index += header_count; + if (index < 0) { + res = 0; + free(bc_makeArray(bc, &i)); + break; + } + header_count = index + 1; } /* check if index is out of bounds */ if (index < 0 || index >= header_count) { - res = SIEVE_FAIL; - goto alldone; + res = 0; + free(bc_makeArray(bc, &i)); + break; } header = headers[index]; @@ -1029,8 +1233,9 @@ } if (-1 == time_from_rfc822(header_data, &t)) { - res = SIEVE_FAIL; - goto alldone; + res = 0; + free(bc_makeArray(bc, &i)); + break; } /* timezone offset */ @@ -1043,8 +1248,9 @@ zone = strrchr(header, ' '); if (!zone || 3 != sscanf(zone + 1, "%c%02d%02d", &sign, &hours, &minutes)) { - res = SIEVE_FAIL; - goto alldone; + res = 0; + free(bc_makeArray(bc, &i)); + break; } timezone_offset = (sign == '-' ? -1 : 1) * ((hours * 60) + (minutes)); @@ -1160,8 +1366,9 @@ /* The entrypoint for bytecode evaluation */ int sieve_eval_bc(sieve_execute_t *exe, int is_incl, sieve_interp_t *i, void *sc, void *m, - strarray_t *imapflags, action_list_t *actions, - notify_list_t *notify_list, const char **errmsg) + variable_list_t *flagvars, action_list_t *actions, + notify_list_t *notify_list, const char **errmsg, + variable_list_t *workingvars) { const char *data; int res=0; @@ -1221,7 +1428,13 @@ #endif for(ip++; ip<ip_max; ) { + /* In this loop, each case must increment ip for the next iteration. + * The when the case is jumped to initially, ip points to the opcode. + * This should probably change to point to the first parameter to + * support future extensions. + */ int copy = 0; + strarray_t *actionflags = NULL; op=ntohl(bc[ip].op); switch(op) { @@ -1229,10 +1442,32 @@ res=1; break; - case B_KEEP:/*1*/ - res = do_keep(actions, imapflags); + case B_KEEP:/*22*/ + { + int x; + int list_len=ntohl(bc[ip+1].len); + + ip+=3; /* skip opcode, list_len, and list data len */ + + if (list_len) { + actionflags = (varlist_extend(flagvars))->var; + } + for (x=0; x<list_len; x++) { + const char *flag; + ip = unwrap_string(bc, ip, &flag, NULL); + if (flag[0]) { + strarray_add_case(actionflags,flag); + } + } + } + copy = ntohl(bc[ip].value); + /* fall through */ + case B_KEEP_ORIG:/*1*/ + res = do_keep(actions, !copy, + actionflags ? actionflags : flagvars->var); if (res == SIEVE_RUN_ERROR) *errmsg = "Keep can not be used with Reject"; + actionflags = NULL; ip++; break; @@ -1251,20 +1486,41 @@ break; - case B_FILEINTO:/*19*/ - copy = ntohl(bc[ip+1].value); + case B_FILEINTO:/*23*/ + { + int x; + int list_len=ntohl(bc[ip+1].len); + + ip+=3; /* skip opcode, list_len, and list data len */ + + if (list_len) { + actionflags = (varlist_extend(flagvars))->var; + } + for (x=0; x<list_len; x++) { + const char *flag; + ip = unwrap_string(bc, ip, &flag, NULL); + if (flag[0]) { + strarray_add_case(actionflags,flag); + } + } + } + /* fall through */ + case B_FILEINTO_COPY:/*19*/ + copy = ntohl(bc[ip].value); ip+=1; /* fall through */ case B_FILEINTO_ORIG:/*4*/ { - ip = unwrap_string(bc, ip+1, &data, NULL); + ip = unwrap_string(bc, ip, &data, NULL); - res = do_fileinto(actions, data, !copy, imapflags); + res = do_fileinto(actions, data, !copy, + actionflags ? actionflags : flagvars->var); if (res == SIEVE_RUN_ERROR) *errmsg = "Fileinto can not be used with Reject"; + actionflags = NULL; break; } @@ -1291,7 +1547,7 @@ int result; ip+=2; - result=eval_bc_test(i, m, bc, &ip); + result=eval_bc_test(i, m, bc, &ip, workingvars->var); if (result<0) { *errmsg = "Invalid test"; @@ -1307,11 +1563,24 @@ case B_MARK:/*7*/ res = do_mark(actions); + { + int n = i->markflags->count; + while (n) { + strarray_add_case(workingvars->var, i->markflags->data[--n]); + } + } ip++; break; case B_UNMARK:/*8*/ res = do_unmark(actions); + { + int n = i->markflags->count; + while (n) { + strarray_remove_all_case(workingvars->var, + i->markflags->data[--n]); + } + } ip++; break; @@ -1326,6 +1595,7 @@ ip = unwrap_string(bc, ip, &data, NULL); res = do_addflag(actions, data); + strarray_add_case(workingvars->var, data); if (res == SIEVE_RUN_ERROR) *errmsg = "addflag can not be used with Reject"; @@ -1340,17 +1610,19 @@ ip+=3; /* skip opcode, list_len, and list data len */ - ip = unwrap_string(bc, ip, &data, NULL); - res = do_setflag(actions, data); + res = do_setflag(actions); + strarray_truncate(workingvars->var, 0); if (res == SIEVE_RUN_ERROR) { *errmsg = "setflag can not be used with Reject"; } else { - for (x=1; x<list_len; x++) { + for (x=0; x<list_len; x++) { ip = unwrap_string(bc, ip, &data, NULL); - + if (data[0]) { res = do_addflag(actions, data); + strarray_add_case(workingvars->var, data); + } if (res == SIEVE_RUN_ERROR) *errmsg = "setflag can not be used with Reject"; @@ -1371,6 +1643,7 @@ ip = unwrap_string(bc, ip, &data, NULL); res = do_removeflag(actions, data); + strarray_remove_all_case(workingvars->var, data); if (res == SIEVE_RUN_ERROR) *errmsg = "removeflag can not be used with Reject"; @@ -1645,8 +1918,8 @@ } res = sieve_eval_bc(exe, 1, i, - sc, m, imapflags, actions, - notify_list, errmsg); + sc, m, flagvars, actions, + notify_list, errmsg, workingvars); break; }
View file
cyrus-imapd-2.5.tar.gz/sieve/bc_generate.c
Changed
@@ -330,16 +330,23 @@ if (codep == -1) return -1; break; case HEADER: - /* BC_HEADER { c: comparator } { headers : string list } - { patterns : string list } - */ + case HASFLAG: + /* BC_HEADER { i: index } { c: comparator } + * { haystacks : string list } { patterns : string list } + * + * BC_HASFLAG { c: comparator } + * { haystacks : string list } { patterns : string list } + */ if(!atleast(retval,codep + 1)) return -1; - retval->data[codep++].op = BC_HEADER; + retval->data[codep++].op = (t->type == HEADER) + ? BC_HEADER : BC_HASFLAG; + if (t->type == HEADER) { /* index */ if(!atleast(retval,codep + 1)) return -1; - retval->data[codep++].value = t->u.ae.index; + retval->data[codep++].value = t->u.h.index; + } /* comparator */ codep = bc_comparator_generate(codep, retval, @@ -348,7 +355,7 @@ t->u.h.comparator); if (codep == -1) return -1; - /* headers */ + /* haystacks */ codep = bc_stringlist_generate(codep, retval, t->u.h.sl); if (codep == -1) return -1; @@ -358,7 +365,11 @@ break; case ADDRESS: case ENVELOPE: - /* (BC_ADDRESS | BC_ENVELOPE) {c : comparator} + /* BC_ADDRESS {i : index } {c : comparator} + (B_ALL | B_LOCALPART | ...) { header : string list } + { pattern : string list } + + BC_ENVELOPE {c : comparator} (B_ALL | B_LOCALPART | ...) { header : string list } { pattern : string list } */ @@ -456,7 +467,7 @@ break; case DATE: case CURRENTDATE: - /* BC_DATE { time-zone: string} { c: comparator } + /* BC_DATE { i: index } { time-zone: string} { c: comparator } * { header-name : string } { date-part: string } * { key-list : string list } * @@ -468,8 +479,10 @@ retval->data[codep++].op = (DATE == t->type) ? BC_DATE : BC_CURRENTDATE; /* index */ - if(!atleast(retval,codep + 1)) return -1; - retval->data[codep++].value = t->u.dt.index; + if (DATE == t->type) { + if(!atleast(retval,codep + 1)) return -1; + retval->data[codep++].value = t->u.dt.index; + } /* zone */ codep = bc_zone_generate(codep, retval, @@ -580,9 +593,16 @@ break; case KEEP: - /* KEEP (no arguments) */ + /* KEEP + STRINGLIST flags + VALUE copy + */ if (!atleast(retval, codep+1)) return -1; retval->data[codep++].op = B_KEEP; + codep = bc_stringlist_generate(codep,retval,c->u.k.flags); + if (codep == -1) return -1; + if(!atleast(retval,codep+1)) return -1; + retval->data[codep++].value = c->u.k.copy; break; case MARK: @@ -667,11 +687,15 @@ case FILEINTO: /* FILEINTO + STRINGLIST flags VALUE copy STRING folder */ - if (!atleast(retval, codep+4)) return -1; + if (!atleast(retval, codep+1)) return -1; retval->data[codep++].op = B_FILEINTO; + codep = bc_stringlist_generate(codep, retval, c->u.f.flags); + if(codep == -1) return -1; + if (!atleast(retval, codep+3)) return -1; retval->data[codep++].value = c->u.f.copy; retval->data[codep++].len = strlen(c->u.f.folder); retval->data[codep++].str = c->u.f.folder;
View file
cyrus-imapd-2.5.tar.gz/sieve/bytecode.h
Changed
@@ -103,8 +103,10 @@ * version 0x05 scripts implemented updated VACATION (:from and :handle) * version 0x06 scripts implemented updated VACATION (:seconds) * version 0x07 scripts implemented updated INCLUDE (:once and :optional) + * version 0x08 scripts implemented DATE and INDEX extensions + * version 0x09 scripts implemented IMAP4FLAGS extension */ -#define BYTECODE_VERSION 0x07 +#define BYTECODE_VERSION 0x09 #define BYTECODE_MIN_VERSION 0x03 /* minimum supported version */ #define BYTECODE_MAGIC "CyrSBytecode" #define BYTECODE_MAGIC_LEN 12 /* Should be multiple of 4 */ @@ -116,7 +118,7 @@ enum bytecode { B_STOP, - B_KEEP, + B_KEEP_ORIG, /* legacy keep w/o support for :copy and :flags */ B_DISCARD, B_REJECT, /* require reject */ B_FILEINTO_ORIG, /* legacy fileinto w/o support for :copy */ @@ -127,9 +129,9 @@ B_MARK, /* require imapflags */ B_UNMARK, /* require imapflags */ - B_ADDFLAG, /* require imapflags */ - B_SETFLAG, /* require imapflags */ - B_REMOVEFLAG, /* require imapflags */ + B_ADDFLAG, /* require imap4flags */ + B_SETFLAG, /* require imap4flags */ + B_REMOVEFLAG, /* require imap4flags */ B_NOTIFY, /* require notify */ B_DENOTIFY, /* require notify */ @@ -141,10 +143,13 @@ B_INCLUDE, /* require include */ B_RETURN, /* require include */ - B_FILEINTO, /* require fileinto */ + B_FILEINTO_COPY, /* legacy fileinto w/o support for :flags */ B_REDIRECT, B_VACATION, /* require vacation */ + + B_KEEP, + B_FILEINTO /* require fileinto */ }; enum bytecode_comps { @@ -155,12 +160,15 @@ BC_SIZE, BC_ANYOF, BC_ALLOF, - BC_ADDRESS, + BC_ADDRESS_PRE_INDEX, BC_ENVELOPE, /* require envelope */ - BC_HEADER, + BC_HEADER_PRE_INDEX, BC_BODY, /* require body */ BC_DATE, /* require date */ - BC_CURRENTDATE /* require date */ + BC_CURRENTDATE, /* require date */ + BC_ADDRESS, + BC_HEADER, + BC_HASFLAG /* require imap4flags */ }; /* currently one enum so as to help determine where values are being misused.
View file
cyrus-imapd-2.5.tar.gz/sieve/flags.c
Added
@@ -0,0 +1,59 @@ +/* + * flags.c + * + * Created on: Oct 6, 2014 + * Author: James Cassell + */ + +#include "flags.h" +#include "imparse.h" +#include "strarray.h" +#include "util.h" +#include "xmalloc.h" + +static int verify_flag(char *f) +{ + if (f[0] == '\\') { + lcase(f); + if (strcmp(f, "\\seen") && strcmp(f, "\\answered") && + strcmp(f, "\\flagged") && strcmp(f, "\\draft") && + strcmp(f, "\\deleted")) { + return 0; + } + return 1; + } + if (!imparse_isatom(f)) { + return 0; + } + return 1; +} + +EXPORTED int verify_flaglist(strarray_t *sl) +{ + int i; + char *joined; + strarray_t *resplit; + // Join all the flags, putting spaces between them + joined = strarray_join(sl, " "); + // Clear out the sl for reuse + strarray_truncate(sl, 0); + // Split the joined flag list at spaces + resplit = strarray_split(joined, " ", STRARRAY_TRIM); + + // Perform duplicate elimination and flag verification + for (i = 0; i < resplit->count ; i++) { + // has the side effect of lower-casing system flags + if (!verify_flag(resplit->data[i])) { + /* [IMAP4FLAGS] Section 2 "General Requirements for Flag + * Handling" says "If a flag validity check fails, the + * flag MUST be ignored", which for us means that we + * simply remove the invalid flag from the list. + */ + continue; + } + strarray_add_case(sl, resplit->data[i]); + } + strarray_free(resplit); + free(joined); + return sl->count; +}
View file
cyrus-imapd-2.5.tar.gz/sieve/flags.h
Added
@@ -0,0 +1,15 @@ +/* + * flags.h + * + * Created on: Oct 6, 2014 + * Author: James Cassell + */ + +#ifndef FLAGS_H_ +#define FLAGS_H_ + +#include "strarray.h" + +EXPORTED int verify_flaglist(strarray_t *sl); + +#endif /* FLAGS_H_ */
View file
cyrus-imapd-2.5.tar.gz/sieve/interp.c
Changed
@@ -131,6 +131,8 @@ if (i->getbody && (config_sieve_extensions & IMAP_ENUM_SIEVE_EXTENSIONS_BODY)) strlcat(i->extensions, " body", EXT_LEN); + if (config_sieve_extensions & IMAP_ENUM_SIEVE_EXTENSIONS_IMAP4FLAGS) + strlcat(i->extensions, " imap4flags", EXT_LEN); /* add match-types */ if (config_sieve_extensions & IMAP_ENUM_SIEVE_EXTENSIONS_RELATIONAL)
View file
cyrus-imapd-2.5.tar.gz/sieve/message.c
Changed
@@ -104,22 +104,40 @@ /* see if this conflicts with any previous actions taken on this message */ while (a != NULL) { - b = a; if (a->a == ACTION_REJECT) return SIEVE_RUN_ERROR; + if (a->a == ACTION_FILEINTO && !strcmp(a->u.fil.mailbox, mbox)) { + /* don't bother doing it twice */ + /* check that we have a valid action */ + if (b == NULL) { + return SIEVE_INTERNAL_ERROR; + } + /* cut this action out of the list */ + b->next = a->next; + a->next = NULL; + /* find the end of the list */ + while (b->next != NULL) { + b = b-> next; + } + /* add the action to the end of the list */ + b->next = a; + break; + } + b = a; a = a->next; } - /* add to the action list */ - a = (action_list_t *) xmalloc(sizeof(action_list_t)); - if (a == NULL) - return SIEVE_NOMEM; + if (a == NULL) { + /* add to the action list */ + a = new_action_list(); + if (a == NULL) + return SIEVE_NOMEM; + b->next = a; + } a->a = ACTION_FILEINTO; - a->cancel_keep = cancel_keep; + a->cancel_keep |= cancel_keep; a->u.fil.mailbox = mbox; a->u.fil.imapflags = imapflags; - b->next = a; - a->next = NULL; return 0; } @@ -157,29 +175,46 @@ * * incompatible with: reject */ -int do_keep(action_list_t *a, strarray_t *imapflags) +int do_keep(action_list_t *a, int cancel_keep, strarray_t *imapflags) { action_list_t *b = NULL; /* see if this conflicts with any previous actions taken on this message */ while (a != NULL) { - b = a; if (a->a == ACTION_REJECT) return SIEVE_RUN_ERROR; - if (a->a == ACTION_KEEP) /* don't bother doing it twice */ - return 0; + if (a->a == ACTION_KEEP) { + /* don't bother doing it twice */ + /* check that we have a valid action */ + if (b == NULL) { + return SIEVE_INTERNAL_ERROR; + } + /* cut this action out of the list */ + b->next = a->next; + a->next = NULL; + /* find the end of the list */ + while (b->next != NULL) { + b = b-> next; + } + /* add the action to the end of the list */ + b->next = a; + break; + } + b = a; a = a->next; } - /* add to the action list */ - a = (action_list_t *) xmalloc(sizeof(action_list_t)); - if (a == NULL) - return SIEVE_NOMEM; + if(a == NULL) { + /* add to the action list */ + a = new_action_list(); + if (a == NULL) + return SIEVE_NOMEM; + a->next = NULL; + b->next = a; + } a->a = ACTION_KEEP; - a->cancel_keep = 1; + a->cancel_keep |= cancel_keep; a->u.keep.imapflags = imapflags; - a->next = NULL; - b->next = a; return 0; } @@ -264,7 +299,7 @@ * * incompatible with: reject */ -int do_setflag(action_list_t *a, const char *flag) +int do_setflag(action_list_t *a) { action_list_t *b = NULL; @@ -282,7 +317,6 @@ return SIEVE_NOMEM; a->a = ACTION_SETFLAG; a->cancel_keep = 0; - a->u.fla.flag = flag; b->next = a; a->next = NULL; return 0;
View file
cyrus-imapd-2.5.tar.gz/sieve/message.h
Changed
@@ -119,12 +119,12 @@ int do_fileinto(action_list_t *m, const char *mbox, int cancel_keep, strarray_t *imapflags); int do_redirect(action_list_t *m, const char *addr, int cancel_keep); -int do_keep(action_list_t *m, strarray_t *imapflags); +int do_keep(action_list_t *m, int cancel_keep, strarray_t *imapflags); int do_discard(action_list_t *m); int do_vacation(action_list_t *m, char *addr, char *fromaddr, char *subj, const char *msg, int seconds, int mime, const char *handle); -int do_setflag(action_list_t *m, const char *flag); +int do_setflag(action_list_t *m); int do_addflag(action_list_t *m, const char *flag); int do_removeflag(action_list_t *m, const char *flag); int do_mark(action_list_t *m);
View file
cyrus-imapd-2.5.tar.gz/sieve/script.c
Changed
@@ -139,6 +139,10 @@ } else { return 0; } + } else if (!strcmp("imap4flags", req) && + (config_sieve_extensions & IMAP_ENUM_SIEVE_EXTENSIONS_IMAP4FLAGS)) { + s->support.imap4flags = 1; + return 1; } else if (!strcmp("notify",req)) { if (s->interp.notify && (config_sieve_extensions & IMAP_ENUM_SIEVE_EXTENSIONS_NOTIFY)) { @@ -325,38 +329,6 @@ return SIEVE_OK; } -static int sieve_addflag(strarray_t *imapflags, const char *flag) -{ - int n; - /* search for flag already in list */ - for (n = 0; n < imapflags->count; n++) { - if (!strcmp(imapflags->data[n], flag)) - break; - } - - /* add flag to list, iff not in list */ - if (n == imapflags->count) - strarray_append(imapflags, flag); - - return SIEVE_OK; -} - -static int sieve_removeflag(strarray_t *imapflags, const char *flag) -{ - int n; - /* search for flag already in list */ - for (n = 0; n < imapflags->count; n++) { - if (!strcmp(imapflags->data[n], flag)) - break; - } - - /* remove flag from list, iff in list */ - if (n < imapflags->count) - free(strarray_remove(imapflags, n)); - - return SIEVE_OK; -} - static int send_notify_callback(sieve_interp_t *interp, void *message_context, void * script_context, notify_list_t *notify, @@ -797,17 +769,14 @@ case ACTION_SETFLAG: strarray_fini(imapflags); - ret = sieve_addflag(imapflags, a->u.fla.flag); - free(interp->lastitem); - interp->lastitem = xstrdup(a->u.fla.flag); break; case ACTION_ADDFLAG: - ret = sieve_addflag(imapflags, a->u.fla.flag); + strarray_add_case(imapflags, a->u.fla.flag); free(interp->lastitem); interp->lastitem = xstrdup(a->u.fla.flag); break; case ACTION_REMOVEFLAG: - ret = sieve_removeflag(imapflags, a->u.fla.flag); + strarray_remove_all_case(imapflags, a->u.fla.flag); free(interp->lastitem); interp->lastitem = xstrdup(a->u.fla.flag); break; @@ -816,8 +785,8 @@ int n = interp->markflags->count; ret = SIEVE_OK; - while (n && ret == SIEVE_OK) { - ret = sieve_addflag(imapflags, + while (n) { + strarray_add_case(imapflags, interp->markflags->data[--n]); } free(interp->lastitem); @@ -829,8 +798,8 @@ int n = interp->markflags->count; ret = SIEVE_OK; - while (n && ret == SIEVE_OK) { - ret = sieve_removeflag(imapflags, + while (n) { + strarray_remove_all_case(imapflags, interp->markflags->data[--n]); } free(interp->lastitem); @@ -863,8 +832,9 @@ /* execute some bytecode */ int sieve_eval_bc(sieve_execute_t *exe, int is_incl, sieve_interp_t *i, void *sc, void *m, - const strarray_t * , action_list_t *actions, - notify_list_t *notify_list, const char **errmsg); + variable_list_t * flagvars, action_list_t *actions, + notify_list_t *notify_list, const char **errmsg, + variable_list_t *workingvars); EXPORTED int sieve_execute_bytecode(sieve_execute_t *exe, sieve_interp_t *interp, void *script_context, void *message_context) @@ -877,7 +847,13 @@ char actions_string[ACTIONS_STRING_LEN] = ""; const char *errmsg = NULL; strarray_t imapflags = STRARRAY_INITIALIZER; + strarray_t workingflags = STRARRAY_INITIALIZER; + variable_list_t flagvars = VARIABLE_LIST_INITIALIZER; + variable_list_t workingvars = VARIABLE_LIST_INITIALIZER; + flagvars.var = &imapflags; + workingvars.var = &workingflags; + if (!interp) return SIEVE_FAIL; if (interp->notify) { @@ -900,7 +876,8 @@ else { ret = sieve_eval_bc(exe, 0, interp, script_context, message_context, - &imapflags, actions, notify_list, &errmsg); + &flagvars, actions, notify_list, &errmsg, + &workingvars); if (ret < 0) { ret = do_sieve_error(SIEVE_RUN_ERROR, interp, @@ -916,7 +893,8 @@ } } - strarray_fini(&imapflags); + varlist_fini(&flagvars); + varlist_fini(&workingvars); return ret; }
View file
cyrus-imapd-2.5.tar.gz/sieve/script.h
Changed
@@ -71,6 +71,7 @@ int date : 1; int index : 1; int vacation_seconds: 1; + int imap4flags : 1; } support; void *script_context;
View file
cyrus-imapd-2.5.tar.gz/sieve/sieve-lex.l
Changed
@@ -118,6 +118,7 @@ <INITIAL>body return BODY; <INITIAL>not return NOT; <INITIAL>size return SIZE; +<INITIAL>hasflag return HASFLAG; <INITIAL>reject return REJCT; <INITIAL>fileinto return FILEINTO; <INITIAL>redirect return REDIRECT; @@ -177,6 +178,7 @@ <INITIAL>:last return LAST; <INITIAL>:zone return ZONE; <INITIAL>:originalzone return ORIGINALZONE; +<INITIAL>:flags return FLAGS; <INITIAL>[ \t\n\r] ; /* ignore whitespace */ <INITIAL>#.* ; /* ignore hash comments */ <INITIAL>"/*"([^\*]|\*[^\/])*\*?"*/" ; /* ignore bracket comments */
View file
cyrus-imapd-2.5.tar.gz/sieve/sieve.y
Changed
@@ -54,6 +54,7 @@ #include "sieve/interp.h" #include "sieve/script.h" #include "sieve/tree.h" +#include "sieve/flags.h" #include "imapurl.h" #include "lib/gmtoff.h" @@ -137,16 +138,24 @@ }; static char *check_reqs(sieve_script_t *script, strarray_t *sl); +struct ftags { + int copy; + strarray_t *flags; +}; + static test_t *build_address(int t, struct aetags *ae, strarray_t *sl, strarray_t *pl); static test_t *build_header(int t, struct htags *h, strarray_t *sl, strarray_t *pl); static test_t *build_body(int t, struct btags *b, strarray_t *pl); static test_t *build_date(int t, struct dttags *dt, char *hn, strarray_t *kl); +static test_t *build_hasflag(int t, struct htags *h, + strarray_t *pl); static commandlist_t *build_vacation(int t, struct vtags *h, char *s); static commandlist_t *build_notify(int t, struct ntags *n); static commandlist_t *build_denotify(int t, struct dtags *n); -static commandlist_t *build_fileinto(int t, int c, char *f); +static commandlist_t *build_keep(int t, struct ftags *f); +static commandlist_t *build_fileinto(int t, struct ftags *f, char *folder); static commandlist_t *build_redirect(int t, int c, char *a); static commandlist_t *build_include(int, struct itags *, char*); static struct aetags *new_aetags(void); @@ -171,6 +180,9 @@ static struct dttags *new_dttags(void); static struct dttags *canon_dttags(struct dttags *dt); static void free_dttags(struct dttags *b); +static struct ftags *new_ftags(void); +static struct ftags *canon_ftags(struct ftags *f); +static void free_ftags(struct ftags *f); static int verify_stringlist(sieve_script_t*, strarray_t *sl, int (*verify)(sieve_script_t*, char *)); static int verify_mailbox(sieve_script_t*, char *s); @@ -178,7 +190,6 @@ static int verify_header(sieve_script_t*, char *s); static int verify_addrheader(sieve_script_t*, char *s); static int verify_envelope(sieve_script_t*, char *s); -static int verify_flag(sieve_script_t*, char *s); static int verify_relat(sieve_script_t*, char *s); static int verify_zone(sieve_script_t*, char *s); static int verify_date_part(sieve_script_t *parse_script, char *dp); @@ -214,13 +225,14 @@ struct dtags *dtag; struct itags *itag; struct dttags *dttag; + struct ftags *ftag; } %token <nval> NUMBER %token <sval> STRING %token IF ELSIF ELSE %token REJCT FILEINTO REDIRECT KEEP STOP DISCARD VACATION REQUIRE -%token SETFLAG ADDFLAG REMOVEFLAG MARK UNMARK +%token SETFLAG ADDFLAG REMOVEFLAG MARK UNMARK HASFLAG FLAGS %token NOTIFY DENOTIFY %token ANYOF ALLOF EXISTS SFALSE STRUE HEADER NOT SIZE ADDRESS ENVELOPE BODY %token COMPARATOR IS CONTAINS MATCHES REGEX COUNT VALUE OVER UNDER @@ -237,7 +249,7 @@ %type <cl> commands command action elsif block %type <sl> stringlist strings %type <test> test -%type <nval> comptag relcomp sizetag addrparttag addrorenv copy +%type <nval> comptag relcomp sizetag addrparttag addrorenv copy rtags %type <testl> testlist tests %type <htag> htags %type <aetag> aetags @@ -248,6 +260,7 @@ %type <itag> itags %type <dttag> dttags %type <nval> priority +%type <ftag> ftags %name-prefix="sieve" %defines @@ -297,19 +310,19 @@ } $$ = new_command(REJCT); $$->u.str = $2; } - | FILEINTO copy STRING { if (!parse_script->support.fileinto) { + | FILEINTO ftags STRING { if (!parse_script->support.fileinto) { yyerror(parse_script, "fileinto MUST be enabled with \"require\""); YYERROR; } if (!verify_mailbox(parse_script, $3)) { YYERROR; /* vm should call yyerror() */ } - $$ = build_fileinto(FILEINTO, $2, $3); } - | REDIRECT copy STRING { if (!verify_address(parse_script, $3)) { + $$ = build_fileinto(FILEINTO, canon_ftags($2), $3); } + | REDIRECT rtags STRING { if (!verify_address(parse_script, $3)) { YYERROR; /* va should call yyerror() */ } $$ = build_redirect(REDIRECT, $2, $3); } - | KEEP { $$ = new_command(KEEP); } + | KEEP ftags { $$ = build_keep(KEEP,canon_ftags($2)); } | STOP { $$ = new_command(STOP); } | DISCARD { $$ = new_command(DISCARD); } | VACATION vtags STRING { if (!parse_script->support.vacation) { @@ -321,30 +334,36 @@ } $$ = build_vacation(VACATION, canon_vtags(parse_script, $2), $3); } - | SETFLAG stringlist { if (!parse_script->support.imapflags) { - yyerror(parse_script, "imapflags MUST be enabled with \"require\""); + | SETFLAG stringlist { if (!(parse_script->support.imapflags || + parse_script->support.imap4flags)) { + yyerror(parse_script, "imap4flags MUST be enabled with \"require\""); YYERROR; } - if (!verify_stringlist(parse_script, $2, verify_flag)) { - YYERROR; /* vf should call yyerror() */ + verify_flaglist($2); + if(!$2->count) { + strarray_add($2, ""); } $$ = new_command(SETFLAG); $$->u.sl = $2; } - | ADDFLAG stringlist { if (!parse_script->support.imapflags) { - yyerror(parse_script, "imapflags MUST be enabled with \"require\""); + | ADDFLAG stringlist { if (!(parse_script->support.imapflags || + parse_script->support.imap4flags)) { + yyerror(parse_script, "imap4flags MUST be enabled with \"require\""); YYERROR; } - if (!verify_stringlist(parse_script, $2, verify_flag)) { - YYERROR; /* vf should call yyerror() */ + verify_flaglist($2); + if(!$2->count) { + strarray_add($2, ""); } $$ = new_command(ADDFLAG); $$->u.sl = $2; } - | REMOVEFLAG stringlist { if (!parse_script->support.imapflags) { - yyerror(parse_script, "imapflags MUST be enabled with \"require\""); + | REMOVEFLAG stringlist { if (!(parse_script->support.imapflags || + parse_script->support.imap4flags)) { + yyerror(parse_script, "imap4flags MUST be enabled with \"require\""); YYERROR; } - if (!verify_stringlist(parse_script, $2, verify_flag)) { - YYERROR; /* vf should call yyerror() */ + verify_flaglist($2); + if(!$2->count) { + strarray_add($2, ""); } $$ = new_command(REMOVEFLAG); $$->u.sl = $2; } @@ -557,6 +576,29 @@ } + + | HASFLAG htags stringlist + { + if (!parse_script->support.imap4flags) { + yyerror(parse_script, "imap4flags MUST be enabled with \"require\""); + YYERROR; + } + + $2 = canon_htags($2); +#ifdef ENABLE_REGEX + if ($2->comptag == REGEX) + { + if (!(verify_regexs(parse_script, $3, $2->comparator))) + { YYERROR; } + } +#endif + $$ = build_hasflag(HASFLAG, $2, $3); + if ($$ == NULL) { + yyerror(parse_script, "unable to find a compatible comparator"); + YYERROR; } + } + + | addrorenv aetags stringlist stringlist { if (($1 == ADDRESS) && @@ -629,6 +671,10 @@ { yyerror(parse_script, "date MUST be enabled with \"require\""); YYERROR; } + if ($2->index != 0) { + yyerror(parse_script, "index argument is not allowed in currentdate"); + YYERROR; } + if ($2->zonetag == ORIGINALZONE) { yyerror(parse_script, "originalzone argument is not allowed in currentdate"); YYERROR; } @@ -882,14 +928,41 @@ | UNDER { $$ = UNDER; } ; -copy: /* empty */ { $$ = 0; } - | COPY { if (!parse_script->support.copy) { +copy: COPY { if (!parse_script->support.copy) { yyerror(parse_script, "copy MUST be enabled with \"require\""); YYERROR; } - $$ = COPY; } + $$ = 1; } ; +ftags: /* empty */ { $$ = new_ftags(); } + | ftags copy { $$ = $1; + if ($$->copy) { + yyerror(parse_script, "duplicate copy tag"); YYERROR; } + else { $$->copy = $2; } } + | ftags FLAGS stringlist { if (!parse_script->support.imap4flags) { + yyerror(parse_script, "imap4flags MUST be enabled with \"require\""); + YYERROR; + } + $$ = $1; + if ($$->flags != NULL) { + yyerror(parse_script, "duplicate flags tag"); YYERROR; } + else { + verify_flaglist($3); + if(!$3->count) { + strarray_add($3, ""); + } + $$->flags = $3; } + } + ; + +rtags: /* empty */ { $$ = 0; } + | rtags copy { $$ = $1; + if ($$) { + yyerror(parse_script, "duplicate copy tag"); YYERROR; } + else { $$ = $2; } } + ; + testlist: '(' tests ')' { $$ = $2; } ; @@ -958,9 +1031,9 @@ static test_t *build_header(int t, struct htags *h, strarray_t *sl, strarray_t *pl) { - test_t *ret = new_test(t); /* can be HEADER */ + test_t *ret = new_test(t); /* can be HEADER or HASFLAG */ - assert(t == HEADER); + assert((t == HEADER) || (t == HASFLAG)); if (ret) { ret->u.h.index = h->index; @@ -974,6 +1047,12 @@ return ret; } +static test_t *build_hasflag(int t, struct htags *h, + strarray_t *pl) +{ + return build_header(t,h,NULL,pl); +} + static test_t *build_body(int t, struct btags *b, strarray_t *pl) { test_t *ret = new_test(t); /* can be BODY */ @@ -1044,14 +1123,29 @@ return ret; } -static commandlist_t *build_fileinto(int t, int copy, char *folder) +static commandlist_t *build_keep(int t, struct ftags *f) +{ + commandlist_t *ret = new_command(t); + + assert(t == KEEP); + + if (ret) { + ret->u.k.copy = f->copy; + ret->u.k.flags = f->flags; f->flags = NULL; + free_ftags(f); + } + return ret; +} + +static commandlist_t *build_fileinto(int t, struct ftags *f, char *folder) { commandlist_t *ret = new_command(t); assert(t == FILEINTO); if (ret) { - ret->u.f.copy = copy; + ret->u.f.copy = f->copy; + ret->u.f.flags = f->flags; f->flags = NULL; if (config_getswitch(IMAPOPT_SIEVE_UTF8FILEINTO)) { ret->u.f.folder = xmalloc(5 * strlen(folder) + 1); UTF8_to_mUTF7(ret->u.f.folder, folder); @@ -1060,6 +1154,7 @@ else { ret->u.f.folder = folder; } + free_ftags(f); } return ret; } @@ -1359,6 +1454,27 @@ free(d); } +static struct ftags *new_ftags(void) +{ + struct ftags *f = (struct ftags *) xmalloc(sizeof(struct ftags)); + + f->copy = 0; + f->flags = NULL; + + return f; +} + +static struct ftags *canon_ftags(struct ftags *f) +{ + return f; +} + +static void free_ftags(struct ftags *f) +{ + if (f->flags) { strarray_free(f->flags); } + free(f); +} + static int verify_stringlist(sieve_script_t *parse_script, strarray_t *sa, int (*verify)(sieve_script_t*, char *)) { int i; @@ -1533,32 +1649,6 @@ return -1; } - - - -static int verify_flag(sieve_script_t *parse_script, char *f) -{ - if (f[0] == '\\') { - lcase(f); - if (strcmp(f, "\\seen") && strcmp(f, "\\answered") && - strcmp(f, "\\flagged") && strcmp(f, "\\draft") && - strcmp(f, "\\deleted")) { - snprintf(parse_script->sieveerr, ERR_BUF_SIZE, - "flag '%s': not a system flag", f); - yyerror(parse_script, parse_script->sieveerr); - return 0; - } - return 1; - } - if (!imparse_isatom(f)) { - snprintf(parse_script->sieveerr, ERR_BUF_SIZE, - "flag '%s': not a valid keyword", f); - yyerror(parse_script, parse_script->sieveerr); - return 0; - } - return 1; -} - #ifdef ENABLE_REGEX static int verify_regex(sieve_script_t *parse_script, char *s, int cflags) {
View file
cyrus-imapd-2.5.tar.gz/sieve/sieve_interface.h
Changed
@@ -52,6 +52,7 @@ #include "strarray.h" #include "sieve/sieve_err.h" +#include "varlist.h" /* external sieve types */ typedef struct sieve_interp sieve_interp_t;
View file
cyrus-imapd-2.5.tar.gz/sieve/sieved.c
Changed
@@ -202,6 +202,7 @@ { int l,x,index; int opcode; + int has_index=0;/* used to differentiate between pre and post index tests */ opcode = ntohl(d[i].value); switch(opcode) { @@ -265,9 +266,31 @@ printf(")\n"); break; - case BC_ADDRESS:/*7*/ + case BC_ADDRESS:/*13*/ + has_index=1; + /*fall-through*/ + case BC_ADDRESS_PRE_INDEX:/*7*/ + if (BC_ADDRESS_PRE_INDEX == opcode) { + /* There was a version of the bytecode that had the index extension + * but did not update the bytecode codepoints, nor did it increment + * the bytecode version number. This tests if the index extension + * was in the bytecode based on the position of the match-type + * argument. + */ + switch (d[i+2].value) { + case B_IS: + case B_CONTAINS: + case B_MATCHES: + case B_REGEX: + case B_COUNT: + case B_VALUE: + has_index = 1; + default: + has_index = 0; + } + } printf("Address ["); - index = ntohl(d[++i].value); + index = has_index ? ntohl(d[++i].value) : 0; i=printComparison(d, i+1); printf(" type: "); switch(ntohl(d[i++].value)) @@ -307,9 +330,31 @@ i=write_list(ntohl(d[i].len), i+1, d); printf(" ]\n"); break; - case BC_HEADER:/*9*/ + case BC_HEADER:/*14*/ + has_index=1; + /*fall-through*/ + case BC_HEADER_PRE_INDEX:/*9*/ + if (BC_HEADER_PRE_INDEX == opcode) { + /* There was a version of the bytecode that had the index extension + * but did not update the bytecode codepoints, nor did it increment + * the bytecode version number. This tests if the index extension + * was in the bytecode based on the position of the match-type + * argument. + */ + switch (ntohl(d[i+2].value)) { + case B_IS: + case B_CONTAINS: + case B_MATCHES: + case B_REGEX: + case B_COUNT: + case B_VALUE: + has_index = 1; + default: + has_index = 0; + } + } printf("Header ["); - index = ntohl(d[++i].value); + index = has_index ? ntohl(d[++i].value) : 0; i= printComparison(d, i+1); if (index != 0) { printf(" Index: %d %s\n", @@ -321,6 +366,15 @@ i=write_list(ntohl(d[i].len), i+1, d); printf(" ]\n"); break; + case BC_HASFLAG:/*15*/ + printf("Hasflag ["); + i= printComparison(d, i+1); + printf(" Variables: "); + i=write_list(ntohl(d[i].len), i+1, d); + printf(" Data: "); + i=write_list(ntohl(d[i].len), i+1, d); + printf(" ]\n"); + break; case BC_BODY:/*10*/ printf("Body ["); i=printComparison(d, i+1); @@ -339,7 +393,47 @@ printf(" ]\n"); break; case BC_DATE:/*11*/ + has_index=1; case BC_CURRENTDATE:/*12*/ + if (BC_CURRENTDATE/*12*/ == opcode || BC_DATE/*11*/ == opcode) { + /* There was a version of the bytecode that had the index extension + * but did not update the bytecode codepoints, nor did it increment + * the bytecode version number. This tests if the index extension + * was in the bytecode based on the position of the match-type + * or comparator argument. This will correctly identify whether + * the index extension was supported in every case except the case + * of a timezone that is 61 minutes offset (since 61 corresponds to + * B_ORIGINALZONE). + * There was also an unnumbered version of BC_CURRENTDATE that did + * allow :index. This also covers that case. + */ + switch (ntohl(d[i+4].value)) { + case B_ASCIICASEMAP: + case B_OCTET: + case B_ASCIINUMERIC: + has_index = 0; + break; + default: + if (B_TIMEZONE == ntohl(d[i+1].value) && + B_ORIGINALZONE != ntohl(d[i+2].value)) { + switch (ntohl(d[i+3].value)) { + case B_IS: + case B_CONTAINS: + case B_MATCHES: + case B_REGEX: + case B_COUNT: + case B_VALUE: + has_index = 0; + break; + default: + has_index = 1; + + } + } else { + has_index = 1; + } + } + } ++i; /* skip opcode */ if (BC_DATE == opcode) { @@ -350,21 +444,18 @@ } /* index */ - index = ntohl(d[i++].value); - printf(" Index: %d %s\n", - abs(index), index < 0 ? "[LAST]" : ""); + index = has_index ? ntohl(d[i++].value) : 0; + if (index != 0) { + printf(" Index: %d %s\n", + abs(index), index < 0 ? "[LAST]" : ""); + } /* zone tag */ { - int zone; - int timezone_offset; - printf("Zone-Tag: "); - zone = ntohl(d[i++].value); - switch (zone) { + switch (ntohl(d[i++].value)) { case B_TIMEZONE: - timezone_offset = ntohl(d[i++].value); - printf("Specific timezone: offset by %d minutes.\n", timezone_offset); + printf("Specific timezone: offset by %d minutes.\n", ntohl(d[i++].value)); break; case B_ORIGINALZONE: printf("Original zone.\n"); @@ -442,7 +533,13 @@ printf("STOP\n"); break; - case B_KEEP:/*1*/ + case B_KEEP:/*22*/ + printf("KEEP FLAGS {%d}\n", ntohl(d[i].listlen)); + i=write_list(ntohl(d[i].listlen), i+1, d); + copy = ntohl(d[i++].value); + printf(" COPY(%d)\n",copy); + break; + case B_KEEP_ORIG:/*1*/ printf("KEEP\n"); break; @@ -455,12 +552,29 @@ printf("REJECT {%d}%s\n", len, data); break; - case B_FILEINTO: /*19*/ + case B_FILEINTO:/*23*/ + printf("FILEINTO FLAGS {%d}\n", ntohl(d[i].listlen)); + i=write_list(ntohl(d[i].listlen), i+1, d); + copy = ntohl(d[i++].value); + i = unwrap_string(d, i, &data, &len); + printf(" COPY(%d) FOLDER({%d}%s)\n", + copy, len, data); + break; + + case B_FILEINTO_COPY : /*19*/ copy = ntohl(d[i++].value); /* fall through */ case B_FILEINTO_ORIG: /*4*/ i = unwrap_string(d, i, &data, &len); + if(B_FILEINTO <= op) { + /* TODO: implement :flags parameter */ + printf("FILEINTO COPY(%d) FOLDER({%d}%s) FLAGS {%d}\n",copy,len,data,ntohl(d[i].listlen)); + i=write_list(ntohl(d[i].listlen), i+1, d); + } + else { printf("FILEINTO COPY(%d) FOLDER({%d}%s)\n",copy,len,data); + } + break; case B_REDIRECT: /*20*/
View file
cyrus-imapd-2.5.tar.gz/sieve/tests/actionExtensions/serverm/ueamail-imap4flags
Added
@@ -0,0 +1,11 @@ +MAIL FROM:<jsmith2+testing@andrew.cmu.edu> +RCPT TO:<jsmith2> +DATA +Date: Mon, 25 Feb 2002 08:51:06 -0500 +From: me +To: you +Subject: imap4flags + +hasflag test + +.
View file
cyrus-imapd-2.5.tar.gz/sieve/tests/actionExtensions/testm/ueatest-imap4flags
Added
@@ -0,0 +1,6 @@ +Date: Mon, 25 Feb 2002 08:51:06 -0500 +From: them +To: you +Subject: imap4flags + +imap4flags test message
View file
cyrus-imapd-2.5.tar.gz/sieve/tests/actionExtensions/uberExtensionActionScript.key
Changed
@@ -52,3 +52,15 @@ echo 'I'll respond in a minute, when i get back' | mail -s 'i'm out of the room' 'me@unspecified-domain' for message '/afs/andrew/system/src/local/cyrus/046/sieve/tests/actionExtensions/testm/ueatest-vacation' keep message + + +'ueatest-imap4flags' + +keeping message + with flags 'keepflag' +filing message into 'INBOX.fileinto.flags' + with flags 'fileinto' 'f2' +filing message into 'INBOX.fileinto.internalflags' + with flags 'existing' 'flag0' 'flag1' 'flag' 'here' +filing message into 'INBOX.fileinto.nullflags' +
View file
cyrus-imapd-2.5.tar.gz/sieve/tests/actionExtensions/uberExtensionActionScript.s
Changed
@@ -1,4 +1,6 @@ -require ["reject", "fileinto", "imapflags", "vacation", "notify", "vacation-seconds"]; +require ["reject", "fileinto", "imapflags", "vacation", "notify", + "vacation-seconds", "copy", "imap4flags", "relational", + "comparator-i;ascii-numeric"]; #this is for the extra thigns we have added to sieve @@ -45,6 +47,23 @@ if header :contains "subject" "rflag" {removeflag "\\answered";} +#IMAP4FLAGS# +############################################## +if header :contains "subject" "imap4flags" +{ +setflag "existing"; +keep :flags "keepflag"; +fileinto :flags ["fileinto f2"] "INBOX.fileinto.flags"; + +addflag ["flag0", "flag1"]; +addflag ["my flag is here"]; +removeflag ["is my"]; + +fileinto "INBOX.fileinto.internalflags"; +fileinto :flags "" "INBOX.fileinto.nullflags"; + +} + #VACATION ############################################# if header :contains "subject" "vacation" @@ -83,3 +102,4 @@ if header :contains "subject" "denotify" {denotify;} +
View file
cyrus-imapd-2.5.tar.gz/sieve/tests/testExtension/serverm/uetmail-hasflag
Added
@@ -0,0 +1,11 @@ +MAIL FROM:<jsmith2+testing@andrew.cmu.edu> +RCPT TO:<jsmith2> +DATA +Date: Mon, 25 Feb 2002 08:51:06 -0500 +From: me +To: you +Subject: imap4flags + +hasflag test + +.
View file
cyrus-imapd-2.5.tar.gz/sieve/tests/testExtension/testm/uetest-hasflag
Added
@@ -0,0 +1,7 @@ +Date: Mon, 25 Feb 2002 08:51:06 -0500 +From: me +To: you +Subject: imap4flags + +hasflag test +
View file
cyrus-imapd-2.5.tar.gz/sieve/tests/testExtension/uberExtensionTestScript.key
Changed
@@ -67,3 +67,17 @@ 'me+goodedetailmatches@blah.com' +uetest-hasflag should be redirected to: + + 'me+good.hasflag.count.lt.1.pos@blah.com' + 'me+good.hasflag.count.le.0.pos@blah.com' + 'me+good.hasflag.count.le.2.pos@blah.com' + 'me+good.hasflag.count.lt.0.neg@blah.com' + 'me+good.hasflag.count.ge.1.neg@blah.com' + 'me+good.hasflag.count.lt.2.neg@blah.com' + 'me+good.hasflag.contains.pos@blah.com' + 'me+good.hasflag.contains.null.pos@blah.com' + 'me+good.hasflag.null.neg@blah.com' + 'me+good.hasflag.contains.neg@blah.com' + 'me+good.hasflag.neg@blah.com' +
View file
cyrus-imapd-2.5.tar.gz/sieve/tests/testExtension/uberExtensionTestScript.s
Changed
@@ -1,4 +1,5 @@ -require ["regex", "relational", "comparator-i;ascii-numeric", "subaddress", "envelope", "date", "index"]; +require ["regex", "relational", "comparator-i;ascii-numeric", "subaddress", + "envelope", "date", "index", "imap4flags"]; #this is for the extra thigns we have added to sieve #test extensions @@ -179,3 +180,91 @@ currentdate :value "lt" "date" "2015-01-01") {redirect "me+cd2014@blah.com";} +###################################################################### +#HASFLAG +###################################################################### + +if header :contains "subject" "imap4flags" +{ + +# +# Positive :count tests +# +setflag ""; + +if hasflag :count "lt" :comparator "i;ascii-numeric" ["1"] +{redirect "me+good.hasflag.count.lt.1.pos@blah.com";} +else +{redirect "me+bad.hasflag.count.lt.1.pos@blah.com";} + +if hasflag :count "le" :comparator "i;ascii-numeric" ["0"] +{redirect "me+good.hasflag.count.le.0.pos@blah.com";} +else +{redirect "me+bad.hasflag.count.le.0.pos@blah.com";} + +setflag "flag1 flag2"; + +if hasflag :count "le" :comparator "i;ascii-numeric" ["2"] +{redirect "me+good.hasflag.count.le.2.pos@blah.com";} +else +{redirect "me+bad.hasflag.count.le.2.pos@blah.com";} + +# +# Negative :count tests +# +setflag ""; + +if hasflag :count "lt" :comparator "i;ascii-numeric" ["0"] +{redirect "me+bad.hasflag.count.lt.0.neg@blah.com";} +else +{redirect "me+good.hasflag.count.lt.0.neg@blah.com";} + +if hasflag :count "ge" :comparator "i;ascii-numeric" ["1"] +{redirect "me+bad.hasflag.count.ge.1.neg@blah.com";} +else +{redirect "me+good.hasflag.count.ge.1.neg@blah.com";} + +setflag "flag1 flag2"; + +if hasflag :count "lt" :comparator "i;ascii-numeric" ["2"] +{redirect "me+bad.hasflag.count.lt.2.neg@blah.com";} +else +{redirect "me+good.hasflag.count.lt.2.neg@blah.com";} + +# +# Positive tests +# +setflag "there"; + +if hasflag :contains ["myflag", "here"] +{redirect "me+good.hasflag.contains.pos@blah.com";} +else +{redirect "me+bad.hasflag.contains.pos@blah.com";} + +if hasflag :contains "" +{redirect "me+good.hasflag.contains.null.pos@blah.com";} +else +{redirect "me+bad.hasflag.contains.null.pos@blah.com";} + +# +# Negative tests +# +setflag "flag"; + +if hasflag "" +{redirect "me+bad.hasflag.null.neg@blah.com";} +else +{redirect "me+good.hasflag.null.neg@blah.com";} + +if hasflag :contains "flags" +{redirect "me+bad.hasflag.contains.neg@blah.com";} +else +{redirect "me+good.hasflag.contains.neg@blah.com";} + +if hasflag "lag" +{redirect "me+bad.hasflag.neg@blah.com";} +else +{redirect "me+good.hasflag.neg@blah.com";} + +} +
View file
cyrus-imapd-2.5.tar.gz/sieve/tree.c
Changed
@@ -137,6 +137,7 @@ case STRUE: break; + case HASFLAG: case HEADER: free(t->u.h.comparator); strarray_free(t->u.h.sl); @@ -192,6 +193,7 @@ case FILEINTO: if (cl->u.f.folder) free(cl->u.f.folder); + if (cl->u.f.flags) strarray_free(cl->u.f.flags); break; case REDIRECT: @@ -215,6 +217,9 @@ break; case KEEP: + if (cl->u.k.flags) strarray_free(cl->u.k.flags); + break; + case STOP: case DISCARD: case RETURN:
View file
cyrus-imapd-2.5.tar.gz/sieve/tree.h
Changed
@@ -69,7 +69,7 @@ union { testlist_t *tl; /* anyof, allof */ strarray_t *sl; /* exists */ - struct { /* it's a header test */ + struct { /* it's a header or hasflag test */ int index; int comptag; char * comparator; @@ -138,10 +138,14 @@ int optional; char *script; } inc; + struct { /* it's a keep action */ + int copy; + strarray_t *flags; + } k; struct { /* it's a fileinto action */ char *folder; int copy; - /* add strarray_t for imap4flags */ + strarray_t *flags; } f; struct { /* it's a redirect action */ char *address;
View file
cyrus-imapd-2.5.tar.gz/sieve/varlist.c
Added
@@ -0,0 +1,79 @@ +/* + * varlist.c + * + * Created on: Sep 29, 2014 + * Author: James Cassell + */ + +#include "varlist.h" +#include <memory.h> +#include "xmalloc.h" + + +EXPORTED variable_list_t *varlist_new(void) { + variable_list_t *vl; + vl = xzmalloc(sizeof(variable_list_t)); + vl->var = strarray_new(); + return vl; +} + +EXPORTED variable_list_t *varlist_select(variable_list_t *vl, const char *name) { + if (!vl) { + return NULL; + } + if ((!name && !vl->name) || (!(name && vl->name) && strcmp(name, vl->name))) { + return vl; + } + return varlist_select(vl->next, name); +} + +EXPORTED variable_list_t *varlist_end(variable_list_t *vl) { + if (!vl) { + return NULL; + } + if (!vl->next) { + return vl; + } + return varlist_end(vl->next); +} + + +EXPORTED variable_list_t *varlist_extend(variable_list_t *vl) { + if (!vl) { + return NULL; + } + return (varlist_end(vl))->next = varlist_new(); +} + +EXPORTED void varlist_fini(variable_list_t *vl) { + if (!vl) { + return; + } + if (vl->name) { + free(vl->name); + vl->name = NULL; + } + if (vl->var) { + strarray_fini(vl->var); + vl->var = NULL; + } + varlist_free(vl->next); + vl->next = NULL; +} + +EXPORTED void varlist_free(variable_list_t *vl) { + variable_list_t *next = NULL; + if (!vl) { + return; + } + next = vl->next; + if (vl->name) { + free(vl->name); + } + if (vl->var) { + strarray_free(vl->var); + } + free(vl); + varlist_free(next); +} +
View file
cyrus-imapd-2.5.tar.gz/sieve/varlist.h
Added
@@ -0,0 +1,32 @@ +/* + * varlist.h + * + * Created on: Sep 29, 2014 + * Author: James Cassell + */ + +#ifndef VARLIST_H_ +#define VARLIST_H_ + +#include "strarray.h" + +typedef struct variable_list { + char *name; + strarray_t *var; + struct variable_list *next; +} variable_list_t; + +#define VARIABLE_LIST_INITIALIZER {NULL, NULL, NULL} + +EXPORTED variable_list_t *varlist_new(void); + +EXPORTED void varlist_fini(variable_list_t *vl); + +EXPORTED void varlist_free(variable_list_t *vl); + +EXPORTED variable_list_t *varlist_extend(variable_list_t *vl); + +EXPORTED variable_list_t *varlist_select(variable_list_t *vl, const char *name); +EXPORTED variable_list_t *varlist_end(variable_list_t *vl); + +#endif /* VARLIST_H_ */
View file
cyrus-imapd-2.5.tar.gz/tools/vzic
Added
+(directory)
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/ChangeLog
Added
@@ -0,0 +1,57 @@ +2006-03-18 Damon Chaplin <damon@gnome.org> + + * Released Vzic 1.3 + +2006-03-18 Damon Chaplin <damon@gnome.org> + + * vzic-output.c (expand_tzname): added special case for America/Nome. + (output_rrule): made hacks a bit more general, to handle Asia/Gaza + which now has a day=4 rule. At some point we should check what newer + versions of Outlook can handle so we can be more accurate. + + * vzic-dump.c (dump_time_zone_names): try looking for timezone info + using original and linked name. + + * README, *.c: fixed spelling 'compatable' -> 'compatible'. + + * vzic.c: patch from Jonathan Guthrie to support a --olson-dir option. + +2003-10-25 Damon Chaplin <damon@gnome.org> + + * Released Vzic 1.2 + +2003-10-25 Damon Chaplin <damon@gnome.org> + + * vzic-output.c: + * Makefile: moved the PRODUCT_ID and TZID_PREFIX settings to the + Makefile and changed the default so people don't accidentally use + the same IDs as Evolution. + + * vzic-parse.c (parse_time): substitute 23:59:59 when we read a time + of 24:00:00. This is a bit of a kludge to avoid problems, since + 24:00:00 is not a valid iCalendar time. Since 24:00:00 is only used + for a few timezones in the 1930s it doesn't matter too much. + + To write a correct fix we'd need to review all the code that deals + with times to see if it would be affected, e.g. a time of 24:00 on + one day should be considered equal to 0:00 the next day. + + We'd also need to adjust the output times to use 0:00 the next day + rather than 24:00. If we need to output recurrence rules that would + be a problem, since 'last saturday at 24:00' can't be easily + converted to another rule that uses 0:00 instead. + +2003-10-22 Damon Chaplin <damon@gnome.org> + + * Released Vzic 1.1 + +2003-10-22 Damon Chaplin <damon@gnome.org> + + * vzic-parse.c (parse_time): allow a time of 24:00, as used in + the America/Montreal and America/Toronto rules in the 1930s! + I'm not 100% sure the rest of the code will handle this OK, but + it only affects the 'pure' output. + +2003-09-01 Damon Chaplin <damon@gnome.org> + + * Released Vzic 1.0
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/Makefile
Added
@@ -0,0 +1,90 @@ + +# +# You will need to set this to the directory that the Olson timezone data +# files are in. +# +OLSON_DIR = . + + +# This is used as the PRODID property on the iCalendar files output. +# It identifies the product which created the iCalendar objects. +# So you need to substitute your own organization name and product. +PRODUCT_ID = -//CyrusIMAP.org//Cyrus %s//EN + +# This is what libical-evolution uses. +#PRODUCT_ID = -//Ximian//NONSGML Evolution Olson-VTIMEZONE Converter//EN + + +# This is used to create unique IDs for each VTIMEZONE component. +# The prefix is put before each timezone city name. It should start and end +# with a '/'. The first part, i.e. 'myorganization.org' below, should be +# a unique vendor ID, e.g. use a hostname. The part after that can be +# anything you want. We use a date and version number for libical. The %D +# gets expanded to today's date. There is also a vzic-merge.pl which can be +# used to merge changes into a master set of VTIMEZONEs. If a VTIMEZONE has +# changed, it bumps the version number on the end of this prefix. */ +#TZID_PREFIX = /myorganization.org/%D_1/ +TZID_PREFIX = + +# This is what libical-evolution uses. +#TZID_PREFIX = /softwarestudio.org/Olson_%D_1/ + + +# Set any -I include directories to find the libical header files, and the +# libical library to link with. You only need these if you want to run the +# tests. You may need to change the '#include <ical.h>' line at the top of +# test-vzic.c as well. +LIBICAL_CFLAGS = +LIBICAL_LDADD = -lical + + +# +# You shouldn't need to change the rest of the file. +# + +GLIB_CFLAGS = `pkg-config --cflags glib-2.0` +GLIB_LDADD = `pkg-config --libs glib-2.0` + +CFLAGS = -g -I../.. -DOLSON_DIR=\"$(OLSON_DIR)\" -DPRODUCT_ID='"$(PRODUCT_ID)"' -DTZID_PREFIX='"$(TZID_PREFIX)"' $(GLIB_CFLAGS) $(LIBICAL_CFLAGS) + +OBJECTS = vzic.o vzic-parse.o vzic-dump.o vzic-output.o + +all: vzic + +vzic: $(OBJECTS) + $(CC) $(OBJECTS) $(GLIB_LDADD) -o vzic + +test-vzic: test-vzic.o + $(CC) test-vzic.o $(LIBICAL_LDADD) -o test-vzic + +# Dependencies. +$(OBJECTS): vzic.h +vzic.o vzic-parse.o: vzic-parse.h +vzic.o vzic-dump.o: vzic-dump.h +vzic.o vzic-output.o: vzic-output.h + +test-parse: vzic + ./vzic-dump.pl $(OLSON_DIR) + ./vzic --dump --pure + @echo + @echo "#" + @echo "# If either of these diff commands outputs anything there may be a problem." + @echo "#" + diff -ru zoneinfo/ZonesPerl zoneinfo/ZonesVzic + diff -ru zoneinfo/RulesPerl zoneinfo/RulesVzic + +test-changes: vzic test-vzic + ./test-vzic --dump-changes + ./vzic --dump-changes --pure + @echo + @echo "#" + @echo "# If this diff command outputs anything there may be a problem." + @echo "#" + diff -ru zoneinfo/ChangesVzic test-output + +clean: + -rm -rf vzic $(OBJECTS) *~ ChangesVzic RulesVzic ZonesVzic RulesPerl ZonesPerl test-vzic test-vzic.o + +.PHONY: clean perl-dump test-parse + +
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/README
Added
@@ -0,0 +1,202 @@ + + +VZIC README +=========== + +This is 'vzic', a program to convert the Olson timezone database files into +VTIMEZONE files compatible with the iCalendar specification (RFC2445). + +(The name is based on the 'zic' program which converts the Olson files into +time zone information files used by several Unix C libraries, including +glibc. See zic(8) and tzfile(5).) + + + +REQUIREMENTS +============ + +You need the Olson timezone database files, which can be found at: + + ftp://elsie.nci.nih.gov/pub/ + + (Old versions can be found at ftp://munnari.oz.au/pub/oldtz/) + + +Vzic also uses the GLib library (for hash tables, dynamic arrays, and date +calculations). You need version 2.0 or higher. You can get this from: + + http://www.gtk.org + + + +BUILDING +======== + +Edit the Makefile to set the OLSON_DIR, PRODUCT_ID and TZID_PREFIX variables. + +Then run 'make'. + + + +RUNNING +======= + +Run 'vzic'. + +The output is placed in the zoneinfo subdirectory by default, +but you can use the --output-dir options to set another toplevel output +directory. + +By default it outputs VTIMEZONEs that try to be compatible with Outlook +(2000, at least). Outlook can't handle certain iCalendar constructs in +VTIMEZONEs, such as RRULEs using BYMONTHDAY, so it has to adjust the RRULEs +slightly to get Outlook to parse them. Unfortunately this means they are +slightly wrong. If given the --pure option, vzic outputs the exact data, +without worrying about compatability. + +NOTE: We don't convert all the Olson files. We skip 'backward', 'etcetera', +'leapseconds', 'pacificnew', 'solar87', 'solar88' and 'solar89', 'factory' +and 'systemv', since these don't really provide any useful timezones. +See vzic.c. + + + +MERGING CHANGES INTO A MASTER SET OF VTIMEZONES +=============================================== + +The Olson timezone files are updated fairly often, so we need to build new +sets of VTIMEZONE files. Though we have to be careful to ensure that the TZID +of updated timezones is also updated, since it must remain unique. + +We use a version number on the end of the TZID prefix (see the TZIDPrefix +variable in vzic-output.c) to ensure this uniqueness. + +But we don't want to update the version numbers of VTIMEZONEs which have not +changed. So we use the vzic-merge.pl Perl script. This merges in the new set +of VTIMEZONEs with a 'master' set. It compares each new VTIMEZONE file with +the one in the master set (ignoring changes to the TZID). If the new +VTIMEZONE file is different, it copies it to the master set and sets the +version number to the old VTIMEZONE's version number + 1. + +To use vzic-merge.pl you must change the $MASTER_ZONEINFO_DIR and +$NEW_ZONEINFO_DIR variables at the top of the file to point to your 2 sets of +VTIMEZONEs. You then just run the script. (I recommend you keep a backup of +the old master VTIMEZONE files, and use diff to compare the new master set +with the old one, in case anything goes wrong.) + +You must merge in changes to the zones.tab file by hand. + +Note that some timezones are renamed or removed occasionally, so applications +should be able to cope with this. + + + +COMPATABILITY NOTES +=================== + +It seems that Microsoft Outlook is very picky about the iCalendar files it +will accept. (I've been testing with Outlook 2000. I hope the other versions +are no worse.) Here's a few problems we've had with the VTIMEZONEs: + + o Outlook doesn't like any years before 1600. We were using '1st Jan 0001' + in all VTIMEZONEs to specify the first UTC offset known for the timezone. + (The Olson data does not give a start date for this.) + + Now we just skip this first component for most timezones. The UTC offset + can still be found from the TZOFFSETFROM property of the first component. + + Though some timezones only specify one UTC offset that applies forever, + so in these cases we output '1st Jan 1970' (Indian/Cocos, + Pacific/Johnston). + + o Outlook doesn't like the BYMONTHDAY specifier in RRULEs. + + We have changed most of the VTIMEZONEs to use things like 'BYDAY=2SU' + rather than 'BYMONTHDAY=8,9,10,11,12,13,14;BYDAY=SU', though some of + them were impossible to convert correctly so they are not always correct. + + o Outlook doesn't like TZOFFSETFROM/TZOFFSETTO properties which include a + seconds component, e.g. 'TZOFFSETFROM:+110628'. + Quite a lot of the Olson timezones include seconds in their UTC offsets, + though no timezones currently have a UTC offset that uses the seconds + value. + + We've rounded all UTC offsets to the nearest minute. Since all timezone + offsets currently used have '00' as the seconds offset, this doesn't lose + us much. + + o Outlook doesn't like lines being split in certain places, even though + the iCalendar spec says they can be split anywhere. + + o Outlook can only handle one RDATE or a pair of RRULEs. So we had to remove + all historical data. + + +TESTING +======= + +Do a 'make test-vic', then run ./test-vic. + +The test-vzic program compares our libical code and VTIMEZONE data against +the Unix functions like mktime(). It steps over a period of time (1970-2037) +converting from UTC to a given timezone and back again every 15 minutes. +Any differences are output into the test-output directory. + +The output matches for all of the timezones, except in a few places where the +result can't be determined. So I think we can be fairly confident that the +VTIMEZONEs are correct. + +Note that you must use the same Olson data in libical that the OS is using +for mktime() etc. For example, I am using RedHat 9 which uses tzdata2002d, +so I converted this to VTIMEZONE files and installed it into the libical +timezone data directory before testing. (You need to use '--pure' when +creating the VTIMEZONE files as well.) + + +Testing the Parsing Code +------------------------ + +Run 'make test-parse'. + +This runs 'vzic --dump' and 'perl-dump' and compares the output. The diff +commands should not produce any output. + +'vzic --dump' dumps all the parsed data out in the original Olson format, +but without comments. The files are written into the ZonesVzic and RulesVzic +subdirectories of the zoneinfo directory. + +'make perl-dump' runs the vzic-dump.pl perl script which outputs the files +in the same format as 'vzic --dump' in the ZonesPerl and RulesPerl +subdirectories. The perl script doesn't actually parse the fields; it only +strips comments and massages the fields so we have the same output format. + +Currently they both produce exactly the same output so we know the parsing +code is OK. + + +Testing the VTIMEZONE Files +--------------------------- + +Run 'make test-changes'. + +This runs 'vzic --dump-changes' and 'test-vzic --dump-changes' and compares +the output. The diff command should not produce any output. + +Both commands output timezone changes for each zone up to a specific year +(2030) into files for each timezone. It outputs the timezone changes in a +list in this format: + + Timezone Name Date and Time of Change in UTC New Offset from UTC + + America/Dawson 26 Oct 1986 2:00:00 -0800 + +Unfortunately there are some differences here, but they all happen before +1970 so it doesn't matter too much. It looks like the libical code has +problems determining things like 'last Sunday of the month' before 1970. +This is because it uses mktime() etc. which can't really handle dates +before 1970. + + + +Damon Chaplin <damon@gnome.org>, 25 Oct 2003. +
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/test-vzic.c
Added
@@ -0,0 +1,422 @@ +/* + * Vzic - a program to convert Olson timezone database files into VZTIMEZONE + * files compatible with the iCalendar specification (RFC2445). + * + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2003 Damon Chaplin. + * + * Author: Damon Chaplin <damon@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +/* + * test-vzic.c - test vzic + libical against mktime() and friends. + * + * Note that when we output VCALENDAR data compatible with Outlook the + * results aren't all correct. + * + * We have to modify some RRULEs which makes these timezones incorrect: + * + * Africa/Cairo + * America/Godthab + * America/Santiago + * Antarctica/Palmer + * Asia/Baghdad + * Asia/Damascus + * Asia/Jerusalem + * + * Also, we can only output one RDATE or a pair of RRULEs which may make some + * other timezones incorrect sometimes (e.g. if they change). + */ + +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> + +#include <ical.h> +/*#include <evolution/ical.h>*/ + +#define CHANGES_MAX_YEAR 2030 + +/* These are the years between which we test against the Unix timezone + functions, inclusive. When using 'vzic --pure' you can test the full + range from 1970 to 2037 and it should match against mktime() etc. + (assuming you are using the same Olson timezone data for both). + + But when using VTIMEZONE's that are compatible with Outlook, it is only + worth testing times in the future. There will be lots of differences in + the past, since we can't include any historical changes in the files. */ +#if 1 +#define DUMP_START_YEAR 2003 +#define DUMP_END_YEAR 2038 +#else +#define DUMP_START_YEAR 1970 +#define DUMP_END_YEAR 2038 +#endif + +/* The maximum size of any complete pathname. */ +#define PATHNAME_BUFFER_SIZE 1024 + +#ifndef FALSE +#define FALSE (0) +#endif + +#ifndef TRUE +#define TRUE (!FALSE) +#endif + +int VzicDumpChanges = FALSE; + +/* We output beneath the current directory for now. */ +char *directory = "test-output"; + +static void usage (void); +static int parse_zone_name (char *name, + char **directory, + char **subdirectory, + char **filename); +static void ensure_directory_exists (char *directory); +static void dump_local_times (icaltimezone *zone, + FILE *fp); + + +int main(int argc, char* argv[]) +{ + icalarray *zones; + icaltimezone *zone; + char *zone_directory, *zone_subdirectory, *zone_filename, *location; + char output_directory[PATHNAME_BUFFER_SIZE]; + char filename[PATHNAME_BUFFER_SIZE]; + FILE *fp; + int i; + int skipping = TRUE; + + /* + * Command-Line Option Parsing. + */ + for (i = 1; i < argc; i++) { + /* --dump-changes: Dumps a list of times when each timezone changed, + and the new local time offset from UTC. */ + if (!strcmp (argv[i], "--dump-changes")) + VzicDumpChanges = TRUE; + + else + usage (); + } + + + zones = icaltimezone_get_builtin_timezones (); + + ensure_directory_exists (directory); + + for (i = 0; i < zones->num_elements; i++) { + zone = icalarray_element_at (zones, i); + + location = icaltimezone_get_location (zone); + +#if 0 + /* Use this to start at a certain zone. */ + if (skipping && strcmp (location, "America/Boise")) + continue; +#endif + + skipping = FALSE; + + /* Use this to only output data for certain timezones. */ +#if 0 + if (strcmp (location, "America/Cancun") + && strcmp (location, "Asia/Baku") + && strcmp (location, "Asia/Nicosia") + && strcmp (location, "Asia/Novosibirsk") + && strcmp (location, "Asia/Samarkand") + && strcmp (location, "Asia/Tashkent") + && strcmp (location, "Asia/Tbilisi") + && strcmp (location, "Asia/Yerevan") + && strcmp (location, "Australia/Broken_Hill") + && strcmp (location, "Europe/Simferopol") + && strcmp (location, "Europe/Tallinn") + && strcmp (location, "Europe/Zaporozhye") + ) + continue; +#endif + +#if 0 + printf ("%s\n", location); +#endif + + parse_zone_name (location, &zone_directory, &zone_subdirectory, + &zone_filename); + + sprintf (output_directory, "%s/%s", directory, zone_directory); + ensure_directory_exists (output_directory); + sprintf (filename, "%s/%s", output_directory, zone_filename); + + if (zone_subdirectory) { + sprintf (output_directory, "%s/%s/%s", directory, zone_directory, + zone_subdirectory); + ensure_directory_exists (output_directory); + sprintf (filename, "%s/%s", output_directory, zone_filename); + } + + fp = fopen (filename, "w"); + if (!fp) { + fprintf (stderr, "Couldn't create file: %s\n", filename); + exit (1); + } + + /* We can run 2 different tests - output all changes for each zone, or + test against mktime()/localtime(). Should have a command-line option + or something. */ + if (VzicDumpChanges) + icaltimezone_dump_changes (zone, CHANGES_MAX_YEAR, fp); + else + dump_local_times (zone, fp); + + if (ferror (fp)) { + fprintf (stderr, "Error writing file: %s\n", filename); + exit (1); + } + + fclose (fp); + } + + return 0; +} + + +static void +usage (void) +{ + fprintf (stderr, "Usage: test-vzic [--dump-changes]\n"); + + exit (1); +} + + +/* This checks that the Zone name only uses the characters in [-+_/a-zA-Z0-9], + and outputs a warning if it isn't. */ +static int +parse_zone_name (char *name, + char **directory, + char **subdirectory, + char **filename) +{ + static int invalid_zone_num = 1; + + char *p, ch, *first_slash_pos = NULL, *second_slash_pos = NULL; + int invalid = FALSE; + + for (p = name; (ch = *p) != 0; p++) { + if ((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z') + && (ch < '0' || ch > '9') && ch != '/' && ch != '_' + && ch != '-' && ch != '+') { + fprintf (stderr, "Warning: Unusual Zone name: %s\n", name); + invalid = TRUE; + break; + } + + if (ch == '/') { + if (!first_slash_pos) { + first_slash_pos = p; + } else if (!second_slash_pos) { + second_slash_pos = p; + } else { + fprintf (stderr, "Warning: More than 2 '/' characters in Zone name: %s\n", name); + invalid = TRUE; + break; + } + } + } + + if (!first_slash_pos) { + fprintf (stderr, "No '/' character in Zone name: %s. Skipping.\n", name); + return FALSE; + } + + if (invalid) { + fprintf (stderr, "Invalid zone name: %s\n", name); + exit (0); + } else { + *first_slash_pos = '\0'; + *directory = icalmemory_strdup (name); + *first_slash_pos = '/'; + + if (second_slash_pos) { + *second_slash_pos = '\0'; + *subdirectory = icalmemory_strdup (first_slash_pos + 1); + *second_slash_pos = '/'; + + *filename = icalmemory_strdup (second_slash_pos + 1); + } else { + *subdirectory = NULL; + *filename = icalmemory_strdup (first_slash_pos + 1); + } + } +} + + +static void +ensure_directory_exists (char *directory) +{ + struct stat filestat; + + if (stat (directory, &filestat) != 0) { + /* If the directory doesn't exist, try to create it. */ + if (errno == ENOENT) { + if (mkdir (directory, 0777) != 0) { + fprintf (stderr, "Can't create directory: %s\n", directory); + exit (1); + } + } else { + fprintf (stderr, "Error calling stat() on directory: %s\n", directory); + exit (1); + } + } else if (!S_ISDIR (filestat.st_mode)) { + fprintf (stderr, "Can't create directory, already exists: %s\n", + directory); + exit (1); + } +} + + +static void +dump_local_times (icaltimezone *zone, FILE *fp) +{ + static char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + icaltimezone *utc_timezone; + struct icaltimetype tt, tt_copy; + struct tm tm, local_tm; + time_t t; + char tzstring[256], *location; + int last_year_output = 0; + int total_error = 0, total_error2 = 0; + + utc_timezone = icaltimezone_get_utc_timezone (); + + /* This is our UTC time that we will use to iterate over the period. */ + tt.year = DUMP_START_YEAR; + tt.month = 1; + tt.day = 1; + tt.hour = 0; + tt.minute = 0; + tt.second = 0; + tt.is_utc = 0; + tt.is_date = 0; + tt.zone = ""; + + tm.tm_year = tt.year - 1900; + tm.tm_mon = tt.month - 1; + tm.tm_mday = tt.day; + tm.tm_hour = tt.hour; + tm.tm_min = tt.minute; + tm.tm_sec = tt.second; + tm.tm_isdst = -1; + + /* Convert it to a time_t by saying it is in UTC. */ + putenv ("TZ=UTC"); + t = mktime (&tm); + + location = icaltimezone_get_location (zone); + sprintf (tzstring, "TZ=%s", location); + + /*printf ("Zone: %s\n", location);*/ + putenv (tzstring); + + /* Loop around converting the UTC time to local time, outputting it, and + then adding on 15 minutes to the UTC time. */ + while (tt.year <= DUMP_END_YEAR) { + if (tt.year > last_year_output) { + last_year_output = tt.year; +#if 0 + printf (" %i\n", last_year_output); + fprintf (fp, " %i\n", last_year_output); +#endif + } + +#if 1 + /* First use the Unix functions. */ + /* Now convert it to a local time in the given timezone. */ + local_tm = *localtime (&t); +#endif + +#if 1 + /* Now use libical. */ + tt_copy = tt; + icaltimezone_convert_time (&tt_copy, utc_timezone, zone); +#endif + +#if 1 + if (local_tm.tm_year + 1900 != tt_copy.year + || local_tm.tm_mon + 1 != tt_copy.month + || local_tm.tm_mday != tt_copy.day + || local_tm.tm_hour != tt_copy.hour + || local_tm.tm_min != tt_copy.minute + || local_tm.tm_sec != tt_copy.second) { + + /* The error format is: + + ERROR: Original-UTC-Time Local-Time-From-mktime Local-Time-From-Libical + + */ + + total_error++; + + fprintf (fp, "ERROR:%2i %s %04i %2i:%02i:%02i UTC", + tt.day, months[tt.month - 1], tt.year, + tt.hour, tt.minute, tt.second); + fprintf (fp, " ->%2i %s %04i %2i:%02i:%02i", + local_tm.tm_mday, months[local_tm.tm_mon], + local_tm.tm_year + 1900, + local_tm.tm_hour, local_tm.tm_min, local_tm.tm_sec); + fprintf (fp, " Us:%2i %s %04i %2i:%02i:%02i\n", + tt_copy.day, months[tt_copy.month - 1], tt_copy.year, + tt_copy.hour, tt_copy.minute, tt_copy.second); + } +#endif + + /* Now convert it back, and check we get the original time. */ + icaltimezone_convert_time (&tt_copy, zone, utc_timezone); + if (tt.year != tt_copy.year + || tt.month != tt_copy.month + || tt.day != tt_copy.day + || tt.hour != tt_copy.hour + || tt.minute != tt_copy.minute + || tt.second != tt_copy.second) { + + total_error2++; + + fprintf (fp, "ERROR 2: %2i %s %04i %2i:%02i:%02i UTC", + tt.day, months[tt.month - 1], tt.year, + tt.hour, tt.minute, tt.second); + fprintf (fp, " Us:%2i %s %04i %2i:%02i:%02i UTC\n", + tt_copy.day, months[tt_copy.month - 1], tt_copy.year, + tt_copy.hour, tt_copy.minute, tt_copy.second); + } + + + /* Increment the time. */ + icaltime_adjust (&tt, 0, 0, 15, 0); + + /* We assume leap seconds are not included in time_t values, which should + be true on POSIX systems. */ + t += 15 * 60; + } + + printf ("Zone: %40s Errors: %i (%i)\n", icaltimezone_get_location (zone), + total_error, total_error2); +}
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic-dump.c
Added
@@ -0,0 +1,409 @@ +/* + * Vzic - a program to convert Olson timezone database files into VZTIMEZONE + * files compatible with the iCalendar specification (RFC2445). + * + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2003 Damon Chaplin. + * + * Author: Damon Chaplin <damon@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +/* + * These functions are for dumping all the parsed Zones and Rules to + * files, to be compared with the output of vzic-dump.pl to check our parsing + * code is OK. Some of the functions are also used for producing debugging + * output. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "vzic.h" +#include "vzic-dump.h" + + +static void dump_add_rule (char *name, + GArray *rule_array, + GPtrArray *name_array); +static int dump_compare_strings (const void *arg1, + const void *arg2); + + +void +dump_zone_data (GArray *zone_data, + char *filename) +{ + static char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + FILE *fp; + ZoneData *zone; + ZoneLineData *zone_line; + int i, j; + gboolean output_month, output_day, output_time; + + fp = fopen (filename, "w"); + if (!fp) { + fprintf (stderr, "Couldn't create file: %s\n", filename); + exit (1); + } + + for (i = 0; i < zone_data->len; i++) { + zone = &g_array_index (zone_data, ZoneData, i); + + fprintf (fp, "Zone\t%s\t", zone->zone_name); + + for (j = 0; j < zone->zone_line_data->len; j++) { + zone_line = &g_array_index (zone->zone_line_data, ZoneLineData, j); + + if (j != 0) + fprintf (fp, "\t\t\t"); + + fprintf (fp, "%s\t", dump_time (zone_line->stdoff_seconds, TIME_WALL, + FALSE)); + + if (zone_line->rules) + fprintf (fp, "%s\t", zone_line->rules); + else if (zone_line->save_seconds != 0) + fprintf (fp, "%s\t", dump_time (zone_line->save_seconds, TIME_WALL, + FALSE)); + else + fprintf (fp, "-\t"); + + fprintf (fp, "%s\t", zone_line->format ? zone_line->format : "-"); + + if (zone_line->until_set) { + fprintf (fp, "%s\t", dump_year (zone_line->until_year)); + + output_month = output_day = output_time = FALSE; + + if (zone_line->until_time_code != TIME_WALL + || zone_line->until_time_seconds != 0) + output_month = output_day = output_time = TRUE; + else if (zone_line->until_day_code != DAY_SIMPLE + || zone_line->until_day_number != 1) + output_month = output_day = TRUE; + else if (zone_line->until_month != 0) + output_month = TRUE; + + if (output_month) + fprintf (fp, "%s", months[zone_line->until_month]); + + fprintf (fp, "\t"); + + if (output_day) + fprintf (fp, "%s", dump_day_coded (zone_line->until_day_code, + zone_line->until_day_number, + zone_line->until_day_weekday)); + + fprintf (fp, "\t"); + + if (output_time) + fprintf (fp, "%s", dump_time (zone_line->until_time_seconds, + zone_line->until_time_code, FALSE)); + + } else { + fprintf (fp, "\t\t\t"); + } + + fprintf (fp, "\n"); + } + } + + fclose (fp); +} + + +void +dump_rule_data (GHashTable *rule_data, + char *filename) +{ + FILE *fp; + GPtrArray *name_array; + GArray *rule_array; + int i; + char *name; + + fp = fopen (filename, "w"); + if (!fp) { + fprintf (stderr, "Couldn't create file: %s\n", filename); + exit (1); + } + + /* We need to sort the rules by their names, so they are in the same order + as the Perl output. So we place all the names in a temporary GPtrArray, + sort it, then output them. */ + name_array = g_ptr_array_new (); + g_hash_table_foreach (rule_data, (GHFunc) dump_add_rule, name_array); + qsort (name_array->pdata, name_array->len, sizeof (char*), + dump_compare_strings); + + for (i = 0; i < name_array->len; i++) { + name = g_ptr_array_index (name_array, i); + rule_array = g_hash_table_lookup (rule_data, name); + if (!rule_array) { + fprintf (stderr, "Couldn't access rules: %s\n", name); + exit (1); + } + dump_rule_array (name, rule_array, fp); + } + + g_ptr_array_free (name_array, TRUE); + + fclose (fp); +} + + +static void +dump_add_rule (char *name, + GArray *rule_array, + GPtrArray *name_array) +{ + g_ptr_array_add (name_array, name); +} + + +static int +dump_compare_strings (const void *arg1, + const void *arg2) +{ + char **a, **b; + + a = (char**) arg1; + b = (char**) arg2; + + return strcmp (*a, *b); +} + + +void +dump_rule_array (char *name, + GArray *rule_array, + FILE *fp) +{ + static char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + + RuleData *rule; + int i; + +#if 0 + fprintf (fp, "\n# Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S"); +#endif + + for (i = 0; i < rule_array->len; i++) { + rule = &g_array_index (rule_array, RuleData, i); + + fprintf (fp, "Rule\t%s\t%s\t", name, dump_year (rule->from_year)); + + if (rule->to_year == rule->from_year) + fprintf (fp, "only\t"); + else + fprintf (fp, "%s\t", dump_year (rule->to_year)); + + fprintf (fp, "%s\t", rule->type ? rule->type : "-"); + + fprintf (fp, "%s\t", months[rule->in_month]); + + fprintf (fp, "%s\t", + dump_day_coded (rule->on_day_code, rule->on_day_number, + rule->on_day_weekday)); + + fprintf (fp, "%s\t", dump_time (rule->at_time_seconds, rule->at_time_code, + FALSE)); + + fprintf (fp, "%s\t", dump_time (rule->save_seconds, TIME_WALL, TRUE)); + + fprintf (fp, "%s", rule->letter_s ? rule->letter_s : "-"); + + fprintf (fp, "\n"); + } +} + + +char* +dump_time (int seconds, + TimeCode time_code, + gboolean use_zero) +{ + static char buffer[256], *sign; + int hours, minutes; + char *code; + + if (time_code == TIME_STANDARD) + code = "s"; + else if (time_code == TIME_UNIVERSAL) + code = "u"; + else + code = ""; + + if (seconds < 0) { + seconds = -seconds; + sign = "-"; + } else { + sign = ""; + } + + hours = seconds / 3600; + minutes = (seconds % 3600) / 60; + seconds = seconds % 60; + + if (use_zero && hours == 0 && minutes == 0 && seconds == 0) + return "0"; + else if (seconds == 0) + sprintf (buffer, "%s%i:%02i%s", sign, hours, minutes, code); + else + sprintf (buffer, "%s%i:%02i:%02i%s", sign, hours, minutes, seconds, code); + + return buffer; +} + + +char* +dump_day_coded (DayCode day_code, + int day_number, + int day_weekday) +{ + static char buffer[256]; + static char *weekdays[] = { "Sun", "Mon", "Tue", "Wed", + "Thu", "Fri", "Sat" }; + + switch (day_code) { + case DAY_SIMPLE: + sprintf (buffer, "%i", day_number); + break; + case DAY_WEEKDAY_ON_OR_AFTER: + sprintf (buffer, "%s>=%i", weekdays[day_weekday], day_number); + break; + case DAY_WEEKDAY_ON_OR_BEFORE: + sprintf (buffer, "%s<=%i", weekdays[day_weekday], day_number); + break; + case DAY_LAST_WEEKDAY: + sprintf (buffer, "last%s", weekdays[day_weekday]); + break; + default: + fprintf (stderr, "Invalid day code: %i\n", day_code); + exit (1); + } + + return buffer; +} + + +char* +dump_year (int year) +{ + static char buffer[256]; + + if (year == YEAR_MINIMUM) + return "min"; + if (year == YEAR_MAXIMUM) + return "max"; + + sprintf (buffer, "%i", year); + return buffer; +} + + +void +dump_time_zone_names (GList *names, + char *output_dir, + GHashTable *zones_hash) +{ + char filename[PATHNAME_BUFFER_SIZE], *zone_name, *zone_name_in_hash = NULL; + char strings_filename[PATHNAME_BUFFER_SIZE]; + FILE *fp, *strings_fp = NULL; + GList *elem; + ZoneDescription *zone_desc; + + sprintf (filename, "%s/zones.tab", output_dir); + sprintf (strings_filename, "%s/zones.h", output_dir); + + fp = fopen (filename, "w"); + if (!fp) { + fprintf (stderr, "Couldn't create file: %s\n", filename); + exit (1); + } + + if (VzicDumpZoneTranslatableStrings) { + strings_fp = fopen (strings_filename, "w"); + if (!strings_fp) { + fprintf (stderr, "Couldn't create file: %s\n", strings_filename); + exit (1); + } + } + + names = g_list_sort (names, (GCompareFunc) strcmp); + + elem = names; + while (elem) { + zone_name = (char*) elem->data; + + zone_desc = g_hash_table_lookup (zones_hash, zone_name); + + /* SPECIAL CASES: These timezones are links from other zones and are + almost exactly the same - they are basically there so users can find + them a bit easier. But they don't have entries in the zone.tab file, + so we use the entry from the timezone linked from. */ + if (!zone_desc) { + if (!strcmp (zone_name, "America/Indiana/Indianapolis")) + zone_name_in_hash = "America/Indianapolis"; + else if (!strcmp (zone_name, "America/Kentucky/Louisville")) + zone_name_in_hash = "America/Louisville"; + else if (!strcmp (zone_name, "Asia/Istanbul")) + zone_name_in_hash = "Europe/Istanbul"; + else if (!strcmp (zone_name, "Europe/Nicosia")) + zone_name_in_hash = "Asia/Nicosia"; + + if (zone_name_in_hash) + zone_desc = g_hash_table_lookup (zones_hash, zone_name_in_hash); + } + + if (zone_desc) { + fprintf (fp, "%+04i%02i%02i %+04i%02i%02i %s\n", + zone_desc->latitude[0], zone_desc->latitude[1], + zone_desc->latitude[2], + zone_desc->longitude[0], zone_desc->longitude[1], + zone_desc->longitude[2], + zone_name); + } else { + g_print ("Zone description not found for: %s\n", zone_name); + fprintf (fp, "%s\n", zone_name); + } + + + if (VzicDumpZoneTranslatableStrings) { +#if 0 + char zone_name_buffer[1024], *src, *dest; + + for (src = zone_name, dest = zone_name_buffer; *src; src++, dest++) + *dest = (*src == '_') ? ' ' : *src; + *dest = '\0'; +#endif + + fprintf (strings_fp, "N_(\"%s\");\n", zone_name); + } + + elem = elem->next; + } + + fclose (fp); + + if (VzicDumpZoneTranslatableStrings) + fclose (strings_fp); +} +
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic-dump.h
Added
@@ -0,0 +1,58 @@ +/* + * Vzic - a program to convert Olson timezone database files into VZTIMEZONE + * files compatible with the iCalendar specification (RFC2445). + * + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2003 Damon Chaplin. + * + * Author: Damon Chaplin <damon@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +/* + * These functions are for dumping all the parsed Zones and Rules to + * files, to be compared with the output of vzic-dump.pl to check our parsing + * code is OK. Some of the functions are also used for producing debugging + * output. + */ + +#ifndef _VZIC_DUMP_H_ +#define _VZIC_DUMP_H_ + +#include <glib.h> + +void dump_zone_data (GArray *zone_data, + char *filename); +void dump_rule_data (GHashTable *rule_data, + char *filename); + +void dump_rule_array (char *name, + GArray *rule_array, + FILE *fp); + +char* dump_year (int year); +char* dump_day_coded (DayCode day_code, + int day_number, + int day_weekday); +char* dump_time (int seconds, + TimeCode time_code, + gboolean use_zero); + +void dump_time_zone_names (GList *names, + char *output_dir, + GHashTable *zones_hash); + +#endif /* _VZIC_DUMP_H_ */
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic-dump.pl
Added
@@ -0,0 +1,222 @@ +#!/usr/bin/perl -w + +# +# Vzic - a program to convert Olson timezone database files into VZTIMEZONE +# files compatible with the iCalendar specification (RFC2445). +# +# Copyright (C) 2000-2001 Ximian, Inc. +# Copyright (C) 2003 Damon Chaplin. +# +# Author: Damon Chaplin <damon@gnome.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +# + +# +# This reads the Olson timezone files, strips any comments, and outputs them +# in a very simple format with tab-separated fields. It is used to compare +# with the output of dump_zone_data() and dump_rule_data() to double-check +# that we have parsed the files correctly. +# + +my $zones_fh = "zonesfile"; +my $rules_fh = "rulesfile"; + +my %Rules; + +if ($#ARGV != 0) { + die "Usage: $0 <OlsonDirectory>"; +} + +my $OLSON_DIR = $ARGV[0]; + +# We place output in subdirectories of the current directory. +my $OUTPUT_DIR = "zoneinfo"; + +if (! -d "$OUTPUT_DIR") { + mkdir ("$OUTPUT_DIR", 0777) + || die "Can't create directory: $OUTPUT_DIR"; +} +if (! -d "$OUTPUT_DIR/ZonesPerl") { + mkdir ("$OUTPUT_DIR/ZonesPerl", 0777) + || die "Can't create directory: $OUTPUT_DIR/ZonesPerl"; +} +if (! -d "$OUTPUT_DIR/RulesPerl") { + mkdir ("$OUTPUT_DIR/RulesPerl", 0777) + || die "Can't create directory: $OUTPUT_DIR/RulesPerl"; +} + + +&ReadOlsonFile ("africa"); +&ReadOlsonFile ("antarctica"); +&ReadOlsonFile ("asia"); +&ReadOlsonFile ("australasia"); +&ReadOlsonFile ("europe"); +&ReadOlsonFile ("northamerica"); +&ReadOlsonFile ("southamerica"); + +# These are backwards-compatability and weird stuff. +#&ReadOlsonFile ("backward"); +#&ReadOlsonFile ("etcetera"); +#&ReadOlsonFile ("leapseconds"); +#&ReadOlsonFile ("pacificnew"); +#&ReadOlsonFile ("solar87"); +#&ReadOlsonFile ("solar88"); +#&ReadOlsonFile ("solar89"); + +# We don't do this one since it is not useful and the use of '"' in the Zone +# line messes up our split() command. +#&ReadOlsonFile ("factory"); + +# We don't do this since the vzic program can't do it. +#&ReadOlsonFile ("systemv"); + + + + +1; + + +sub ReadOlsonFile { + my ($file) = @_; + +# print ("Reading olson file: $file\n"); + + open (OLSONFILE, "$OLSON_DIR/$file") + || die "Can't open file: $file"; + + open ($zones_fh, ">$OUTPUT_DIR/ZonesPerl/$file") + || die "Can't open file: $OUTPUT_DIR/ZonesPerl/$file"; + + open ($rules_fh, ">$OUTPUT_DIR/RulesPerl/$file") + || die "Can't open file: $OUTPUT_DIR/RulesPerl/$file"; + + %Rules = (); + + my $zone_continues = 0; + + while (<OLSONFILE>) { + next if (m/^#/); + + # '#' characters can appear in strings, but the Olson files don't use + # that feature at present so we treat all '#' as comments for now. + s/#.*//; + + next if (m/^\s*$/); + + if ($zone_continues) { + $zone_continues = &ReadZoneContinuationLine; + + } elsif (m/^Rule\s/) { + &ReadRuleLine; + + } elsif (m/^Zone\s/) { + $zone_continues = &ReadZoneLine; + + } elsif (m/^Link\s/) { +# print "Link: $link_from, $link_to\n"; + + } elsif (m/^Leap\s/) { +# print "Leap\n"; + + } else { + die "Invalid line: $_"; + } + } + +# print ("Read olson file: $file\n"); + + foreach $key (sort (keys (%Rules))) { + print $rules_fh "$Rules{$key}" + } + + close ($zones_fh); + close ($rules_fh); + close (OLSONFILE); +} + + +sub ReadZoneLine { + my ($zone, $name, $gmtoff, $rules_save, $format, + $until_year, $until_month, $until_day, $until_time, $remainder) + = split ' ', $_, 10; + + return &ReadZoneLineCommon ($zone, $name, $gmtoff, $rules_save, $format, + $until_year, $until_month, $until_day, + $until_time); +} + + +sub ReadZoneContinuationLine { + my ($gmtoff, $rules_save, $format, + $until_year, $until_month, $until_day, $until_time, $remainder) + = split ' ', $_, 8; + + return &ReadZoneLineCommon ("", "", $gmtoff, $rules_save, $format, + $until_year, $until_month, $until_day, + $until_time); +} + + +sub ReadZoneLineCommon { + my ($zone, $name, $gmtoff, $rules_save, $format, + $until_year, $until_month, $until_day, $until_time) = @_; + + if (!defined ($until_year)) { $until_year = ""; } + if (!defined ($until_month)) { $until_month = ""; } + if (!defined ($until_day)) { $until_day = ""; } + if (!defined ($until_time)) { $until_time = ""; } + + # A few of the gmtoffsets have an unnecessary :00 seconds. + $gmtoff =~ s/(\d+):(\d+):00/$1:$2/; + + # Make sure the gmtoff does have minutes. + $gmtoff =~ s/^(-?\d+)$/$1:00/; + + # Fix a few other bits so they all use the same format. + if ($gmtoff eq "0") { $gmtoff = "0:00"; } + $until_time =~ s/^0(\d):/$1:/; + if ($until_time eq "0:00") { $until_time = ""; } + if ($until_day eq "1" && $until_time eq "") { $until_day = ""; } + if ($until_month eq "Jan" && $until_day eq "" && $until_time eq "") { + $until_month = ""; + } + + # For Zone continuation lines we need to insert an extra TAB. + if (!$zone) { $zone = "\t" }; + + print $zones_fh "$zone\t$name\t$gmtoff\t$rules_save\t$format\t$until_year\t$until_month\t$until_day\t$until_time\n"; + + if (defined ($until_year) && $until_year) { + return 1; + } else { + return 0; + } +} + + +sub ReadRuleLine { + my ($rule, $name, $from, $to, $type, $in, $on, $at, $save, $letter_s, + $remainder) = split; + + $at =~ s/(\d+:\d+):00/$1/; + $save =~ s/(\d+:\d+):00/$1/; + if ($save eq "0:00") { $save = "0"; } + + $Rules{$name} .= "$rule\t$name\t$from\t$to\t$type\t$in\t$on\t$at\t$save\t$letter_s\n"; + +# print $rules_fh "$rule\t$name\t$from\t$to\t$type\t$in\t$on\t$at\t$save\t$letter_s\n"; +} +
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic-merge.pl
Added
@@ -0,0 +1,177 @@ +#!/usr/bin/perl -w + +# +# Vzic - a program to convert Olson timezone database files into VZTIMEZONE +# files compatible with the iCalendar specification (RFC2445). +# +# Copyright (C) 2001 Ximian, Inc. +# Copyright (C) 2003 Damon Chaplin. +# +# Author: Damon Chaplin <damon@gnome.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +# + +# +# This merges in a new set of VTIMEZONE files with the 'master' set. It only +# updates the files in the master set if the VTIMEZONE component has really +# been changes. Note that the TZID normally includes the date the VTIMEZONE +# file was generated on, so we have to ignore this when comparing the files. +# + +# Set these to the toplevel directories of the 2 sets of VTIMEZONE files. +$NEW_ZONEINFO_DIR = "zoneinfo"; + +# Set this to 1 if you have version numbers in the TZID like libical. +$LIBICAL_VERSIONING = 0; + +# Set this to 0 for dry-runs, and 1 to actually update. +$DO_UPDATES = 1; + +# Save this so we can restore it later. +$input_record_separator = $/; + +sub read_conf { + my $file = shift; + + open CONF, $file or die "can't open $file"; + while (<CONF>) { + if (/^#/) { + next; + } + if (/\@include:\s+(.*)$/) { + push @configs, $1; + } + if (/^configdirectory:\s+(.*)$/) { + $confdir = $1; + } + } + close CONF; +} + +$imapdconf = shift || "/etc/imapd.conf"; + +push @configs, $imapdconf; + +while ($conf = shift @configs) { + read_conf($conf); +} + +if (! $confdir) { $confdir = "/var/imap"; } + +$MASTER_ZONEINFO_DIR = $confdir . "/zoneinfo"; + + +chdir $NEW_ZONEINFO_DIR + || die "Can't cd to $NEW_ZONEINFO_DIR"; + +foreach $new_file (`find -name "*.ics"`) { + # Get rid of './' at start and whitespace at end. + $new_file =~ s/^\.\///; + $new_file =~ s/\s+$//; + +# print "File: $new_file\n"; + + open (NEWZONEFILE, "$new_file") + || die "Can't open file: $NEW_ZONEINFO_DIR/$new_file"; + undef $/; + $new_contents = <NEWZONEFILE>; + $/ = $input_record_separator; + close (NEWZONEFILE); + + $master_file = $MASTER_ZONEINFO_DIR . "/$new_file"; + +# print "Master File: $master_file\n"; + + $copy_to_master = 0; + + # If the ics file exists in the master copy we have to compare them, + # otherwise we can just copy the new file into the master directory. + if (-l $new_file) { + $link_to = readlink($new_file); + + if (! -e $master_file || ! -l $master_file || + (readlink($master_file) ne $link_to)) { + + print "Linking $new_file to $link_to...\n"; + + if ($DO_UPDATES) { + unlink($master_file); + symlink($link_to, $master_file); + } + } + } elsif (-e $master_file) { + open (MASTERZONEFILE, "$master_file") + || die "Can't open file: $master_file"; + undef $/; + $master_contents = <MASTERZONEFILE>; + $/ = $input_record_separator; + close (MASTERZONEFILE); + + $new_contents_copy = $new_contents; + + # Strip the TZID from both contents. +# $new_contents_copy =~ s/^TZID:\S+\r$//m; +# $new_tzid = $&; +# $master_contents =~ s/^TZID:\S+\r$//m; +# $master_tzid = $&; + + # Strip the PRODID from both contents. + $new_contents_copy =~ s/^PRODID:.*$//m; + $master_contents =~ s/^PRODID:.*$//m; + + # Strip the LAST-MODIFIED from both contents. + $new_contents_copy =~ s/^LAST-MODIFIED:(\S+)\r$//m; + $master_contents =~ s/^LAST-MODIFIED:(\S+)\r$//m; + +# print "Matched: $master_tzid\n"; + + + if ($new_contents_copy ne $master_contents) { + print "$new_file has changed. Updating...\n"; + $copy_to_master = 1; + + if ($LIBICAL_VERSIONING) { + # We bump the version number in the new file. +# $master_tzid =~ m%_(\d+)/%; + $version_num = $1; +# print "Version: $version_num\n"; + + $version_num++; + $new_tzid =~ s%_(\d+)/%_$version_num/%; + +# print "New TZID: $new_tzid\n"; + $new_contents =~ s/^TZID:\S+$/$new_tzid/m; + } + } + + } else { + print "$new_file doesn't exist in master directory. Copying...\n"; + $copy_to_master = 1; + } + + if ($copy_to_master) { +# print "Updating: $new_file\n"; + + if ($DO_UPDATES) { + open (MASTERZONEFILE, ">$master_file") + || die "Can't create file: $master_file"; + print MASTERZONEFILE $new_contents; + close (MASTERZONEFILE); + } + } + +} +
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic-output.c
Added
@@ -0,0 +1,2434 @@ +/* + * Vzic - a program to convert Olson timezone database files into VZTIMEZONE + * files compatible with the iCalendar specification (RFC2445). + * + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2003 Damon Chaplin. + * + * Author: Damon Chaplin <damon@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +/* ALGORITHM: + * + * First we expand all the Rule arrays, so that each element only represents 1 + * year. If a Rule extends to infinity we expand it up to a few years past the + * maximum UNTIL year used in any of the timezones. We do this to make sure + * that the last of the expanded Rules (which may be infinite) is only used + * in the last of the time periods (i.e. the last Zone line). + * + * The Rule arrays are also sorted by the start time (FROM + IN + ON + AT). + * Doing all this makes it much easier to find which rules apply to which + * periods. + * + * For each timezone (i.e. ZoneData element), we step through each of the + * time periods, the ZoneLineData elements (which represent each Zone line + * from the Olson file.) + * + * We calculate the start & end time of the period. + * - For the first line the start time is -infinity. + * - For the last line the end time is +infinity. + * - The end time of each line is also the start time of the next. + * + * We create an array of time changes which occur in this period, including + * the one implied by the Zone line itself (though this is later taken out + * if it is found to be at exactly the same time as the first Rule). + * + * Now we iterate over the time changes, outputting them as STANDARD or + * DAYLIGHT components. We also try to merge them together into RRULEs or + * use RDATEs. + */ + + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "vzic.h" +#include "vzic-output.h" + +#include "vzic-dump.h" + +#include "xversion.h" + + +/* These come from the Makefile. See the comments there. */ +char *ProductID = PRODUCT_ID; +char *TZIDPrefix = TZID_PREFIX; + +/* We expand the TZIDPrefix, replacing %D with the date, in here. */ +char TZIDPrefixExpanded[1024]; + + +/* We only use RRULEs if there are at least MIN_RRULE_OCCURRENCES occurrences, + since otherwise RDATEs are more efficient. Actually, I've set this high + so we only use RRULEs for infinite recurrences. Since expanding RRULEs is + very time-consuming, this seems sensible. */ +#define MIN_RRULE_OCCURRENCES 3 + + +/* The year we go up to when dumping the list of timezone changes (used + for testing & debugging). */ +#define MAX_CHANGES_YEAR 2030 + +/* This is the maximum year that time_t value can typically hold on 32-bit + systems. */ +#define MAX_TIME_T_YEAR 2038 + + +/* The year we use to start RRULEs. */ +#define RRULE_START_YEAR 1970 + +/* The year we use for RDATEs. */ +#define RDATE_YEAR 1970 + + +static char *WeekDays[] = { "SU", "MO", "TU", "WE", "TH", "FR", "SA" }; +static int DaysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + +char *CurrentZoneName; + + +typedef struct _VzicTime VzicTime; +struct _VzicTime +{ + /* Normal years, e.g. 2001. */ + int year; + + /* 0 (Jan) to 11 (Dec). */ + int month; + + /* The day, either a simple month day number, 1-31, or a rule such as + the last Sunday, or the first Monday on or after the 8th. */ + DayCode day_code; + int day_number; /* 1 to 31. */ + int day_weekday; /* 0 (Sun) to 6 (Sat). */ + + /* The time, in seconds from midnight. The code specifies whether the + time is a wall clock time, local standard time, or universal time. */ + int time_seconds; + TimeCode time_code; + + /* The offset from UTC for local standard time. */ + int stdoff; + + /* The offset from UTC for local wall clock time. If this is different to + stdoff then this is a DAYLIGHT component. This is TZOFFSETTO. */ + int walloff; + + /* TRUE if the time change recurs every year to infinity. */ + gboolean is_infinite; + + /* The last instance of a recurring time change, if not infinite */ + VzicTime *until; + + /* TRUE if the change has already been output. */ + gboolean output; + + /* These are the offsets of the previous VzicTime, and are used when + calculating the time of the change. We place them here in + output_zone_components() to simplify the output code. */ + int prev_stdoff; + int prev_walloff; + + /* The abbreviated form of the timezone name. Note that this may not be + unique. */ + char *tzname; +}; + + +static void expand_and_sort_rule_array (gpointer key, + gpointer value, + gpointer data); +static int rule_sort_func (const void *arg1, + const void *arg2); +static void output_zone (char *directory, + ZoneData *zone, + char *zone_name, + ZoneDescription *zone_desc, + GHashTable *rule_data); +static gboolean parse_zone_name (char *name, + char **directory, + char **subdirectory, + char **filename); +static void output_zone_to_files (ZoneData *zone, + char *zone_name, + ZoneDescription *zone_desc, + GHashTable *rule_data, + FILE *fp, + FILE *changes_fp); +static gboolean add_rule_changes (ZoneLineData *zone_line, + char *zone_name, + GArray *changes, + GHashTable *rule_data, + VzicTime *start, + VzicTime *end, + char **start_letter_s, + int *save_seconds); +static char* expand_tzname (char *zone_name, + char *format, + gboolean have_letter_s, + char *letter_s, + gboolean is_daylight); +static int compare_times (VzicTime *time1, + int stdoff1, + int walloff1, + VzicTime *time2, + int stdoff2, + int walloff2); +static gboolean times_match (VzicTime *time1, + int stdoff1, + int walloff1, + VzicTime *time2, + int stdoff2, + int walloff2); +static void output_zone_components (FILE *fp, + char *name, + ZoneDescription *zone_desc, + GArray *changes); +static void set_previous_offsets (GArray *changes); +static gboolean check_for_recurrence (FILE *fp, + GArray *changes, + int idx); +static void check_for_rdates (FILE *fp, + GArray *changes, + int idx); +static gboolean timezones_match (char *tzname1, + char *tzname2); +static int output_component_start (char *buffer, + VzicTime *vzictime, + gboolean output_rdate, + gboolean use_same_tz_offset); +static void output_component_end (FILE *fp, + VzicTime *vzictime); + +static void vzictime_init (VzicTime *vzictime); +static int calculate_actual_time (VzicTime *vzictime, + TimeCode time_code, + int stdoff, + int walloff); +static int calculate_wall_time (int time, + TimeCode time_code, + int stdoff, + int walloff, + int *day_offset); +static int calculate_until_time (int time, + TimeCode time_code, + int stdoff, + int walloff, + int *year, + int *month, + int *day); +static void fix_time_overflow (int *year, + int *month, + int *day, + int day_offset); + +static char* format_time (int year, + int month, + int day, + int time); +static char* format_tz_offset (int tz_offset, + gboolean round_seconds); +static gboolean output_rrule (char *rrule_buffer, + int month, + DayCode day_code, + int day_number, + int day_weekday, + int day_offset, + char *until); +static gboolean output_rrule_2 (char *buffer, + int month, + int day_number, + int day_weekday); + +static char* format_vzictime (VzicTime *vzictime); + +static void dump_changes (FILE *fp, + char *zone_name, + GArray *changes); +static void dump_change (FILE *fp, + char *zone_name, + VzicTime *vzictime, + int year); + +static void expand_tzid_prefix (void); + + +void +output_vtimezone_files (char *directory, + GArray *zone_data, + GHashTable *rule_data, + GHashTable *link_data, + GHashTable *zones_hash, + int max_until_year) +{ + ZoneData *zone; + ZoneDescription *zone_desc; + GList *links; + char *link_to; + int i; + + /* Insert today's date into the TZIDs we output. */ + expand_tzid_prefix (); + + /* Expand the rule data so that each entry specifies only one year, and + sort it so we can easily find the rules applicable to each Zone span. */ + g_hash_table_foreach (rule_data, expand_and_sort_rule_array, + GINT_TO_POINTER (max_until_year)); + + /* Output each timezone. */ + for (i = 0; i < zone_data->len; i++) { + zone = &g_array_index (zone_data, ZoneData, i); + zone_desc = g_hash_table_lookup (zones_hash, zone->zone_name); + output_zone (directory, zone, zone->zone_name, zone_desc, rule_data); + + /* Look for any links from this zone. */ + links = g_hash_table_lookup (link_data, zone->zone_name); + + while (links) { + link_to = links->data; + + /* We ignore Links that don't have a '/' in them (things like 'EST5EDT'). + */ + if (strchr (link_to, '/')) { + output_zone (directory, zone, link_to, NULL, rule_data); + } + + links = links->next; + } + } +} + + +static void +expand_and_sort_rule_array (gpointer key, + gpointer value, + gpointer data) +{ + char *name = key; + GArray *rule_array = value; + RuleData *rule, tmp_rule; + int len, max_year, i, from, to, year; + gboolean is_infinite; + + /* We expand the rule data to a year greater than any year used in a Zone + UNTIL value. This is so that we can easily get parts of the array to + use for each Zone line. */ + max_year = GPOINTER_TO_INT (data) + 2; + + /* If any of the rules apply to several years, we turn it into a single rule + for each year. If the Rule is infinite we go up to max_year. + We change the FROM field in the copies of the Rule, setting it to each + of the years, and set TO to FROM, except if TO was YEAR_MAXIMUM we set + the last TO to YEAR_MAXIMUM, so we still know the Rule is infinite. */ + len = rule_array->len; + for (i = 0; i < len; i++) { + rule = &g_array_index (rule_array, RuleData, i); + + /* None of the Rules currently use the TYPE field, but we'd better check. + */ + if (rule->type) { + fprintf (stderr, "Rules %s has a TYPE: %s\n", name, rule->type); + exit (1); + } + + if (rule->from_year != rule->to_year) { + from = rule->from_year; + to = rule->to_year; + + tmp_rule = *rule; + + /* Flag that this is a shallow copy so we don't free anything twice. */ + tmp_rule.is_shallow_copy = TRUE; + + /* See if it is an infinite Rule. */ + if (to == YEAR_MAXIMUM) { + is_infinite = TRUE; + to = max_year; + if (from < to) + rule->to_year = rule->from_year; + } else { + is_infinite = FALSE; + } + + /* Create a copy of the Rule for each year. */ + for (year = from + 1; year <= to; year++) { + tmp_rule.from_year = year; + + /* If the Rule is infinite, mark the last copy as infinite. */ + if (year == to && is_infinite) + tmp_rule.to_year = YEAR_MAXIMUM; + else + tmp_rule.to_year = year; + + g_array_append_val (rule_array, tmp_rule); + } + } + } + + /* Now sort the rules. */ + qsort (rule_array->data, rule_array->len, sizeof (RuleData), rule_sort_func); + +#if 0 + dump_rule_array (name, rule_array, stdout); +#endif +} + + +/* This is used to sort the rules, after the rules have all been expanded so + that each one is only for one year. */ +static int +rule_sort_func (const void *arg1, + const void *arg2) +{ + RuleData *rule1, *rule2; + int time1_year, time1_month, time1_day; + int time2_year, time2_month, time2_day; + int month_diff, result; + VzicTime t1, t2; + + rule1 = (RuleData*) arg1; + rule2 = (RuleData*) arg2; + + time1_year = rule1->from_year; + time1_month = rule1->in_month; + time2_year = rule2->from_year; + time2_month = rule2->in_month; + + /* If there is more that one month difference we don't need to calculate + the day or time. */ + month_diff = (time1_year - time2_year) * 12 + time1_month - time2_month; + + if (month_diff > 1) + return 1; + if (month_diff < -1) + return -1; + + /* Now we have to calculate the day and time of the Rule start and the + VzicTime, using the given offsets. */ + t1.year = time1_year; + t1.month = time1_month; + t1.day_code = rule1->on_day_code; + t1.day_number = rule1->on_day_number; + t1.day_weekday = rule1->on_day_weekday; + t1.time_code = rule1->at_time_code; + t1.time_seconds = rule1->at_time_seconds; + + t2.year = time2_year; + t2.month = time2_month; + t2.day_code = rule2->on_day_code; + t2.day_number = rule2->on_day_number; + t2.day_weekday = rule2->on_day_weekday; + t2.time_code = rule2->at_time_code; + t2.time_seconds = rule2->at_time_seconds; + + /* FIXME: We don't know the offsets yet, but I don't think any Rules are + close enough together that the offsets can make a difference. Should + check this. */ + calculate_actual_time (&t1, TIME_WALL, 0, 0); + calculate_actual_time (&t2, TIME_WALL, 0, 0); + + /* Now we can compare the entire time. */ + if (t1.year > t2.year) + result = 1; + else if (t1.year < t2.year) + result = -1; + + else if (t1.month > t2.month) + result = 1; + else if (t1.month < t2.month) + result = -1; + + else if (t1.day_number > t2.day_number) + result = 1; + else if (t1.day_number < t2.day_number) + result = -1; + + else if (t1.time_seconds > t2.time_seconds) + result = 1; + else if (t1.time_seconds < t2.time_seconds) + result = -1; + + else { + printf ("WARNING: Rule dates matched.\n"); + result = 0; + } + + return result; +} + + +static void +output_zone (char *directory, + ZoneData *zone, + char *zone_name, + ZoneDescription *zone_desc, + GHashTable *rule_data) +{ + FILE *fp, *changes_fp = NULL; + char output_directory[PATHNAME_BUFFER_SIZE]; + char filename[PATHNAME_BUFFER_SIZE]; + char changes_filename[PATHNAME_BUFFER_SIZE]; + char *zone_directory, *zone_subdirectory, *zone_filename; + + /* Set a global for the zone_name, to be used only for debug messages. */ + CurrentZoneName = zone_name; + + /* Use this to only output a particular zone. */ +#if 0 + if (strcmp (zone_name, "Atlantic/Azores")) + return; +#endif + +#if 0 + printf ("Outputting Zone: %s\n", zone_name); +#endif + + if (!parse_zone_name (zone_name, &zone_directory, &zone_subdirectory, + &zone_filename)) + return; + + if (VzicDumpZoneNamesAndCoords) { + VzicTimeZoneNames = g_list_prepend (VzicTimeZoneNames, + g_strdup (zone_name)); + } + + if (zone_subdirectory) { + sprintf (output_directory, "%s/%s/%s", directory, zone_directory, + zone_subdirectory); + ensure_directory_exists (output_directory); + sprintf (filename, "%s/%s.ics", output_directory, zone_filename); + + if (VzicDumpChanges) { + sprintf (output_directory, "%s/ChangesVzic/%s/%s", directory, + zone_directory, zone_subdirectory); + ensure_directory_exists (output_directory); + sprintf (changes_filename, "%s/%s", output_directory, zone_filename); + } + } + else if (zone_directory) { + sprintf (output_directory, "%s/%s", directory, zone_directory); + ensure_directory_exists (output_directory); + sprintf (filename, "%s/%s.ics", output_directory, zone_filename); + + if (VzicDumpChanges) { + sprintf (output_directory, "%s/ChangesVzic/%s", directory, zone_directory); + ensure_directory_exists (output_directory); + sprintf (changes_filename, "%s/%s", output_directory, zone_filename); + } + } + else { + sprintf (output_directory, "%s", directory); + ensure_directory_exists (output_directory); + sprintf (filename, "%s/%s.ics", output_directory, zone_filename); + + if (VzicDumpChanges) { + sprintf (output_directory, "%s/ChangesVzic", directory); + ensure_directory_exists (output_directory); + sprintf (changes_filename, "%s/%s", output_directory, zone_filename); + } + } + + + /* Create the files. */ + fp = fopen (filename, "w"); + if (!fp) { + fprintf (stderr, "Couldn't create file: %s\n", filename); + exit (1); + } + + if (VzicDumpChanges) { + changes_fp = fopen (changes_filename, "w"); + if (!changes_fp) { + fprintf (stderr, "Couldn't create file: %s\n", changes_filename); + exit (1); + } + } + + fprintf (fp, "BEGIN:VCALENDAR\r\nPRODID:"); + fprintf (fp, ProductID, _CYRUS_VERSION); + fprintf (fp, "\r\nVERSION:2.0\r\n"); + + + output_zone_to_files (zone, zone_name, zone_desc, rule_data, fp, changes_fp); + + if (ferror (fp)) { + fprintf (stderr, "Error writing file: %s\n", filename); + exit (1); + } + + fprintf (fp, "END:VCALENDAR\r\n"); + + fclose (fp); + + g_free (zone_directory); + g_free (zone_subdirectory); + g_free (zone_filename); +} + + +/* This checks that the Zone name only uses the characters in [-+_/a-zA-Z0-9], + and outputs a warning if it isn't. */ +static gboolean +parse_zone_name (char *name, + char **directory, + char **subdirectory, + char **filename) +{ + static int invalid_zone_num = 1; + + char *p, ch, *first_slash_pos = NULL, *second_slash_pos = NULL; + gboolean invalid = FALSE; + + for (p = name; (ch = *p) != 0; p++) { + if ((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z') + && (ch < '0' || ch > '9') && ch != '/' && ch != '_' + && ch != '-' && ch != '+') { + fprintf (stderr, "WARNING: Unusual Zone name: %s\n", name); + invalid = TRUE; + break; + } + + if (ch == '/') { + if (!first_slash_pos) { + first_slash_pos = p; + } else if (!second_slash_pos) { + second_slash_pos = p; + } else { + fprintf (stderr, "WARNING: More than 2 '/' characters in Zone name: %s\n", name); + invalid = TRUE; + break; + } + } + } +#if 0 + if (!first_slash_pos) { +#if 0 + fprintf (stderr, "No '/' character in Zone name: %s. Skipping.\n", name); +#endif + return FALSE; + } +#endif + if (invalid) { + *directory = g_strdup ("Invalid"); + *filename = g_strdup_printf ("Zone%i", invalid_zone_num++); + } else if (!first_slash_pos) { + *directory = NULL; + *subdirectory = NULL; + *filename = g_strdup (name); + } else { + *first_slash_pos = '\0'; + *directory = g_strdup (name); + *first_slash_pos = '/'; + + if (second_slash_pos) { + *second_slash_pos = '\0'; + *subdirectory = g_strdup (first_slash_pos + 1); + *second_slash_pos = '/'; + + *filename = g_strdup (second_slash_pos + 1); + } else { + *subdirectory = NULL; + *filename = g_strdup (first_slash_pos + 1); + } + } + + return invalid ? FALSE : TRUE; +} + + +static void +output_zone_to_files (ZoneData *zone, + char *zone_name, + ZoneDescription *zone_desc, + GHashTable *rule_data, + FILE *fp, + FILE *changes_fp) +{ + ZoneLineData *zone_line; + GArray *changes; + int i, stdoff, walloff, start_index, save_seconds; + VzicTime start, end, *vzictime_start, *vzictime, *vzictime_first_rule_change; + gboolean is_daylight, found_letter_s; + char *start_letter_s; + + changes = g_array_new (FALSE, FALSE, sizeof (VzicTime)); + + vzictime_init (&start); + vzictime_init (&end); + + /* The first period starts at -infinity. */ + start.year = YEAR_MINIMUM; + + for (i = 0; i < zone->zone_line_data->len; i++) { + zone_line = &g_array_index (zone->zone_line_data, ZoneLineData, i); + + /* This is the local standard time offset from GMT for this period. */ + start.stdoff = stdoff = zone_line->stdoff_seconds; + start.walloff = walloff = stdoff + zone_line->save_seconds; + + if (zone_line->until_set) { + end.year = zone_line->until_year; + end.month = zone_line->until_month; + end.day_code = zone_line->until_day_code; + end.day_number = zone_line->until_day_number; + end.day_weekday = zone_line->until_day_weekday; + end.time_seconds = zone_line->until_time_seconds; + end.time_code = zone_line->until_time_code; + } else { + /* The last period ends at +infinity. */ + end.year = YEAR_MAXIMUM; + } + + /* Add a time change for the start of the period. This may be removed + later if one of the rules expands to exactly the same time. */ + start_index = changes->len; + g_array_append_val (changes, start); + + /* If there are Rules associated with this period, add all the relevant + time changes. */ + save_seconds = 0; + if (zone_line->rules) + found_letter_s = add_rule_changes (zone_line, zone_name, changes, + rule_data, &start, &end, + &start_letter_s, &save_seconds); + else + found_letter_s = FALSE; + + /* FIXME: I'm not really sure what to do about finding a LETTER_S for the + first part of the period (i.e. before the first Rule comes into effect). + Currently we try to use the same LETTER_S as the first Rule of the + period which is in local standard time. */ + if (zone_line->save_seconds) + save_seconds = zone_line->save_seconds; + is_daylight = save_seconds ? TRUE : FALSE; + vzictime_start = &g_array_index (changes, VzicTime, start_index); + walloff = vzictime_start->walloff = stdoff + save_seconds; + + /* TEST: See if the first Rule time is exactly the same as the change from + the Zone line. In which case we can remove the Zone line change. */ + if (changes->len > start_index + 1) { + int prev_stdoff, prev_walloff; + + if (start_index > 0) { + VzicTime *v = &g_array_index (changes, VzicTime, start_index - 1); + prev_stdoff = v->stdoff; + prev_walloff = v->walloff; + } else { + prev_stdoff = 0; + prev_walloff = 0; + } + vzictime_first_rule_change = &g_array_index (changes, VzicTime, + start_index + 1); + if (times_match (vzictime_start, prev_stdoff, prev_walloff, + vzictime_first_rule_change, stdoff, walloff)) { +#if 0 + printf ("Removing zone-line change (using new offsets)\n"); +#endif + g_array_remove_index (changes, start_index); + vzictime_start = NULL; + } else if (times_match (vzictime_start, prev_stdoff, prev_walloff, + vzictime_first_rule_change, prev_stdoff, prev_walloff)) { +#if 0 + printf ("Removing zone-line change (using previous offsets)\n"); +#endif + g_array_remove_index (changes, start_index); + vzictime_start = NULL; + } + } + + + if (vzictime_start) { + vzictime_start->tzname = expand_tzname (zone_name, zone_line->format, + found_letter_s, + start_letter_s, is_daylight); + } + + /* The start of the next Zone line is the end time of this one. */ + start = end; + } + + set_previous_offsets (changes); + + output_zone_components (fp, zone_name, zone_desc, changes); + + if (VzicDumpChanges) + dump_changes (changes_fp, zone_name, changes); + + /* Free all the TZNAME fields. */ + for (i = 0; i < changes->len; i++) { + vzictime = &g_array_index (changes, VzicTime, i); + g_free (vzictime->tzname); + } + + g_array_free (changes, TRUE); +} + + +/* This appends any timezone changes specified by the rules associated with + the timezone, that happen between the start and end times. + It returns the letter_s field of the first STANDARD rule found in the + search. We need this to fill in any %s in the FORMAT field of the first + component of the time period (the Zone line). */ +static gboolean +add_rule_changes (ZoneLineData *zone_line, + char *zone_name, + GArray *changes, + GHashTable *rule_data, + VzicTime *start, + VzicTime *end, + char **start_letter_s, + int *save_seconds) +{ + GArray *rule_array; + RuleData *rule, *prev_rule = NULL; + int stdoff, walloff, i, prev_stdoff, prev_walloff; + VzicTime vzictime; + gboolean is_daylight, found_start_letter_s = FALSE; + gboolean checked_for_previous = FALSE; + + *save_seconds = 0; + + rule_array = g_hash_table_lookup (rule_data, zone_line->rules); + if (!rule_array) { + fprintf (stderr, "Couldn't access rules: %s\n", zone_line->rules); + exit (1); + } + + /* The stdoff is the same for all the rules. */ + stdoff = start->stdoff; + + /* The walloff changes as we go through the rules. */ + walloff = start->walloff; + + /* Get the stdoff & walloff from the last change before this period. */ + if (changes->len >= 2) { + VzicTime *change = &g_array_index (changes, VzicTime, changes->len - 2); + prev_stdoff = change->stdoff; + prev_walloff = change->walloff; + } else { + prev_stdoff = prev_walloff = 0; + } + + + for (i = 0; i < rule_array->len; i++) { + rule = &g_array_index (rule_array, RuleData, i); + + is_daylight = rule->save_seconds != 0 ? TRUE : FALSE; + + vzictime_init (&vzictime); + vzictime.year = rule->from_year; + vzictime.month = rule->in_month; + vzictime.day_code = rule->on_day_code; + vzictime.day_number = rule->on_day_number; + vzictime.day_weekday = rule->on_day_weekday; + vzictime.time_seconds = rule->at_time_seconds; + vzictime.time_code = rule->at_time_code; + vzictime.stdoff = stdoff; + vzictime.walloff = stdoff + rule->save_seconds; + vzictime.is_infinite = (rule->to_year == YEAR_MAXIMUM) ? TRUE : FALSE; + + /* If the rule time is before or on the given start time, skip it. */ + if (compare_times (&vzictime, stdoff, walloff, + start, prev_stdoff, prev_walloff) <= 0) { + /* Our next rule may start while this one is in effect + so we keep track of its name. + + This seems to eliminate the need to guess in expand_tzname() + but hasn't had enough testing to prove foolproof as of yet. */ + found_start_letter_s = TRUE; + *start_letter_s = rule->letter_s; + + continue; + } + + /* If the previous Rule was a daylight Rule, then we may want to use the + walloff from that. */ + if (!checked_for_previous) { + checked_for_previous = TRUE; + if (i > 0) { + prev_rule = &g_array_index (rule_array, RuleData, i - 1); + if (prev_rule->save_seconds) { + walloff = start->walloff = stdoff + prev_rule->save_seconds; + *save_seconds = prev_rule->save_seconds; + found_start_letter_s = TRUE; + *start_letter_s = prev_rule->letter_s; +#if 0 + printf ("Could use save_seconds from previous Rule: %s\n", + zone_name); +#endif + } + } + } + + /* If an end time has been given, then if the rule time is on or after it + break out of the loop. */ + if (end->year != YEAR_MAXIMUM + && compare_times (&vzictime, stdoff, walloff, + end, stdoff, walloff) >= 0) + break; + + vzictime.tzname = expand_tzname (zone_name, zone_line->format, TRUE, + rule->letter_s, is_daylight); + + g_array_append_val (changes, vzictime); + + /* When we find the first STANDARD time we set letter_s. */ + if (!found_start_letter_s && !is_daylight) { + found_start_letter_s = TRUE; + *start_letter_s = rule->letter_s; + } + + /* Now that we have added the Rule, the new walloff comes into effect + for any following Rules. */ + walloff = vzictime.walloff; + } + + return found_start_letter_s; +} + + +/* This expands the Zone line FORMAT field, using the given LETTER_S from a + Rule line. There are 3 types of FORMAT field: + 1. a string with an %s in, e.g. "WE%sT". The %s is replaced with LETTER_S. + 2. a string with an '/' in, e.g. "CAT/CAWT". The first part is used for + standard time and the second part for when daylight-saving is in effect. + 3. a plain string, e.g. "LMT", which we leave as-is. + Note that (1) is the only type in which letter_s is required. +*/ +static char* +expand_tzname (char *zone_name, + char *format, + gboolean have_letter_s, + char *letter_s, + gboolean is_daylight) +{ + char *p, buffer[256], *guess = NULL; + int len; + +#if 0 + printf ("Expanding %s with %s\n", format, letter_s); +#endif + + if (!format || !format[0]) { + fprintf (stderr, "Missing FORMAT\n"); + exit (1); + } + + /* 1. Look for a "%s". */ + p = strchr (format, '%'); + if (p && *(p + 1) == 's') { + if (!have_letter_s) { + + /* NOTE: These are a few hard-coded TZNAMEs that I've looked up myself. + These are needed in a few places where a Zone line comes into effect + but no Rule has been found, so we have no LETTER_S to use. + I've tried to use whatever is the normal LETTER_S in the Rules for + the particular zone, in local standard time. */ + if (!strcmp (zone_name, "Asia/Macao") + && !strcmp (format, "C%sT")) + guess = "CST"; + else if (!strcmp (zone_name, "Asia/Macau") + && !strcmp (format, "C%sT")) + guess = "CST"; + else if (!strcmp (zone_name, "Asia/Ashgabat") + && !strcmp (format, "ASH%sT")) + guess = "ASHT"; + else if (!strcmp (zone_name, "Asia/Ashgabat") + && !strcmp (format, "TM%sT")) + guess = "TMT"; + else if (!strcmp (zone_name, "Asia/Samarkand") + && !strcmp (format, "TAS%sT")) + guess = "TAST"; + else if (!strcmp (zone_name, "Atlantic/Azores") + && !strcmp (format, "WE%sT")) + guess = "WET"; + else if (!strcmp (zone_name, "Europe/Paris") + && !strcmp (format, "WE%sT")) + guess = "WET"; + else if (!strcmp (zone_name, "Europe/Warsaw") + && !strcmp (format, "CE%sT")) + guess = "CET"; + else if (!strcmp (zone_name, "America/Phoenix") + && !strcmp (format, "M%sT")) + guess = "MST"; + else if (!strcmp (zone_name, "America/Nome") + && !strcmp (format, "Y%sT")) + guess = "YST"; + + if (guess) { +#if 0 + fprintf (stderr, + "WARNING: Couldn't find a LETTER_S to use in FORMAT: %s in Zone: %s Guessing: %s\n", + format, zone_name, guess); +#endif + return g_strdup (guess); + } + +#if 1 + fprintf (stderr, + "WARNING: Couldn't find a LETTER_S to use in FORMAT: %s in Zone: %s Leaving TZNAME empty\n", + format, zone_name); +#endif + +#if 0 + /* This is useful to spot exactly which component had a problem. */ + sprintf (buffer, "FIXME: %s", format); + return g_strdup (buffer); +#else + /* We give up and don't output a TZNAME. */ + return NULL; +#endif + } + + sprintf (buffer, format, letter_s ? letter_s : ""); + return g_strdup (buffer); + } + + /* 2. Look for a "/". */ + p = strchr (format, '/'); + if (p) { + if (is_daylight) { + return g_strdup (p + 1); + } else { + len = p - format; + strncpy (buffer, format, len); + buffer[len] = '\0'; + return g_strdup (buffer); + } + } + + /* 3. Just use format as it is. */ + return g_strdup (format); +} + + +/* Compares 2 VzicTimes, returning strcmp()-like values, i.e. 0 if equal, + 1 if the 1st is after the 2nd and -1 if the 1st is before the 2nd. */ +static int +compare_times (VzicTime *time1, + int stdoff1, + int walloff1, + VzicTime *time2, + int stdoff2, + int walloff2) +{ + VzicTime t1, t2; + int result; + + t1 = *time1; + t2 = *time2; + + calculate_actual_time (&t1, TIME_UNIVERSAL, stdoff1, walloff1); + calculate_actual_time (&t2, TIME_UNIVERSAL, stdoff2, walloff2); + + /* Now we can compare the entire time. */ + if (t1.year > t2.year) + result = 1; + else if (t1.year < t2.year) + result = -1; + + else if (t1.month > t2.month) + result = 1; + else if (t1.month < t2.month) + result = -1; + + else if (t1.day_number > t2.day_number) + result = 1; + else if (t1.day_number < t2.day_number) + result = -1; + + else if (t1.time_seconds > t2.time_seconds) + result = 1; + else if (t1.time_seconds < t2.time_seconds) + result = -1; + + else + result = 0; + +#if 0 + printf ("%i/%i/%i %i <=> %i/%i/%i %i -> %i\n", + t1.day_number, t1.month + 1, t1.year, t1.time_seconds, + t2.day_number, t2.month + 1, t2.year, t2.time_seconds, + result); +#endif + + return result; +} + + +/* Returns TRUE if the 2 times are exactly the same. It will calculate the + actual day, but doesn't convert times. */ +static gboolean +times_match (VzicTime *time1, + int stdoff1, + int walloff1, + VzicTime *time2, + int stdoff2, + int walloff2) +{ + VzicTime t1, t2; + + t1 = *time1; + t2 = *time2; + + calculate_actual_time (&t1, TIME_UNIVERSAL, stdoff1, walloff1); + calculate_actual_time (&t2, TIME_UNIVERSAL, stdoff2, walloff2); + + if (t1.year == t2.year + && t1.month == t2.month + && t1.day_number == t2.day_number + && t1.time_seconds == t2.time_seconds) + return TRUE; + + return FALSE; +} + + +/* Convert degrees-minutes-seconds into decimal degrees */ +static float +dms_to_dd (int dms[]) +{ + return dms[0] + dms[1]/60.0 + dms[2]/3600.0; +} + + +static void +output_zone_components (FILE *fp, + char *name, + ZoneDescription *zone_desc, + GArray *changes) +{ + VzicTime *vzictime; + int i, start_index = 0; + gboolean only_one_change = FALSE; + char start_buffer[1024]; + time_t now = time(0); + struct tm *tm = gmtime(&now); + + fprintf (fp, "BEGIN:VTIMEZONE\r\nTZID:%s%s\r\n", TZIDPrefixExpanded, name); + + if (zone_desc) { + /* Add COMMENT */ + fprintf (fp, "COMMENT:[%.2s] ", zone_desc->country_code); + if (zone_desc->comment) { + const char *p; + + for (p = zone_desc->comment; *p; p++) { + switch (*p) { + case '\\': + case ',': + case ';': + fprintf (fp, "\\"); + break; + } + + fprintf (fp, "%c", *p); + } + } + fprintf (fp, "\r\n"); + +#if 0 + /* Add GEO */ + fprintf (fp, "GEO:%+.6f,%+.6f\r\n", + dms_to_dd(zone_desc->latitude), dms_to_dd(zone_desc->longitude)); +#endif + } + + /* Use current time as LAST-MODIFIED */ + fprintf (fp, "LAST-MODIFIED:%04i%02i%02iT%02i%02i%02iZ\r\n", + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec); + + if (VzicUrlPrefix != NULL) + fprintf (fp, "TZURL:%s/%s\r\n", VzicUrlPrefix, name); + + /* We use an 'X-' property to place the city name in. */ + fprintf (fp, "X-LIC-LOCATION:%s\r\n", name); + + /* We use an 'X-' property to place the proleptic tzname in. */ + vzictime = &g_array_index (changes, VzicTime, 0); + if (vzictime->tzname) + fprintf( fp, "X-PROLEPTIC-TZNAME:%s\r\n", vzictime->tzname); + + /* We try to find any recurring components first, or they may get output + as lots of RDATES instead. */ + if (!VzicNoRRules) { + int num_rrules_output = 0; + + for (i = 1; i < changes->len; i++) { + if (check_for_recurrence (fp, changes, i)) { + num_rrules_output++; + } + } + +#if 0 + printf ("Zone: %s had %i infinite RRULEs\n", CurrentZoneName, + num_rrules_output); +#endif + + if (!VzicPureOutput && num_rrules_output == 2) { +#if 0 + printf ("Zone: %s using 2 RRULEs\n", CurrentZoneName); +#endif + fprintf (fp, "END:VTIMEZONE\r\n"); + return; + } + } + + /* We skip the first change, which starts at -infinity, unless it is the only + change for the timezone. */ + if (changes->len > 1) + start_index = 1; + else + only_one_change = TRUE; + + /* For pure output, we start at the start of the array and step through it + outputting RDATEs. For Outlook-compatible output we start at the end + and step backwards to find the first STANDARD time to output. */ + if (VzicPureOutput) + i = start_index - 1; + else + i = changes->len; + + for (;;) { + if (VzicPureOutput) + i++; + else + i--; + + if (VzicPureOutput) { + if (i >= changes->len) + break; + } else { + if (i < start_index) + break; + } + + vzictime = &g_array_index (changes, VzicTime, i); + + /* If this has been flagged as an RRULE, then output it now */ + if (vzictime->until) { + char until[256], rrule_buffer[2048]; + VzicTime vzictime_start_copy; + int day_offset; + + if (vzictime->until->is_infinite) { + until[0] = '\0'; + } else { + VzicTime t1 = *vzictime->until; + + calculate_actual_time (&t1, TIME_UNIVERSAL, vzictime->prev_stdoff, + vzictime->prev_walloff); + + /* Output UNTIL, in UTC. */ + sprintf (until, ";UNTIL=%sZ", format_time (t1.year, t1.month, + t1.day_number, + t1.time_seconds)); + } + + /* Change the year to our minimum start year. */ + vzictime_start_copy = *vzictime; + if (!VzicPureOutput) + vzictime_start_copy.year = RRULE_START_YEAR; + + day_offset = output_component_start (start_buffer, &vzictime_start_copy, + FALSE, FALSE); + fprintf (fp, "%s", start_buffer); + + if (output_rrule (rrule_buffer, vzictime_start_copy.month, + vzictime_start_copy.day_code, + vzictime_start_copy.day_number, + vzictime_start_copy.day_weekday, day_offset, until)) { + fprintf (fp, "%s", rrule_buffer); + } + + output_component_end (fp, vzictime); + + continue; + } + + /* If we have already output this component as part of an RRULE or RDATE, + then we skip it. */ + if (vzictime->output) + continue; + + /* For Outlook-compatible output we only want to output the last STANDARD + time as a DTSTART, so skip any DAYLIGHT changes. */ + if (!VzicPureOutput && vzictime->stdoff != vzictime->walloff) { + printf ("Skipping DAYLIGHT change\n"); + continue; + } + +#if 0 + printf ("Zone: %s using DTSTART Year: %i\n", CurrentZoneName, + vzictime->year); +#endif + + if (VzicPureOutput) { + output_component_start (start_buffer, vzictime, TRUE, only_one_change); + } else { + /* For Outlook compatability we don't output the RDATE and use the same + TZOFFSET for TZOFFSETFROM and TZOFFSETTO. */ + vzictime->year = RDATE_YEAR; + vzictime->month = 0; + vzictime->day_code = DAY_SIMPLE; + vzictime->day_number = 1; + vzictime->time_code = TIME_WALL; + vzictime->time_seconds = 0; + + output_component_start (start_buffer, vzictime, FALSE, TRUE); + } + + fprintf (fp, "%s", start_buffer); + + /* This will look for matching components and output them as RDATEs + instead of separate components. */ + if (VzicPureOutput && !VzicNoRDates) + check_for_rdates (fp, changes, i); + + output_component_end (fp, vzictime); + + vzictime->output = TRUE; + + if (!VzicPureOutput) + break; + } + + fprintf (fp, "END:VTIMEZONE\r\n"); +} + + +/* This sets the prev_stdoff and prev_walloff (i.e. the TZOFFSETFROM) of each + VzicTime, using the stdoff and walloff of the previous VzicTime. It makes + the rest of the code much simpler. */ +static void +set_previous_offsets (GArray *changes) +{ + VzicTime *vzictime, *prev_vzictime; + int i; + + prev_vzictime = &g_array_index (changes, VzicTime, 0); + prev_vzictime->prev_stdoff = 0; + prev_vzictime->prev_walloff = 0; + + for (i = 1; i < changes->len; i++) { + vzictime = &g_array_index (changes, VzicTime, i); + + vzictime->prev_stdoff = prev_vzictime->stdoff; + vzictime->prev_walloff = prev_vzictime->walloff; + + prev_vzictime = vzictime; + } +} + + +/* Returns TRUE if we output an infinite recurrence. */ +static gboolean +check_for_recurrence (FILE *fp, + GArray *changes, + int idx) +{ + VzicTime *vzictime_start, *vzictime; + gboolean is_daylight_start, is_daylight; + int last_match, i, next_year; + GList *matching_elements = NULL, *elem; + + vzictime_start = &g_array_index (changes, VzicTime, idx); + + /* If this change has already been output, skip it. */ + if (vzictime_start->output) + return FALSE; + + /* There can't possibly be an RRULE starting from YEAR_MINIMUM. */ + if (vzictime_start->year == YEAR_MINIMUM) + return FALSE; + + is_daylight_start = (vzictime_start->stdoff != vzictime_start->walloff) + ? TRUE : FALSE; + +#if 0 + printf ("\nChecking: %s OFFSETFROM: %i %s\n", + format_vzictime (vzictime_start), vzictime_start->prev_walloff, + is_daylight_start ? "DAYLIGHT" : ""); +#endif + + /* If this is an infinitely recurring change, output the RRULE and return. + There won't be any changes after it that we could merge. */ + if (vzictime_start->is_infinite) { + vzictime_start->until = vzictime_start; + vzictime_start->output = TRUE; + return TRUE; + } + + last_match = idx; + next_year = vzictime_start->year + 1; + for (i = idx + 1; i < changes->len; i++) { + vzictime = &g_array_index (changes, VzicTime, i); + + is_daylight = (vzictime->stdoff != vzictime->walloff) ? TRUE : FALSE; + + if (vzictime->output) + continue; + +#if 0 + printf (" %s OFFSETFROM: %i %s\n", + format_vzictime (vzictime), vzictime->prev_walloff, + is_daylight ? "DAYLIGHT" : ""); +#endif + + /* If it is more than one year ahead, we are finished, since we want + consecutive years. */ + if (vzictime->year > next_year) { + break; + } + + /* It must be the same type of component - STANDARD or DAYLIGHT. */ + if (is_daylight != is_daylight_start) { + continue; + } + + /* It must be the following year, with the same month, day & time. + It is possible that the time has a different code but does in fact + match when normalized, but we don't care (for now at least). */ + if (vzictime->year != next_year + || vzictime->month != vzictime_start->month + || vzictime->day_code != vzictime_start->day_code + || vzictime->day_number != vzictime_start->day_number + || vzictime->day_weekday != vzictime_start->day_weekday + || vzictime->time_seconds != vzictime_start->time_seconds + || vzictime->time_code != vzictime_start->time_code) { + continue; + } + + /* The TZOFFSETFROM and TZOFFSETTO must match. */ + if (vzictime->prev_walloff != vzictime_start->prev_walloff) { + continue; + } + + if (vzictime->walloff != vzictime_start->walloff) { + continue; + } + + /* TZNAME must match. */ + if (!timezones_match (vzictime->tzname, vzictime_start->tzname)) { + continue; + } + + /* We have a match. */ + last_match = i; + next_year = vzictime->year + 1; + + matching_elements = g_list_prepend (matching_elements, vzictime); + } + + if (last_match == idx) + return FALSE; + +#if 0 + printf ("Found recurrence %i - %i!!!\n", vzictime_start->year, + next_year - 1); +#endif + + vzictime = &g_array_index (changes, VzicTime, last_match); + +/* We only use RRULEs if there are at least MIN_RRULE_OCCURRENCES occurrences, + since otherwise RDATEs are more efficient. */ + if (!vzictime->is_infinite) { + int years = vzictime->year - vzictime_start->year + 1; +#if 0 + printf ("RRULE Years: %i\n", years); +#endif + if (years < MIN_RRULE_OCCURRENCES) + return FALSE; + } + + vzictime_start->until = vzictime; + + /* Mark all the changes as output. */ + vzictime_start->output = TRUE; + for (elem = matching_elements; elem; elem = elem->next) { + vzictime = elem->data; + vzictime->output = TRUE; + } + + g_list_free (matching_elements); + + return TRUE; +} + + +static void +check_for_rdates (FILE *fp, + GArray *changes, + int idx) +{ + VzicTime *vzictime_start, *vzictime, tmp_vzictime; + gboolean is_daylight_start, is_daylight; + int i, year, month, day, time; + + vzictime_start = &g_array_index (changes, VzicTime, idx); + + is_daylight_start = (vzictime_start->stdoff != vzictime_start->walloff) + ? TRUE : FALSE; + +#if 0 + printf ("\nChecking: %s OFFSETFROM: %i %s\n", + format_vzictime (vzictime_start), vzictime_start->prev_walloff, + is_daylight_start ? "DAYLIGHT" : ""); +#endif + + /* We want to go backwards through the array now, for Outlook compatability. + (It only looks at the first DTSTART/RDATE.) */ + for (i = idx + 1; i < changes->len; i++) { + vzictime = &g_array_index (changes, VzicTime, i); + + is_daylight = (vzictime->stdoff != vzictime->walloff) ? TRUE : FALSE; + + if (vzictime->output) + continue; + +#if 0 + printf (" %s OFFSETFROM: %i %s\n", format_vzictime (vzictime), + vzictime->prev_walloff, is_daylight ? "DAYLIGHT" : ""); +#endif + + /* It must be the same type of component - STANDARD or DAYLIGHT. */ + if (is_daylight != is_daylight_start) { + continue; + } + + /* The TZOFFSETFROM and TZOFFSETTO must match. */ + if (vzictime->prev_walloff != vzictime_start->prev_walloff) { + continue; + } + + if (vzictime->walloff != vzictime_start->walloff) { + continue; + } + + /* TZNAME must match. */ + if (!timezones_match (vzictime->tzname, vzictime_start->tzname)) { + continue; + } + + /* We have a match. */ + + tmp_vzictime = *vzictime; + calculate_actual_time (&tmp_vzictime, TIME_WALL, vzictime->prev_stdoff, + vzictime->prev_walloff); + + fprintf (fp, "RDATE:%s\r\n", format_time (tmp_vzictime.year, + tmp_vzictime.month, + tmp_vzictime.day_number, + tmp_vzictime.time_seconds)); + + vzictime->output = TRUE; + } +} + + +static gboolean +timezones_match (char *tzname1, + char *tzname2) +{ + if (tzname1 && tzname2 && !strcmp (tzname1, tzname2)) + return TRUE; + + if (!tzname1 && !tzname2) + return TRUE; + + return FALSE; +} + + +/* Outputs the start of a VTIMEZONE component, with the BEGIN line, + the DTSTART, TZOFFSETFROM, TZOFFSETTO & TZNAME properties. */ +static int +output_component_start (char *buffer, + VzicTime *vzictime, + gboolean output_rdate, + gboolean use_same_tz_offset) +{ + gboolean is_daylight, skip_day_offset = FALSE; + gint year, month, day, time, day_offset = 0; + GDate old_date, new_date; + char *formatted_time; + char line1[1024], line2[1024], line3[1024]; + char line4[1024], line5[1024], line6[1024]; + VzicTime tmp_vzictime; + int prev_walloff; + + is_daylight = (vzictime->stdoff != vzictime->walloff) ? TRUE : FALSE; + + tmp_vzictime = *vzictime; + day_offset = calculate_actual_time (&tmp_vzictime, TIME_WALL, + vzictime->prev_stdoff, + vzictime->prev_walloff); + + sprintf (line1, "BEGIN:%s\r\n", is_daylight ? "DAYLIGHT" : "STANDARD"); + + /* If the timezone only has one change, that means it uses the same offset + forever, so we use the same TZOFFSETFROM as the TZOFFSETTO. (If the zone + has more than one change, we don't output the first one.) */ + if (use_same_tz_offset) + prev_walloff = vzictime->walloff; + else + prev_walloff = vzictime->prev_walloff; + + if (vzictime->tzname) + sprintf (line2, "TZNAME:%s\r\n", vzictime->tzname); + else + line2[0] = '\0'; + + sprintf (line3, "TZOFFSETFROM:%s\r\n", + format_tz_offset (prev_walloff, !VzicPureOutput)); + + sprintf (line4, "TZOFFSETTO:%s\r\n", + format_tz_offset (vzictime->walloff, !VzicPureOutput)); + + formatted_time = format_time (tmp_vzictime.year, tmp_vzictime.month, + tmp_vzictime.day_number, + tmp_vzictime.time_seconds); + sprintf (line5, "DTSTART:%s\r\n", formatted_time); +#if 0 /* The RDATE matching DTSTART is unnecessary */ + if (output_rdate) + sprintf (line6, "RDATE:%s\r\n", formatted_time); + else +#endif + line6[0] = '\0'; + + sprintf (buffer, "%s%s%s%s%s%s", line1, line2, line3, line4, line5, line6); + + return day_offset; +} + + +/* Outputs the END line of the VTIMEZONE component. */ +static void +output_component_end (FILE *fp, + VzicTime *vzictime) +{ + gboolean is_daylight; + + is_daylight = (vzictime->stdoff != vzictime->walloff) ? TRUE : FALSE; + + fprintf (fp, "END:%s\r\n", is_daylight ? "DAYLIGHT" : "STANDARD"); +} + + +/* Initializes a VzicTime to 1st Jan in YEAR_MINIMUM at midnight, with all + offsets set to 0. */ +static void +vzictime_init (VzicTime *vzictime) +{ + vzictime->year = YEAR_MINIMUM; + vzictime->month = 0; + vzictime->day_code = DAY_SIMPLE; + vzictime->day_number = 1; + vzictime->day_weekday = 0; + vzictime->time_seconds = 0; + vzictime->time_code = TIME_UNIVERSAL; + vzictime->stdoff = 0; + vzictime->walloff = 0; + vzictime->is_infinite = FALSE; + vzictime->until = NULL; + vzictime->output = FALSE; + vzictime->prev_stdoff = 0; + vzictime->prev_walloff = 0; + vzictime->tzname = NULL; +} + + +/* This calculates the actual local time that a change will occur, given + the offsets from standard and wall-clock time. It returns -1 or 1 if it + had to move backwards or forwards one day while converting to local time. + If it does this then we need to change the RRULEs we output. */ +static int +calculate_actual_time (VzicTime *vzictime, + TimeCode time_code, + int stdoff, + int walloff) +{ + GDate date; + gint day_offset, days_in_month, weekday, offset, result; + + vzictime->time_seconds = calculate_wall_time (vzictime->time_seconds, + vzictime->time_code, + stdoff, walloff, &day_offset); + + if (vzictime->day_code != DAY_SIMPLE) { + if (vzictime->year == YEAR_MINIMUM || vzictime->year == YEAR_MAXIMUM) { + fprintf (stderr, "In calculate_actual_time: invalid year\n"); + exit (0); + } + + g_date_clear (&date, 1); + days_in_month = g_date_days_in_month (vzictime->month + 1, vzictime->year); + + /* Note that the day_code refers to the date before we convert it to + a wall-clock date and time. So we find the day it was referring to, + then make any adjustments needed due to converting the time. */ + if (vzictime->day_code == DAY_LAST_WEEKDAY) { + /* Find out what day the last day of the month is. */ + g_date_set_dmy (&date, days_in_month, vzictime->month + 1, + vzictime->year); + weekday = g_date_weekday (&date) % 7; + + /* Calculate how many days we have to go back to get to day_weekday. */ + offset = (weekday + 7 - vzictime->day_weekday) % 7; + + vzictime->day_number = days_in_month - offset; + } else { + /* Find out what day day_number actually is. */ + g_date_set_dmy (&date, vzictime->day_number, vzictime->month + 1, + vzictime->year); + weekday = g_date_weekday (&date) % 7; + + if (vzictime->day_code == DAY_WEEKDAY_ON_OR_AFTER) + offset = (vzictime->day_weekday + 7 - weekday) % 7; + else + offset = - ((weekday + 7 - vzictime->day_weekday) % 7); + + vzictime->day_number = vzictime->day_number + offset; + } + + vzictime->day_code = DAY_SIMPLE; + + if (vzictime->day_number <= 0 || vzictime->day_number > days_in_month) { + fprintf (stderr, "Day overflow: %i\n", vzictime->day_number); + exit (1); + } + } + +#if 0 + fprintf (stderr, "%s -> %i/%i/%i\n", + dump_day_coded (vzictime->day_code, vzictime->day_number, + vzictime->day_weekday), + vzictime->day_number, vzictime->month + 1, vzictime->year); +#endif + + fix_time_overflow (&vzictime->year, &vzictime->month, + &vzictime->day_number, day_offset); + + /* If we want UTC time, we have to convert it now. */ + if (time_code == TIME_UNIVERSAL) { + vzictime->time_seconds = calculate_until_time (vzictime->time_seconds, + TIME_WALL, stdoff, walloff, + &vzictime->year, + &vzictime->month, + &vzictime->day_number); + } + + return day_offset; +} + + +/* This converts the given time into universal time (UTC), to be used in + the UNTIL property. */ +static int +calculate_until_time (int time, + TimeCode time_code, + int stdoff, + int walloff, + int *year, + int *month, + int *day) +{ + int result, day_offset; + + day_offset = 0; + + switch (time_code) { + case TIME_WALL: + result = time - walloff; + break; + case TIME_STANDARD: + result = time - stdoff; + break; + case TIME_UNIVERSAL: + return time; + default: + fprintf (stderr, "Invalid time code\n"); + exit (1); + } + + if (result < 0) { + result += 24 * 60 * 60; + day_offset = -1; + } else if (result >= 24 * 60 * 60) { + result -= 24 * 60 * 60; + day_offset = 1; + } + + /* Sanity check - we shouldn't have an overflow any more. */ + if (result < 0 || result >= 24 * 60 * 60) { + fprintf (stderr, "Time overflow: %i\n", result); + abort (); + } + + fix_time_overflow (year, month, day, day_offset); + + return result; +} + + +/* This converts the given time into wall clock time (the local standard time + with any adjustment for daylight-saving). */ +static int +calculate_wall_time (int time, + TimeCode time_code, + int stdoff, + int walloff, + int *day_offset) +{ + int result; + + *day_offset = 0; + + switch (time_code) { + case TIME_WALL: + /* We don't just return here so we can handle 24:00:00 below */ + result = time; + break; + case TIME_STANDARD: + /* We have a local standard time, so we have to subtract stdoff to get + back to UTC, then add walloff to get wall time. */ + result = time - stdoff + walloff; + break; + case TIME_UNIVERSAL: + result = time + walloff; + break; + default: + fprintf (stderr, "Invalid time code\n"); + exit (1); + } + + if (result < 0) { + result += 24 * 60 * 60; + *day_offset = -1; + } else if (result >= 24 * 60 * 60) { + result -= 24 * 60 * 60; + *day_offset = 1; + } + + /* Sanity check - we shouldn't have an overflow any more. */ + if (result < 0 || result >= 24 * 60 * 60) { + fprintf (stderr, "Time overflow: %i\n", result); + exit (1); + } + +#if 0 + printf ("%s -> ", dump_time (time, time_code, TRUE)); + printf ("%s (%i)\n", dump_time (result, TIME_WALL, TRUE), *day_offset); +#endif + + return result; +} + + +static void +fix_time_overflow (int *year, + int *month, + int *day, + int day_offset) +{ + if (day_offset == -1) { + *day = *day - 1; + + if (*day == 0) { + *month = *month - 1; + if (*month == -1) { + *month = 11; + *year = *year - 1; + } + *day = g_date_days_in_month (*month + 1, *year); + } + } else if (day_offset == 1) { + *day = *day + 1; + + if (*day > g_date_days_in_month (*month + 1, *year)) { + *month = *month + 1; + if (*month == 12) { + *month = 0; + *year = *year + 1; + } + *day = 1; + } + } +} + + +static char* +format_time (int year, + int month, + int day, + int time) +{ + static char buffer[128]; + int hour, minute, second; + + /* When we are outputting the first component year will be YEAR_MINIMUM. + We used to use 1 when outputting this, but Outlook doesn't like any years + less that 1600, so we use 1600 instead. We don't output the first change + for most zones now, so it doesn't matter too much. */ + if (year == YEAR_MINIMUM) + year = 1601; + + /* We just use 9999 here, so we keep to 4 characters. But this should only + be needed when debugging - it shouldn't be needed in the VTIMEZONEs. */ + if (year == YEAR_MAXIMUM) { + fprintf (stderr, "format_time: YEAR_MAXIMUM used\n"); + year = 9999; + } + + hour = time / 3600; + minute = (time % 3600) / 60; + second = time % 60; + + sprintf (buffer, "%04i%02i%02iT%02i%02i%02i", + year, month + 1, day, hour, minute, second); + + return buffer; +} + + +/* Outlook doesn't support 6-digit values, i.e. including the seconds, so + we round to the nearest minute. No current offsets use the seconds value, + so we aren't losing much. */ +static char* +format_tz_offset (int tz_offset, + gboolean round_seconds) +{ + static char buffer[128]; + char *sign = "+"; + int hours, minutes, seconds; + + if (tz_offset < 0) { + tz_offset = -tz_offset; + sign = "-"; + } + + if (round_seconds) + tz_offset += 30; + + hours = tz_offset / 3600; + minutes = (tz_offset % 3600) / 60; + seconds = tz_offset % 60; + + if (round_seconds) + seconds = 0; + + /* Sanity check. Standard timezone offsets shouldn't be much more than 12 + hours, and daylight saving shouldn't change it by more than a few hours. + (The maximum offset is 15 hours 56 minutes at present.) */ + if (hours < 0 || hours >= 24 || minutes < 0 || minutes >= 60 + || seconds < 0 || seconds >= 60) { + fprintf (stderr, "WARNING: Strange timezone offset: H:%i M:%i S:%i\n", + hours, minutes, seconds); + } + + if (seconds == 0) + sprintf (buffer, "%s%02i%02i", sign, hours, minutes); + else + sprintf (buffer, "%s%02i%02i%02i", sign, hours, minutes, seconds); + + return buffer; +} + + +static gboolean +output_rrule (char *rrule_buffer, + int month, + DayCode day_code, + int day_number, + int day_weekday, + int day_offset, + char *until) +{ + char buffer[1024], buffer2[1024]; + + buffer[0] = '\0'; + + if (day_offset > 1 || day_offset < -1) { + fprintf (stderr, "Invalid day_offset: %i\n", day_offset); + exit (0); + } + + /* If the DTSTART time was moved to another day when converting to local + time, we need to adjust the RRULE accordingly. e.g. If the original RRULE + was on the 19th of the month, but DTSTART was moved 1 day forward, then + we output the 20th of the month instead. */ + if (day_offset == 1) { + if (day_code != DAY_LAST_WEEKDAY) + day_number++; + day_weekday = (day_weekday + 1) % 7; + + /* Check we don't use February 29th. */ + if (month == 1 && day_number > 28) { + fprintf (stderr, "Can't format RRULE - out of bounds. Month: %i Day number: %i\n", month + 1, day_number); + exit (0); + } + + /* If we go past the end of the month, move to the next month. */ + if (day_code != DAY_LAST_WEEKDAY && day_number > DaysInMonth[month]) { + month++; + day_number = 1; + } + + } else if (day_offset == -1) { + if (day_code != DAY_LAST_WEEKDAY) + day_number--; + day_weekday = (day_weekday + 6) % 7; + + if (day_code != DAY_LAST_WEEKDAY && day_number < 1) + fprintf (stderr, "Month: %i Day number: %i\n", month + 1, day_number); + } + + switch (day_code) { + case DAY_SIMPLE: + /* Outlook (2000) will not parse the simple YEARLY RRULEs in VTIMEZONEs, + or BYMONTHDAY, or BYYEARDAY, which makes this option difficult! + Currently we use something like BYDAY=1SU, which will be incorrect + at times. This only affects Asia/Baghdad, Asia/Gaza, Asia/Jerusalem & + Asia/Damascus at present (and Jerusalem doesn't have specific rules + at the moment anyway, so that isn't a big loss). */ + if (!VzicPureOutput) { + if (day_number < 8) { + printf ("WARNING: %s: Outputting BYDAY=1SU instead of BYMONTHDAY=1-7 for Outlook compatability\n", CurrentZoneName); + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=1SU", + month + 1); + } else if (day_number < 15) { + printf ("WARNING: %s: Outputting BYDAY=2SU instead of BYMONTHDAY=8-14 for Outlook compatability\n", CurrentZoneName); + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=2SU", + month + 1); + } else if (day_number < 22) { + printf ("WARNING: %s: Outputting BYDAY=3SU instead of BYMONTHDAY=15-21 for Outlook compatability\n", CurrentZoneName); + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=3SU", + month + 1); + } else { + printf ("ERROR: %s: Couldn't output RRULE (day=%i) compatible with Outlook\n", CurrentZoneName, day_number); + exit (1); + } + } else { + sprintf (buffer, "RRULE:FREQ=YEARLY"); + } + break; + + case DAY_WEEKDAY_ON_OR_AFTER: + if (day_number > DaysInMonth[month] - 6) { + /* This isn't actually needed at present. */ +#if 0 + fprintf (stderr, "DAY_WEEKDAY_ON_OR_AFTER: %i %i\n", day_number, + month + 1); +#endif + + if (!VzicPureOutput) { + printf ("ERROR: %s: Couldn't output RRULE (day>=x) compatible with Outlook\n", CurrentZoneName); + exit (1); + } else { + /* We do 6 days at the end of this month, and 1 at the start of the + next. We can't do this if we want Outlook compatability, as it + needs BYMONTHDAY, which Outlook doesn't support. */ +/* + sprintf (buffer, + "RRULE:FREQ=YEARLY;BYMONTH=%i;BYMONTHDAY=%i,%i,%i,%i,%i,%i;BYDAY=%s", + month + 1, + day_number, day_number + 1, day_number + 2, day_number + 3, + day_number + 4, day_number + 5, + WeekDays[day_weekday]); + + sprintf (buffer2, + "RRULE:FREQ=YEARLY;BYMONTH=%i;BYMONTHDAY=1;BYDAY=%s", + (month + 1) % 12 + 1, + WeekDays[day_weekday]); + + sprintf (rrule_buffer, "%s%s\n%s%s\r\n", + buffer, until, buffer2, until); +*/ + /* Multiple RRULEs within the component are illegal according to new iCal RFC 5545, + so combine the above RRULEs (commented) into a single RRULE using BYYEARDAY */ + day_number = 0; + int i; + for (i = month+1; i < 12; i++) { + day_number += DaysInMonth[i]; + } + sprintf (rrule_buffer, "RRULE:FREQ=YEARLY;BYYEARDAY=-%i,-%i,-%i,-%i,-%i,-%i,-%i;BYDAY=%s%s\r\n", + day_number, day_number+1, day_number+2, day_number+3, + day_number+4, day_number+5, day_number+6, WeekDays[day_weekday], until); + + return TRUE; + } + } + + if (!output_rrule_2 (buffer, month, day_number, day_weekday)) + return FALSE; + + break; + + case DAY_WEEKDAY_ON_OR_BEFORE: + if (day_number < 7) { + /* FIXME: This is unimplemented, but it isn't needed at present anway. */ + fprintf (stderr, "DAY_WEEKDAY_ON_OR_BEFORE: %i. Unimplemented. Exiting...\n", day_number); + exit (0); + } + + if (!output_rrule_2 (buffer, month, day_number - 6, day_weekday)) + return FALSE; + + break; + + case DAY_LAST_WEEKDAY: + if (day_offset == 1) { + if (month == 1) { + fprintf (stderr, "DAY_LAST_WEEKDAY - day moved, in February - can't fix\n"); + exit (0); + } + + /* This is only used once at present, for Africa/Cairo. */ +#if 0 + fprintf (stderr, "DAY_LAST_WEEKDAY - day moved\n"); +#endif + + if (!VzicPureOutput) { + printf ("WARNING: %s: Modifying RRULE (last weekday) for Outlook compatability\n", CurrentZoneName); + sprintf (buffer, + "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=-1%s", + month + 1, WeekDays[day_weekday]); + printf (" Outputting: %s\n", buffer); + } else { + /* We do 6 days at the end of this month, and 1 at the start of the + next. We can't do this if we want Outlook compatability, as it needs + BYMONTHDAY, which Outlook doesn't support. */ +/* + day_number = DaysInMonth[month]; + sprintf (buffer, + "RRULE:FREQ=YEARLY;BYMONTH=%i;BYMONTHDAY=%i,%i,%i,%i,%i,%i;BYDAY=%s", + month + 1, + day_number - 5, day_number - 4, day_number - 3, + day_number - 2, day_number - 1, day_number, + WeekDays[day_weekday]); + + sprintf (buffer2, + "RRULE:FREQ=YEARLY;BYMONTH=%i;BYMONTHDAY=1;BYDAY=%s", + (month + 1) % 12 + 1, + WeekDays[day_weekday]); + + sprintf (rrule_buffer, "%s%s\r\n%s%s\r\n", + buffer, until, buffer2, until); +*/ + /* Multiple RRULEs within the component are illegal according to new iCal RFC 5545, + so combine the above RRULEs (commented) into a single RRULE using BYYEARDAY */ + day_number = 0; + int i; + for (i = month+1; i < 12; i++) { + day_number += DaysInMonth[i]; + } + sprintf (rrule_buffer, "RRULE:FREQ=YEARLY;BYYEARDAY=-%i,-%i,-%i,-%i,-%i,-%i,-%i;BYDAY=%s%s\r\n", + day_number, day_number+1, day_number+2, day_number+3, + day_number+4, day_number+5, day_number+6, WeekDays[day_weekday], until); + + return TRUE; + } + + } else if (day_offset == -1) { + /* We do 7 days 1 day before the end of this month. */ + day_number = DaysInMonth[month]; + + if (!output_rrule_2 (buffer, month, day_number - 7, day_weekday)) + return FALSE; + + sprintf (rrule_buffer, "%s%s\r\n", buffer, until); + return TRUE; + } + + sprintf (buffer, + "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=-1%s", + month + 1, WeekDays[day_weekday]); + break; + + default: + fprintf (stderr, "Invalid day code\n"); + exit (1); + } + + sprintf (rrule_buffer, "%s%s\r\n", buffer, until); + return TRUE; +} + + +/* This tries to convert a RRULE like 'BYMONTHDAY=8,9,10,11,12,13,14;BYDAY=FR' + into 'BYDAY=2FR'. We need this since Outlook doesn't accept BYMONTHDAY. + It returns FALSE if conversion is not possible. */ +static gboolean +output_rrule_2 (char *buffer, + int month, + int day_number, + int day_weekday) +{ + + if (day_number == 1) { + /* Convert it to a BYDAY=1SU type of RRULE. */ + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=1%s", + month + 1, WeekDays[day_weekday]); + + } else if (day_number == 8) { + /* Convert it to a BYDAY=2SU type of RRULE. */ + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=2%s", + month + 1, WeekDays[day_weekday]); + + } else if (day_number == 15) { + /* Convert it to a BYDAY=3SU type of RRULE. */ + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=3%s", + month + 1, WeekDays[day_weekday]); + + } else if (day_number == 22) { + /* Convert it to a BYDAY=4SU type of RRULE. (Currently not used.) */ + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=4%s", + month + 1, WeekDays[day_weekday]); + + } else if (month != 1 && day_number == DaysInMonth[month] - 6) { + /* Convert it to a BYDAY=-1SU type of RRULE. (But never for February.) */ + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=-1%s", + month + 1, WeekDays[day_weekday]); + + } else { + /* Can't convert to a correct RRULE. If we want Outlook compatability we + have to use a slightly incorrect RRULE, so the time change will be 1 + week out every 7 or so years. Alternatively we could possibly move the + change by an hour or so so we would always be 1 or 2 hours out, but + never 1 week out. Yes, that sounds a better idea. */ + if (!VzicPureOutput) { + printf ("WARNING: %s: Modifying RRULE to be compatible with Outlook (day >= %i, month = %i)\n", CurrentZoneName, day_number, month + 1); + + if (day_number == 2) { + /* Convert it to a BYDAY=1SU type of RRULE. + This is needed for Asia/Karachi. */ + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=1%s", + month + 1, WeekDays[day_weekday]); + } else if (day_number == 9) { + /* Convert it to a BYDAY=2SU type of RRULE. + This is needed for Antarctica/Palmer & America/Santiago. */ + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=2%s", + month + 1, WeekDays[day_weekday]); + } else if (month != 1 && day_number <= DaysInMonth[month] - 7 && day_number > DaysInMonth[month] - 14) { + /* Convert it to a BYDAY=-1SU type of RRULE. (But never for February.) + This is needed for America/Godthab. */ + sprintf (buffer, "RRULE:FREQ=YEARLY;BYMONTH=%i;BYDAY=-1%s", + month + 1, WeekDays[day_weekday]); + } else { + printf ("ERROR: %s: Couldn't modify RRULE to be compatible with Outlook (day >= %i, month = %i)\n", CurrentZoneName, day_number, month + 1); + exit (1); + } + + } else { + sprintf (buffer, + "RRULE:FREQ=YEARLY;BYMONTH=%i;BYMONTHDAY=%i,%i,%i,%i,%i,%i,%i;BYDAY=%s", + month + 1, + day_number, day_number + 1, day_number + 2, day_number + 3, + day_number + 4, day_number + 5, day_number + 6, + WeekDays[day_weekday]); + } + } + + return TRUE; +} + + +static char* +format_vzictime (VzicTime *vzictime) +{ + static char buffer[1024]; + + sprintf (buffer, "%s %2i %s %s %i %i %s", + dump_year (vzictime->year), vzictime->month + 1, + dump_day_coded (vzictime->day_code, vzictime->day_number, + vzictime->day_weekday), + dump_time (vzictime->time_seconds, vzictime->time_code, TRUE), + vzictime->stdoff, vzictime->walloff, + vzictime->is_infinite ? "INFINITE" : ""); + + return buffer; +} + + +static void +dump_changes (FILE *fp, + char *zone_name, + GArray *changes) +{ + VzicTime *vzictime, *vzictime2 = NULL; + int i, year_offset, year; + + for (i = 0; i < changes->len; i++) { + vzictime = &g_array_index (changes, VzicTime, i); + + if (vzictime->year > MAX_CHANGES_YEAR) + return; + + dump_change (fp, zone_name, vzictime, vzictime->year); + } + + if (changes->len < 2) + return; + + /* Now see if the changes array ends with a pair of recurring changes. */ + vzictime = &g_array_index (changes, VzicTime, changes->len - 2); + vzictime2 = &g_array_index (changes, VzicTime, changes->len - 1); + if (!vzictime->is_infinite || !vzictime2->is_infinite) + return; + + year_offset = 1; + for (;;) { + year = vzictime->year + year_offset; + if (year > MAX_CHANGES_YEAR) + break; + dump_change (fp, zone_name, vzictime, year); + + year = vzictime2->year + year_offset; + if (year > MAX_CHANGES_YEAR) + break; + dump_change (fp, zone_name, vzictime2, year); + + year_offset++; + } +} + + +static void +dump_change (FILE *fp, + char *zone_name, + VzicTime *vzictime, + int year) +{ + int hour, minute, second; + VzicTime tmp_vzictime; + static char *months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + + /* Output format is: + + Zone-Name [tab] Date [tab] Time [tab] UTC-Offset + + The Date and Time fields specify the time change in UTC. + + The UTC Offset is for local (wall-clock) time. It is the amount of time + to add to UTC to get local time. + */ + + fprintf (fp, "%s\t", zone_name); + + if (year == YEAR_MINIMUM) { + fprintf (fp, " 1 Jan 0001\t 0:00:00", zone_name); + } else if (year == YEAR_MAXIMUM) { + fprintf (stderr, "Maximum year found in change time\n"); + exit (1); + } else { + tmp_vzictime = *vzictime; + tmp_vzictime.year = year; + calculate_actual_time (&tmp_vzictime, TIME_UNIVERSAL, + vzictime->prev_stdoff, vzictime->prev_walloff); + + hour = tmp_vzictime.time_seconds / 3600; + minute = (tmp_vzictime.time_seconds % 3600) / 60; + second = tmp_vzictime.time_seconds % 60; + + fprintf (fp, "%2i %s %04i\t%2i:%02i:%02i", + tmp_vzictime.day_number, months[tmp_vzictime.month], + tmp_vzictime.year, hour, minute, second); + } + + fprintf (fp, "\t%s", format_tz_offset (vzictime->walloff, FALSE)); + + fprintf (fp, "\r\n"); +} + + +void +ensure_directory_exists (char *directory) +{ + struct stat filestat; + + if (stat (directory, &filestat) != 0) { + /* If the directory doesn't exist, try to create it. */ + if (errno == ENOENT) { + if (mkdir (directory, 0777) != 0) { + fprintf (stderr, "Can't create directory: %s\n", directory); + exit (1); + } + } else { + fprintf (stderr, "Error calling stat() on directory: %s\n", directory); + exit (1); + } + } else if (!S_ISDIR (filestat.st_mode)) { + fprintf (stderr, "Can't create directory, already exists: %s\n", + directory); + exit (1); + } +} + + +static void +expand_tzid_prefix (void) +{ + char *src, *dest; + char date_buf[16]; + char ch1, ch2; + time_t t; + struct tm *tm; + + /* Get today's date as a string in the format "YYYYMMDD". */ + t = time (NULL); + tm = localtime (&t); + sprintf (date_buf, "%4i%02i%02i", tm->tm_year + 1900, + tm->tm_mon + 1, tm->tm_mday); + + src = TZIDPrefix; + dest = TZIDPrefixExpanded; + + while (ch1 = *src++) { + + /* Look for a '%'. */ + if (ch1 == '%') { + ch2 = *src++; + + if (ch2 == 'D') { + /* '%D' gets expanded into the date string. */ + strcpy (dest, date_buf); + dest += strlen (dest); + } else if (ch2 == '%') { + /* '%%' gets converted into one '%'. */ + *dest++ = '%'; + } else { + /* Anything else is output as is. */ + *dest++ = '%'; + *dest++ = ch2; + } + } else { + *dest++ = ch1; + } + } + +#if 0 + printf ("TZID : %s\n", TZIDPrefix); + printf ("Expanded: %s\n", TZIDPrefixExpanded); +#endif +}
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic-output.h
Added
@@ -0,0 +1,39 @@ +/* + * Vzic - a program to convert Olson timezone database files into VZTIMEZONE + * files compatible with the iCalendar specification (RFC2445). + * + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2003 Damon Chaplin. + * + * Author: Damon Chaplin <damon@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef _VZIC_OUTPUT_H_ +#define _VZIC_OUTPUT_H_ + +#include <glib.h> + +void output_vtimezone_files (char *directory, + GArray *zone_data, + GHashTable *rule_data, + GHashTable *link_data, + GHashTable *zones_hash, + int max_until_year); + +void ensure_directory_exists (char *directory); + +#endif /* _VZIC_OUTPUT_H_ */
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic-parse.c
Added
@@ -0,0 +1,950 @@ +/* + * Vzic - a program to convert Olson timezone database files into VZTIMEZONE + * files compatible with the iCalendar specification (RFC2445). + * + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2003 Damon Chaplin. + * + * Author: Damon Chaplin <damon@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +#include <ctype.h> +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <libgen.h> + +#include "vzic.h" +#include "vzic-parse.h" + +/* This is the maximum line length we allow. */ +#define MAX_LINE_LEN 1024 + +/* The maximum number of fields on a line. */ +#define MAX_FIELDS 12 + +#define CREATE_SYMLINK 1 + +typedef enum +{ + ZONE_ID = 0, /* The 'Zone' at the start of the line. */ + ZONE_NAME = 1, + ZONE_GMTOFF = 2, + ZONE_RULES_SAVE = 3, + ZONE_FORMAT = 4, + ZONE_UNTIL_YEAR = 5, + ZONE_UNTIL_MONTH = 6, + ZONE_UNTIL_DAY = 7, + ZONE_UNTIL_TIME = 8 +} ZoneFieldNumber; + + +typedef enum +{ + RULE_ID = 0, /* The 'Rule' at the start of the line. */ + RULE_NAME = 1, + RULE_FROM = 2, + RULE_TO = 3, + RULE_TYPE = 4, + RULE_IN = 5, + RULE_ON = 6, + RULE_AT = 7, + RULE_SAVE = 8, + RULE_LETTER_S = 9 +} RuleFieldNumber; + + +typedef enum +{ + LINK_ID = 0, /* The 'Link' at the start of the line. */ + LINK_FROM = 1, + LINK_TO = 2 +} LinkFieldNumber; + + +/* This struct contains information used while parsing the files, and is + passed to most parsing functions. */ +typedef struct _ParsingData ParsingData; +struct _ParsingData +{ + /* This is the line being parsed. buffer is a copy that we break into fields + and sub-fields as it is parsed. */ + char line[MAX_LINE_LEN]; + char buffer[MAX_LINE_LEN]; + + /* These are pointers to the start of each field in buffer. */ + char *fields[MAX_FIELDS]; + int num_fields; + + /* These are just for producing error messages. */ + char *filename; + int line_number; + + + /* This is an array of ZoneData structs, 1 for each timezone read. */ + GArray *zone_data; + + /* This is a hash table of arrays of RuleData structs. As each Rule line is + read in, a new RuleData struct is filled in and appended to the + appropriate GArray in the hash table. */ + GHashTable *rule_data; + + /* A hash containing data on the Link lines. The keys are the timezones + where the link is from (i.e. the timezone we will be outputting anyway) + and the data is a GList of timezones to link to (where we will copy the + timezone data to). */ + GHashTable *link_data; + + int max_until_year; +}; + + +/* + * Parsing functions, used when reading the Olson timezone data file. + */ +static void parse_fields (ParsingData *data); +static gboolean parse_zone_line (ParsingData *data); +static gboolean parse_zone_continuation_line (ParsingData *data); +static gboolean parse_zone_common (ParsingData *data, + int offset); +static void parse_rule_line (ParsingData *data); +static void parse_link_line (ParsingData *data); + +static int parse_year (ParsingData *data, + char *field, + gboolean accept_only, + int only_value); +static int parse_month (ParsingData *data, + char *field); +static DayCode parse_day (ParsingData *data, + char *field, + int *day, + int *weekday); +static int parse_weekday (ParsingData *data, + char *field); +static int parse_time (ParsingData *data, + char *field, + TimeCode *time_code); +static int parse_number (ParsingData *data, + char **num); +static int parse_rules_save (ParsingData *data, + char *field, + char **rules); + +static void parse_coord (char *coord, + int len, + int *result); + +void +parse_olson_file (char *filename, + GArray **zone_data, + GHashTable **rule_data, + GHashTable **link_data, + int *max_until_year) +{ + ParsingData data; + FILE *fp; + int zone_continues = 0; + + *zone_data = g_array_new (FALSE, FALSE, sizeof (ZoneData)); + *rule_data = g_hash_table_new (g_str_hash, g_str_equal); + *link_data = g_hash_table_new (g_str_hash, g_str_equal); + + fp = fopen (filename, "r"); + if (!fp) { + fprintf (stderr, "Couldn't open file: %s\n", filename); + exit (1); + } + + data.filename = filename; + data.zone_data = *zone_data; + data.rule_data = *rule_data; + data.link_data = *link_data; + data.max_until_year = 0; + + for (data.line_number = 0; ; data.line_number++) { + if (fgets (data.line, sizeof (data.line), fp) != data.line) + break; + + strcpy (data.buffer, data.line); + + parse_fields (&data); + if (data.num_fields == 0) + continue; + + if (zone_continues) { + zone_continues = parse_zone_continuation_line (&data); + } else if (!strcmp (data.fields[0], "Zone")) { + zone_continues = parse_zone_line (&data); + } else if (!strcmp (data.fields[0], "Rule")) { + parse_rule_line (&data); + } else if (!strcmp (data.fields[0], "Link")) { + parse_link_line (&data); + } else if (!strcmp (data.fields[0], "Leap")) { + /* We don't care about Leap lines. */ + } else { + fprintf (stderr, "%s:%i: Invalid line.\n%s\n", filename, + data.line_number, data.line); + exit (1); + } + } + + if (ferror (fp)) { + fprintf (stderr, "Error reading file: %s\n", filename); + exit (1); + } + + if (zone_continues) { + fprintf (stderr, "%s:%i: Zone continuation line expected.\n%s\n", + filename, data.line_number, data.line); + exit (1); + } + + fclose (fp); + +#if 0 + printf ("Max UNTIL year: %i\n", data.max_until_year); +#endif + *max_until_year = data.max_until_year; +} + + +/* Converts the line into fields. */ +static void +parse_fields (ParsingData *data) +{ + int i; + char *p, *s, ch; + + /* Reset all fields to NULL. */ + for (i = 0; i < MAX_FIELDS; i++) + data->fields[i] = 0; + + data->num_fields = 0; + p = data->buffer; + + for (;;) { + /* Skip whitespace. */ + while (isspace (*p)) + p++; + + /* See if we have reached the end of the line or a comment. */ + if (*p == '\0' || *p == '#') + break; + + /* We must have another field, so save the start position. */ + data->fields[data->num_fields++] = p; + + /* Now find the end of the field. If the field contains '"' characters + they are removed and we have to move the rest of the chars back. */ + s = p; + for (;;) { + ch = *p; + if (ch == '\0' || ch == '#') { + /* Don't move p on since this is the end of the line. */ + *s = '\0'; + break; + } else if (isspace (ch)) { + *s = '\0'; + p++; + break; + } else if (ch == '"') { + p++; + for (;;) { + ch = *p; + if (ch == '\0') { + fprintf (stderr, + "%s:%i: Closing quote character ('\"') missing.\n%s\n", + data->filename, data->line_number, data->line); + exit (1); + } else if (ch == '"') { + p++; + break; + } else { + *s++ = ch; + } + p++; + } + } else { + *s++ = ch; + } + p++; + } + } + +#if 0 + printf ("%i fields: ", data->num_fields); + for (i = 0; i < data->num_fields; i++) + printf ("'%s' ", data->fields[i]); + printf ("\n"); +#endif +} + + +static gboolean +parse_zone_line (ParsingData *data) +{ + ZoneData zone; + + /* All 5 fields up to FORMAT must be present. */ + if (data->num_fields < 5 || data->num_fields > 9) { + fprintf (stderr, "%s:%i: Invalid Zone line - %i fields.\n%s\n", + data->filename, data->line_number, data->num_fields, + data->line); + exit (1); + } + + zone.zone_name = g_strdup (data->fields[ZONE_NAME]); + zone.zone_line_data = g_array_new (FALSE, FALSE, sizeof (ZoneLineData)); + + g_array_append_val (data->zone_data, zone); + + return parse_zone_common (data, 0); +} + + +static gboolean +parse_zone_continuation_line (ParsingData *data) +{ + /* All 3 fields up to FORMAT must be present. */ + if (data->num_fields < 3 || data->num_fields > 7) { + fprintf (stderr, + "%s:%i: Invalid Zone continuation line - %i fields.\n%s\n", + data->filename, data->line_number, data->num_fields, + data->line); + exit (1); + } + + return parse_zone_common (data, -2); +} + + +static gboolean +parse_zone_common (ParsingData *data, + int offset) +{ + ZoneData *zone; + ZoneLineData zone_line; + TimeCode time_code; + + zone_line.stdoff_seconds = parse_time (data, + data->fields[ZONE_GMTOFF + offset], + &time_code); + zone_line.save_seconds = parse_rules_save (data, + data->fields[ZONE_RULES_SAVE + offset], + &zone_line.rules); + + if (!VzicPureOutput) { + /* We round the UTC offsets to the nearest minute, to be compatible with + Outlook. This also works with -ve numbers, I think. + -56 % 60 = -59. -61 % 60 = -1. */ + if (zone_line.stdoff_seconds >= 0) + zone_line.stdoff_seconds += 30; + else + zone_line.stdoff_seconds -= 29; + zone_line.stdoff_seconds -= zone_line.stdoff_seconds % 60; + + if (zone_line.save_seconds >= 0) + zone_line.save_seconds += 30; + else + zone_line.save_seconds -= 29; + zone_line.save_seconds -= zone_line.save_seconds % 60; + } + + zone_line.format = g_strdup (data->fields[ZONE_FORMAT + offset]); + + if (data->num_fields - offset >= 6) { + zone_line.until_set = TRUE; + zone_line.until_year = parse_year (data, + data->fields[ZONE_UNTIL_YEAR + offset], + FALSE, 0); + zone_line.until_month = parse_month (data, + data->fields[ZONE_UNTIL_MONTH + offset]); + zone_line.until_day_code = parse_day (data, + data->fields[ZONE_UNTIL_DAY + offset], + &zone_line.until_day_number, + &zone_line.until_day_weekday); + zone_line.until_time_seconds = parse_time (data, + data->fields[ZONE_UNTIL_TIME + offset], + &zone_line.until_time_code); + + /* We also want to know the maximum year used in any UNTIL value, so we + know where to expand all the infinite Rule data to. */ + if (zone_line.until_year != YEAR_MAXIMUM + && zone_line.until_year != YEAR_MINIMUM) + data->max_until_year = MAX (data->max_until_year, zone_line.until_year); + + } else { + zone_line.until_set = FALSE; + } + + /* Append it to the last Zone, since that is the one we are currently + reading. */ + zone = &g_array_index (data->zone_data, ZoneData, data->zone_data->len - 1); + g_array_append_val (zone->zone_line_data, zone_line); + + return zone_line.until_set; +} + + +static void +parse_rule_line (ParsingData *data) +{ + GArray *rule_array; + RuleData rule; + char *name; + TimeCode time_code; + + /* All 10 fields must be present. */ + if (data->num_fields != 10) { + fprintf (stderr, "%s:%i: Invalid Rule line - %i fields.\n%s\n", + data->filename, data->line_number, data->num_fields, + data->line); + exit (1); + } + + name = data->fields[RULE_NAME]; + + /* Create the GArray and add it to the hash table if it doesn't already + exist. */ + rule_array = g_hash_table_lookup (data->rule_data, name); + if (!rule_array) { + rule_array = g_array_new (FALSE, FALSE, sizeof (RuleData)); + g_hash_table_insert (data->rule_data, g_strdup (name), rule_array); + } + + rule.from_year = parse_year (data, data->fields[RULE_FROM], FALSE, 0); + if (rule.from_year == YEAR_MAXIMUM) { + fprintf (stderr, "%s:%i: Invalid Rule FROM value: '%s'\n", + data->filename, data->line_number, data->fields[RULE_FROM]); + exit (1); + } + + rule.to_year = parse_year (data, data->fields[RULE_TO], TRUE, + rule.from_year); + if (rule.to_year == YEAR_MINIMUM) { + fprintf (stderr, "%s:%i: Invalid Rule TO value: %s\n", + data->filename, data->line_number, data->fields[RULE_TO]); + exit (1); + } + + /* We also want to know the maximum year used in any TO/FROM value, so we + know where to expand all the infinite Rule data to. */ + if (rule.to_year != YEAR_MAXIMUM) + data->max_until_year = MAX (data->max_until_year, rule.to_year); + else if (rule.from_year != YEAR_MINIMUM) + data->max_until_year = MAX (data->max_until_year, rule.from_year); + + if (!strcmp (data->fields[RULE_TYPE], "-")) + rule.type = NULL; + else { + printf ("Type: %s\n", data->fields[RULE_TYPE]); + rule.type = g_strdup (data->fields[RULE_TYPE]); + } + + rule.in_month = parse_month (data, data->fields[RULE_IN]); + rule.on_day_code = parse_day (data, data->fields[RULE_ON], + &rule.on_day_number, &rule.on_day_weekday); + rule.at_time_seconds = parse_time (data, data->fields[RULE_AT], + &rule.at_time_code); + rule.save_seconds = parse_time (data, data->fields[RULE_SAVE], &time_code); + + if (!strcmp (data->fields[RULE_LETTER_S], "-")) { + rule.letter_s = NULL; + } else { + rule.letter_s = g_strdup (data->fields[RULE_LETTER_S]); + } + + rule.is_shallow_copy = FALSE; + + g_array_append_val (rule_array, rule); +} + + +static void +parse_link_line (ParsingData *data) +{ + char *from, *to, *old_from; + GList *zone_list; + + /* We must have 3 fields for a Link. */ + if (data->num_fields != 3) { + fprintf (stderr, "%s:%i: Invalid Rule line - %i fields.\n%s\n", + data->filename, data->line_number, data->num_fields, + data->line); + exit (1); + } + + from = data->fields[LINK_FROM]; + to = data->fields[LINK_TO]; + +#if 0 + printf ("LINK FROM: %s\tTO: %s\n", from, to); +#endif + +#if CREATE_SYMLINK + { + int len = strnlen(to,254); + int dirs = 0; + int i; + for (i = 0; i < len; i++) { + dirs += to[i] == '/' ? 1 : 0; + } + if (dirs >= 0) { + char rel_from[255]; + char to_dir[255]; + char to_path[255]; + if (dirs == 0) { + sprintf(rel_from, "%s.ics", from); + } else if (dirs == 1) { + sprintf(rel_from, "../%s.ics", from); + } else if (dirs == 2) { + sprintf(rel_from, "../../%s.ics", from); + } else { + return; + } + sprintf(to_path, "%s/%s.ics", VzicOutputDir, to); + strncpy(to_dir, to_path, 254); + ensure_directory_exists(dirname(to_dir)); + //printf("Creating symlink from %s to %s\n", rel_from, to_path); + symlink(rel_from, to_path); + } + } +#else + if (g_hash_table_lookup_extended (data->link_data, from, + (gpointer) &old_from, + (gpointer) &zone_list)) { + from = old_from; + } else { + from = g_strdup (from); + zone_list = NULL; + } + + zone_list = g_list_prepend (zone_list, g_strdup (to)); + + g_hash_table_insert (data->link_data, from, zone_list); +#endif +} + + +static int +parse_year (ParsingData *data, + char *field, + gboolean accept_only, + int only_value) +{ + int len, year = 0; + char *p; + + if (!field) { + fprintf (stderr, "%s:%i: Missing year.\n%s\n", data->filename, + data->line_number, data->line); + exit (1); + } + + len = strlen (field); + if (accept_only && !strncmp (field, "only", len)) + return only_value; + if (len >= 2) { + if (!strncmp (field, "maximum", len)) + return YEAR_MAXIMUM; + else if (!strncmp (field, "minimum", len)) + return YEAR_MINIMUM; + } + + for (p = field; *p; p++) { + if (*p < '0' || *p > '9') { + fprintf (stderr, "%s:%i: Invalid year: %s\n%s\n", data->filename, + data->line_number, field, data->line); + exit (1); + } + + year = year * 10 + *p - '0'; + } + + if (year < 1000 || year > 2038) { + fprintf (stderr, "%s:%i: Strange year: %s\n%s\n", data->filename, + data->line_number, field, data->line); + exit (1); + } + + return year; +} + + +/* Parses a month name, returning 0 (Jan) to 11 (Dec). */ +static int +parse_month (ParsingData *data, + char *field) +{ + static char* months[] = { "january", "february", "march", "april", "may", + "june", "july", "august", "september", "october", + "november", "december" }; + char *p; + int len, i; + + /* If the field is missing, it must be the optional UNTIL month, so we return + 0 for January. */ + if (!field) + return 0; + + for (p = field, len = 0; *p; p++, len++) { + *p = tolower (*p); + } + + for (i = 0; i < 12; i++) { + if (!strncmp (field, months[i], len)) + return i; + } + + fprintf (stderr, "%s:%i: Invalid month: %s\n%s\n", data->filename, + data->line_number, field, data->line); + exit (1); +} + + +/* Parses a day specifier, returning a code representing the type of match + together with a day of the month and a weekday number (0=Sun). */ +static DayCode +parse_day (ParsingData *data, + char *field, + int *day, + int *weekday) +{ + char *day_part, *p; + DayCode day_code; + + if (!field) { + *day = 1; + return DAY_SIMPLE; + } + + *day = *weekday = 0; + + if (!strncmp (field, "last", 4)) { + *weekday = parse_weekday (data, field + 4); + /* We set the day to the end of the month to make sorting Rules easy. */ + *day = 31; + return DAY_LAST_WEEKDAY; + } + + day_part = field; + day_code = DAY_SIMPLE; + + for (p = field; *p; p++) { + if (*p == '<' || *p == '>') { + if (*(p + 1) == '=') { + day_code = (*p == '<') ? DAY_WEEKDAY_ON_OR_BEFORE + : DAY_WEEKDAY_ON_OR_AFTER; + *p = '\0'; + *weekday = parse_weekday (data, field); + day_part = p + 2; + break; + } + + fprintf (stderr, "%s:%i: Invalid day: %s\n%s\n", data->filename, + data->line_number, field, data->line); + exit (1); + } + } + + for (p = day_part; *p; p++) { + if (*p < '0' || *p > '9') { + fprintf (stderr, "%s:%i: Invalid day: %s\n%s\n", data->filename, + data->line_number, field, data->line); + exit (1); + } + + *day = *day * 10 + *p - '0'; + } + + if (*day < 1 || *day > 31) { + fprintf (stderr, "%s:%i: Invalid day: %s\n%s\n", data->filename, + data->line_number, field, data->line); + exit (1); + } + + return day_code; +} + + +/* Parses a weekday name, returning 0 (Sun) to 6 (Sat). */ +static int +parse_weekday (ParsingData *data, + char *field) +{ + static char* weekdays[] = { "sunday", "monday", "tuesday", "wednesday", + "thursday", "friday", "saturday" }; + char *p; + int len, i; + + for (p = field, len = 0; *p; p++, len++) { + *p = tolower (*p); + } + + for (i = 0; i < 7; i++) { + if (!strncmp (field, weekdays[i], len)) + return i; + } + + fprintf (stderr, "%s:%i: Invalid weekday: %s\n%s\n", data->filename, + data->line_number, field, data->line); + exit (1); +} + + +/* Parses a time (hour + minute + second) and returns the result in seconds, + together with a time code specifying whether it is Wall clock time, + local standard time, or universal time. + The time can start with a '-' in which case it will be negative. */ +static int +parse_time (ParsingData *data, + char *field, + TimeCode *time_code) +{ + char *p; + int hours = 0, minutes = 0, seconds = 0, result, negative = 0; + + if (!field) { + *time_code = TIME_WALL; + return 0; + } + + p = field; + if (*p == '-') { + p++; + negative = 1; + } + + hours = parse_number (data, &p); + + if (*p == ':') { + p++; + minutes = parse_number (data, &p); + + if (*p == ':') { + p++; + seconds = parse_number (data, &p); + } + } + + if (hours < 0 || hours > 24 + || minutes < 0 || minutes > 59 + || seconds < 0 || seconds > 59 + || (hours == 24 && (minutes != 0 || seconds != 0))) { + fprintf (stderr, "%s:%i: Invalid time: %s\n%s\n", data->filename, + data->line_number, field, data->line); + exit (1); + } + +#if 0 + /* Hack to work around older libical that doesn't support BYDAY + BYYEARDAY */ + if (hours == 24) { + hours = 23; + minutes = 59; + seconds = 59; + } +#endif + +#if 0 + printf ("Time: %s -> %i:%02i:%02i\n", field, hours, minutes, seconds); +#endif + + result = hours * 3600 + minutes * 60 + seconds; + if (negative) + result = -result; + + if (*p == '\0') { + *time_code = TIME_WALL; + return result; + } + + if (*(p + 1) == '\0') { + if (*p == 'w') { + *time_code = TIME_WALL; + return result; + } else if (*p == 's') { + *time_code = TIME_STANDARD; + return result; + } else if (*p == 'u' || *p == 'g' || *p == 'z') { + *time_code = TIME_UNIVERSAL; + return result; + } + } + + fprintf (stderr, "%s:%i: Invalid time: %s\n%s\n", data->filename, + data->line_number, field, data->line); + exit (1); +} + + +/* Parses a simple number and returns the result. The pointer argument + is moved to the first character after the number. */ +static int +parse_number (ParsingData *data, + char **num) +{ + char *p; + int result; + + p = *num; + +#if 0 + printf ("In parse_number p:%s\n", p); +#endif + + // potential null value where '-' is specified. assume zero. + if (*p == NULL) { + return 0; + } + + if (*p < '0' || *p > '9') { + fprintf (stderr, "%s:%i: Invalid number: %s\n%s\n", data->filename, + data->line_number, *num, data->line); + exit (1); + } + + result = *p++ - '0'; + + while (*p >= '0' && *p <= '9') + result = result * 10 + *p++ - '0'; + + *num = p; + return result; +} + + +static int +parse_rules_save (ParsingData *data, + char *field, + char **rules) +{ + TimeCode time_code; + + *rules = NULL; + + /* Check for just "-". */ + if (field[0] == '-' && field[1] == '\0') + return 0; + + /* Check for a time to add to local standard time. We don't care about a + time code here, since it is just an offset. */ + if (*field == '-' || (*field >= '0' && *field <= '9')) + return parse_time (data, field, &time_code); + + /* It must be a rules name. */ + *rules = g_strdup (field); + return 0; +} + + + + + +GHashTable* +parse_zone_tab (char *filename) +{ + GHashTable *zones_hash; + ZoneDescription *zone_desc; + FILE *fp; + char buf[4096]; + gchar **fields, *zone_name, *latitude, *longitude, *p; + + + fp = fopen (filename, "r"); + if (!fp) { + fprintf (stderr, "Couldn't open file: %s\n", filename); + exit (1); + } + + zones_hash = g_hash_table_new (g_str_hash, g_str_equal); + + while (fgets (buf, sizeof(buf), fp)) { + if (*buf == '#') continue; + + g_strchomp (buf); + fields = g_strsplit (buf,"\t", 4); + + if (strlen (fields[0]) != 2) { + fprintf (stderr, "Invalid zone description line: %s\n", buf); + exit (1); + } + + zone_name = g_strdup (fields[2]); + + zone_desc = g_new (ZoneDescription, 1); + zone_desc->country_code[0] = fields[0][0]; + zone_desc->country_code[1] = fields[0][1]; + zone_desc->comment = (fields[3] && fields[3][0]) ? g_strdup (fields[3]) + : NULL; + + /* Now parse the latitude and longitude. */ + latitude = fields[1]; + longitude = latitude + 1; + while (*longitude != '+' && *longitude != '-') + longitude++; + + parse_coord (latitude, longitude - latitude, zone_desc->latitude); + parse_coord (longitude, strlen (longitude), zone_desc->longitude); + + g_hash_table_insert (zones_hash, zone_name, zone_desc); + +#if 0 + g_print ("Found zone: %s %i %02i %02i,%i %02i %02i\n", zone_name, + zone_desc->latitude[0], zone_desc->latitude[1], + zone_desc->latitude[2], + zone_desc->longitude[0], zone_desc->longitude[1], + zone_desc->longitude[2]); +#endif + } + + fclose (fp); + + return zones_hash; +} + + +static void +parse_coord (char *coord, + int len, + int *result) +{ + int degrees = 0, minutes = 0, seconds = 0; + + if (len == 5) + sscanf (coord + 1, "%2d%2d", °rees, &minutes); + else if (len == 6) + sscanf (coord + 1, "%3d%2d", °rees, &minutes); + else if (len == 7) + sscanf (coord + 1, "%2d%2d%2d", °rees, &minutes, &seconds); + else if (len == 8) + sscanf (coord + 1, "%3d%2d%2d", °rees, &minutes, &seconds); + else { + fprintf (stderr, "Invalid coordinate: %s\n", coord); + exit (1); + } + + if (coord[0] == '-') + degrees = -degrees; + + result[0] = degrees; + result[1] = minutes; + result[2] = seconds; +} +
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic-parse.h
Added
@@ -0,0 +1,38 @@ +/* + * Vzic - a program to convert Olson timezone database files into VZTIMEZONE + * files compatible with the iCalendar specification (RFC2445). + * + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2003 Damon Chaplin. + * + * Author: Damon Chaplin <damon@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef _VZIC_PARSE_H_ +#define _VZIC_PARSE_H_ + +#include <glib.h> + +void parse_olson_file (char *filename, + GArray **zone_data, + GHashTable **rule_data, + GHashTable **link_data, + int *max_until_year); + +GHashTable* parse_zone_tab (char *filename); + +#endif /* _VZIC_PARSE_H_ */
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic-test.pl
Added
@@ -0,0 +1,164 @@ +#!/usr/bin/perl -w + +# +# Vzic - a program to convert Olson timezone database files into VZTIMEZONE +# files compatible with the iCalendar specification (RFC2445). +# +# Copyright (C) 2001 Ximian, Inc. +# Copyright (C) 2003 Damon Chaplin. +# +# Author: Damon Chaplin <damon@gnome.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +# + +# +# This outputs an iCalendar file containing one event in each timezone, +# as well as all the VTIMEZONEs. We use it for testing compatability with +# other iCalendar apps like Outlook, by trying to import it there. +# +# Currently we have 377 timezones (with tzdata2001d). +# + +# Set this to the toplevel directory of the VTIMEZONE files. +$ZONEINFO_DIR = "/home/damon/src/zoneinfo"; + +$output_file = "calendar.ics"; + + +# Save this so we can restore it later. +$input_record_separator = $/; + +chdir $ZONEINFO_DIR + || die "Can't cd to $ZONEINFO_DIR"; + +# Create the output file, to contain all the VEVENTs & VTIMEZONEs. +open (OUTPUTFILE, ">$output_file") + || die "Can't create file: $output_file"; + +# Output the standard header. + print OUTPUTFILE <<EOF; +BEGIN:VCALENDAR +PRODID:-//Ximian//NONSGML Vzic Test//EN +VERSION:2.0 +METHOD:PUBLISH +EOF + +$zone_num = 0; + +# 365 days in a non-leap year. +@days_in_month = ( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ); + +foreach $file (`find -name "*.ics"`) { + # Get rid of './' at start and whitespace at end. + $file =~ s/^\.\///; + $file =~ s/\s+$//; + + if ($file eq $output_file) { + next; + } + +# print "File: $file\n"; + + # Get the VTIMEZONE data. + open (ZONEFILE, "$file") + || die "Can't open file: $ZONEINFO_DIR/$file"; + undef $/; + $vtimezone = <ZONEFILE>; + $/ = $input_record_separator; + close (ZONEFILE); + + # Strip the stuff before and after the VTIMEZONE component + $vtimezone =~ s/^.*BEGIN:VTIMEZONE/BEGIN:VTIMEZONE/s; + $vtimezone =~ s/END:VTIMEZONE.*$/END:VTIMEZONE\n/s; + + print OUTPUTFILE $vtimezone; + + # Find the TZID. + $vtimezone =~ m/TZID:(.*)/; + $tzid = $1; +# print "TZID: $tzid\n"; + + # Find the location. + $file =~ m/(.*)\.ics/; + $location = $1; +# print "LOCATION: $location\n"; + + # Try to find the current UTC offset that Outlook will use. + # If there is an RRULE, we look for the first 2 TZOFFSETTO properties, + # else we just get the first one. + if ($vtimezone =~ m/RRULE/) { + $vtimezone =~ m/TZOFFSETTO:([+-]?\d+)/; + $tzoffsetto = $1; + $vtimezone =~ m/TZOFFSETFROM:([+-]?\d+)/; + $tzoffsetfrom = $1; + $tzoffset = "$tzoffsetfrom/$tzoffsetto"; + } else { + $vtimezone =~ m/TZOFFSETTO:([+-]?\d+)/s; + $tzoffset = $1; + } +# print "TZOFFSET: $tzoffset\n"; + + # We put each event on a separate day in 2001 and Jan 2002. + $day_num = $zone_num; + if ($day_num >= 365) { + $year = 2002; + $day_num -= 365; + } else { + $year = 2001; + } + $month = -1; + for ($i = 0; $i < 12; $i++) { + if ($day_num < $days_in_month[$i]) { + $month = $i; + last; + } + $day_num -= $days_in_month[$i] + } + if ($month == -1) { + die "month = -1"; + } + + $month++; + $day_num++; + $date = sprintf ("%i%02i%02i", $year, $month, $day_num); +# print "Date: $date\n"; + + # Output a VEVENT using the timezone. + print OUTPUTFILE <<EOF; +BEGIN:VEVENT +UID:vzic-test-${zone_num} +DTSTAMP:20010101T000000Z +DTSTART;TZID=${tzid}:${date}T120000 +DTEND;TZID=${tzid}:${date}T130000 +RRULE:FREQ=MONTHLY;BYMONTHDAY=${day_num} +SUMMARY:($tzoffset) ${location} 12:00-13:00 UTC +SEQUENCE:1 +END:VEVENT +EOF + + $zone_num++; + + # Use this to stop after a certain number. +# last if ($zone_num == 100); +} + +# Output the standard footer. + print OUTPUTFILE <<EOF; +END:VCALENDAR +EOF + +close (OUTPUTFILE); +
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic.c
Added
@@ -0,0 +1,324 @@ +/* + * Vzic - a program to convert Olson timezone database files into VZTIMEZONE + * files compatible with the iCalendar specification (RFC2445). + * + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2003 Damon Chaplin. + * + * Author: Damon Chaplin <damon@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "vzic.h" +#include "vzic-parse.h" +#include "vzic-dump.h" +#include "vzic-output.h" + + +/* + * Global command-line options. + */ + +/* By default we output Outlook-compatible output. If --pure is used we + output pure output, with no changes to be compatible with Outlook. */ +gboolean VzicPureOutput = FALSE; + +gboolean VzicDumpOutput = FALSE; +gboolean VzicDumpChanges = FALSE; +gboolean VzicDumpZoneNamesAndCoords = TRUE; +gboolean VzicDumpZoneTranslatableStrings= FALSE; +gboolean VzicNoRRules = FALSE; +gboolean VzicNoRDates = FALSE; +char* VzicOutputDir = "zoneinfo"; +char* VzicUrlPrefix = NULL; +char* VzicOlsonDir = OLSON_DIR; + +GList* VzicTimeZoneNames = NULL; + +static void convert_olson_file (char *olson_file, + GHashTable *zones_hash); + +static void usage (void); + +static void free_zone_data (GArray *zone_data); +static void free_rule_array (gpointer key, + gpointer value, + gpointer data); +static void free_link_data (gpointer key, + gpointer value, + gpointer data); + + +int +main (int argc, + char *argv[]) +{ + int i; + char directory[PATHNAME_BUFFER_SIZE]; + char filename[PATHNAME_BUFFER_SIZE]; + GHashTable *zones_hash; + + /* + * Command-Line Option Parsing. + */ + for (i = 1; i < argc; i++) { + /* + * User Options. + */ + + /* --pure: Output the perfect VCALENDAR data, which Outlook won't parse + as it has problems with certain iCalendar constructs. */ + if (!strcmp (argv[i], "--pure")) + VzicPureOutput = TRUE; + + /* --output-dir: specify where to output all the files beneath. The + default is the current directory. */ + else if (argc > i + 1 && !strcmp (argv[i], "--output-dir")) + VzicOutputDir = argv[++i]; + + /* --url-prefix: Used as the base for the TZURL property in each + VTIMEZONE. The default is to not output TZURL properties. */ + else if (argc > i + 1 && !strcmp (argv[i], "--url-prefix")) { + int length; + VzicUrlPrefix = argv[++i]; + /* remove the trailing '/' if there is one */ + length = strlen (VzicUrlPrefix); + if (VzicUrlPrefix[length - 1] == '/') + VzicUrlPrefix[length - 1] = '\0'; + } + + else if (argc > i + 1 && !strcmp (argv[i], "--olson-dir")) { + VzicOlsonDir = argv[++i]; + } + + /* + * Debugging Options. + */ + + /* --dump: Dump the Rule and Zone data that we parsed from the Olson + timezone files. This is used to test the parsing code. */ + else if (!strcmp (argv[i], "--dump")) + VzicDumpOutput = TRUE; + + /* --dump-changes: Dumps a list of times when each timezone changed, + and the new local time offset from UTC. */ + else if (!strcmp (argv[i], "--dump-changes")) + VzicDumpChanges = TRUE; + + /* --no-rrules: Don't output RRULE properties in the VTIMEZONEs. Instead + it will just output RDATEs for each year up to a certain year. */ + else if (!strcmp (argv[i], "--no-rrules")) + VzicNoRRules = TRUE; + + /* --no-rdates: Don't output multiple RDATEs in a single VTIMEZONE + component. Instead they will be output separately. */ + else if (!strcmp (argv[i], "--no-rdates")) + VzicNoRDates = TRUE; + + else + usage (); + } + + /* + * Create any necessary directories. + */ + ensure_directory_exists (VzicOutputDir); + + if (VzicDumpOutput) { + /* Create the directories for the dump output, if they don't exist. */ + sprintf (directory, "%s/ZonesVzic", VzicOutputDir); + ensure_directory_exists (directory); + sprintf (directory, "%s/RulesVzic", VzicOutputDir); + ensure_directory_exists (directory); + } + + if (VzicDumpChanges) { + /* Create the directory for the changes output, if it doesn't exist. */ + sprintf (directory, "%s/ChangesVzic", VzicOutputDir); + ensure_directory_exists (directory); + } + + sprintf (filename, "%s/zone.tab", VzicOlsonDir); + zones_hash = parse_zone_tab (filename); + + /* + * Convert the Olson timezone files. + */ + convert_olson_file ("africa", zones_hash); + convert_olson_file ("antarctica", zones_hash); + convert_olson_file ("asia", zones_hash); + convert_olson_file ("australasia", zones_hash); + convert_olson_file ("europe", zones_hash); + convert_olson_file ("northamerica", zones_hash); + convert_olson_file ("southamerica", zones_hash); + + /* These are backwards-compatability and weird stuff. */ + convert_olson_file ("backward", zones_hash); + convert_olson_file ("etcetera", zones_hash); +#if 0 + convert_olson_file ("leapseconds", zones_hash); + convert_olson_file ("pacificnew", zones_hash); + convert_olson_file ("solar87", zones_hash); + convert_olson_file ("solar88", zones_hash); + convert_olson_file ("solar89", zones_hash); +#endif + + /* This doesn't really do anything and it messes up vzic-dump.pl so we + don't bother. */ +#if 0 + convert_olson_file ("factory", zones_hash); +#endif + + /* This is old System V stuff, which we don't currently support since it + uses 'min' as a Rule FROM value which messes up our algorithm, making + it too slow and use too much memory. */ +#if 0 + convert_olson_file ("systemv", zones_hash); +#endif + + /* Output the timezone names and coordinates in a zone.tab file, and + the translatable strings to feed to gettext. */ + if (VzicDumpZoneNamesAndCoords) { + dump_time_zone_names (VzicTimeZoneNames, VzicOutputDir, zones_hash); + } + + return 0; +} + + +static void +convert_olson_file (char *olson_file, + GHashTable *zones_hash) +{ + + char input_filename[PATHNAME_BUFFER_SIZE]; + GArray *zone_data; + GHashTable *rule_data, *link_data; + char dump_filename[PATHNAME_BUFFER_SIZE]; + ZoneData *zone; + int i, max_until_year; + + sprintf (input_filename, "%s/%s", VzicOlsonDir, olson_file); + + parse_olson_file (input_filename, &zone_data, &rule_data, &link_data, + &max_until_year); + + if (VzicDumpOutput) { + sprintf (dump_filename, "%s/ZonesVzic/%s", VzicOutputDir, olson_file); + dump_zone_data (zone_data, dump_filename); + + sprintf (dump_filename, "%s/RulesVzic/%s", VzicOutputDir, olson_file); + dump_rule_data (rule_data, dump_filename); + } + + output_vtimezone_files (VzicOutputDir, zone_data, rule_data, link_data, + zones_hash, max_until_year); + + free_zone_data (zone_data); + g_hash_table_foreach (rule_data, free_rule_array, NULL); + g_hash_table_destroy (rule_data); + g_hash_table_foreach (link_data, free_link_data, NULL); + g_hash_table_destroy (link_data); +} + + +static void +usage (void) +{ + fprintf (stderr, "Usage: vzic [--dump] [--dump-changes] [--no-rrules] [--no-rdates] [--pure] [--output-dir <directory>] [--url-prefix <url>] [--olson-dir <directory>]\n"); + + exit (1); +} + + + + +/* + * Functions to free the data structures. + */ + +static void +free_zone_data (GArray *zone_data) +{ + ZoneData *zone; + ZoneLineData *zone_line; + int i, j; + + for (i = 0; i < zone_data->len; i++) { + zone = &g_array_index (zone_data, ZoneData, i); + + g_free (zone->zone_name); + + for (j = 0; j < zone->zone_line_data->len; j++) { + zone_line = &g_array_index (zone->zone_line_data, ZoneLineData, j); + + g_free (zone_line->rules); + g_free (zone_line->format); + } + + g_array_free (zone->zone_line_data, TRUE); + } + + g_array_free (zone_data, TRUE); +} + + +static void +free_rule_array (gpointer key, + gpointer value, + gpointer data) +{ + char *name = key; + GArray *rule_array = value; + RuleData *rule; + int i; + + for (i = 0; i < rule_array->len; i++) { + rule = &g_array_index (rule_array, RuleData, i); + + if (!rule->is_shallow_copy) { + g_free (rule->type); + g_free (rule->letter_s); + } + } + + g_array_free (rule_array, TRUE); + + g_free (name); +} + + +static void +free_link_data (gpointer key, + gpointer value, + gpointer data) +{ + GList *link = data; + + g_free (key); + + while (link) { + g_free (link->data); + link = link->next; + } + + g_list_free (data); +} +
View file
cyrus-imapd-2.5.tar.gz/tools/vzic/vzic.h
Added
@@ -0,0 +1,197 @@ +/* + * Vzic - a program to convert Olson timezone database files into VZTIMEZONE + * files compatible with the iCalendar specification (RFC2445). + * + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2003 Damon Chaplin. + * + * Author: Damon Chaplin <damon@gnome.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + */ + +#ifndef _VZIC_H_ +#define _VZIC_H_ + +#include <glib.h> + + +/* + * Global command-line options. + */ + +/* By default we output Outlook-compatible output. If --pure is used we output + pure output, with no changes to be compatible with Outlook. */ +extern gboolean VzicPureOutput; + +extern gboolean VzicDumpOutput; +extern gboolean VzicDumpChanges; +extern gboolean VzicDumpZoneNamesAndCoords; +extern gboolean VzicDumpZoneTranslatableStrings; +extern gboolean VzicNoRRules; +extern gboolean VzicNoRDates; +extern char* VzicUrlPrefix; +extern char* VzicOutputDir; + +extern GList* VzicTimeZoneNames; + +/* The minimum & maximum years we can use. */ +#define YEAR_MINIMUM G_MININT +#define YEAR_MAXIMUM G_MAXINT + +/* The maximum size of any complete pathname. */ +#define PATHNAME_BUFFER_SIZE 1024 + +/* Days can be expressed either as a simple month day number, 1-31, or a rule + such as the last Sunday, or the first Monday on or after the 8th. */ +typedef enum +{ + DAY_SIMPLE, + DAY_WEEKDAY_ON_OR_AFTER, + DAY_WEEKDAY_ON_OR_BEFORE, + DAY_LAST_WEEKDAY +} DayCode; + + +/* Times can be given either as universal time (UTC), local standard time + (without daylight-saving adjustments) or wall clock time (local standard + time plus daylight-saving adjustments, i.e. what you would see on a clock + on the wall!). */ +typedef enum +{ + TIME_WALL, + TIME_STANDARD, + TIME_UNIVERSAL +} TimeCode; + + +/* This represents one timezone, e.g. "Africa/Algiers". + It contains the timezone name, and an array of ZoneLineData structs which + hold data from each Zone line, including the continuation lines. */ +typedef struct _ZoneData ZoneData; +struct _ZoneData +{ + char *zone_name; + + /* An array of ZoneLineData, one for each Zone & Zone continuation line + read in. */ + GArray *zone_line_data; +}; + + +typedef struct _ZoneLineData ZoneLineData; +struct _ZoneLineData +{ + /* The amount of time to add to UTC to get local standard time for the + current time range, in seconds. */ + int stdoff_seconds; + + /* Either rules is set to the name of a set of rules, or rules is NULL and + save is set to the time to add to local standard time to get wall time, in + seconds. If save is 0 as well, then standard time always applies. */ + char *rules; + int save_seconds; + + /* The format to use for the abbreviated timezone name, e.g. WE%sT. + The %s is replaced by variable part of the name. (See the letter_s field + in the RuleData struct below). */ + char *format; + + /* TRUE if an UNTIL time is given. */ + gboolean until_set; + + /* The UNTIL year, e.g. 2000. */ + int until_year; + + /* The UNTIL month 0 (Jan) to 11 (Dec). */ + int until_month; + + /* The UNTIL day, either a simple month day number, 1-31, or a rule such as + the last Sunday, or the first Monday on or after the 8th. */ + DayCode until_day_code; + int until_day_number; /* 1 to 31. */ + int until_day_weekday; /* 0 (Sun) to 6 (Sat). */ + + /* The UNTIL time, in seconds from midnight. The code specifies whether the + time is a wall clock time, local standard time, or universal time. */ + int until_time_seconds; + TimeCode until_time_code; +}; + + +typedef struct _RuleData RuleData; +struct _RuleData +{ + /* The first year that the rule applies to, e.g. 1996. + Can also be YEAR_MINIMUM. */ + int from_year; + + /* The last year that the rule applies to, e.g. 1996. + Can also be YEAR_MAXIMUM. */ + int to_year; + + /* A string used to only match certain years between from and to. + The rule only applies to the years which match. If type is NULL the rule + applies to all years betweeen from and to. + zic uses an external program called yearistype to check the string. + Currently it is not used in the Olson database. */ + char *type; + + /* The month of the rule 0 (Jan) to 11 (Dec). */ + int in_month; + + /* The day, either a simple month day number, 1-31, or a rule such as + the last Sunday, or the first Monday on or after the 8th. */ + DayCode on_day_code; + int on_day_number; + int on_day_weekday; /* 0 (Sun) to 6 (Sat). */ + + /* The time, in seconds from midnight. The code specifies whether the + time is a wall clock time, local standard time, or universal time. */ + int at_time_seconds; + TimeCode at_time_code; + + /* The amount of time to add to local standard time when the rule is in + effect, in seconds. If this is not 0 then it must be a daylight-saving + time. */ + int save_seconds; + + /* The letter(s) to use as the variable part in the abbreviated timezone + name. If this is NULL then no variable part is used. (See the format field + in the ZoneLineData struct above.) */ + char *letter_s; + + + /* This is set to TRUE if this element is a shallow copy of another one, + in which case we don't free any of the fields. */ + gboolean is_shallow_copy; +}; + + +typedef struct _ZoneDescription ZoneDescription; +struct _ZoneDescription +{ + /* 2-letter ISO 3166 country code. */ + char country_code[2]; + + /* latitude and longitude in degrees, minutes & seconds. The degrees value + holds the sign of the entire latitude/longitude. */ + int latitude[3]; + int longitude[3]; + + char *comment; +}; + +#endif /* _VZIC_H_ */
View file
cyrus-imapd.dsc
Changed
@@ -2,7 +2,7 @@ Source: cyrus-imapd Binary: cyrus-imapd Architecture: any -Version: 2.5~dev2014101701-0~kolab1 +Version: 2.5~dev2014103001-0~kolab1 Maintainer: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> Uploaders: Paul Klos <kolab@klos2day.nl> Homepage: http://www.cyrusimap.org/
View file
debian.changelog
Changed
@@ -1,3 +1,10 @@ +cyrus-imapd (2.5~dev2014103001-0~kolab1) unstable; urgency=low + + * Ship a GIT development snapshot from git.cyrusimap.org + 273403fcb26e095462cecd1b0dbf9b4f8e3f71d7 + + -- Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> Thu, 30 Oct 2014 11:30:13 +0100 + cyrus-imapd (2.5~dev2014101701-0~kolab1) unstable; urgency=low * Ship a GIT development snapshot from git.cyrusimap.org
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.