diff --git a/imap/imapd.c b/imap/imapd.c index 0da65d4b8..e9fee710b 100644 --- a/imap/imapd.c +++ b/imap/imapd.c @@ -1,13971 +1,13967 @@ /* * 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 #ifdef HAVE_UNISTD_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_SSL #include #include #endif /* HAVE_SSL */ #include "acl.h" #include "annotate.h" #include "append.h" #include "auth.h" #ifdef USE_AUTOCREATE #include "autocreate.h" #endif // USE_AUTOCREATE #include "assert.h" #include "backend.h" #include "bsearch.h" #include "bufarray.h" #include "charset.h" #include "dlist.h" #include "idle.h" #include "global.h" #include "times.h" #include "proxy.h" #include "imap_proxy.h" #include "imapd.h" #include "imapurl.h" #include "imparse.h" #include "index.h" #include "mailbox.h" #include "message.h" #include "mboxevent.h" #include "mboxkey.h" #include "mboxlist.h" #include "mboxname.h" #include "mbdump.h" #include "mupdate-client.h" #include "partlist.h" #include "proc.h" #include "prometheus.h" #include "quota.h" #include "seen.h" #include "statuscache.h" #include "sync_log.h" #include "sync_support.h" #include "telemetry.h" #include "tls.h" #include "user.h" #include "userdeny.h" #include "util.h" #include "version.h" #include "xmalloc.h" #include "xstrlcat.h" #include "xstrlcpy.h" #include "ptrarray.h" #include "xstats.h" /* generated headers are not necessarily in current directory */ #include "imap/imap_err.h" #include "iostat.h" extern int optind; extern char *optarg; /* global state */ const int config_need_data = CONFIG_NEED_PARTITION_DATA; static int imaps = 0; static sasl_ssf_t extprops_ssf = 0; static int nosaslpasswdcheck = 0; static int apns_enabled = 0; static size_t maxmsgsize = 0; static int64_t maxargssize = 0; static uint64_t maxargssize_mark = 0; /* PROXY STUFF */ /* we want a list of our outgoing connections here and which one we're currently piping */ static const int ultraparanoid = 1; /* should we kick after every operation? */ unsigned int proxy_cmdcnt; static int referral_kick = 0; /* kick after next command received, for referrals that are likely to change the mailbox list */ /* global conversations database holder to avoid re-opening during * status command or list responses */ struct conversations_state *global_conversations = NULL; /* all subscription commands go to the backend server containing the user's inbox */ struct backend *backend_inbox = NULL; /* the current server most commands go to */ struct backend *backend_current = NULL; /* our cached connections */ ptrarray_t backend_cached = PTRARRAY_INITIALIZER; /* cached connection to mupdate master (for multiple XFER and MUPDATEPUSH) */ static mupdate_handle *mupdate_h = NULL; /* are we doing virtdomains with multiple IPs? */ static int disable_referrals; /* has the client issued an RLIST, RLSUB, or LIST (REMOTE)? */ static int supports_referrals; /* end PROXY STUFF */ /* per-user/session state */ static int imapd_timeout; struct protstream *imapd_out = NULL; struct protstream *imapd_in = NULL; static struct protgroup *protin = NULL; static const char *imapd_clienthost = "[local]"; static int imapd_logfd = -1; char *imapd_userid = NULL, *proxy_userid = NULL; static char *imapd_magicplus = NULL; struct auth_state *imapd_authstate = 0; static int imapd_userisadmin = 0; static int imapd_userisproxyadmin = 0; static sasl_conn_t *imapd_saslconn; /* the sasl connection context */ static int imapd_starttls_done = 0; /* have we done a successful starttls? */ static int imapd_tls_required = 0; /* is tls required? */ 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 int sync_sieve_mailbox_enabled = 0; #define QUIRK_SEARCHFUZZY (1<<0) static struct id_data { struct attvaluelist *params; int did_id; int quirks; } imapd_id; #ifdef HAVE_SSL /* our tls connection, if any */ static SSL *tls_conn = NULL; #endif /* HAVE_SSL */ /* stage(s) for APPEND */ struct appendstage { struct stagemsg *stage; FILE *f; strarray_t flags; time_t internaldate; int binary; struct entryattlist *annotations; }; static ptrarray_t stages = PTRARRAY_INITIALIZER; /* the sasl proxy policy context */ static struct proxy_context imapd_proxyctx = { 1, 1, &imapd_authstate, &imapd_userisadmin, &imapd_userisproxyadmin }; /* current sub-user state */ static struct index_state *imapd_index; /* current namespace */ struct namespace imapd_namespace; /* track if we're idling */ static int idling = 0; const struct mbox_name_attribute mbox_name_attributes[] = { /* from RFC 3501 */ { MBOX_ATTRIBUTE_NOINFERIORS, "\\Noinferiors" }, { MBOX_ATTRIBUTE_NOSELECT, "\\Noselect" }, { MBOX_ATTRIBUTE_MARKED, "\\Marked" }, { MBOX_ATTRIBUTE_UNMARKED, "\\Unmarked" }, /* from RFC 5258 */ { MBOX_ATTRIBUTE_NONEXISTENT, "\\NonExistent" }, { MBOX_ATTRIBUTE_SUBSCRIBED, "\\Subscribed" }, { MBOX_ATTRIBUTE_REMOTE, "\\Remote" }, { MBOX_ATTRIBUTE_HASCHILDREN, "\\HasChildren" }, { MBOX_ATTRIBUTE_HASNOCHILDREN, "\\HasNoChildren" }, { 0, NULL } }; /* * These bitmasks define how List selection options can be combined: * list_select_mod_opts may only be used if at least one list_select_base_opt * is also present. * For example, (RECURSIVEMATCH) and (RECURSIVEMATCH REMOTE) are invalid, but * (RECURSIVEMATCH SUBSCRIBED) is ok. */ static const int list_select_base_opts = LIST_SEL_SUBSCRIBED; static const int list_select_mod_opts = LIST_SEL_RECURSIVEMATCH; /* structure that list_data passes its callbacks */ struct list_rock { struct listargs *listargs; strarray_t *subs; char *last_name; mbentry_t *last_mbentry; uint32_t last_attributes; int last_category; hash_table server_table; /* for proxying */ }; /* Information about one mailbox name that LIST returns */ struct list_entry { char *extname; mbentry_t *mbentry; uint32_t attributes; /* bitmap of MBOX_ATTRIBUTE_* */ }; /* structure that list_data_recursivematch passes its callbacks */ struct list_rock_recursivematch { struct listargs *listargs; struct hash_table table; /* maps mailbox names to list_entries */ int count; /* # of entries in table */ struct list_entry *array; }; /* CAPABILITIES are defined here, not including TLS/SASL ones, and those that are configurable */ enum { CAPA_PREAUTH = 0x1, CAPA_POSTAUTH = 0x2 }; struct capa_struct { const char *str; int mask; }; static struct capa_struct base_capabilities[] = { /* pre-auth capabilities */ { "IMAP4rev1", 3 }, { "LITERAL+", 3 }, /* This is in RFC 7888, but likely the implementation is for RFC 2088 */ /* LITERAL- (RFC 7888) sent instead of LITERAL+ when literalminus=yes */ { "ID", 3 }, /* RFC 2971 */ { "ENABLE", 3 }, /* RFC 5161 */ /* post-auth capabilities * this is kept sorted, so that it can be easily compared to https://www.iana.org/assignments/imap-capabilities/imap-capabilities.xhtml */ { "ACL", 2 }, /* RFC 4314 */ { "ANNOTATE-EXPERIMENT-1", 2 }, /* RFC 5257 */ /* APPENDLIMIT= RFC 7889 is announced in capa_response() */ /* AUTH= RFC 3501 is announced conditionally in capa_response() */ { "BINARY", 2 }, /* RFC 3516 */ { "CATENATE", 2 }, /* RFC 4469 */ { "CHILDREN", 2 }, /* RFC 3348 */ /* COMPRESS=DEFLATE RFC 4498 is announced conditionally in capa_response() */ { "CONDSTORE", 2 }, /* RFC 7162, but the implementation is likely from RFC 4551 */ /* CONTEXT=SEARCH RFC 5267 is not implemented. From that RFC only ESORT is ready */ /* CONTEXT=SORT RFC 5267 is not implemented. From that RFC only ESORT is ready */ /* CONVERT RFC 5259 is not implemented */ { "CREATE-SPECIAL-USE", 2 }, /* RFC 6154 */ { "ESEARCH", 2 }, /* RFC 4731 */ { "ESORT", 2 }, /* RFC 5267 */ /* FILTERS RFC 5466 is not implemented */ /* I18NLEVEL=1 RFC 5255 is not implemented */ /* I18NLEVEL=2 RFC 5255 is not implemented */ /* IDLE RFC 2177 is announced conditionally in capa_response() */ /* IMAPSIEVE= RFC 6785 is not implemented */ /* LANGUAGE RFC 5255 is not implemented */ { "LIST-EXTENDED", 2 }, /* RFC 5258 */ { "LIST-MYRIGHTS", 2 }, /* RFC 8440 */ { "LIST-STATUS", 2 }, /* RFC 5819 */ /* LOGIN-REFERRALS RFC 2221 is not implemented */ /* LOGINDISABLED RFC 2595/RFC 3591 is announced conditionally in capa_response() */ { "MAILBOX-REFERRALS", 2 }, /* RFC 2193 */ { "METADATA", 2 }, /* RFC 5464 */ /*METADATA-SERVER RFC 5464. Sending METADATA implies METADATA-SERVER */ { "MOVE", 2 }, /* RFC 6851 */ { "MULTIAPPEND", 2 }, /* RFC 3502 */ /* MULTISEARCH RFC 7377 is not implemented */ { "NAMESPACE", 2 }, /* RFC 2342 */ /* NOTIFY RFC 5465 is not implemented */ { "OBJECTID", 2 }, /* RFC 8474 */ { "PREVIEW", 2 }, /* RFC 8970 */ { "QRESYNC", 2 }, /* RFC 7162, but the implementation is likely from RFC 4551 and RFC 5162 */ { "QUOTA", 2 }, /* RFC 2087 */ /* REPLACE RFC 8508 is not implemented */ { "RIGHTS=kxten", 2 }, /* RFC 4314 */ /* SASL-IR RFC 4959 is announced in capa_response() */ { "SAVEDATE", 2 }, /* RFC 8514 */ { "SEARCH=FUZZY", 2 }, /* RFC 6203 */ /* SEARCHRES RFC 5182 is not implemented */ { "SORT", 2 }, /* RFC 5256 */ { "SORT=DISPLAY", 2 }, /* RFC 5957 */ { "SPECIAL-USE", 2 }, /* RFC 6154 */ /* STARTTLS RFC 2595, RFC 3501 is announced in capa_response() */ { "STATUS=SIZE", 2 }, /* RFC 8438 */ { "THREAD=ORDEREDSUBJECT", 2 }, /* RFC 5256 */ { "THREAD=REFERENCES", 2 }, /* RFC 5256 */ { "UIDPLUS", 2 }, /* RFC 4315 */ /* UNAUTHENTICATE RFC 8437 is implemented */ { "UNSELECT", 2 }, /* RFC 3691 */ /* URL-PARTIAL RFC 5550 is not implemented */ #ifdef HAVE_SSL { "URLAUTH", 2 }, /* RFC 4467 */ { "URLAUTH=BINARY", 2 }, /* RFC 5524 */ #endif /* UTF8=ACCEPT RFC 6855 is not implemented */ /* UTF8=ONLY RFC 6855 is not implemented */ { "WITHIN", 2 }, /* RFC 5032 */ /* drafts, non-standard */ { "ANNOTATEMORE", 2 }, /* legacy SETANNOTATION/GETANNOTATION commands */ { "DIGEST=SHA1", 2 }, /* Cyrus custom */ { "LIST-METADATA", 2 }, /* not standard */ { "NO_ATOMIC_RENAME", 2 }, { "SCAN", 2 }, { "SORT=MODSEQ", 2 }, { "SORT=UID", 2 }, /* not standard */ { "THREAD=REFS", 2 }, /* draft-ietf-morg-inthread */ { "X-CREATEDMODSEQ", 2 }, /* Cyrus custom */ { "X-REPLICATION", 2 }, /* Cyrus custom */ { "X-SIEVE-MAILBOX", 2 }, /* Cyrus custom */ { "XLIST", 2 }, /* not standard */ { "XMOVE", 2 }, /* not standard */ /* keep this to mark the end of the list */ { 0, 0 } }; static void motd_file(void); void shut_down(int code); void fatal(const char *s, int code); static void cmdloop(void); static void cmd_login(char *tag, char *user); static void cmd_authenticate(char *tag, char *authtype, char *resp); static void cmd_unauthenticate(char *tag); static void cmd_noop(char *tag, char *cmd); static void capa_response(int flags); static void cmd_capability(char *tag); static void cmd_append(char *tag, char *name, const char *cur_name); static void cmd_select(char *tag, char *cmd, char *name); static void cmd_close(char *tag, char *cmd); static int parse_fetch_args(const char *tag, const char *cmd, int allow_vanished, struct fetchargs *fa); static void cmd_fetch(char *tag, char *sequence, int usinguid); static void cmd_store(char *tag, char *sequence, int usinguid); static void cmd_search(char *tag, int usinguid); static void cmd_sort(char *tag, int usinguid); static void cmd_thread(char *tag, int usinguid); static void cmd_copy(char *tag, char *sequence, char *name, int usinguid, int ismove); static void cmd_expunge(char *tag, char *sequence); static void cmd_create(char *tag, char *name, struct dlist *extargs, int localonly); static void cmd_delete(char *tag, char *name, int localonly, int force); static void cmd_dump(char *tag, char *name, int uid_start); static void cmd_undump(char *tag, char *name); static void cmd_xfer(const char *tag, const char *name, const char *toserver, const char *topart); static void cmd_rename(char *tag, char *oldname, char *newname, char *partition); static void cmd_reconstruct(const char *tag, const char *name, int recursive); static void getlistargs(char *tag, struct listargs *listargs); static void cmd_list(char *tag, struct listargs *listargs); static void cmd_changesub(char *tag, char *namespace, char *name, int add); static void cmd_getacl(const char *tag, const char *name); static void cmd_listrights(char *tag, char *name, char *identifier); static void cmd_myrights(const char *tag, const char *name); static void cmd_setacl(char *tag, const char *name, const char *identifier, const char *rights); static void cmd_getquota(const char *tag, const char *name); static void cmd_getquotaroot(const char *tag, const char *name); static void cmd_setquota(const char *tag, const char *quotaroot); static void cmd_status(char *tag, char *name); static void cmd_namespace(char* tag); static void cmd_mupdatepush(char *tag, char *name); static void cmd_id(char* tag); static void cmd_idle(char* tag); static void cmd_starttls(char *tag, int imaps); static void cmd_xstats(char *tag); static void cmd_xapplepushservice(const char *tag, struct applepushserviceargs *applepushserviceargs); static void cmd_xbackup(const char *tag, const char *mailbox, const char *channel); #ifdef HAVE_SSL static void cmd_urlfetch(char *tag); static void cmd_genurlauth(char *tag); static void cmd_resetkey(char *tag, char *mailbox, char *mechanism); #endif #ifdef HAVE_ZLIB static void cmd_compress(char *tag, char *alg); #endif static void cmd_getannotation(const char* tag, char *mboxpat); static void cmd_getmetadata(const char* tag); static void cmd_setannotation(const char* tag, char *mboxpat); static void cmd_setmetadata(const char* tag, char *mboxpat); static void cmd_xrunannotator(const char *tag, const char *sequence, int usinguid); static void cmd_xwarmup(const char *tag); static void cmd_enable(char* tag); static void cmd_syncget(const char *tag, struct dlist *kl); static void cmd_syncapply(const char *tag, struct dlist *kl, struct sync_reserve_list *reserve_list); static void cmd_syncrestart(const char *tag, struct sync_reserve_list **reserve_listp, int realloc); static void cmd_syncrestore(const char *tag, struct dlist *kin, struct sync_reserve_list *reserve_list); static void cmd_xkillmy(const char *tag, const char *cmdname); static void cmd_xforever(const char *tag); static void cmd_xmeid(const char *tag, const char *id); static int parsecreateargs(struct dlist **extargs); static int parse_annotate_fetch_data(const char *tag, int permessage_flag, strarray_t *entries, strarray_t *attribs); static int parse_metadata_string_or_list(const char *tag, strarray_t *sa, int *is_list); static int parse_annotate_store_data(const char *tag, int permessage_flag, struct entryattlist **entryatts); static int parse_metadata_store_data(const char *tag, struct entryattlist **entryatts); static int getlistselopts(char *tag, struct listargs *args); static int getlistretopts(char *tag, struct listargs *args); static int getsortcriteria(char *tag, struct sortcrit **sortcrit); static int getdatetime(time_t *date); static void appendfieldlist(struct fieldlist **l, char *section, strarray_t *fields, char *trail, void *d, size_t size); static void freefieldlist(struct fieldlist *l); static int set_haschildren(const mbentry_t *entry, void *rock); static char *canonical_list_pattern(const char *reference, const char *pattern); static void canonical_list_patterns(const char *reference, strarray_t *patterns); static int list_cb(struct findall_data *data, void *rock); static int subscribed_cb(struct findall_data *data, void *rock); static void list_data(struct listargs *listargs); static int list_data_remote(struct backend *be, char *tag, struct listargs *listargs, strarray_t *subs); static void clear_id(); extern int saslserver(sasl_conn_t *conn, const char *mech, const char *init_resp, const char *resp_prefix, const char *continuation, const char *empty_resp, struct protstream *pin, struct protstream *pout, int *sasl_result, char **success_data); /* Enable the resetting of a sasl_conn_t */ static int reset_saslconn(sasl_conn_t **conn); static struct saslprops_t saslprops = SASLPROPS_INITIALIZER; static int imapd_canon_user(sasl_conn_t *conn, void *context, const char *user, unsigned ulen, unsigned flags, const char *user_realm, char *out, unsigned out_max, unsigned *out_ulen) { char userbuf[MAX_MAILBOX_BUFFER], *p; size_t n; int r; if (!ulen) ulen = strlen(user); if (config_getswitch(IMAPOPT_IMAPMAGICPLUS)) { /* make a working copy of the auth[z]id */ if (ulen >= MAX_MAILBOX_BUFFER) { sasl_seterror(conn, 0, "buffer overflow while canonicalizing"); return SASL_BUFOVER; } memcpy(userbuf, user, ulen); userbuf[ulen] = '\0'; user = userbuf; /* See if we're using the magic plus */ if ((p = strchr(userbuf, '+'))) { n = config_virtdomains ? strcspn(p, "@") : strlen(p); if (flags & SASL_CU_AUTHZID) { /* make a copy of the magic plus */ if (imapd_magicplus) free(imapd_magicplus); imapd_magicplus = xstrndup(p, n); } /* strip the magic plus from the auth[z]id */ memmove(p, p+n, strlen(p+n)+1); ulen -= n; } } r = mysasl_canon_user(conn, context, user, ulen, flags, user_realm, out, out_max, out_ulen); if (!r && imapd_magicplus && flags == SASL_CU_AUTHZID) { /* If we're only doing the authzid, put back the magic plus in case its used in the challenge/response calculation */ n = strlen(imapd_magicplus); if (*out_ulen + n > out_max) { sasl_seterror(conn, 0, "buffer overflow while canonicalizing"); r = SASL_BUFOVER; } else { p = (config_virtdomains && (p = strchr(out, '@'))) ? p : out + *out_ulen; memmove(p+n, p, strlen(p)+1); memcpy(p, imapd_magicplus, n); *out_ulen += n; } } return r; } static int imapd_proxy_policy(sasl_conn_t *conn, void *context, const char *requested_user, unsigned rlen, const char *auth_identity, unsigned alen, const char *def_realm, unsigned urlen, struct propctx *propctx) { char userbuf[MAX_MAILBOX_BUFFER]; if (config_getswitch(IMAPOPT_IMAPMAGICPLUS)) { size_t n; char *p; /* make a working copy of the authzid */ if (!rlen) rlen = strlen(requested_user); if (rlen >= MAX_MAILBOX_BUFFER) { sasl_seterror(conn, 0, "buffer overflow while proxying"); return SASL_BUFOVER; } memcpy(userbuf, requested_user, rlen); userbuf[rlen] = '\0'; requested_user = userbuf; /* See if we're using the magic plus */ if ((p = strchr(userbuf, '+'))) { n = config_virtdomains ? strcspn(p, "@") : strlen(p); /* strip the magic plus from the authzid */ memmove(p, p+n, strlen(p+n)+1); rlen -= n; } } return mysasl_proxy_policy(conn, context, requested_user, rlen, auth_identity, alen, def_realm, urlen, propctx); } static int imapd_sasl_log(void *context __attribute__((unused)), int level, const char *message) { int syslog_level = LOG_INFO; switch (level) { case SASL_LOG_ERR: case SASL_LOG_FAIL: syslog_level = LOG_ERR; break; case SASL_LOG_WARN: syslog_level = LOG_WARNING; break; case SASL_LOG_DEBUG: case SASL_LOG_TRACE: case SASL_LOG_PASS: syslog_level = LOG_DEBUG; break; } syslog(syslog_level, "SASL %s", message); return SASL_OK; } static const struct sasl_callback mysasl_cb[] = { { SASL_CB_GETOPT, (mysasl_cb_ft *) &mysasl_config, NULL }, { SASL_CB_PROXY_POLICY, (mysasl_cb_ft *) &imapd_proxy_policy, (void*) &imapd_proxyctx }, { SASL_CB_CANON_USER, (mysasl_cb_ft *) &imapd_canon_user, (void*) &disable_referrals }, { SASL_CB_LOG, (mysasl_cb_ft *) &imapd_sasl_log, NULL }, { SASL_CB_LIST_END, NULL, NULL } }; /* imapd_refer() issues a referral to the client. */ static void imapd_refer(const char *tag, const char *server, const char *mailbox) { struct imapurl imapurl; char url[MAX_MAILBOX_PATH+1]; memset(&imapurl, 0, sizeof(struct imapurl)); imapurl.server = server; imapurl.mailbox = mailbox; imapurl.auth = !strcmp(imapd_userid, "anonymous") ? "anonymous" : "*"; imapurl_toURL(url, &imapurl); prot_printf(imapd_out, "%s NO [REFERRAL %s] Remote mailbox.\r\n", tag, url); free(imapurl.freeme); } /* wrapper for mboxlist_lookup that will force a referral if we are remote * returns IMAP_SERVER_UNAVAILABLE if we don't have a place to send the client * (that'd be a bug). * returns IMAP_MAILBOX_MOVED if we referred the client */ /* ext_name is the external name of the mailbox */ /* you can avoid referring the client by setting tag or ext_name to NULL. */ struct mbox_refer_rock { const char *tag; const char *ext_name; }; static int mbox_refer_proc(mbentry_t *mbentry, void *rock) { struct mbox_refer_rock *mrock = (struct mbox_refer_rock *) rock; int r; /* do we have rights on the mailbox? */ if (!imapd_userisadmin && (!mbentry->acl || !(cyrus_acl_myrights(imapd_authstate, mbentry->acl) & ACL_LOOKUP))) { r = IMAP_MAILBOX_NONEXISTENT; } else if (mrock->tag && mrock->ext_name && mbentry->server) { imapd_refer(mrock->tag, mbentry->server, mrock->ext_name); r = IMAP_MAILBOX_MOVED; } else if (config_mupdate_server) { r = IMAP_SERVER_UNAVAILABLE; } else { r = IMAP_MAILBOX_NOTSUPPORTED; } return r; } static int mlookup(const char *tag, const char *ext_name, const char *name, mbentry_t **mbentryp) { mbentry_t *mbentry = NULL; struct mbox_refer_rock rock = { tag, ext_name }; struct mbox_refer refer = { &mbox_refer_proc, &rock }; int r = proxy_mlookup(name, &mbentry, NULL, &refer); if (!r && mbentryp) *mbentryp = mbentry; else mboxlist_entry_free(&mbentry); /* we don't actually want it! */ return r; } static void imapd_reset(void) { int i; uint64_t bytes_in = 0; uint64_t bytes_out = 0; /* run delayed commands first before closing anything */ libcyrus_run_delayed(); proc_cleanup(); /* close backend connections */ for (i = 0; i < ptrarray_size(&backend_cached); i++) { struct backend *be = ptrarray_nth(&backend_cached, i); proxy_downserver(be); if (be->last_result.s) { free(be->last_result.s); } free(be); } ptrarray_fini(&backend_cached); backend_inbox = backend_current = NULL; if (mupdate_h) mupdate_disconnect(&mupdate_h); mupdate_h = NULL; proxy_cmdcnt = 0; disable_referrals = 0; supports_referrals = 0; index_text_extractor_destroy(); if (imapd_index) { if (config_getswitch(IMAPOPT_AUTOEXPUNGE) && index_hasrights(imapd_index, ACL_EXPUNGE)) index_expunge(imapd_index, NULL, 1); index_close(&imapd_index); } if (imapd_in) { /* Flush the incoming buffer */ prot_NONBLOCK(imapd_in); prot_fill(imapd_in); bytes_in = prot_bytes_in(imapd_in); prot_free(imapd_in); } if (imapd_out) { /* Flush the outgoing buffer */ prot_flush(imapd_out); bytes_out = prot_bytes_out(imapd_out); prot_free(imapd_out); } if (config_auditlog) syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", session_id(), bytes_in, bytes_out); imapd_in = imapd_out = NULL; if (protin) protgroup_reset(protin); #ifdef HAVE_SSL if (tls_conn) { if (tls_reset_servertls(&tls_conn) == -1) { fatal("tls_reset() failed", EX_TEMPFAIL); } tls_conn = NULL; } #endif cyrus_reset_stdio(); imapd_clienthost = "[local]"; if (imapd_logfd != -1) { close(imapd_logfd); imapd_logfd = -1; } if (imapd_userid != NULL) { free(imapd_userid); imapd_userid = NULL; } if (proxy_userid != NULL) { free(proxy_userid); proxy_userid = NULL; } if (imapd_magicplus != NULL) { free(imapd_magicplus); imapd_magicplus = NULL; } if (imapd_authstate) { auth_freestate(imapd_authstate); imapd_authstate = NULL; } imapd_userisadmin = 0; imapd_userisproxyadmin = 0; client_capa = 0; if (imapd_saslconn) { sasl_dispose(&imapd_saslconn); free(imapd_saslconn); imapd_saslconn = NULL; } imapd_compress_done = 0; imapd_tls_comp = NULL; imapd_starttls_done = 0; plaintextloginalert = NULL; saslprops_reset(&saslprops); clear_id(); } /* * run once when process is forked; * MUST NOT exit directly; must return with non-zero error code */ int service_init(int argc, char **argv, char **envp) { int opt, events; if (geteuid() == 0) fatal("must run as the Cyrus user", EX_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); /* setup for sending IMAP IDLE notifications */ idle_init(); /* setup for mailbox event notifications */ events = mboxevent_init(); apns_enabled = (events & EVENT_APPLEPUSHSERVICE) && config_getstring(IMAPOPT_APS_TOPIC); while ((opt = getopt(argc, argv, "Np:sq")) != EOF) { switch (opt) { case 's': /* imaps (do starttls right away) */ imaps = 1; if (!tls_enabled()) { syslog(LOG_ERR, "imaps: required OpenSSL options not present"); fatal("imaps: required OpenSSL options not present", EX_CONFIG); } break; case 'p': /* external protection */ extprops_ssf = atoi(optarg); break; case 'N': /* bypass SASL password check. Not recommended unless * you know what you're doing! */ nosaslpasswdcheck = 1; break; case 'q': /* don't enforce quotas */ ignorequota = 1; break; default: break; } } /* Initialize the annotatemore extension */ if (config_mupdate_server) annotate_init(annotate_fetch_proxy, annotate_store_proxy); else annotate_init(NULL, NULL); annotatemore_open(); /* Create a protgroup for input from the client and selected backend */ protin = protgroup_new(2); prometheus_increment(CYRUS_IMAP_READY_LISTENERS); maxmsgsize = config_getint(IMAPOPT_MAXMESSAGESIZE); if (!maxmsgsize) maxmsgsize = UINT32_MAX; maxargssize = config_getint(IMAPOPT_MAXARGSSIZE); if (maxargssize <= 0) maxargssize = UINT32_MAX; return 0; } /* * run for each accepted connection */ #ifdef ID_SAVE_CMDLINE int service_main(int argc, char **argv, char **envp __attribute__((unused))) #else int service_main(int argc __attribute__((unused)), char **argv __attribute__((unused)), char **envp __attribute__((unused))) #endif { sasl_security_properties_t *secprops = NULL; const char *localip, *remoteip; struct mboxevent *mboxevent = NULL; struct io_count *io_count_start = NULL; struct io_count *io_count_stop = NULL; /* fatal/shut_down will adjust these, so we need to set them early */ prometheus_decrement(CYRUS_IMAP_READY_LISTENERS); prometheus_increment(CYRUS_IMAP_ACTIVE_CONNECTIONS); if (config_iolog) { io_count_start = xmalloc (sizeof (struct io_count)); io_count_stop = xmalloc (sizeof (struct io_count)); read_io_count(io_count_start); } session_new_id(); signals_poll(); #ifdef ID_SAVE_CMDLINE /* get command line args for use in ID before getopt mangles them */ id_getcmdline(argc, argv); #endif imapd_in = prot_new(0, 0); imapd_out = prot_new(1, 1); /* Allow LITERAL+ */ prot_setisclient(imapd_in, 1); protgroup_insert(protin, imapd_in); /* Find out name of client host */ imapd_clienthost = get_clienthost(0, &localip, &remoteip); if (localip && remoteip) { buf_setcstr(&saslprops.ipremoteport, remoteip); buf_setcstr(&saslprops.iplocalport, localip); } /* create the SASL connection */ if (sasl_server_new("imap", config_servername, NULL, buf_cstringnull_ifempty(&saslprops.iplocalport), buf_cstringnull_ifempty(&saslprops.ipremoteport), NULL, 0, &imapd_saslconn) != SASL_OK) { fatal("SASL failed initializing: sasl_server_new()", EX_TEMPFAIL); } secprops = mysasl_secprops(0); if (sasl_setprop(imapd_saslconn, SASL_SEC_PROPS, secprops) != SASL_OK) fatal("Failed to set SASL property", EX_TEMPFAIL); if (sasl_setprop(imapd_saslconn, SASL_SSF_EXTERNAL, &extprops_ssf) != SASL_OK) fatal("Failed to set SASL property", EX_TEMPFAIL); imapd_tls_required = config_getswitch(IMAPOPT_TLS_REQUIRED); proc_register(config_ident, imapd_clienthost, NULL, NULL, NULL); /* Set inactivity timer */ imapd_timeout = config_getduration(IMAPOPT_TIMEOUT, 'm'); if (imapd_timeout < 30 * 60) imapd_timeout = 30 * 60; prot_settimeout(imapd_in, imapd_timeout); prot_setflushonread(imapd_in, imapd_out); /* we were connected on imaps port so we should do TLS negotiation immediately */ if (imaps == 1) cmd_starttls(NULL, 1); /* count the connection, now that it's established */ prometheus_increment(CYRUS_IMAP_CONNECTIONS_TOTAL); /* Setup a default namespace until replaced after authentication. */ mboxname_init_namespace(&imapd_namespace, /*isadmin*/1); mboxevent_setnamespace(&imapd_namespace); index_text_extractor_init(imapd_in); cmdloop(); /* LOGOUT executed */ prot_flush(imapd_out); prometheus_decrement(CYRUS_IMAP_ACTIVE_CONNECTIONS); /* send a Logout event notification */ if ((mboxevent = mboxevent_new(EVENT_LOGOUT))) { mboxevent_set_access(mboxevent, buf_cstringnull_ifempty(&saslprops.iplocalport), buf_cstringnull_ifempty(&saslprops.ipremoteport), imapd_userid, NULL, 1); mboxevent_notify(&mboxevent); mboxevent_free(&mboxevent); } /* cleanup */ imapd_reset(); if (config_iolog) { read_io_count(io_count_stop); syslog(LOG_INFO, "IMAP session stats : I/O read : %d bytes : I/O write : %d bytes", io_count_stop->io_read_count - io_count_start->io_read_count, io_count_stop->io_write_count - io_count_start->io_write_count); free (io_count_start); free (io_count_stop); } prometheus_increment(CYRUS_IMAP_READY_LISTENERS); return 0; } /* Called by service API to shut down the service */ void service_abort(int error) { shut_down(error); } /* * Try to find a motd file; if found spit out message as an [ALERT] */ static void motd_file(void) { char *filename = NULL; int fd = -1; struct protstream *motd_in = NULL; char buf[MAX_MAILBOX_PATH+1]; char *p; filename = strconcat(config_dir, "/msg/motd", (char *)NULL); fd = open(filename, O_RDONLY, 0); if (fd < 0) goto out; motd_in = prot_new(fd, 0); prot_fgets(buf, sizeof(buf), motd_in); if ((p = strchr(buf, '\r'))!=NULL) *p = 0; if ((p = strchr(buf, '\n'))!=NULL) *p = 0; for (p = buf; *p == '['; p++); /* can't have [ be first char, sigh */ prot_printf(imapd_out, "* OK [ALERT] %s\r\n", p); out: if (motd_in) prot_free(motd_in); if (fd >= 0) close(fd); free(filename); } /* * Cleanly shut down and exit */ void shut_down(int code) __attribute__((noreturn)); void shut_down(int code) { int i; uint64_t bytes_in = 0; uint64_t bytes_out = 0; in_shutdown = 1; /* run delayed commands before we take away all the environment */ libcyrus_run_delayed(); proc_cleanup(); for (i = 0; i < ptrarray_size(&backend_cached); i++) { struct backend *be = ptrarray_nth(&backend_cached, i); proxy_downserver(be); if (be->last_result.s) { free(be->last_result.s); } free(be); } ptrarray_fini(&backend_cached); if (mupdate_h) mupdate_disconnect(&mupdate_h); index_text_extractor_destroy(); if (idling) idle_stop(index_mboxname(imapd_index)); if (imapd_index) { if (config_getswitch(IMAPOPT_AUTOEXPUNGE) && index_hasrights(imapd_index, ACL_EXPUNGE)) index_expunge(imapd_index, NULL, 1); index_close(&imapd_index); } seen_done(); mboxkey_done(); annotatemore_close(); annotate_done(); idle_done(); partlist_local_done(); if (imapd_in) { /* Flush the incoming buffer */ prot_NONBLOCK(imapd_in); prot_fill(imapd_in); bytes_in = prot_bytes_in(imapd_in); prot_free(imapd_in); } if (imapd_out) { /* Flush the outgoing buffer */ prot_flush(imapd_out); bytes_out = prot_bytes_out(imapd_out); prot_free(imapd_out); /* one less active connection */ prometheus_decrement(CYRUS_IMAP_ACTIVE_CONNECTIONS); } else { /* one less ready listener */ prometheus_decrement(CYRUS_IMAP_READY_LISTENERS); } prometheus_increment(code ? CYRUS_IMAP_SHUTDOWN_TOTAL_STATUS_ERROR : CYRUS_IMAP_SHUTDOWN_TOTAL_STATUS_OK); if (config_auditlog) syslog(LOG_NOTICE, "auditlog: traffic sessionid=<%s>" " bytes_in=<%" PRIu64 "> bytes_out=<%" PRIu64 ">", session_id(), bytes_in, bytes_out); if (protin) protgroup_free(protin); #ifdef HAVE_SSL tls_shutdown_serverengine(); #endif saslprops_free(&saslprops); cyrus_done(); exit(code); } EXPORTED void fatal(const char *s, int code) { static int recurse_code = 0; if (recurse_code) { /* We were called recursively. Just give up */ proc_cleanup(); if (imapd_out) { /* one less active connection */ prometheus_decrement(CYRUS_IMAP_ACTIVE_CONNECTIONS); } else { /* one less ready listener */ prometheus_decrement(CYRUS_IMAP_READY_LISTENERS); } prometheus_increment(CYRUS_IMAP_SHUTDOWN_TOTAL_STATUS_ERROR); exit(recurse_code); } recurse_code = code; if (imapd_out) { prot_printf(imapd_out, "* BYE %s%s\r\n", *s == '[' /* resp-text-code */ ? "" : "Fatal error: ", s); prot_flush(imapd_out); } if (stages.count) { /* Cleanup the stage(s) */ struct appendstage *curstage; while ((curstage = ptrarray_pop(&stages))) { if (curstage->f != NULL) fclose(curstage->f); append_removestage(curstage->stage); strarray_fini(&curstage->flags); freeentryatts(curstage->annotations); free(curstage); } ptrarray_fini(&stages); } syslog(LOG_ERR, "Fatal error: %s", s); shut_down(code); } /* * Check the currently selected mailbox for updates. * * 'be' is the backend (if any) that we just proxied a command to. */ static void imapd_check(struct backend *be, int usinguid) { if (backend_current && backend_current != be) { /* remote mailbox */ char mytag[128]; proxy_gentag(mytag, sizeof(mytag)); prot_printf(backend_current->out, "%s Noop\r\n", mytag); pipe_until_tag(backend_current, mytag, 0); } else { /* local mailbox */ index_check(imapd_index, usinguid, 0); } } #define IS_EOL(c, pin) ((c = (c == '\r') ? prot_getc(pin) : c) == '\n') /* * Top-level command loop parsing */ static void cmdloop(void) { int c; int usinguid, havepartition, havenamespace, recursive; static struct buf tag, cmd, arg1, arg2, arg3; char *p, shut[MAX_MAILBOX_PATH+1], cmdname[100]; const char *err; const char * commandmintimer; double commandmintimerd = 0.0; struct sync_reserve_list *reserve_list = sync_reserve_list_create(SYNC_MESSAGE_LIST_HASH_SIZE); struct applepushserviceargs applepushserviceargs; int readonly = config_getswitch(IMAPOPT_READONLY); prot_printf(imapd_out, "* OK [CAPABILITY "); capa_response(CAPA_PREAUTH); prot_printf(imapd_out, "]"); if (config_serverinfo) prot_printf(imapd_out, " %s", config_servername); if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) { prot_printf(imapd_out, " Cyrus IMAP %s", CYRUS_VERSION); } prot_printf(imapd_out, " server ready\r\n"); /* clear cancelled flag if present before the next command */ cmd_cancelled(/*insearch*/0); motd_file(); /* Get command timer logging paramater. This string * is a time in seconds. Any command that takes >= * this time to execute is logged */ commandmintimer = config_getstring(IMAPOPT_COMMANDMINTIMER); cmdtime_settimer(commandmintimer ? 1 : 0); if (commandmintimer) { commandmintimerd = atof(commandmintimer); } for (;;) { /* Release any held index */ index_release(imapd_index); /* ensure we didn't leak anything! */ assert(!open_mailboxes_exist()); assert(!open_mboxlocks_exist()); sync_log_reset(); /* Flush any buffered output */ prot_flush(imapd_out); if (backend_current) prot_flush(backend_current->out); /* command no longer running */ proc_register(config_ident, imapd_clienthost, imapd_userid, index_mboxname(imapd_index), NULL); /* run any delayed cleanup while a user isn't waiting on a reply */ libcyrus_run_delayed(); /* Check for shutdown file */ if ( !imapd_userisadmin && imapd_userid && (shutdown_file(shut, sizeof(shut)) || userdeny(imapd_userid, config_ident, shut, sizeof(shut)))) { for (p = shut; *p == '['; p++); /* can't have [ be first char */ prot_printf(imapd_out, "* BYE [ALERT] %s\r\n", p); telemetry_rusage(imapd_userid); shut_down(0); } signals_poll(); if (!proxy_check_input(protin, imapd_in, imapd_out, backend_current ? backend_current->in : NULL, NULL, 0)) { /* No input from client */ continue; } /* Parse tag */ c = getword(imapd_in, &tag); if (c == EOF) { if ((err = prot_error(imapd_in))!=NULL && strcmp(err, PROT_EOF_STRING)) { syslog(LOG_WARNING, "%s, closing connection", err); prot_printf(imapd_out, "* BYE %s\r\n", err); } goto done; } if (c != ' ' || !imparse_isatom(tag.s) || (tag.s[0] == '*' && !tag.s[1])) { prot_printf(imapd_out, "* BAD Invalid tag\r\n"); eatline(imapd_in, c); continue; } /* Parse command name */ c = getword(imapd_in, &cmd); if (!cmd.s[0]) { prot_printf(imapd_out, "%s BAD Null command\r\n", tag.s); eatline(imapd_in, c); continue; } lcase(cmd.s); xstrncpy(cmdname, cmd.s, 99); cmd.s[0] = toupper((unsigned char) cmd.s[0]); if (config_getswitch(IMAPOPT_CHATTY)) syslog(LOG_NOTICE, "command: %s %s", tag.s, cmd.s); proc_register(config_ident, imapd_clienthost, imapd_userid, index_mboxname(imapd_index), cmd.s); /* if we need to force a kick, do so */ if (referral_kick) { kick_mupdate(); referral_kick = 0; } if (plaintextloginalert) { prot_printf(imapd_out, "* OK [ALERT] %s\r\n", plaintextloginalert); plaintextloginalert = NULL; } /* Only Authenticate/Enable/Login/Logout/Noop/Capability/Id/Starttls allowed when not logged in */ if (!imapd_userid && !strchr("AELNCIS", cmd.s[0])) goto nologin; /* Set limit on the total number of bytes allowed for arguments */ maxargssize_mark = prot_bytes_in(imapd_in) + maxargssize; /* Start command timer */ cmdtime_starttimer(); /* note that about half the commands (the common ones that don't hit the mailboxes file) now close the mailboxes file just in case it was open. */ switch (cmd.s[0]) { case 'A': if (!strcmp(cmd.s, "Authenticate")) { int haveinitresp = 0; if (c != ' ') goto missingargs; c = getword(imapd_in, &arg1); if (!imparse_isatom(arg1.s)) { prot_printf(imapd_out, "%s BAD Invalid authenticate mechanism\r\n", tag.s); eatline(imapd_in, c); continue; } if (c == ' ') { haveinitresp = 1; c = getword(imapd_in, &arg2); if (c == EOF) goto missingargs; } if (!IS_EOL(c, imapd_in)) goto extraargs; if (imapd_userid) { prot_printf(imapd_out, "%s BAD Already authenticated\r\n", tag.s); continue; } cmd_authenticate(tag.s, arg1.s, haveinitresp ? arg2.s : NULL); /* prometheus stat is counted by cmd_authenticate based on success/failure */ } else if (!imapd_userid) goto nologin; else if (!strcmp(cmd.s, "Append")) { if (readonly) goto noreadonly; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; cmd_append(tag.s, arg1.s, NULL); prometheus_increment(CYRUS_IMAP_APPEND_TOTAL); } else goto badcmd; break; case 'C': if (!strcmp(cmd.s, "Capability")) { if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_capability(tag.s); prometheus_increment(CYRUS_IMAP_CAPABILITY_TOTAL); } else if (!imapd_userid) goto nologin; #ifdef HAVE_ZLIB else if (!strcmp(cmd.s, "Compress")) { if (c != ' ') goto missingargs; c = getword(imapd_in, &arg1); if (c == EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_compress(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_COMPRESS_TOTAL); } #endif /* HAVE_ZLIB */ else if (!strcmp(cmd.s, "Check")) { if (!imapd_index && !backend_current) goto nomailbox; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_noop(tag.s, cmd.s); prometheus_increment(CYRUS_IMAP_CHECK_TOTAL); } else if (!strcmp(cmd.s, "Copy")) { if (readonly) goto noreadonly; if (!imapd_index && !backend_current) goto nomailbox; usinguid = 0; if (c != ' ') goto missingargs; copy: c = getword(imapd_in, &arg1); if (c == '\r') goto missingargs; if (c != ' ' || !imparse_issequence(arg1.s)) goto badsequence; c = getastring(imapd_in, imapd_out, &arg2); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_copy(tag.s, arg1.s, arg2.s, usinguid, /*ismove*/0); prometheus_increment(CYRUS_IMAP_COPY_TOTAL); } else if (!strcmp(cmd.s, "Create")) { if (readonly) goto noreadonly; struct dlist *extargs = NULL; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; if (c == ' ') { c = parsecreateargs(&extargs); if (c == EOF) goto badpartition; } if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_create(tag.s, arg1.s, extargs, 0); dlist_free(&extargs); prometheus_increment(CYRUS_IMAP_CREATE_TOTAL); } else if (!strcmp(cmd.s, "Close")) { if (!imapd_index && !backend_current) goto nomailbox; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_close(tag.s, cmd.s); prometheus_increment(CYRUS_IMAP_CLOSE_TOTAL); } else goto badcmd; break; case 'D': if (!strcmp(cmd.s, "Delete")) { if (readonly) goto noreadonly; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_delete(tag.s, arg1.s, 0, 0); prometheus_increment(CYRUS_IMAP_DELETE_TOTAL); } else if (!strcmp(cmd.s, "Deleteacl")) { if (readonly) goto noreadonly; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg2); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_setacl(tag.s, arg1.s, arg2.s, NULL); prometheus_increment(CYRUS_IMAP_DELETEACL_TOTAL); } else if (!strcmp(cmd.s, "Dump")) { if (readonly) goto noreadonly; int uid_start = 0; if(c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if(c == ' ') { c = getastring(imapd_in, imapd_out, &arg2); if(!imparse_isnumber(arg2.s)) goto extraargs; uid_start = atoi(arg2.s); } if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_dump(tag.s, arg1.s, uid_start); prometheus_increment(CYRUS_IMAP_DUMP_TOTAL); } else goto badcmd; break; case 'E': if (!imapd_userid) goto nologin; else if (!strcmp(cmd.s, "Enable")) { if (c != ' ') goto missingargs; cmd_enable(tag.s); } else if (!strcmp(cmd.s, "Expunge")) { if (readonly) goto noreadonly; if (!imapd_index && !backend_current) goto nomailbox; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_expunge(tag.s, 0); prometheus_increment(CYRUS_IMAP_EXPUNGE_TOTAL); } else if (!strcmp(cmd.s, "Examine")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; prot_ungetc(c, imapd_in); cmd_select(tag.s, cmd.s, arg1.s); prometheus_increment(CYRUS_IMAP_EXAMINE_TOTAL); } else goto badcmd; break; case 'F': if (!strcmp(cmd.s, "Fetch")) { if (!imapd_index && !backend_current) goto nomailbox; usinguid = 0; if (c != ' ') goto missingargs; fetch: c = getword(imapd_in, &arg1); if (c == '\r') goto missingargs; if (c != ' ' || !imparse_issequence(arg1.s)) goto badsequence; cmd_fetch(tag.s, arg1.s, usinguid); prometheus_increment(CYRUS_IMAP_FETCH_TOTAL); } else goto badcmd; break; case 'G': if (!strcmp(cmd.s, "Getacl")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_getacl(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_GETACL_TOTAL); } else if (!strcmp(cmd.s, "Getannotation")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; cmd_getannotation(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_GETANNOTATION_TOTAL); } else if (!strcmp(cmd.s, "Getmetadata")) { if (c != ' ') goto missingargs; cmd_getmetadata(tag.s); prometheus_increment(CYRUS_IMAP_GETMETADATA_TOTAL); } else if (!strcmp(cmd.s, "Getquota")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_getquota(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_GETQUOTA_TOTAL); } else if (!strcmp(cmd.s, "Getquotaroot")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_getquotaroot(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_GETQUOTAROOT_TOTAL); } #ifdef HAVE_SSL else if (!strcmp(cmd.s, "Genurlauth")) { if (c != ' ') goto missingargs; cmd_genurlauth(tag.s); prometheus_increment(CYRUS_IMAP_GENURLAUTH_TOTAL); } #endif else goto badcmd; break; case 'I': if (!strcmp(cmd.s, "Id")) { if (c != ' ') goto missingargs; cmd_id(tag.s); prometheus_increment(CYRUS_IMAP_ID_TOTAL); } else if (!imapd_userid) goto nologin; else if (!strcmp(cmd.s, "Idle") && idle_enabled()) { if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_idle(tag.s); prometheus_increment(CYRUS_IMAP_IDLE_TOTAL); } else goto badcmd; break; case 'L': if (!strcmp(cmd.s, "Login")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if(c != ' ') goto missingargs; cmd_login(tag.s, arg1.s); /* prometheus stat is counted by cmd_login based on success/failure */ } else if (!strcmp(cmd.s, "Logout")) { if (!IS_EOL(c, imapd_in)) goto extraargs; prometheus_increment(CYRUS_IMAP_LOGOUT_TOTAL); /* force any responses from our selected backend */ if (backend_current) imapd_check(NULL, 0); prot_printf(imapd_out, "* BYE %s\r\n", error_message(IMAP_BYE_LOGOUT)); prot_printf(imapd_out, "%s OK %s\r\n", tag.s, error_message(IMAP_OK_COMPLETED)); if (imapd_userid && *imapd_userid) { telemetry_rusage(imapd_userid); } goto done; } else if (!imapd_userid) goto nologin; else if (!strcmp(cmd.s, "List")) { struct listargs listargs; if (c != ' ') goto missingargs; memset(&listargs, 0, sizeof(struct listargs)); listargs.ret = LIST_RET_CHILDREN; getlistargs(tag.s, &listargs); if (listargs.pat.count) cmd_list(tag.s, &listargs); prometheus_increment(CYRUS_IMAP_LIST_TOTAL); } else if (!strcmp(cmd.s, "Lsub")) { struct listargs listargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg2); if (!IS_EOL(c, imapd_in)) goto extraargs; memset(&listargs, 0, sizeof(struct listargs)); listargs.cmd = LIST_CMD_LSUB; listargs.sel = LIST_SEL_SUBSCRIBED; if (!strcasecmpsafe(imapd_magicplus, "+dav")) listargs.sel |= LIST_SEL_DAV; listargs.ref = arg1.s; strarray_append(&listargs.pat, arg2.s); cmd_list(tag.s, &listargs); prometheus_increment(CYRUS_IMAP_LSUB_TOTAL); } else if (!strcmp(cmd.s, "Listrights")) { c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg2); if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_listrights(tag.s, arg1.s, arg2.s); prometheus_increment(CYRUS_IMAP_LISTRIGHTS_TOTAL); } else if (!strcmp(cmd.s, "Localappend")) { if (readonly) goto noreadonly; /* create a local-only mailbox */ if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg2); if (c != ' ') goto missingargs; cmd_append(tag.s, arg1.s, *arg2.s ? arg2.s : NULL); prometheus_increment(CYRUS_IMAP_APPEND_TOTAL); } else if (!strcmp(cmd.s, "Localcreate")) { if (readonly) goto noreadonly; /* create a local-only mailbox */ struct dlist *extargs = NULL; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; if (c == ' ') { c = parsecreateargs(&extargs); if (c == EOF) goto badpartition; } if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_create(tag.s, arg1.s, extargs, 1); dlist_free(&extargs); /* XXX prometheus_increment(CYRUS_IMAP_CREATE_TOTAL); */ } else if (!strcmp(cmd.s, "Localdelete")) { if (readonly) goto noreadonly; /* delete a mailbox locally only */ if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_delete(tag.s, arg1.s, 1, 1); /* XXX prometheus_increment(CYRUS_IMAP_DELETE_TOTAL); */ } else goto badcmd; break; case 'M': if (!strcmp(cmd.s, "Myrights")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_myrights(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_MYRIGHTS_TOTAL); } else if (!strcmp(cmd.s, "Mupdatepush")) { if (readonly) goto noreadonly; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if(c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_mupdatepush(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_MUPDATEPUSH_TOTAL); } else if (!strcmp(cmd.s, "Move")) { if (readonly) goto noreadonly; if (!imapd_index && !backend_current) goto nomailbox; usinguid = 0; if (c != ' ') goto missingargs; move: c = getword(imapd_in, &arg1); if (c == '\r') goto missingargs; if (c != ' ' || !imparse_issequence(arg1.s)) goto badsequence; c = getastring(imapd_in, imapd_out, &arg2); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_copy(tag.s, arg1.s, arg2.s, usinguid, /*ismove*/1); prometheus_increment(CYRUS_IMAP_COPY_TOTAL); } else goto badcmd; break; case 'N': if (!strcmp(cmd.s, "Noop")) { if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_noop(tag.s, cmd.s); /* XXX prometheus_increment(CYRUS_IMAP_NOOP_TOTAL); */ } else if (!imapd_userid) goto nologin; else if (!strcmp(cmd.s, "Namespace")) { if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_namespace(tag.s); /* XXX prometheus_increment(CYRUS_IMAP_NAMESPACE_TOTAL); */ } else goto badcmd; break; case 'R': if (!strcmp(cmd.s, "Rename")) { if (readonly) goto noreadonly; havepartition = 0; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg2); if (c <= EOF) goto missingargs; if (c == ' ') { havepartition = 1; c = getword(imapd_in, &arg3); if (!imparse_isatom(arg3.s)) goto badpartition; } if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_rename(tag.s, arg1.s, arg2.s, havepartition ? arg3.s : 0); /* XXX prometheus_increment(CYRUS_IMAP_RENAME_TOTAL); */ } else if(!strcmp(cmd.s, "Reconstruct")) { if (readonly) goto noreadonly; recursive = 0; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if(c == ' ') { /* Optional RECURSIVE argument */ c = getword(imapd_in, &arg2); if(!imparse_isatom(arg2.s)) goto extraargs; else if(!strcasecmp(arg2.s, "RECURSIVE")) recursive = 1; else goto extraargs; } if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_reconstruct(tag.s, arg1.s, recursive); /* XXX prometheus_increment(CYRUS_IMAP_RECONSTRUCT_TOTAL); */ } else if (!strcmp(cmd.s, "Rlist")) { struct listargs listargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg2); if (!IS_EOL(c, imapd_in)) goto extraargs; memset(&listargs, 0, sizeof(struct listargs)); listargs.sel = LIST_SEL_REMOTE; listargs.ret = LIST_RET_CHILDREN; listargs.ref = arg1.s; strarray_append(&listargs.pat, arg2.s); cmd_list(tag.s, &listargs); /* XXX prometheus_increment(prom_handle, CYRUS_IMAP_LIST_TOTAL); */ } else if (!strcmp(cmd.s, "Rlsub")) { struct listargs listargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg2); if (!IS_EOL(c, imapd_in)) goto extraargs; memset(&listargs, 0, sizeof(struct listargs)); listargs.cmd = LIST_CMD_LSUB; listargs.sel = LIST_SEL_REMOTE | LIST_SEL_SUBSCRIBED; listargs.ref = arg1.s; strarray_append(&listargs.pat, arg2.s); cmd_list(tag.s, &listargs); /* XXX prometheus_increment(prom_handle, CYRUS_IMAP_LSUB_TOTAL); */ } #ifdef HAVE_SSL else if (!strcmp(cmd.s, "Resetkey")) { int have_mbox = 0, have_mech = 0; if (c == ' ') { have_mbox = 1; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; if (c == ' ') { have_mech = 1; c = getword(imapd_in, &arg2); } } if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_resetkey(tag.s, have_mbox ? arg1.s : 0, have_mech ? arg2.s : 0); /* XXX prometheus_increment(CYRUS_IMAP_RESETKEY_TOTAL); */ } #endif else goto badcmd; break; case 'S': if (!strcmp(cmd.s, "Starttls")) { if (!tls_enabled()) { /* we don't support starttls */ goto badcmd; } if (!IS_EOL(c, imapd_in)) goto extraargs; /* XXX discard any input pipelined after STARTTLS */ prot_flush(imapd_in); /* if we've already done SASL fail */ if (imapd_userid != NULL) { prot_printf(imapd_out, "%s BAD Can't Starttls after authentication\r\n", tag.s); continue; } /* if we've already done COMPRESS fail */ if (imapd_compress_done == 1) { prot_printf(imapd_out, "%s BAD Can't Starttls after Compress\r\n", tag.s); continue; } /* check if already did a successful tls */ if (imapd_starttls_done == 1) { prot_printf(imapd_out, "%s BAD Already did a successful Starttls\r\n", tag.s); continue; } cmd_starttls(tag.s, 0); prometheus_increment(CYRUS_IMAP_STARTTLS_TOTAL); continue; } if (!imapd_userid) { goto nologin; } else if (!strcmp(cmd.s, "Store")) { if (readonly) goto noreadonly; if (!imapd_index && !backend_current) goto nomailbox; usinguid = 0; if (c != ' ') goto missingargs; store: c = getword(imapd_in, &arg1); if (c != ' ' || !imparse_issequence(arg1.s)) goto badsequence; cmd_store(tag.s, arg1.s, usinguid); prometheus_increment(CYRUS_IMAP_STORE_TOTAL); } else if (!strcmp(cmd.s, "Select")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; prot_ungetc(c, imapd_in); cmd_select(tag.s, cmd.s, arg1.s); prometheus_increment(CYRUS_IMAP_SELECT_TOTAL); } else if (!strcmp(cmd.s, "Search")) { if (!imapd_index && !backend_current) goto nomailbox; usinguid = 0; if (c != ' ') goto missingargs; search: cmd_search(tag.s, usinguid); prometheus_increment(CYRUS_IMAP_SEARCH_TOTAL); } else if (!strcmp(cmd.s, "Subscribe")) { if (readonly) goto noreadonly; if (c != ' ') goto missingargs; havenamespace = 0; c = getastring(imapd_in, imapd_out, &arg1); if (c == ' ') { havenamespace = 1; c = getastring(imapd_in, imapd_out, &arg2); } if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; if (havenamespace) { cmd_changesub(tag.s, arg1.s, arg2.s, 1); } else { cmd_changesub(tag.s, (char *)0, arg1.s, 1); } prometheus_increment(CYRUS_IMAP_SUBSCRIBE_TOTAL); } else if (!strcmp(cmd.s, "Setacl")) { if (readonly) goto noreadonly; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg2); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg3); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_setacl(tag.s, arg1.s, arg2.s, arg3.s); prometheus_increment(CYRUS_IMAP_SETACL_TOTAL); } else if (!strcmp(cmd.s, "Setannotation")) { if (readonly) goto noreadonly; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; cmd_setannotation(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_SETANNOTATION_TOTAL); } else if (!strcmp(cmd.s, "Setmetadata")) { if (readonly) goto noreadonly; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; cmd_setmetadata(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_SETMETADATA_TOTAL); } else if (!strcmp(cmd.s, "Setquota")) { if (readonly) goto noreadonly; if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; cmd_setquota(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_SETQUOTA_TOTAL); } else if (!strcmp(cmd.s, "Sort")) { if (!imapd_index && !backend_current) goto nomailbox; usinguid = 0; if (c != ' ') goto missingargs; sort: cmd_sort(tag.s, usinguid); prometheus_increment(CYRUS_IMAP_SORT_TOTAL); } else if (!strcmp(cmd.s, "Status")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; cmd_status(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_STATUS_TOTAL); } else if (!strcmp(cmd.s, "Scan")) { struct listargs listargs; c = getastring(imapd_in, imapd_out, &arg1); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg2); if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg3); if (!IS_EOL(c, imapd_in)) goto extraargs; memset(&listargs, 0, sizeof(struct listargs)); listargs.ref = arg1.s; strarray_append(&listargs.pat, arg2.s); listargs.scan = arg3.s; cmd_list(tag.s, &listargs); prometheus_increment(CYRUS_IMAP_SCAN_TOTAL); } else if (!strcmp(cmd.s, "Syncapply")) { if (!imapd_userisadmin) goto badcmd; struct dlist *kl = sync_parseline(imapd_in); if (kl) { cmd_syncapply(tag.s, kl, reserve_list); dlist_free(&kl); } else goto badrepl; } else if (!strcmp(cmd.s, "Syncget")) { if (!imapd_userisadmin) goto badcmd; struct dlist *kl = sync_parseline(imapd_in); if (kl) { cmd_syncget(tag.s, kl); dlist_free(&kl); } else goto badrepl; } else if (!strcmp(cmd.s, "Syncrestart")) { if (!imapd_userisadmin) goto badcmd; if (!IS_EOL(c, imapd_in)) goto extraargs; /* just clear the GUID cache */ cmd_syncrestart(tag.s, &reserve_list, 1); } else if (!strcmp(cmd.s, "Syncrestore")) { if (!imapd_userisadmin) goto badcmd; struct dlist *kl = sync_parseline(imapd_in); if (kl) { cmd_syncrestore(tag.s, kl, reserve_list); dlist_free(&kl); } else goto badrepl; } else goto badcmd; break; case 'T': if (!strcmp(cmd.s, "Thread")) { if (!imapd_index && !backend_current) goto nomailbox; usinguid = 0; if (c != ' ') goto missingargs; thread: cmd_thread(tag.s, usinguid); prometheus_increment(CYRUS_IMAP_THREAD_TOTAL); } else goto badcmd; break; case 'U': if (!strcmp(cmd.s, "Uid")) { if (!imapd_index && !backend_current) goto nomailbox; usinguid = 1; if (c != ' ') goto missingargs; c = getword(imapd_in, &arg1); if (c != ' ') goto missingargs; lcase(arg1.s); xstrncpy(cmdname, arg1.s, 99); if (!strcmp(arg1.s, "fetch")) { goto fetch; } else if (!strcmp(arg1.s, "store")) { if (readonly) goto noreadonly; goto store; } else if (!strcmp(arg1.s, "search")) { goto search; } else if (!strcmp(arg1.s, "sort")) { goto sort; } else if (!strcmp(arg1.s, "thread")) { goto thread; } else if (!strcmp(arg1.s, "copy")) { if (readonly) goto noreadonly; goto copy; } else if (!strcmp(arg1.s, "move")) { if (readonly) goto noreadonly; goto move; } else if (!strcmp(arg1.s, "xmove")) { if (readonly) goto noreadonly; goto move; } else if (!strcmp(arg1.s, "expunge")) { if (readonly) goto noreadonly; c = getword(imapd_in, &arg1); if (!imparse_issequence(arg1.s)) goto badsequence; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_expunge(tag.s, arg1.s); prometheus_increment(CYRUS_IMAP_EXPUNGE_TOTAL); } else if (!strcmp(arg1.s, "xrunannotator")) { if (readonly) goto noreadonly; goto xrunannotator; } else { prot_printf(imapd_out, "%s BAD Unrecognized UID subcommand\r\n", tag.s); eatline(imapd_in, c); } } else if (!strcmp(cmd.s, "Unauthenticate")) { if (!imapd_userisadmin) goto badcmd; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_unauthenticate(tag.s); prometheus_increment(CYRUS_IMAP_UNAUTHENTICATE_TOTAL); } else if (!strcmp(cmd.s, "Unsubscribe")) { if (readonly) goto noreadonly; if (c != ' ') goto missingargs; havenamespace = 0; c = getastring(imapd_in, imapd_out, &arg1); if (c == ' ') { havenamespace = 1; c = getastring(imapd_in, imapd_out, &arg2); } if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; if (havenamespace) { cmd_changesub(tag.s, arg1.s, arg2.s, 0); } else { cmd_changesub(tag.s, (char *)0, arg1.s, 0); } prometheus_increment(CYRUS_IMAP_UNSUBSCRIBE_TOTAL); } else if (!strcmp(cmd.s, "Unselect")) { if (!imapd_index && !backend_current) goto nomailbox; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_close(tag.s, cmd.s); prometheus_increment(CYRUS_IMAP_UNSELECT_TOTAL); } else if (!strcmp(cmd.s, "Undump")) { if (readonly) goto noreadonly; if(c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); /* we want to get a list at this point */ if(c != ' ') goto missingargs; cmd_undump(tag.s, arg1.s); /* XXX prometheus_increment(CYRUS_IMAP_UNDUMP_TOTAL); */ } #ifdef HAVE_SSL else if (!strcmp(cmd.s, "Urlfetch")) { if (c != ' ') goto missingargs; cmd_urlfetch(tag.s); /* XXX prometheus_increment(CYRUS_IMAP_URLFETCH_TOTAL); */ } #endif else goto badcmd; break; case 'X': if (!strcmp(cmd.s, "Xbackup")) { if (readonly) goto noreadonly; int havechannel = 0; if (!config_getswitch(IMAPOPT_XBACKUP_ENABLED)) goto badcmd; /* user */ if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); /* channel */ if (c == ' ') { havechannel = 1; c = getword(imapd_in, &arg2); if (c == EOF) goto missingargs; } if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_xbackup(tag.s, arg1.s, havechannel ? arg2.s : NULL); prometheus_increment(CYRUS_IMAP_XBACKUP_TOTAL); } else if (!strcmp(cmd.s, "Xfer")) { if (readonly) goto noreadonly; int havepartition = 0; /* Mailbox */ if(c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); /* Dest Server */ if(c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg2); if(c == ' ') { /* Dest Partition */ c = getastring(imapd_in, imapd_out, &arg3); if (!imparse_isatom(arg3.s)) goto badpartition; havepartition = 1; } if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_xfer(tag.s, arg1.s, arg2.s, (havepartition ? arg3.s : NULL)); /* XXX prometheus_increment(CYRUS_IMAP_XFER_TOTAL); */ } else if (!strcmp(cmd.s, "Xlist")) { struct listargs listargs; if (c != ' ') goto missingargs; memset(&listargs, 0, sizeof(struct listargs)); listargs.cmd = LIST_CMD_XLIST; listargs.ret = LIST_RET_CHILDREN | LIST_RET_SPECIALUSE; getlistargs(tag.s, &listargs); if (listargs.pat.count) cmd_list(tag.s, &listargs); prometheus_increment(CYRUS_IMAP_LIST_TOTAL); } else if (!strcmp(cmd.s, "Xmove")) { if (readonly) goto noreadonly; if (!imapd_index && !backend_current) goto nomailbox; usinguid = 0; if (c != ' ') goto missingargs; goto move; } else if (!strcmp(cmd.s, "Xrunannotator")) { if (readonly) goto noreadonly; if (!imapd_index && !backend_current) goto nomailbox; usinguid = 0; if (c != ' ') goto missingargs; xrunannotator: c = getword(imapd_in, &arg1); if (!arg1.len || !imparse_issequence(arg1.s)) goto badsequence; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_xrunannotator(tag.s, arg1.s, usinguid); /* XXX prometheus_increment(CYRUS_IMAP_XRUNANNOTATOR_TOTAL); */ } else if (!strcmp(cmd.s, "Xstats")) { if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_xstats(tag.s); } else if (!strcmp(cmd.s, "Xwarmup")) { /* XWARMUP doesn't need a mailbox to be selected */ if (c != ' ') goto missingargs; cmd_xwarmup(tag.s); /* XXX prometheus_increment(CYRUS_IMAP_XWARMUP_TOTAL); */ } else if (!strcmp(cmd.s, "Xkillmy")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c == EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_xkillmy(tag.s, arg1.s); } else if (!strcmp(cmd.s, "Xforever")) { if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_xforever(tag.s); } else if (!strcmp(cmd.s, "Xmeid")) { if (c != ' ') goto missingargs; c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto missingargs; if (!IS_EOL(c, imapd_in)) goto extraargs; cmd_xmeid(tag.s, arg1.s); } else if (apns_enabled && !strcmp(cmd.s, "Xapplepushservice")) { if (c != ' ') goto missingargs; memset(&applepushserviceargs, 0, sizeof(struct applepushserviceargs)); do { c = getastring(imapd_in, imapd_out, &arg1); if (c <= EOF) goto aps_missingargs; if (!strcmp(arg1.s, "mailboxes")) { c = prot_getc(imapd_in); if (c != '(') goto aps_missingargs; c = prot_getc(imapd_in); if (c != ')') { prot_ungetc(c, imapd_in); do { c = getastring(imapd_in, imapd_out, &arg2); if (c <= EOF) break; strarray_push(&applepushserviceargs.mailboxes, arg2.s); } while (c == ' '); } if (c != ')') goto aps_missingargs; c = prot_getc(imapd_in); } else { c = getastring(imapd_in, imapd_out, &arg2); // regular key/value if (!strcmp(arg1.s, "aps-version")) { if (!imparse_isnumber(arg2.s)) goto aps_extraargs; applepushserviceargs.aps_version = atoi(arg2.s); } else if (!strcmp(arg1.s, "aps-account-id")) buf_copy(&applepushserviceargs.aps_account_id, &arg2); else if (!strcmp(arg1.s, "aps-device-token")) buf_copy(&applepushserviceargs.aps_device_token, &arg2); else if (!strcmp(arg1.s, "aps-subtopic")) buf_copy(&applepushserviceargs.aps_subtopic, &arg2); else goto aps_extraargs; } } while (c == ' '); if (!IS_EOL(c, imapd_in)) goto aps_extraargs; cmd_xapplepushservice(tag.s, &applepushserviceargs); } else goto badcmd; break; default: badcmd: prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag.s); eatline(imapd_in, c); } /* End command timer - don't log "idle" commands */ if (commandmintimer && strcmp("idle", cmdname)) { double cmdtime, nettime; const char *mboxname = index_mboxname(imapd_index); cmdtime_endtimer(&cmdtime, &nettime); if (cmdtime >= commandmintimerd) { xsyslog(LOG_NOTICE, "cmdtimer", "sessionid=<%s> userid=<%s> command=<%s>" " mailbox=<%s> cmdtime=<%f> nettime=<%f>" " total=<%f>", session_id(), imapd_userid ? imapd_userid : "", cmdname, mboxname ? mboxname : "", cmdtime, nettime, cmdtime + nettime); } } continue; nologin: prot_printf(imapd_out, "%s BAD Please login first\r\n", tag.s); eatline(imapd_in, c); continue; nomailbox: prot_printf(imapd_out, "%s BAD Please select a mailbox first\r\n", tag.s); eatline(imapd_in, c); continue; noreadonly: prot_printf(imapd_out, "%s NO %s\r\n", tag.s, error_message(IMAP_CONNECTION_READONLY)); eatline(imapd_in, c); continue; aps_missingargs: buf_free(&applepushserviceargs.aps_account_id); buf_free(&applepushserviceargs.aps_device_token); buf_free(&applepushserviceargs.aps_subtopic); strarray_fini(&applepushserviceargs.mailboxes); missingargs: if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; prot_printf(imapd_out, "%s BAD Missing required argument to %s\r\n", tag.s, cmd.s); eatline(imapd_in, c); continue; aps_extraargs: buf_free(&applepushserviceargs.aps_account_id); buf_free(&applepushserviceargs.aps_device_token); buf_free(&applepushserviceargs.aps_subtopic); strarray_fini(&applepushserviceargs.mailboxes); extraargs: if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; prot_printf(imapd_out, "%s BAD Unexpected extra arguments to %s\r\n", tag.s, cmd.s); eatline(imapd_in, c); continue; maxliteral: prot_printf(imapd_out, "%s NO %s in %s\r\n", tag.s, error_message(IMAP_LITERAL_TOO_LARGE), cmd.s); continue; badsequence: prot_printf(imapd_out, "%s BAD Invalid sequence in %s\r\n", tag.s, cmd.s); eatline(imapd_in, c); continue; badpartition: prot_printf(imapd_out, "%s BAD Invalid partition name in %s\r\n", tag.s, cmd.s); eatline(imapd_in, c); continue; badrepl: prot_printf(imapd_out, "%s BAD Replication parse failure in %s\r\n", tag.s, cmd.s); /* n.b. sync_parseline already ate the bad line */ continue; } done: cmd_syncrestart(NULL, &reserve_list, 0); } #ifdef USE_AUTOCREATE /* * Autocreate Inbox and subfolders upon login */ static void autocreate_inbox(void) { if (imapd_userisadmin) return; if (imapd_userisproxyadmin) return; if (config_getint(IMAPOPT_AUTOCREATE_QUOTA) >= 0) { char *inboxname = mboxname_user_mbox(imapd_userid, NULL); int r = mboxlist_lookup(inboxname, NULL, NULL); free(inboxname); if (r != IMAP_MAILBOX_NONEXISTENT) return; autocreate_user(&imapd_namespace, imapd_userid); } } #endif // USE_AUTOCREATE static void authentication_success(void) { int r; struct mboxevent *mboxevent; /* authstate already created by mysasl_proxy_policy() */ imapd_userisadmin = global_authisa(imapd_authstate, IMAPOPT_ADMINS); /* Create telemetry log */ imapd_logfd = telemetry_log(imapd_userid, imapd_in, imapd_out, 0); /* Set namespace */ r = mboxname_init_namespace(&imapd_namespace, imapd_userisadmin || imapd_userisproxyadmin); mboxevent_setnamespace(&imapd_namespace); if (r) { syslog(LOG_ERR, "%s", error_message(r)); fatal(error_message(r), EX_CONFIG); } /* Make a copy of the external userid for use in proxying */ proxy_userid = xstrdup(imapd_userid); /* send a Login event notification */ if ((mboxevent = mboxevent_new(EVENT_LOGIN))) { mboxevent_set_access(mboxevent, buf_cstringnull_ifempty(&saslprops.iplocalport), buf_cstringnull_ifempty(&saslprops.ipremoteport), imapd_userid, NULL, 1); mboxevent_notify(&mboxevent); mboxevent_free(&mboxevent); } #ifdef USE_AUTOCREATE autocreate_inbox(); #endif // USE_AUTOCREATE } static int checklimits(const char *tag) { struct proc_limits limits; limits.procname = "imapd"; limits.clienthost = imapd_clienthost; limits.userid = imapd_userid; if (proc_checklimits(&limits)) { const char *sep = ""; char part1[1024] = ""; char part2[1024] = ""; prot_printf(imapd_out, "%s NO Too many open connections (", tag); if (limits.maxhost) { prot_printf(imapd_out, "%s%d of %d from %s", sep, limits.host, limits.maxhost, imapd_clienthost); snprintf(part1, sizeof(part1), "%s%d of %d from %s", sep, limits.host, limits.maxhost, imapd_clienthost); sep = ", "; } if (limits.maxuser) { prot_printf(imapd_out, "%s%d of %d for %s", sep, limits.user, limits.maxuser, imapd_userid); snprintf(part2, sizeof(part2), "%s%d of %d for %s", sep, limits.user, limits.maxuser, imapd_userid); } prot_printf(imapd_out, ")\r\n"); syslog(LOG_ERR, "Too many open connections (%s%s)", part1, part2); free(imapd_userid); imapd_userid = NULL; auth_freestate(imapd_authstate); imapd_authstate = NULL; return 1; } return 0; } /* * Perform a LOGIN command */ static void cmd_login(char *tag, char *user) { char userbuf[MAX_MAILBOX_BUFFER]; char replybuf[MAX_MAILBOX_BUFFER]; unsigned userlen; const char *canon_user = userbuf; const void *val; int c; struct buf passwdbuf; char *passwd; const char *reply = NULL; int r; int failedloginpause; if (imapd_userid) { eatline(imapd_in, ' '); prot_printf(imapd_out, "%s BAD Already logged in\r\n", tag); return; } r = imapd_canon_user(imapd_saslconn, NULL, user, 0, SASL_CU_AUTHID | SASL_CU_AUTHZID, NULL, userbuf, sizeof(userbuf), &userlen); if (r) { eatline(imapd_in, ' '); syslog(LOG_NOTICE, "badlogin: %s plaintext (%s) invalid user", imapd_clienthost, beautify_string(user)); prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_INVALID_USER)); return; } /* possibly disallow login */ if (imapd_tls_required || (!imapd_starttls_done && (extprops_ssf < 2) && !config_getswitch(IMAPOPT_ALLOWPLAINTEXT) && !is_userid_anonymous(canon_user))) { eatline(imapd_in, ' '); prot_printf(imapd_out, "%s NO Login only available under a layer\r\n", tag); return; } memset(&passwdbuf,0,sizeof(struct buf)); c = getastring(imapd_in, imapd_out, &passwdbuf); if (!IS_EOL(c, imapd_in)) { buf_free(&passwdbuf); if (c == IMAP_LITERAL_TOO_LARGE) { prot_printf(imapd_out, "%s NO %s in LOGIN\r\n", tag, error_message(c)); } else { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to LOGIN\r\n", tag); eatline(imapd_in, c); } return; } passwd = passwdbuf.s; if (is_userid_anonymous(canon_user)) { if (config_getswitch(IMAPOPT_ALLOWANONYMOUSLOGIN)) { passwd = beautify_string(passwd); if (strlen(passwd) > 500) passwd[500] = '\0'; syslog(LOG_NOTICE, "login: %s anonymous %s", imapd_clienthost, passwd); reply = "Anonymous access granted"; imapd_userid = xstrdup("anonymous"); } else { syslog(LOG_NOTICE, "badlogin: %s anonymous login refused", imapd_clienthost); prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_ANONYMOUS_NOT_PERMITTED)); buf_free(&passwdbuf); return; } } else if ( nosaslpasswdcheck ) { snprintf(replybuf, sizeof(replybuf), "User logged in SESSIONID=<%s>", session_id()); reply = replybuf; imapd_userid = xstrdup(canon_user); imapd_authstate = auth_newstate(canon_user); syslog(LOG_NOTICE, "login: %s %s%s nopassword%s %s", imapd_clienthost, imapd_userid, imapd_magicplus ? imapd_magicplus : "", imapd_starttls_done ? "+TLS" : "", reply); } else if ((r = sasl_checkpass(imapd_saslconn, canon_user, strlen(canon_user), passwd, strlen(passwd))) != SASL_OK) { syslog(LOG_NOTICE, "badlogin: %s plaintext (%s) [%s]", imapd_clienthost, canon_user, sasl_errdetail(imapd_saslconn)); failedloginpause = config_getduration(IMAPOPT_FAILEDLOGINPAUSE, 's'); if (failedloginpause != 0) { sleep(failedloginpause); } /* Don't allow user probing */ if (r == SASL_NOUSER) r = SASL_BADAUTH; if ((reply = sasl_errstring(r, NULL, NULL)) != NULL) { prot_printf(imapd_out, "%s NO Login failed: %s\r\n", tag, reply); } else { prot_printf(imapd_out, "%s NO Login failed: %d\r\n", tag, r); } prometheus_increment(CYRUS_IMAP_AUTHENTICATE_TOTAL_RESULT_NO); buf_free(&passwdbuf); return; } else { r = sasl_getprop(imapd_saslconn, SASL_USERNAME, &val); if(r != SASL_OK) { if ((reply = sasl_errstring(r, NULL, NULL)) != NULL) { prot_printf(imapd_out, "%s NO Login failed: %s\r\n", tag, reply); } else { prot_printf(imapd_out, "%s NO Login failed: %d\r\n", tag, r); } prometheus_increment(CYRUS_IMAP_AUTHENTICATE_TOTAL_RESULT_NO); buf_free(&passwdbuf); return; } snprintf(replybuf, sizeof(replybuf), "User logged in SESSIONID=<%s>", session_id()); reply = replybuf; imapd_userid = xstrdup((const char *) val); prometheus_increment(CYRUS_IMAP_AUTHENTICATE_TOTAL_RESULT_YES); syslog(LOG_NOTICE, "login: %s %s%s plaintext%s %s", imapd_clienthost, imapd_userid, imapd_magicplus ? imapd_magicplus : "", imapd_starttls_done ? "+TLS" : "", reply ? reply : ""); /* Apply penalty only if not under layer */ if (!imapd_starttls_done) { int plaintextloginpause = config_getduration(IMAPOPT_PLAINTEXTLOGINPAUSE, 's'); if (plaintextloginpause) { sleep(plaintextloginpause); } /* Fetch plaintext login nag message */ plaintextloginalert = config_getstring(IMAPOPT_PLAINTEXTLOGINALERT); } } buf_free(&passwdbuf); if (checklimits(tag)) return; prot_printf(imapd_out, "%s OK [CAPABILITY ", tag); capa_response(CAPA_PREAUTH|CAPA_POSTAUTH); prot_printf(imapd_out, "] %s\r\n", reply); authentication_success(); } /* * Perform an AUTHENTICATE command */ static void cmd_authenticate(char *tag, char *authtype, char *resp) { int sasl_result; const void *val; const char *ssfmsg = NULL; const char *canon_user; int r; int failedloginpause; if (imapd_tls_required) { prot_printf(imapd_out, "%s NO Authenticate only available under a layer\r\n", tag); return; } r = saslserver(imapd_saslconn, authtype, resp, "", "+ ", "", imapd_in, imapd_out, &sasl_result, NULL); if (r) { const char *errorstring = NULL; const char *userid = "-notset-"; switch (r) { case IMAP_SASL_CANCEL: prot_printf(imapd_out, "%s BAD Client canceled authentication\r\n", tag); break; case IMAP_SASL_PROTERR: errorstring = prot_error(imapd_in); prot_printf(imapd_out, "%s NO Error reading client response: %s\r\n", tag, errorstring ? errorstring : ""); break; default: /* failed authentication */ if (sasl_result != SASL_NOUSER) sasl_getprop(imapd_saslconn, SASL_USERNAME, (const void **) &userid); syslog(LOG_NOTICE, "badlogin: %s %s (%s) [%s]", imapd_clienthost, authtype, userid, sasl_errdetail(imapd_saslconn)); prometheus_increment(CYRUS_IMAP_AUTHENTICATE_TOTAL_RESULT_NO); failedloginpause = config_getduration(IMAPOPT_FAILEDLOGINPAUSE, 's'); if (failedloginpause != 0) { sleep(failedloginpause); } /* Don't allow user probing */ if (sasl_result == SASL_NOUSER) sasl_result = SASL_BADAUTH; errorstring = sasl_errstring(sasl_result, NULL, NULL); if (errorstring) { prot_printf(imapd_out, "%s NO %s\r\n", tag, errorstring); } else { prot_printf(imapd_out, "%s NO Error authenticating\r\n", tag); } } reset_saslconn(&imapd_saslconn); return; } /* successful authentication */ /* get the userid from SASL --- already canonicalized from * mysasl_proxy_policy() */ sasl_result = sasl_getprop(imapd_saslconn, SASL_USERNAME, &val); if (sasl_result != SASL_OK) { prot_printf(imapd_out, "%s NO weird SASL error %d SASL_USERNAME\r\n", tag, sasl_result); syslog(LOG_ERR, "weird SASL error %d getting SASL_USERNAME", sasl_result); reset_saslconn(&imapd_saslconn); return; } canon_user = (const char *) val; /* If we're proxying, the authzid may contain a magic plus, so re-canonify it */ if (config_getswitch(IMAPOPT_IMAPMAGICPLUS) && strchr(canon_user, '+')) { char userbuf[MAX_MAILBOX_BUFFER]; unsigned userlen; sasl_result = imapd_canon_user(imapd_saslconn, NULL, canon_user, 0, SASL_CU_AUTHID | SASL_CU_AUTHZID, NULL, userbuf, sizeof(userbuf), &userlen); if (sasl_result != SASL_OK) { prot_printf(imapd_out, "%s NO SASL canonification error %d\r\n", tag, sasl_result); reset_saslconn(&imapd_saslconn); return; } imapd_userid = xstrdup(userbuf); } else { imapd_userid = xstrdup(canon_user); } syslog(LOG_NOTICE, "login: %s %s%s %s%s User logged in SESSIONID=<%s>", imapd_clienthost, imapd_userid, imapd_magicplus ? imapd_magicplus : "", authtype, imapd_starttls_done ? "+TLS" : "", session_id()); sasl_getprop(imapd_saslconn, SASL_SSF, &val); saslprops.ssf = *((sasl_ssf_t *) val); /* really, we should be doing a sasl_getprop on SASL_SSF_EXTERNAL, but the current libsasl doesn't allow that. */ if (imapd_starttls_done) { switch(saslprops.ssf) { case 0: ssfmsg = "tls protection"; break; case 1: ssfmsg = "tls plus integrity protection"; break; default: ssfmsg = "tls plus privacy protection"; break; } } else { switch(saslprops.ssf) { case 0: ssfmsg = "no protection"; break; case 1: ssfmsg = "integrity protection"; break; default: ssfmsg = "privacy protection"; break; } } prometheus_increment(CYRUS_IMAP_AUTHENTICATE_TOTAL_RESULT_YES); if (checklimits(tag)) { reset_saslconn(&imapd_saslconn); return; } if (!saslprops.ssf) { prot_printf(imapd_out, "%s OK [CAPABILITY ", tag); capa_response(CAPA_PREAUTH|CAPA_POSTAUTH); prot_printf(imapd_out, "] Success (%s) SESSIONID=<%s>\r\n", ssfmsg, session_id()); } else { prot_printf(imapd_out, "%s OK Success (%s) SESSIONID=<%s>\r\n", tag, ssfmsg, session_id()); } prot_setsasl(imapd_in, imapd_saslconn); prot_setsasl(imapd_out, imapd_saslconn); authentication_success(); } /* * Perform an UNAUTHENTICATE command */ static void cmd_unauthenticate(char *tag) { /* Unselect any open mailbox */ if (backend_current) { /* remote mailbox */ char mytag[128]; proxy_gentag(mytag, sizeof(mytag)); prot_printf(backend_current->out, "%s Unselect\r\n", mytag); /* do not fatal() here, because we don't really care about this * server anymore anyway */ pipe_until_tag(backend_current, mytag, 1); /* remove backend_current from the protgroup */ protgroup_delete(protin, backend_current->in); backend_current = NULL; } else if (imapd_index) { if (config_getswitch(IMAPOPT_AUTOEXPUNGE) && index_hasrights(imapd_index, ACL_EXPUNGE)) index_expunge(imapd_index, NULL, 1); index_close(&imapd_index); } /* Reset authentication state */ if (imapd_userid != NULL) { free(imapd_userid); imapd_userid = NULL; } if (proxy_userid != NULL) { free(proxy_userid); proxy_userid = NULL; } if (imapd_magicplus != NULL) { free(imapd_magicplus); imapd_magicplus = NULL; } if (imapd_authstate) { auth_freestate(imapd_authstate); imapd_authstate = NULL; } imapd_userisadmin = 0; imapd_userisproxyadmin = 0; plaintextloginalert = NULL; saslprops_reset(&saslprops); clear_id(); /* Reset client-enabled extensions */ client_capa = 0; sync_sieve_mailbox_enabled = 0; /* Send response (MUST be done with current SASL and/or commpression layer still active) */ prot_printf(imapd_out, "%s OK [CAPABILITY ", tag); capa_response(CAPA_PREAUTH); prot_printf(imapd_out, "] %s\r\n", error_message(IMAP_OK_COMPLETED)); prot_flush(imapd_out); /* Reset connection state (other than TLS) */ #ifdef HAVE_ZLIB if (imapd_compress_done) { /* disable (de)compression on the prot layer */ prot_unsetcompress(imapd_in); prot_unsetcompress(imapd_out); imapd_compress_done = 0; } #endif if (imapd_saslconn) { /* disable SASL on the prot layer */ prot_unsetsasl(imapd_out); prot_unsetsasl(imapd_in); reset_saslconn(&imapd_saslconn); } } /* * Perform a NOOP command */ static void cmd_noop(char *tag, char *cmd) { if (backend_current) { /* remote mailbox */ prot_printf(backend_current->out, "%s %s\r\n", tag, cmd); pipe_including_tag(backend_current, tag, 0); return; } index_check(imapd_index, 1, 0); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } static void clear_id() { if (imapd_id.params) { freeattvalues(imapd_id.params); } memset(&imapd_id, 0, sizeof(struct id_data)); } /* * Parse and perform an ID command. * * the command has been parsed up to the parameter list. * * we only allow one ID in non-authenticated state from a given client. * we only allow MAXIDFAILED consecutive failed IDs from a given client. * we only record MAXIDLOG ID responses from a given client. */ static void cmd_id(char *tag) { int c = EOF, npair = 0; static struct buf arg, field; /* check if we've already had an ID in non-authenticated state */ if (!imapd_userid && imapd_id.did_id) { prot_printf(imapd_out, "%s OK NIL\r\n", tag); eatline(imapd_in, c); return; } clear_id(); /* ok, accept parameter list */ c = getword(imapd_in, &arg); /* check for "NIL" or start of parameter list */ if (strcasecmp(arg.s, "NIL") && c != '(') { prot_printf(imapd_out, "%s BAD Invalid parameter list in Id\r\n", tag); eatline(imapd_in, c); return; } /* parse parameter list */ if (c == '(') { for (;;) { if (c == ')') { /* end of string/value pairs */ break; } /* get field name */ c = getstring(imapd_in, imapd_out, &field); if (c != ' ' || /* get field value */ (c = getnstring(imapd_in, imapd_out, &arg)) == EOF || (c != ' ' && c != ')')) { if (c == IMAP_LITERAL_TOO_LARGE) { prot_printf(imapd_out, "%s NO %s in Id\r\n", tag, error_message(c)); } else { prot_printf(imapd_out, "%s BAD Invalid field-value pair in Id\r\n", tag); eatline(imapd_in, c); } return; } /* ok, we're anal, but we'll still process the ID command */ if (strlen(field.s) > MAXIDFIELDLEN) { prot_printf(imapd_out, "%s BAD field longer than %u octets in Id\r\n", tag, MAXIDFIELDLEN); eatline(imapd_in, c); return; } if (arg.len > MAXIDVALUELEN) { prot_printf(imapd_out, "%s BAD value longer than %u octets in Id\r\n", tag, MAXIDVALUELEN); eatline(imapd_in, c); return; } if (++npair > MAXIDPAIRS) { prot_printf(imapd_out, "%s BAD too many (%u) field-value pairs in ID\r\n", tag, MAXIDPAIRS); eatline(imapd_in, c); return; } if (!strcmp(field.s, "os") && !strcmp(arg.s, "iOS")) { imapd_id.quirks |= QUIRK_SEARCHFUZZY; } /* ok, we're happy enough */ appendattvalue(&imapd_id.params, field.s, &arg); } if (c != ')') { /* erp! */ prot_printf(imapd_out, "%s BAD trailing junk\r\n", tag); eatline(imapd_in, c); return; } c = prot_getc(imapd_in); } /* check for CRLF */ if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Id\r\n", tag); eatline(imapd_in, c); return; } /* log the client's ID string. eventually this should be a callback or something. */ if (npair) { struct buf logbuf = BUF_INITIALIZER; struct attvaluelist *pptr; for (pptr = imapd_id.params; pptr; pptr = pptr->next) { const char *val = buf_cstring(&pptr->value); /* should we check for and format literals here ??? */ buf_printf(&logbuf, " \"%s\" ", pptr->attrib); if (!val || !strcmp(val, "NIL")) buf_printf(&logbuf, "NIL"); else buf_printf(&logbuf, "\"%s\"", val); } syslog(LOG_INFO, "client id sessionid=<%s>:%s", session_id(), buf_cstring(&logbuf)); buf_free(&logbuf); } /* spit out our ID string. eventually this might be configurable. */ if (config_getswitch(IMAPOPT_IMAPIDRESPONSE) && (imapd_authstate || (config_serverinfo == IMAP_ENUM_SERVERINFO_ON))) { id_response(imapd_out); prot_printf(imapd_out, ")\r\n"); } else if (config_serverinfo) { prot_printf(imapd_out, "* ID ("); prot_printf(imapd_out, "\"name\" \"Cyrus IMAPD\""); if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) { prot_printf(imapd_out, " \"version\" \"%s\"", CYRUS_VERSION); } prot_printf(imapd_out, " \"vendor\" \"Project Cyrus\""); prot_printf(imapd_out, " \"support-url\" \"https://www.cyrusimap.org\""); prot_printf(imapd_out, ")\r\n"); } else { prot_printf(imapd_out, "* ID NIL\r\n"); } imapd_check(NULL, 0); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); imapd_id.did_id = 1; } static bool deadline_exceeded(const struct timespec *d) { struct timespec now; if (d->tv_sec <= 0) { /* No deadline configured */ return false; } errno = 0; if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) { syslog(LOG_ERR, "clock_gettime (%d %m): error reading clock", errno); return false; } return now.tv_sec > d->tv_sec || (now.tv_sec == d->tv_sec && now.tv_nsec > d->tv_nsec); } /* * Perform an IDLE command */ static void cmd_idle(char *tag) { int c = EOF; int flags; static struct buf arg; static int idle_period = -1; static time_t idle_timeout = -1; struct timespec deadline = { 0, 0 }; if (idle_timeout == -1) { idle_timeout = config_getduration(IMAPOPT_IMAPIDLETIMEOUT, 'm'); if (idle_timeout <= 0) { idle_timeout = config_getduration(IMAPOPT_TIMEOUT, 'm'); } } if (idle_timeout > 0) { errno = 0; if (clock_gettime(CLOCK_MONOTONIC, &deadline) == -1) { syslog(LOG_ERR, "clock_gettime (%d %m): error reading clock", errno); } else { deadline.tv_sec += idle_timeout; } } if (!backend_current) { /* Local mailbox */ /* Tell client we are idling and waiting for end of command */ prot_printf(imapd_out, "+ idling\r\n"); prot_flush(imapd_out); /* Start doing mailbox updates */ index_check(imapd_index, 1, 0); idle_start(index_mboxname(imapd_index)); /* use this flag so if getc causes a shutdown due to * connection abort we tell idled about it */ idling = 1; index_release(imapd_index); prot_flush(imapd_out); while ((flags = idle_wait(imapd_in->fd))) { if (deadline_exceeded(&deadline)) { syslog(LOG_DEBUG, "timeout for user '%s' while idling", imapd_userid); shut_down(0); break; } if (flags & IDLE_INPUT) { /* Get continuation data */ c = getword(imapd_in, &arg); break; } /* Send unsolicited untagged responses to the client */ if (flags & IDLE_MAILBOX) index_check(imapd_index, 1, 0); if (flags & IDLE_ALERT) { char shut[MAX_MAILBOX_PATH+1]; if (! imapd_userisadmin && (shutdown_file(shut, sizeof(shut)) || (imapd_userid && userdeny(imapd_userid, config_ident, shut, sizeof(shut))))) { char *p; for (p = shut; *p == '['; p++); /* can't have [ be first char */ prot_printf(imapd_out, "* BYE [ALERT] %s\r\n", p); shut_down(0); } } index_release(imapd_index); prot_flush(imapd_out); } /* Stop updates and do any necessary cleanup */ idling = 0; idle_stop(index_mboxname(imapd_index)); } else { /* Remote mailbox */ int done = 0; enum { shutdown_skip, shutdown_bye, shutdown_silent } shutdown = shutdown_skip; char buf[2048]; /* get polling period */ if (idle_period == -1) { idle_period = config_getduration(IMAPOPT_IMAPIDLEPOLL, 's'); } if (CAPA(backend_current, CAPA_IDLE)) { /* Start IDLE on backend */ prot_printf(backend_current->out, "%s IDLE\r\n", tag); if (!prot_fgets(buf, sizeof(buf), backend_current->in)) { /* If we received nothing from the backend, fail */ prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_SERVER_UNAVAILABLE)); return; } if (buf[0] != '+') { /* If we received anything but a continuation response, spit out what we received and quit */ prot_write(imapd_out, buf, strlen(buf)); return; } } /* Tell client we are idling and waiting for end of command */ prot_printf(imapd_out, "+ idling\r\n"); prot_flush(imapd_out); /* Pipe updates to client while waiting for end of command */ while (!done) { if (deadline_exceeded(&deadline)) { syslog(LOG_DEBUG, "timeout for user '%s' while idling on remote mailbox", imapd_userid); shutdown = shutdown_silent; goto done; } /* Flush any buffered output */ prot_flush(imapd_out); /* Check for shutdown file */ if (!imapd_userisadmin && (shutdown_file(buf, sizeof(buf)) || (imapd_userid && userdeny(imapd_userid, config_ident, buf, sizeof(buf))))) { done = 1; shutdown = shutdown_bye; goto done; } done = proxy_check_input(protin, imapd_in, imapd_out, backend_current->in, NULL, idle_period); /* If not running IDLE on backend, poll the mailbox for updates */ if (!CAPA(backend_current, CAPA_IDLE)) { imapd_check(NULL, 0); } } /* Get continuation data */ c = getword(imapd_in, &arg); done: if (CAPA(backend_current, CAPA_IDLE)) { /* Either the client timed out, or ended the command. In either case we're done, so terminate IDLE on backend */ prot_printf(backend_current->out, "Done\r\n"); pipe_until_tag(backend_current, tag, 0); } switch (shutdown) { case shutdown_bye: ; char *p; for (p = buf; *p == '['; p++); /* can't have [ be first char */ prot_printf(imapd_out, "* BYE [ALERT] %s\r\n", p); /* fallthrough */ GCC_FALLTHROUGH case shutdown_silent: shut_down(0); break; case shutdown_skip: default: break; } } imapd_check(NULL, 1); if (c != EOF) { if (!strcasecmp(arg.s, "Done") && IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } else { prot_printf(imapd_out, "%s BAD Invalid Idle continuation\r\n", tag); eatline(imapd_in, c); } } } static void capa_response(int flags) { const char *sasllist; /* the list of SASL mechanisms */ int mechcount; int need_space = 0; int i; int lminus = config_getswitch(IMAPOPT_LITERALMINUS); for (i = 0; base_capabilities[i].str; i++) { const char *capa = base_capabilities[i].str; /* Filter capabilities if requested */ if (capa_is_disabled(capa)) continue; /* Don't show "MAILBOX-REFERRALS" if disabled by config */ if (config_getswitch(IMAPOPT_PROXYD_DISABLE_MAILBOX_REFERRALS) && !strcmp(capa, "MAILBOX-REFERRALS")) continue; /* Don't show "ANNOTATEMORE" if not enabled by config */ if (!config_getswitch(IMAPOPT_ANNOTATION_ENABLE_LEGACY_COMMANDS) && !strcmp(capa, "ANNOTATEMORE")) continue; /* Don't show if they're not shown at this level of login */ if (!(base_capabilities[i].mask & flags)) continue; /* cheap and nasty version of LITERAL- (RFC 7888) support - just say so */ if (lminus && !strcmp(capa, "LITERAL+")) capa = "LITERAL-"; /* print the capability */ if (need_space) prot_putc(' ', imapd_out); else need_space = 1; prot_printf(imapd_out, "%s", base_capabilities[i].str); } if (config_mupdate_server) { prot_printf(imapd_out, " MUPDATE=mupdate://%s/", config_mupdate_server); } if (apns_enabled) { prot_printf(imapd_out, " XAPPLEPUSHSERVICE"); } if (tls_enabled() && !imapd_starttls_done && !imapd_authstate) { prot_printf(imapd_out, " STARTTLS"); } if (imapd_tls_required || imapd_authstate || (!imapd_starttls_done && (extprops_ssf < 2) && !config_getswitch(IMAPOPT_ALLOWPLAINTEXT))) { prot_printf(imapd_out, " LOGINDISABLED"); } /* add the SASL mechs */ if (!imapd_tls_required && (!imapd_authstate || saslprops.ssf) && sasl_listmech(imapd_saslconn, NULL, "AUTH=", " AUTH=", !imapd_authstate ? " SASL-IR" : "", &sasllist, NULL, &mechcount) == SASL_OK && mechcount > 0) { prot_printf(imapd_out, " %s", sasllist); } else { /* else don't show anything */ } if (!(flags & CAPA_POSTAUTH)) return; if (imapd_authstate && imapd_userisadmin) { prot_printf(imapd_out, " UNAUTHENTICATE"); } if (config_getswitch(IMAPOPT_CONVERSATIONS)) prot_printf(imapd_out, " XCONVERSATIONS"); #ifdef HAVE_ZLIB if (!imapd_compress_done && !imapd_tls_comp) { prot_printf(imapd_out, " COMPRESS=DEFLATE"); } #endif // HAVE_ZLIB for (i = 0 ; i < QUOTA_NUMRESOURCES ; i++) prot_printf(imapd_out, " X-QUOTA=%s", quota_names[i]); if (idle_enabled()) { prot_printf(imapd_out, " IDLE"); } prot_printf(imapd_out, " APPENDLIMIT=%zu", maxmsgsize); } /* * Perform a CAPABILITY command */ static void cmd_capability(char *tag) { imapd_check(NULL, 0); prot_printf(imapd_out, "* CAPABILITY "); capa_response(CAPA_PREAUTH|CAPA_POSTAUTH); prot_printf(imapd_out, "\r\n%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } /* * Parse and perform an APPEND command. * The command has been parsed up to and including * the mailbox name. */ static int isokflag(char *s, int *isseen) { if (s[0] == '\\') { lcase(s); if (!strcmp(s, "\\seen")) { *isseen = 1; return 1; } if (!strcmp(s, "\\answered")) return 1; if (!strcmp(s, "\\flagged")) return 1; if (!strcmp(s, "\\draft")) return 1; if (!strcmp(s, "\\deleted")) return 1; /* uh oh, system flag i don't recognize */ return 0; } else { /* valid user flag? */ return imparse_isatom(s); } } static int getliteralsize(const char *tag, const char *p, int c, size_t maxsize, unsigned *size, int *binary, const char **parseerr) { int isnowait = 0; uint32_t num; static int lminus = -1; if (lminus == -1) lminus = config_getswitch(IMAPOPT_LITERALMINUS); /* Check for literal8 */ if (*p == '~') { p++; *binary = 1; } /* check for start of literal */ if (*p != '{') { *parseerr = "Missing required argument to Append command"; return IMAP_PROTOCOL_ERROR; } /* Read size from literal */ if (parseuint32(p+1, &p, &num)) { *parseerr = "Literal size not a number"; return IMAP_PROTOCOL_ERROR; } if (*p == '+') { /* LITERAL- says maximum size is 4096! */ if (lminus && num > 4096) { /* Fail per RFC 7888, Section 4, choice 2 */ prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_LITERAL_MINUS_TOO_LARGE)); fatal(error_message(IMAP_LITERAL_MINUS_TOO_LARGE), EX_PROTOCOL); } if (num > maxsize) { /* Fail per RFC 7888, Section 4, choice 2 */ prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_MESSAGE_TOOBIG)); fatal(error_message(IMAP_MESSAGE_TOOBIG), EX_PROTOCOL); } isnowait++; p++; } if (c == '\r') { c = prot_getc(imapd_in); } if (*p != '}' || p[1] || c != '\n') { *parseerr = "Invalid literal in Append command"; return IMAP_PROTOCOL_ERROR; } if (num > maxsize) return IMAP_MESSAGE_TOO_LARGE; if (!isnowait) { /* Tell client to send the message */ prot_printf(imapd_out, "+ go ahead\r\n"); prot_flush(imapd_out); } *size = num; return 0; } static int catenate_text(const char *tag, FILE *f, size_t maxsize, unsigned *totalsize, int *binary, const char **parseerr) { int c; static struct buf arg; unsigned size = 0; char buf[4096+1]; unsigned n; int r; c = getword(imapd_in, &arg); /* Read size from literal */ r = getliteralsize(tag, arg.s, c, maxsize - *totalsize, &size, binary, parseerr); if (r) return r; /* Catenate message part to stage */ while (size) { n = prot_read(imapd_in, buf, size > 4096 ? 4096 : size); if (!n) { syslog(LOG_ERR, "DISCONNECT: client disconnected during upload of literal"); return IMAP_IOERROR; } buf[n] = '\0'; if (!*binary && (n != strlen(buf))) r = IMAP_MESSAGE_CONTAINSNULL; size -= n; if (r) continue; /* XXX do we want to try and validate the message like we do in message_copy_strict()? */ if (f) fwrite(buf, n, 1, f); } *totalsize += size; return r; } static int catenate_url(const char *s, const char *cur_name, FILE *f, size_t maxsize, unsigned *totalsize, const char **parseerr) { struct imapurl url; struct index_state *state; uint32_t msgno; int r = 0, doclose = 0; unsigned long size = 0; r = imapurl_fromURL(&url, s); if (r) { *parseerr = "Improperly specified URL"; r = IMAP_BADURL; } else if (url.server) { *parseerr = "Only relative URLs are supported"; r = IMAP_BADURL; #if 0 } else if (url.server && strcmp(url.server, config_servername)) { *parseerr = "Cannot catenate messages from another server"; r = IMAP_BADURL; #endif } else if (!url.mailbox && !imapd_index && !cur_name) { *parseerr = "No mailbox is selected or specified"; r = IMAP_BADURL; } else if (url.mailbox || (url.mailbox = cur_name)) { mbentry_t *mbentry = NULL; /* lookup the location of the mailbox */ char *intname = mboxname_from_external(url.mailbox, &imapd_namespace, imapd_userid); r = mlookup(NULL, NULL, intname, &mbentry); if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) { /* remote mailbox */ struct backend *be; be = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (be) { r = proxy_catenate_url(be, &url, f, maxsize - *totalsize, &size, parseerr); *totalsize += size; } else r = IMAP_SERVER_UNAVAILABLE; free(url.freeme); mboxlist_entry_free(&mbentry); free(intname); return r; } mboxlist_entry_free(&mbentry); /* local mailbox */ if (!r) { struct index_init init; memset(&init, 0, sizeof(init)); init.userid = imapd_userid; init.authstate = imapd_authstate; init.out = imapd_out; r = index_open(intname, &init, &state); seqset_free(&init.vanishedlist); } if (!r) doclose = 1; if (!r && !(state->myrights & ACL_READ)) r = (imapd_userisadmin || (state->myrights & ACL_LOOKUP)) ? IMAP_PERMISSION_DENIED : IMAP_MAILBOX_NONEXISTENT; if (r) { *parseerr = error_message(r); r = IMAP_BADURL; } free(intname); } else { state = imapd_index; } if (r) { /* nothing to do, handled up top */ } else if (url.uidvalidity && (state->mailbox->i.uidvalidity != url.uidvalidity)) { *parseerr = "Uidvalidity of mailbox has changed"; r = IMAP_BADURL; } else if (!url.uid || !(msgno = index_finduid(state, url.uid)) || (index_getuid(state, msgno) != url.uid)) { *parseerr = "No such message in mailbox"; r = IMAP_BADURL; } else { /* Catenate message part to stage */ struct protstream *s = prot_new(fileno(f), 1); r = index_urlfetch(state, msgno, 0, url.section, url.start_octet, url.octet_count, s, maxsize - *totalsize, &size); if (r == IMAP_BADURL) *parseerr = "No such message part"; else if (!r) { *totalsize += size; } prot_flush(s); prot_free(s); /* XXX do we want to try and validate the message like we do in message_copy_strict()? */ } free(url.freeme); if (doclose) index_close(&state); return r; } static int append_catenate(const char *tag, FILE *f, const char *cur_name, size_t maxsize, unsigned *totalsize, int *binary, const char **parseerr, const char **url) { int c, r = 0; static struct buf arg; do { c = getword(imapd_in, &arg); if (c != ' ') { *parseerr = "Missing message part data in Append command"; return IMAP_PROTOCOL_ERROR; } if (!strcasecmp(arg.s, "TEXT")) { int r1 = catenate_text(tag, f, maxsize, totalsize, binary, parseerr); if (r1) return r1; /* if we see a SP, we're trying to catenate more than one part */ /* Parse newline terminating command */ c = prot_getc(imapd_in); } else if (!strcasecmp(arg.s, "URL")) { c = getastring(imapd_in, imapd_out, &arg); if (c == IMAP_LITERAL_TOO_LARGE) return c; if (c != ' ' && c != ')') { *parseerr = "Missing URL in Append command"; return IMAP_PROTOCOL_ERROR; } if (!r) { r = catenate_url(arg.s, cur_name, f, maxsize, totalsize, parseerr); if (r) { *url = arg.s; return r; } } } else { *parseerr = "Invalid message part type in Append command"; return IMAP_PROTOCOL_ERROR; } fflush(f); } while (c == ' '); if (c != ')') { *parseerr = "Missing space or ) after catenate list in Append command"; return IMAP_PROTOCOL_ERROR; } if (ferror(f) || fsync(fileno(f))) { syslog(LOG_ERR, "IOERROR: writing message: %m"); return IMAP_IOERROR; } return r; } /* If an APPEND is proxied from another server, * 'cur_name' is the name of the currently selected mailbox (if any) * in case we have to resolve relative URLs */ static void cmd_append(char *tag, char *name, const char *cur_name) { int c; static struct buf arg; time_t now = time(NULL); quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_DONTCARE_INITIALIZER; unsigned size; int sync_seen = 0; int r; int i; struct appendstate appendstate; unsigned long uidvalidity = 0; long doappenduid = 0; const char *parseerr = NULL, *url = NULL; struct appendstage *curstage; mbentry_t *mbentry = NULL; memset(&appendstate, 0, sizeof(struct appendstate)); /* See if we can append */ char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(tag, name, intname, &mbentry); if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) { /* remote mailbox */ struct backend *s = NULL; if (supports_referrals) { imapd_refer(tag, mbentry->server, name); /* Eat the argument */ eatline(imapd_in, prot_getc(imapd_in)); mboxlist_entry_free(&mbentry); free(intname); return; } s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (!s) r = IMAP_SERVER_UNAVAILABLE; mboxlist_entry_free(&mbentry); imapd_check(s, 0); if (!r) { int is_active = 1; s->context = (void*) &is_active; if (imapd_index) { const char *mboxname = index_mboxname(imapd_index); prot_printf(s->out, "%s Localappend {" SIZE_T_FMT "+}\r\n%s" " {" SIZE_T_FMT "+}\r\n%s ", tag, strlen(name), name, strlen(mboxname), mboxname); } else { prot_printf(s->out, "%s Localappend {" SIZE_T_FMT "+}\r\n%s" " \"\" ", tag, strlen(name), name); } if (!(r = pipe_command(s, 16384))) { pipe_including_tag(s, tag, 0); } s->context = NULL; } else { eatline(imapd_in, prot_getc(imapd_in)); } if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, prot_error(imapd_in) ? prot_error(imapd_in) : error_message(r)); } free(intname); return; } mboxlist_entry_free(&mbentry); /* local mailbox */ if (!r) { qdiffs[QUOTA_ANNOTSTORAGE] = 0; qdiffs[QUOTA_STORAGE] = 0; qdiffs[QUOTA_MESSAGE] = 1; r = append_check(intname, imapd_authstate, ACL_INSERT, ignorequota ? NULL : qdiffs); } if (r) { eatline(imapd_in, ' '); prot_printf(imapd_out, "%s NO %s%s\r\n", tag, (r == IMAP_MAILBOX_NONEXISTENT && mboxlist_createmailboxcheck(intname, 0, 0, imapd_userisadmin, imapd_userid, imapd_authstate, NULL, NULL, 0) == 0) ? "[TRYCREATE] " : "", error_message(r)); free(intname); return; } c = ' '; /* just parsed a space */ /* we loop, to support MULTIAPPEND */ while (!r && c == ' ') { curstage = xzmalloc(sizeof(*curstage)); ptrarray_push(&stages, curstage); /* Set limit on the total number of bytes allowed for mailbox+append-opts */ maxargssize_mark = prot_bytes_in(imapd_in) + (maxargssize - strlen(name)); /* now parsing "append-opts" in the ABNF */ /* Parse flags */ c = getword(imapd_in, &arg); if (c == '(' && !arg.s[0]) { strarray_init(&curstage->flags); do { c = getword(imapd_in, &arg); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (!curstage->flags.count && !arg.s[0] && c == ')') break; /* empty list */ if (!isokflag(arg.s, &sync_seen)) { parseerr = "Invalid flag in Append command"; r = IMAP_PROTOCOL_ERROR; goto done; } strarray_append(&curstage->flags, arg.s); } while (c == ' '); if (c != ')') { parseerr = "Missing space or ) after flag name in Append command"; r = IMAP_PROTOCOL_ERROR; goto done; } c = prot_getc(imapd_in); if (c != ' ') { parseerr = "Missing space after flag list in Append command"; r = IMAP_PROTOCOL_ERROR; goto done; } c = getword(imapd_in, &arg); } /* Parse internaldate */ if (c == '\"' && !arg.s[0]) { prot_ungetc(c, imapd_in); c = getdatetime(&(curstage->internaldate)); if (c != ' ') { parseerr = "Invalid date-time in Append command"; r = IMAP_PROTOCOL_ERROR; goto done; } c = getword(imapd_in, &arg); } /* try to parse a sequence of "append-ext" */ for (;;) { if (!strcasecmp(arg.s, "ANNOTATION")) { /* RFC 5257 */ if (c != ' ') { parseerr = "Missing annotation data in Append command"; r = IMAP_PROTOCOL_ERROR; goto done; } c = parse_annotate_store_data(tag, /*permessage_flag*/1, &curstage->annotations); if (c <= EOF) { goto cleanup; } qdiffs[QUOTA_ANNOTSTORAGE] += sizeentryatts(curstage->annotations); c = getword(imapd_in, &arg); } else break; /* not a known extension keyword */ } /* Stage the message */ curstage->f = append_newstage(intname, now, stages.count, &(curstage->stage)); if (!curstage->f) { r = IMAP_IOERROR; goto done; } /* now parsing "append-data" in the ABNF */ if (!strcasecmp(arg.s, "CATENATE")) { if (c != ' ' || (c = prot_getc(imapd_in) != '(')) { parseerr = "Missing message part(s) in Append command"; r = IMAP_PROTOCOL_ERROR; goto done; } /* Catenate the message part(s) to stage */ size = 0; r = append_catenate(tag, curstage->f, cur_name, maxmsgsize, &size, &(curstage->binary), &parseerr, &url); if (r) goto done; } else { /* Read size from literal */ r = getliteralsize(tag, arg.s, c, maxmsgsize, &size, &(curstage->binary), &parseerr); if (!r && size == 0) r = IMAP_ZERO_LENGTH_LITERAL; if (r) goto done; /* Copy message to stage */ r = message_copy_strict(imapd_in, curstage->f, size, curstage->binary); } qdiffs[QUOTA_STORAGE] += size; /* If this is a non-BINARY message, close the stage file. * Otherwise, leave it open so we can encode the binary parts. * * XXX For BINARY MULTIAPPEND, we may have to close the stage files * anyways to avoid too many open files. */ if (!curstage->binary) { fclose(curstage->f); curstage->f = NULL; } /* if we see a SP, we're trying to append more than one message */ /* Parse newline terminating command */ c = prot_getc(imapd_in); } done: switch (r) { case IMAP_ZERO_LENGTH_LITERAL: case IMAP_MESSAGE_TOO_LARGE: break; case 0: /* we should be looking at the end of the line */ if (IS_EOL(c, imapd_in)) break; parseerr = "junk after literal"; r = IMAP_PROTOCOL_ERROR; GCC_FALLTHROUGH default: eatline(imapd_in, c); break; } /* Append from the stage(s) */ if (!r) { qdiffs[QUOTA_MESSAGE] = stages.count; r = append_setup(&appendstate, intname, imapd_userid, imapd_authstate, ACL_INSERT, ignorequota ? NULL : qdiffs, &imapd_namespace, (imapd_userisadmin || imapd_userisproxyadmin), EVENT_MESSAGE_APPEND); } if (!r) { /* make sure appending to this mailbox is allowed */ r = insert_into_mailbox_allowed(appendstate.mailbox); } if (!r) { struct body *body; doappenduid = (appendstate.myrights & ACL_READ); uidvalidity = append_uidvalidity(&appendstate); for (i = 0; !r && i < stages.count ; i++) { curstage = stages.data[i]; body = NULL; if (curstage->binary) { /* XXX we might have fname here, but it's hidden inside opaque * curstage->stage field */ r = message_parse_binary_file(curstage->f, &body, NULL); fclose(curstage->f); curstage->f = NULL; /* free this up again - that way we re-parse the fixed up file */ message_free_body(body); free(body); body = NULL; } if (!r) { r = append_fromstage(&appendstate, &body, curstage->stage, curstage->internaldate, /*createdmodseq*/0, &curstage->flags, 0, &curstage->annotations); } if (body) { message_free_body(body); free(body); body = NULL; } } } if (!r) { r = append_commit(&appendstate); } else { append_abort(&appendstate); } imapd_check(NULL, 1); if (r == IMAP_PROTOCOL_ERROR && parseerr) { prot_printf(imapd_out, "%s BAD %s\r\n", tag, parseerr); } else if (r == IMAP_LITERAL_TOO_LARGE) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else if (r == IMAP_BADURL) { prot_printf(imapd_out, "%s NO [BADURL \"%s\"] %s\r\n", tag, url, parseerr); } else if (r) { const char *respcode = ""; if (r == IMAP_MAILBOX_NOTSUPPORTED) { respcode = "[CANNOT] "; } else if (r == IMAP_MESSAGE_TOO_LARGE) { respcode = "[TOOBIG] "; } else if (r == IMAP_MAILBOX_NONEXISTENT && mboxlist_createmailboxcheck(intname, 0, 0, imapd_userisadmin, imapd_userid, imapd_authstate, NULL, NULL, 0) == 0) { respcode = "[TRYCREATE] "; } prot_printf(imapd_out, "%s NO %s%s\r\n", tag, respcode, error_message(r)); } else if (doappenduid) { /* is this a space separated list or sequence list? */ prot_printf(imapd_out, "%s OK [APPENDUID %lu ", tag, uidvalidity); if (appendstate.nummsg == 1) { prot_printf(imapd_out, "%u", appendstate.baseuid); } else { prot_printf(imapd_out, "%u:%u", appendstate.baseuid, appendstate.baseuid + appendstate.nummsg - 1); } prot_printf(imapd_out, "] %s\r\n", error_message(IMAP_OK_COMPLETED)); } else { index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } cleanup: /* Cleanup the stage(s) */ while ((curstage = ptrarray_pop(&stages))) { if (curstage->f != NULL) fclose(curstage->f); append_removestage(curstage->stage); strarray_fini(&curstage->flags); freeentryatts(curstage->annotations); free(curstage); } free(intname); ptrarray_fini(&stages); } /* * Warn if mailbox is close to or over any quota resource. * * Warn if the following possibilities occur: * - quotawarnkb not set + quotawarn hit * - quotawarnkb set larger than mailbox + quotawarn hit * - quotawarnkb set + hit + quotawarn hit * - quotawarnmsg not set + quotawarn hit * - quotawarnmsg set larger than mailbox + quotawarn hit * - quotawarnmsg set + hit + quotawarn hit */ static void warn_about_quota(const char *quotaroot) { time_t now = time(NULL); struct quota q; int res; int r; int thresholds[QUOTA_NUMRESOURCES]; int pc_threshold = config_getint(IMAPOPT_QUOTAWARN); int pc_usage; struct buf msg = BUF_INITIALIZER; static char lastqr[MAX_MAILBOX_PATH+1] = ""; static time_t nextalert = 0; if (!quotaroot || !*quotaroot) return; /* no quota, nothing to do */ /* rate limit checks and warnings to every 10 min */ if (!strcmp(quotaroot, lastqr) && now < nextalert) return; strlcpy(lastqr, quotaroot, sizeof(lastqr)); nextalert = now + 600; quota_init(&q, quotaroot); r = quota_read_withconversations(&q); if (r) goto out; /* failed to read */ memset(thresholds, 0, sizeof(thresholds)); thresholds[QUOTA_STORAGE] = config_getint(IMAPOPT_QUOTAWARNKB); thresholds[QUOTA_MESSAGE] = config_getint(IMAPOPT_QUOTAWARNMSG); thresholds[QUOTA_ANNOTSTORAGE] = config_getint(IMAPOPT_QUOTAWARNKB); for (res = 0 ; res < QUOTA_NUMRESOURCES ; res++) { if (q.limits[res] < 0) continue; /* this resource is unlimited */ buf_reset(&msg); if (thresholds[res] <= 0 || thresholds[res] >= q.limits[res] || q.useds[res] > ((quota_t) (q.limits[res] - thresholds[res])) * quota_units[res]) { pc_usage = (int)(((double) q.useds[res] * 100.0) / (double) ((quota_t) q.limits[res] * quota_units[res])); if (q.useds[res] > (quota_t) q.limits[res] * quota_units[res]) buf_printf(&msg, error_message(IMAP_NO_OVERQUOTA), quota_names[res]); else if (pc_usage > pc_threshold) buf_printf(&msg, error_message(IMAP_NO_CLOSEQUOTA), pc_usage, quota_names[res]); } if (msg.len) prot_printf(imapd_out, "* NO [ALERT] %s\r\n", buf_cstring(&msg)); } buf_reset(&msg); out: quota_free(&q); } /* * Perform a SELECT/EXAMINE/BBOARD command */ static void cmd_select(char *tag, char *cmd, char *name) { int c; int r = 0; int doclose = 0; mbentry_t *mbentry = NULL; struct backend *backend_next = NULL; struct index_init init; int wasopen = 0; int allowdeleted = config_getswitch(IMAPOPT_ALLOWDELETED); struct vanished_params *v = &init.vanished; memset(&init, 0, sizeof(struct index_init)); c = prot_getc(imapd_in); if (c == ' ') { static struct buf arg, parm1, parm2; c = prot_getc(imapd_in); if (c != '(') goto badlist; c = getword(imapd_in, &arg); if (arg.s[0] == '\0') goto badlist; for (;;) { if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); ucase(arg.s); if (!strcmp(arg.s, "CONDSTORE")) { client_capa |= CAPA_CONDSTORE; } else if ((client_capa & CAPA_QRESYNC) && !strcmp(arg.s, "QRESYNC")) { if (c != ' ') goto badqresync; c = prot_getc(imapd_in); if (c != '(') goto badqresync; c = getuint32(imapd_in, &v->uidvalidity); if (c != ' ' || !v->uidvalidity) goto badqresync; c = getmodseq(imapd_in, &v->modseq); if (c == EOF) goto badqresync; if (c == ' ') { c = prot_getc(imapd_in); if (c != '(') { /* optional UID sequence */ prot_ungetc(c, imapd_in); c = getword(imapd_in, &arg); if (!imparse_issequence(arg.s)) goto badqresync; v->sequence = arg.s; if (c == ' ') { c = prot_getc(imapd_in); if (c != '(') goto badqresync; } } if (c == '(') { /* optional sequence match data */ c = getword(imapd_in, &parm1); if (!imparse_issequence(parm1.s)) goto badqresync; v->match_seq = parm1.s; if (c != ' ') goto badqresync; c = getword(imapd_in, &parm2); if (!imparse_issequence(parm2.s)) goto badqresync; v->match_uid = parm2.s; if (c != ')') goto badqresync; c = prot_getc(imapd_in); } } if (c != ')') goto badqresync; c = prot_getc(imapd_in); } else if (!strcmp(arg.s, "ANNOTATE")) { /* * RFC 5257 requires us to parse this keyword, which * indicates that the client wants unsolicited * ANNOTATION responses in this session, but we don't * actually have to do anything with it, so we won't. */ ; } else if (allowdeleted && !strcmp(arg.s, "VENDOR.CMU-INCLUDE-EXPUNGED")) { init.want_expunged = 1; } else { prot_printf(imapd_out, "%s BAD Invalid %s modifier %s\r\n", tag, cmd, arg.s); eatline(imapd_in, c); return; } if (c == ' ') c = getword(imapd_in, &arg); else break; } if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close parenthesis in %s\r\n", tag, cmd); eatline(imapd_in, c); return; } c = prot_getc(imapd_in); } if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to %s\r\n", tag, cmd); eatline(imapd_in, c); return; } if (imapd_index) { if (config_getswitch(IMAPOPT_AUTOEXPUNGE) && index_hasrights(imapd_index, ACL_EXPUNGE)) index_expunge(imapd_index, NULL, 1); index_close(&imapd_index); wasopen = 1; } if (backend_current) { /* remove backend_current from the protgroup */ protgroup_delete(protin, backend_current->in); wasopen = 1; } char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(tag, name, intname, &mbentry); if (r == IMAP_MAILBOX_MOVED) { free(intname); return; } if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) { char mytag[128]; if (supports_referrals) { imapd_refer(tag, mbentry->server, name); mboxlist_entry_free(&mbentry); free(intname); return; } backend_next = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (!backend_next) r = IMAP_SERVER_UNAVAILABLE; if (backend_current && backend_current != backend_next) { /* switching servers; flush old server output */ proxy_gentag(mytag, sizeof(mytag)); prot_printf(backend_current->out, "%s Unselect\r\n", mytag); /* do not fatal() here, because we don't really care about this * server anymore anyway */ pipe_until_tag(backend_current, mytag, 1); } backend_current = backend_next; if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); mboxlist_entry_free(&mbentry); free(intname); return; } if (client_capa) { /* Enable client capabilities on new backend */ proxy_gentag(mytag, sizeof(mytag)); prot_printf(backend_current->out, "%s Enable", mytag); if (client_capa & CAPA_QRESYNC) prot_printf(backend_current->out, " Qresync"); else if (client_capa & CAPA_CONDSTORE) prot_printf(backend_current->out, " Condstore"); prot_printf(backend_current->out, "\r\n"); pipe_until_tag(backend_current, mytag, 0); } /* Send SELECT command to backend */ prot_printf(backend_current->out, "%s %s {" SIZE_T_FMT "+}\r\n%s", tag, cmd, strlen(name), name); if (v->uidvalidity) { prot_printf(backend_current->out, " (QRESYNC (%u " MODSEQ_FMT, v->uidvalidity, v->modseq); if (v->sequence) { prot_printf(backend_current->out, " %s", v->sequence); } if (v->match_seq && v->match_uid) { prot_printf(backend_current->out, " (%s %s)", v->match_seq, v->match_uid); } prot_printf(backend_current->out, "))"); } prot_printf(backend_current->out, "\r\n"); switch (pipe_including_tag(backend_current, tag, 0)) { case PROXY_OK: syslog(LOG_DEBUG, "open: user %s opened %s on %s", imapd_userid, name, mbentry->server); /* add backend_current to the protgroup */ protgroup_insert(protin, backend_current->in); break; default: syslog(LOG_DEBUG, "open: user %s failed to open %s", imapd_userid, name); /* not successfully selected */ backend_current = NULL; break; } mboxlist_entry_free(&mbentry); free(intname); return; } mboxlist_entry_free(&mbentry); /* local mailbox */ if (backend_current) { char mytag[128]; /* switching servers; flush old server output */ proxy_gentag(mytag, sizeof(mytag)); prot_printf(backend_current->out, "%s Unselect\r\n", mytag); /* do not fatal() here, because we don't really care about this * server anymore anyway */ pipe_until_tag(backend_current, mytag, 1); } backend_current = NULL; if (wasopen) prot_printf(imapd_out, "* OK [CLOSED] Ok\r\n"); init.userid = imapd_userid; init.authstate = imapd_authstate; init.out = imapd_out; init.examine_mode = (cmd[0] == 'E') || config_getswitch(IMAPOPT_READONLY); init.select = 1; if (!strcasecmpsafe(imapd_magicplus, "+dav")) init.want_dav = 1; if (!imapd_userisadmin && !allowdeleted && mboxname_isdeletedmailbox(intname, NULL)) r = IMAP_MAILBOX_NONEXISTENT; else r = index_open(intname, &init, &imapd_index); if (!r) doclose = 1; if (!r && !index_hasrights(imapd_index, ACL_READ)) { r = (imapd_userisadmin || index_hasrights(imapd_index, ACL_LOOKUP)) ? IMAP_PERMISSION_DENIED : IMAP_MAILBOX_NONEXISTENT; } if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); seqset_free(&init.vanishedlist); if (doclose) index_close(&imapd_index); free(intname); return; } if (index_hasrights(imapd_index, ACL_EXPUNGE)) warn_about_quota(mailbox_quotaroot(imapd_index->mailbox)); index_select(imapd_index, &init); seqset_free(&init.vanishedlist); prot_printf(imapd_out, "%s OK [READ-%s] %s\r\n", tag, index_hasrights(imapd_index, ACL_READ_WRITE) ? "WRITE" : "ONLY", error_message(IMAP_OK_COMPLETED)); syslog(LOG_DEBUG, "open: user %s opened %s", imapd_userid, name); free(intname); return; badlist: prot_printf(imapd_out, "%s BAD Invalid modifier list in %s\r\n", tag, cmd); eatline(imapd_in, c); return; badqresync: prot_printf(imapd_out, "%s BAD Invalid QRESYNC parameter list in %s\r\n", tag, cmd); eatline(imapd_in, c); return; } /* * Perform a CLOSE/UNSELECT command */ static void cmd_close(char *tag, char *cmd) { if (backend_current) { /* remote mailbox */ prot_printf(backend_current->out, "%s %s\r\n", tag, cmd); /* xxx do we want this to say OK if the connection is gone? * saying NO is clearly wrong, hense the fatal request. */ pipe_including_tag(backend_current, tag, 0); /* remove backend_current from the protgroup */ protgroup_delete(protin, backend_current->in); backend_current = NULL; return; } /* local mailbox */ if ((cmd[0] == 'C' || config_getswitch(IMAPOPT_AUTOEXPUNGE)) && index_hasrights(imapd_index, ACL_EXPUNGE)) { index_expunge(imapd_index, NULL, 1); /* don't tell changes here */ } index_close(&imapd_index); /* http://www.rfc-editor.org/errata_search.php?rfc=5162 * Errata ID: 1808 - don't send HIGHESTMODSEQ to a close * command, because it can lose synchronisation */ prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } /* * Append to the section list. */ static void section_list_append(struct section **l, const char *name, const struct octetinfo *oi) { struct section **tail = l; while (*tail) tail = &(*tail)->next; *tail = xzmalloc(sizeof(struct section)); (*tail)->name = xstrdup(name); (*tail)->octetinfo = *oi; (*tail)->next = NULL; } static void section_list_free(struct section *l) { struct section *n; while (l) { n = l->next; free(l->name); free(l); l = n; } } /* * Parse the syntax for a partial fetch: * "<" number "." nz-number ">" */ #define PARSE_PARTIAL(start_octet, octet_count) do { \ (start_octet) = (octet_count) = 0; \ if (*p == '<' && Uisdigit(p[1])) { \ (start_octet) = p[1] - '0'; \ p += 2; \ while (Uisdigit((int) *p)) { \ (start_octet) = \ (start_octet) * 10 + *p++ - '0'; \ } \ \ if (*p == '.' && p[1] >= '1' && p[1] <= '9') { \ (octet_count) = p[1] - '0'; \ p[0] = '>'; p[1] = '\0'; /* clip off the octet count \ (its not used in the reply) */ \ p += 2; \ while (Uisdigit(*p)) { \ (octet_count) = \ (octet_count) * 10 + *p++ - '0'; \ } \ } \ else p--; \ \ if (*p != '>') { \ prot_printf(imapd_out, \ "%s BAD Invalid body partial\r\n", tag); \ eatline(imapd_in, c); \ goto freeargs; \ } \ p++; \ } \ } while(0) static int parse_fetch_args(const char *tag, const char *cmd, int allow_vanished, struct fetchargs *fa) { static struct buf fetchatt, fieldname; int c; int inlist = 0; char *p, *section; struct octetinfo oi; strarray_t *newfields = strarray_new(); c = getword(imapd_in, &fetchatt); if (c == '(' && !fetchatt.s[0]) { inlist = 1; c = getword(imapd_in, &fetchatt); } for (;;) { if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); ucase(fetchatt.s); switch (fetchatt.s[0]) { case 'A': if (!inlist && !strcmp(fetchatt.s, "ALL")) { fa->fetchitems |= FETCH_ALL; } else if (!strcmp(fetchatt.s, "ANNOTATION")) { fa->fetchitems |= FETCH_ANNOTATION; if (c != ' ') goto badannotation; c = prot_getc(imapd_in); if (c != '(') goto badannotation; c = parse_annotate_fetch_data(tag, /*permessage_flag*/1, &fa->entries, &fa->attribs); if (c <= EOF) { goto freeargs; } if (c != ')') { badannotation: prot_printf(imapd_out, "%s BAD invalid Annotation\r\n", tag); eatline(imapd_in, c); goto freeargs; } c = prot_getc(imapd_in); } else goto badatt; break; case 'B': if (!strcmp(fetchatt.s, "BASECID") && config_getswitch(IMAPOPT_CONVERSATIONS)) { fa->fetchitems |= FETCH_BASECID; } else if (!strncmp(fetchatt.s, "BINARY[", 7) || !strncmp(fetchatt.s, "BINARY.PEEK[", 12) || !strncmp(fetchatt.s, "BINARY.SIZE[", 12)) { int binsize = 0; p = section = fetchatt.s + 7; if (!strncmp(p, "PEEK[", 5)) { p = section += 5; } else if (!strncmp(p, "SIZE[", 5)) { p = section += 5; binsize = 1; } else { fa->fetchitems |= FETCH_SETSEEN; } while (Uisdigit(*p) || *p == '.') { if (*p == '.' && !Uisdigit(p[-1])) break; /* Part number cannot begin with '0' */ if (*p == '0' && !Uisdigit(p[-1])) break; p++; } if (*p != ']') { prot_printf(imapd_out, "%s BAD Invalid binary section\r\n", tag); eatline(imapd_in, c); goto freeargs; } p++; if (!binsize) { PARSE_PARTIAL(oi.start_octet, oi.octet_count); } if (*p) { prot_printf(imapd_out, "%s BAD Junk after binary section\r\n", tag); eatline(imapd_in, c); goto freeargs; } if (binsize) section_list_append(&fa->sizesections, section, &oi); else section_list_append(&fa->binsections, section, &oi); } else if (!strcmp(fetchatt.s, "BODY")) { fa->fetchitems |= FETCH_BODY; } else if (!strcmp(fetchatt.s, "BODYSTRUCTURE")) { fa->fetchitems |= FETCH_BODYSTRUCTURE; } else if (!strncmp(fetchatt.s, "BODY[", 5) || !strncmp(fetchatt.s, "BODY.PEEK[", 10)) { p = section = fetchatt.s + 5; if (!strncmp(p, "PEEK[", 5)) { p = section += 5; } else { fa->fetchitems |= FETCH_SETSEEN; } while (Uisdigit(*p) || *p == '.') { if (*p == '.' && !Uisdigit(p[-1])) break; /* Obsolete section 0 can only occur before close brace */ if (*p == '0' && !Uisdigit(p[-1]) && p[1] != ']') break; p++; } if (*p == 'H' && !strncmp(p, "HEADER.FIELDS", 13) && (p == section || p[-1] == '.') && (p[13] == '\0' || !strcmp(p+13, ".NOT"))) { /* * If not top-level or a HEADER.FIELDS.NOT, can't pull * the headers out of the cache. */ if (p != section || p[13] != '\0') { fa->cache_atleast = BIT32_MAX; } if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing required argument to %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } c = prot_getc(imapd_in); if (c != '(') { prot_printf(imapd_out, "%s BAD Missing required open parenthesis in %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } do { c = getastring(imapd_in, imapd_out, &fieldname); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (c == IMAP_LITERAL_TOO_LARGE) { prot_printf(imapd_out, "%s NO %s in %s %s\r\n", tag, error_message(c), cmd, fetchatt.s); goto freeargs; } for (p = fieldname.s; *p; p++) { if (*p <= ' ' || *p & 0x80 || *p == ':') break; } if (*p || !*fieldname.s) { prot_printf(imapd_out, "%s BAD Invalid field-name in %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } strarray_append(newfields, fieldname.s); if (fa->cache_atleast < BIT32_MAX) { bit32 this_ver = mailbox_cached_header(fieldname.s); if(this_ver > fa->cache_atleast) fa->cache_atleast = this_ver; } } while (c == ' '); if (c != ')') { prot_printf(imapd_out, "%s BAD Missing required close parenthesis in %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } /* Grab/parse the ] part */ c = getword(imapd_in, &fieldname); p = fieldname.s; if (*p++ != ']') { prot_printf(imapd_out, "%s BAD Missing required close bracket after %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } PARSE_PARTIAL(oi.start_octet, oi.octet_count); if (*p) { prot_printf(imapd_out, "%s BAD Junk after body section\r\n", tag); eatline(imapd_in, c); goto freeargs; } appendfieldlist(&fa->fsections, section, newfields, fieldname.s, &oi, sizeof(oi)); /* old 'newfields' is managed by the fieldlist now */ newfields = strarray_new(); break; } switch (*p) { case 'H': if (p != section && p[-1] != '.') break; if (!strncmp(p, "HEADER]", 7)) p += 6; break; case 'M': if (!strncmp(p-1, ".MIME]", 6)) p += 4; break; case 'T': if (p != section && p[-1] != '.') break; if (!strncmp(p, "TEXT]", 5)) p += 4; break; } if (*p != ']') { prot_printf(imapd_out, "%s BAD Invalid body section\r\n", tag); eatline(imapd_in, c); goto freeargs; } p++; PARSE_PARTIAL(oi.start_octet, oi.octet_count); if (*p) { prot_printf(imapd_out, "%s BAD Junk after body section\r\n", tag); eatline(imapd_in, c); goto freeargs; } section_list_append(&fa->bodysections, section, &oi); } else goto badatt; break; case 'C': if (!strcmp(fetchatt.s, "CID") && config_getswitch(IMAPOPT_CONVERSATIONS)) { fa->fetchitems |= FETCH_CID; } else if (!strcmp(fetchatt.s, "CREATEDMODSEQ")) { fa->fetchitems |= FETCH_CREATEDMODSEQ; } else goto badatt; break; case 'D': if (!strcmp(fetchatt.s, "DIGEST.SHA1")) { fa->fetchitems |= FETCH_GUID; } else goto badatt; break; case 'E': if (!strcmp(fetchatt.s, "ENVELOPE")) { fa->fetchitems |= FETCH_ENVELOPE; } else if (!strcmp(fetchatt.s, "EMAILID")) { /* RFC 8474 */ fa->fetchitems |= FETCH_EMAILID; } else goto badatt; break; case 'F': if (!inlist && !strcmp(fetchatt.s, "FAST")) { fa->fetchitems |= FETCH_FAST; } else if (!inlist && !strcmp(fetchatt.s, "FULL")) { fa->fetchitems |= FETCH_FULL; } else if (!strcmp(fetchatt.s, "FLAGS")) { fa->fetchitems |= FETCH_FLAGS; } else if (!strcmp(fetchatt.s, "FOLDER")) { fa->fetchitems |= FETCH_FOLDER; } else goto badatt; break; case 'I': if (!strcmp(fetchatt.s, "INTERNALDATE")) { fa->fetchitems |= FETCH_INTERNALDATE; } else goto badatt; break; case 'L': if (!strcmp(fetchatt.s, "LASTUPDATED")) { fa->fetchitems |= FETCH_LASTUPDATED; } else goto badatt; break; case 'M': if (config_getswitch(IMAPOPT_CONVERSATIONS) && !strcmp(fetchatt.s, "MAILBOXES")) { fa->fetchitems |= FETCH_MAILBOXES; } else if (config_getswitch(IMAPOPT_CONVERSATIONS) && !strcmp(fetchatt.s, "MAILBOXIDS")) { fa->fetchitems |= FETCH_MAILBOXIDS; } else if (!strcmp(fetchatt.s, "MODSEQ")) { fa->fetchitems |= FETCH_MODSEQ; } else goto badatt; break; case 'P': if (!strcmp(fetchatt.s, "PREVIEW")) { fa->fetchitems |= FETCH_PREVIEW; if (c == ' ') { c = prot_getc(imapd_in); if (c == '(') { c = ' '; while (c == ' ') { c = getword(imapd_in, &fieldname); if (strcasecmp(fieldname.s, "LAZY")) { prot_printf(imapd_out, "%s BAD FETCH contains invalid preview modifier (%s)\r\n", tag, fieldname.s); eatline(imapd_in, c); goto freeargs; } } if (c == ')') c = prot_getc(imapd_in); else { prot_printf(imapd_out, "%s BAD PREVIEW list not finished.\r\n", tag); eatline(imapd_in, c); goto freeargs; } } else { prot_ungetc(c, imapd_in); c = ' '; } } } else goto badatt; break; case 'R': if (!strcmp(fetchatt.s, "RFC822")) { fa->fetchitems |= FETCH_RFC822|FETCH_SETSEEN; } else if (!strcmp(fetchatt.s, "RFC822.HEADER")) { fa->fetchitems |= FETCH_HEADER; } else if (!strcmp(fetchatt.s, "RFC822.PEEK")) { fa->fetchitems |= FETCH_RFC822; } else if (!strcmp(fetchatt.s, "RFC822.SIZE")) { fa->fetchitems |= FETCH_SIZE; } else if (!strcmp(fetchatt.s, "RFC822.TEXT")) { fa->fetchitems |= FETCH_TEXT|FETCH_SETSEEN; } else if (!strcmp(fetchatt.s, "RFC822.SHA1")) { fa->fetchitems |= FETCH_SHA1; } else if (!strcmp(fetchatt.s, "RFC822.FILESIZE")) { fa->fetchitems |= FETCH_FILESIZE; } else if (!strcmp(fetchatt.s, "RFC822.TEXT.PEEK")) { fa->fetchitems |= FETCH_TEXT; } else if (!strcmp(fetchatt.s, "RFC822.HEADER.LINES") || !strcmp(fetchatt.s, "RFC822.HEADER.LINES.NOT")) { if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing required argument to %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } c = prot_getc(imapd_in); if (c != '(') { prot_printf(imapd_out, "%s BAD Missing required open parenthesis in %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } do { c = getastring(imapd_in, imapd_out, &fieldname); if (c == IMAP_LITERAL_TOO_LARGE) { prot_printf(imapd_out, "%s NO %s in %s %s\r\n", tag, error_message(c), cmd, fetchatt.s); goto freeargs; } for (p = fieldname.s; *p; p++) { if (*p <= ' ' || *p & 0x80 || *p == ':') break; } if (*p || !*fieldname.s) { prot_printf(imapd_out, "%s BAD Invalid field-name in %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } lcase(fieldname.s); /* 19 is magic number -- length of * "RFC822.HEADERS.NOT" */ strarray_append(strlen(fetchatt.s) == 19 ? &fa->headers : &fa->headers_not, fieldname.s); if (strlen(fetchatt.s) != 19) { fa->cache_atleast = BIT32_MAX; } if (fa->cache_atleast < BIT32_MAX) { bit32 this_ver = mailbox_cached_header(fieldname.s); if(this_ver > fa->cache_atleast) fa->cache_atleast = this_ver; } } while (c == ' '); if (c != ')') { prot_printf(imapd_out, "%s BAD Missing required close parenthesis in %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } c = prot_getc(imapd_in); } else goto badatt; break; case 'S': if (!strcmp(fetchatt.s, "SAVEDATE")) { fa->fetchitems |= FETCH_SAVEDATE; } else goto badatt; break; case 'T': if (!strcmp(fetchatt.s, "THREADID")) { /* RFC 8474 */ fa->fetchitems |= FETCH_THREADID; } else goto badatt; break; case 'U': if (!strcmp(fetchatt.s, "UID")) { fa->fetchitems |= FETCH_UID; } else if (!strcmp(fetchatt.s, "UIDVALIDITY")) { fa->fetchitems |= FETCH_UIDVALIDITY; } else goto badatt; break; default: badatt: prot_printf(imapd_out, "%s BAD Invalid %s attribute %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } if (inlist && c == ' ') c = getword(imapd_in, &fetchatt); else break; } if (inlist && c == ')') { inlist = 0; c = prot_getc(imapd_in); } if (inlist) { prot_printf(imapd_out, "%s BAD Missing close parenthesis in %s\r\n", tag, cmd); eatline(imapd_in, c); goto freeargs; } if (c == ' ') { /* Grab/parse the modifier(s) */ c = prot_getc(imapd_in); if (c != '(') { prot_printf(imapd_out, "%s BAD Missing required open parenthesis in %s modifiers\r\n", tag, cmd); eatline(imapd_in, c); goto freeargs; } do { c = getword(imapd_in, &fetchatt); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); ucase(fetchatt.s); if (!strcmp(fetchatt.s, "CHANGEDSINCE")) { if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing required argument to %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } c = getmodseq(imapd_in, &fa->changedsince); if (c == EOF) { prot_printf(imapd_out, "%s BAD Invalid argument to %s %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } fa->fetchitems |= FETCH_MODSEQ; } else if (allow_vanished && !strcmp(fetchatt.s, "VANISHED")) { fa->vanished = 1; } else { prot_printf(imapd_out, "%s BAD Invalid %s modifier %s\r\n", tag, cmd, fetchatt.s); eatline(imapd_in, c); goto freeargs; } } while (c == ' '); if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close parenthesis in %s\r\n", tag, cmd); eatline(imapd_in, c); goto freeargs; } c = prot_getc(imapd_in); } if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to %s\r\n", tag, cmd); eatline(imapd_in, c); goto freeargs; } if (!fa->fetchitems && !fa->bodysections && !fa->fsections && !fa->binsections && !fa->sizesections && !fa->headers.count && !fa->headers_not.count) { prot_printf(imapd_out, "%s BAD Missing required argument to %s\r\n", tag, cmd); goto freeargs; } if (fa->vanished && !fa->changedsince) { prot_printf(imapd_out, "%s BAD Missing required argument to %s\r\n", tag, cmd); goto freeargs; } if (fa->fetchitems & FETCH_MODSEQ) { if (!(client_capa & CAPA_CONDSTORE)) { client_capa |= CAPA_CONDSTORE; if (imapd_index) prot_printf(imapd_out, "* OK [HIGHESTMODSEQ " MODSEQ_FMT "] CONDSTORE enabled by MODSEQ fetch\r\n", index_highestmodseq(imapd_index)); } } if (fa->fetchitems & (FETCH_ANNOTATION|FETCH_FOLDER|FETCH_MAILBOXES)) { fa->namespace = &imapd_namespace; fa->userid = imapd_userid; } if (fa->fetchitems & FETCH_ANNOTATION) { fa->isadmin = imapd_userisadmin || imapd_userisproxyadmin; fa->authstate = imapd_authstate; } if (config_getswitch(IMAPOPT_CONVERSATIONS) && (fa->fetchitems & (FETCH_MAILBOXIDS|FETCH_MAILBOXES))) { int r = conversations_open_user(imapd_userid, 0/*shared*/, &fa->convstate); if (r) { syslog(LOG_WARNING, "error opening conversations for %s: %s", imapd_userid, error_message(r)); } } strarray_free(newfields); return 0; freeargs: strarray_free(newfields); return IMAP_PROTOCOL_BAD_PARAMETERS; } static void fetchargs_fini (struct fetchargs *fa) { section_list_free(fa->binsections); section_list_free(fa->sizesections); section_list_free(fa->bodysections); freefieldlist(fa->fsections); strarray_fini(&fa->headers); strarray_fini(&fa->headers_not); strarray_fini(&fa->entries); strarray_fini(&fa->attribs); conversations_commit(&fa->convstate); memset(fa, 0, sizeof(struct fetchargs)); } /* * Parse and perform a FETCH/UID FETCH command * The command has been parsed up to and including * the sequence */ static void cmd_fetch(char *tag, char *sequence, int usinguid) { const char *cmd = usinguid ? "UID Fetch" : "Fetch"; struct fetchargs fetchargs; int fetchedsomething, r; clock_t start = clock(); char mytime[100]; if (backend_current) { /* remote mailbox */ prot_printf(backend_current->out, "%s %s %s ", tag, cmd, sequence); if (!pipe_command(backend_current, 65536)) { pipe_including_tag(backend_current, tag, 0); } return; } /* local mailbox */ memset(&fetchargs, 0, sizeof(struct fetchargs)); r = parse_fetch_args(tag, cmd, (usinguid && (client_capa & CAPA_QRESYNC)), &fetchargs); if (r) goto freeargs; if (usinguid) fetchargs.fetchitems |= FETCH_UID; r = index_fetch(imapd_index, sequence, usinguid, &fetchargs, &fetchedsomething); snprintf(mytime, sizeof(mytime), "%2.3f", (clock() - start) / (double) CLOCKS_PER_SEC); if (r) { prot_printf(imapd_out, "%s NO %s (%s sec)\r\n", tag, error_message(r), mytime); } else if (fetchedsomething || usinguid) { prot_printf(imapd_out, "%s OK %s (%s sec)\r\n", tag, error_message(IMAP_OK_COMPLETED), mytime); } else { /* normal FETCH, nothing came back */ prot_printf(imapd_out, "%s NO %s (%s sec)\r\n", tag, error_message(IMAP_NO_NOSUCHMSG), mytime); } freeargs: fetchargs_fini(&fetchargs); } static int do_xbackup(const char *channel, const ptrarray_t *list) { int partial_success = 0; int mbox_count = 0; int i, r = 0; struct sync_client_state sync_cs = SYNC_CLIENT_STATE_INITIALIZER; sync_cs.servername = sync_get_config(channel, "sync_host"); sync_cs.channel = channel; syslog(LOG_INFO, "XBACKUP: connecting to server '%s' for channel '%s'", sync_cs.servername, channel); r = sync_connect(&sync_cs); if (r) { syslog(LOG_ERR, "XBACKUP: failed to connect to server '%s' for channel '%s'", sync_cs.servername, channel); return r; } for (i = 0; i < list->count; i++) { const mbname_t *mbname = ptrarray_nth(list, i); if (!mbname) continue; const char *userid = mbname_userid(mbname); const char *intname = mbname_intname(mbname); const char *extname = mbname_extname(mbname, &imapd_namespace, NULL); if (userid) { syslog(LOG_INFO, "XBACKUP: replicating user %s", userid); r = sync_do_user(&sync_cs, userid, NULL); } else { struct sync_name_list *mboxname_list = sync_name_list_create(); syslog(LOG_INFO, "XBACKUP: replicating mailbox %s", intname); sync_name_list_add(mboxname_list, intname); r = sync_do_mailboxes(&sync_cs, mboxname_list, NULL, sync_cs.flags); mbox_count++; sync_name_list_free(&mboxname_list); } if (r) { prot_printf(imapd_out, "* NO %s %s (%s)\r\n", userid ? "USER" : "MAILBOX", userid ? userid : extname, error_message(r)); } else { partial_success++; prot_printf(imapd_out, "* OK %s %s\r\n", userid ? "USER" : "MAILBOX", userid ? userid : extname); } prot_flush(imapd_out); /* send RESTART after each user, or 1000 mailboxes */ if (!r && i < list->count - 1 && (userid || mbox_count >= 1000)) { mbox_count = 0; r = sync_do_restart(&sync_cs); if (r) goto done; } } /* send a final RESTART */ sync_do_restart(&sync_cs); if (partial_success) r = 0; done: sync_disconnect(&sync_cs); free(sync_cs.backend); return r; } static int xbackup_addmbox(struct findall_data *data, void *rock) { if (!data) return 0; if (!data->is_exactmatch) return 0; ptrarray_t *list = (ptrarray_t *) rock; /* Only add shared mailboxes or user INBOXes */ if (!mbname_localpart(data->mbname) || (!mbname_isdeleted(data->mbname) && !strarray_size(mbname_boxes(data->mbname)))) { ptrarray_append(list, mbname_dup(data->mbname)); } return 0; } /* Parse and perform an XBACKUP command. */ void cmd_xbackup(const char *tag, const char *mailbox, const char *channel) { ptrarray_t list = PTRARRAY_INITIALIZER; int i, r; /* admins only please */ if (!imapd_userisadmin && !imapd_userisproxyadmin) { r = IMAP_PERMISSION_DENIED; goto done; } if (!config_getswitch(IMAPOPT_XBACKUP_ENABLED)) { /* shouldn't get here, but just in case */ r = IMAP_PERMISSION_DENIED; goto done; } mboxlist_findall(NULL, mailbox, 1, NULL, NULL, xbackup_addmbox, &list); if (list.count) { r = do_xbackup(channel, &list); for (i = 0; i < list.count; i++) { mbname_t *mbname = ptrarray_nth(&list, i); if (mbname) mbname_free(&mbname); } ptrarray_fini(&list); } else { r = IMAP_MAILBOX_NONEXISTENT; } done: imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } } #undef PARSE_PARTIAL /* cleanup */ /* * Parse and perform a STORE/UID STORE command * The command has been parsed up to and including * the sequence */ static void cmd_store(char *tag, char *sequence, int usinguid) { const char *cmd = usinguid ? "UID Store" : "Store"; struct storeargs storeargs; static struct buf operation, flagname; int len, c; int flagsparsed = 0, inlist = 0; char *modified = NULL; int r; if (backend_current) { /* remote mailbox */ prot_printf(backend_current->out, "%s %s %s ", tag, cmd, sequence); if (!pipe_command(backend_current, 65536)) { pipe_including_tag(backend_current, tag, 0); } return; } /* local mailbox */ memset(&storeargs, 0, sizeof storeargs); storeargs.unchangedsince = ~0ULL; storeargs.usinguid = usinguid; strarray_init(&storeargs.flags); c = prot_getc(imapd_in); if (c == '(') { /* Grab/parse the modifier(s) */ static struct buf storemod; do { c = getword(imapd_in, &storemod); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); ucase(storemod.s); if (!strcmp(storemod.s, "UNCHANGEDSINCE")) { if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing required argument to %s %s\r\n", tag, cmd, storemod.s); eatline(imapd_in, c); return; } c = getmodseq(imapd_in, &storeargs.unchangedsince); if (c == EOF) { prot_printf(imapd_out, "%s BAD Invalid argument to %s UNCHANGEDSINCE\r\n", tag, cmd); eatline(imapd_in, c); return; } } else { prot_printf(imapd_out, "%s BAD Invalid %s modifier %s\r\n", tag, cmd, storemod.s); eatline(imapd_in, c); return; } } while (c == ' '); if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close paren in store modifier entry \r\n", tag); eatline(imapd_in, c); return; } c = prot_getc(imapd_in); if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing required argument to %s\r\n", tag, cmd); eatline(imapd_in, c); return; } } else prot_ungetc(c, imapd_in); c = getword(imapd_in, &operation); if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing required argument to %s\r\n", tag, cmd); eatline(imapd_in, c); return; } lcase(operation.s); len = strlen(operation.s); if (len > 7 && !strcmp(operation.s+len-7, ".silent")) { storeargs.silent = 1; operation.s[len-7] = '\0'; } if (!strcmp(operation.s, "+flags")) { storeargs.operation = STORE_ADD_FLAGS; } else if (!strcmp(operation.s, "-flags")) { storeargs.operation = STORE_REMOVE_FLAGS; } else if (!strcmp(operation.s, "flags")) { storeargs.operation = STORE_REPLACE_FLAGS; } else if (!strcmp(operation.s, "annotation")) { storeargs.operation = STORE_ANNOTATION; /* ANNOTATION has implicit .SILENT behaviour */ storeargs.silent = 1; c = parse_annotate_store_data(tag, /*permessage_flag*/1, &storeargs.entryatts); if (c <= EOF) { goto freeflags; } storeargs.namespace = &imapd_namespace; storeargs.isadmin = imapd_userisadmin; storeargs.userid = imapd_userid; storeargs.authstate = imapd_authstate; goto notflagsdammit; } else { prot_printf(imapd_out, "%s BAD Invalid %s attribute\r\n", tag, cmd); eatline(imapd_in, ' '); return; } for (;;) { c = getword(imapd_in, &flagname); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (c == '(' && !flagname.s[0] && !flagsparsed && !inlist) { inlist = 1; continue; } if (!flagname.s[0]) break; if (flagname.s[0] == '\\') { lcase(flagname.s); if (!strcmp(flagname.s, "\\seen")) { storeargs.seen = 1; } else if (!strcmp(flagname.s, "\\answered")) { storeargs.system_flags |= FLAG_ANSWERED; } else if (!strcmp(flagname.s, "\\flagged")) { storeargs.system_flags |= FLAG_FLAGGED; } else if (!strcmp(flagname.s, "\\deleted")) { storeargs.system_flags |= FLAG_DELETED; } else if (!strcmp(flagname.s, "\\draft")) { storeargs.system_flags |= FLAG_DRAFT; } else { prot_printf(imapd_out, "%s BAD Invalid system flag in %s command\r\n", tag, cmd); eatline(imapd_in, c); goto freeflags; } } else if (!imparse_isatom(flagname.s)) { prot_printf(imapd_out, "%s BAD Invalid flag name %s in %s command\r\n", tag, flagname.s, cmd); eatline(imapd_in, c); goto freeflags; } else strarray_append(&storeargs.flags, flagname.s); flagsparsed++; if (c != ' ') break; } if (!inlist && !flagsparsed) { prot_printf(imapd_out, "%s BAD Missing required argument to %s\r\n", tag, cmd); eatline(imapd_in, c); return; } if (inlist && c == ')') { inlist = 0; c = prot_getc(imapd_in); } if (inlist) { prot_printf(imapd_out, "%s BAD Missing close parenthesis in %s\r\n", tag, cmd); eatline(imapd_in, c); goto freeflags; } notflagsdammit: if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to %s\r\n", tag, cmd); eatline(imapd_in, c); goto freeflags; } if ((storeargs.unchangedsince != ULONG_MAX) && !(client_capa & CAPA_CONDSTORE)) { client_capa |= CAPA_CONDSTORE; prot_printf(imapd_out, "* OK [HIGHESTMODSEQ " MODSEQ_FMT "] CONDSTORE enabled by UNCHANGEDSINCE\r\n", index_highestmodseq(imapd_index)); } r = index_store(imapd_index, sequence, &storeargs); /* format the MODIFIED response code */ if (storeargs.modified) { char *seqstr = seqset_cstring(storeargs.modified); assert(seqstr); modified = strconcat("[MODIFIED ", seqstr, "] ", (char *)NULL); free(seqstr); } else { modified = xstrdup(""); } if (r) { prot_printf(imapd_out, "%s NO %s%s\r\n", tag, modified, error_message(r)); } else { index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK %s%s\r\n", tag, modified, error_message(IMAP_OK_COMPLETED)); } freeflags: strarray_fini(&storeargs.flags); freeentryatts(storeargs.entryatts); seqset_free(&storeargs.modified); free(modified); } static void cmd_search(char *tag, int usinguid) { int c; struct searchargs *searchargs; clock_t start = clock(); char mytime[100]; int n; if (backend_current) { /* remote mailbox */ const char *cmd = usinguid ? "UID Search" : "Search"; prot_printf(backend_current->out, "%s %s ", tag, cmd); if (!pipe_command(backend_current, 65536)) { pipe_including_tag(backend_current, tag, 0); } return; } /* local mailbox */ searchargs = new_searchargs(tag, GETSEARCH_CHARSET_KEYWORD|GETSEARCH_RETURN, &imapd_namespace, imapd_userid, imapd_authstate, imapd_userisadmin || imapd_userisproxyadmin); searchargs->maxargssize_mark = maxargssize_mark; /* Set FUZZY search according to config and quirks */ static const char *annot = IMAP_ANNOT_NS "search-fuzzy-always"; char *inbox = mboxname_user_mbox(imapd_userid, NULL); struct buf val = BUF_INITIALIZER; if (imapd_id.quirks & QUIRK_SEARCHFUZZY) { /* Quirks overrule anything */ searchargs->fuzzy_depth++; } else if (!annotatemore_lookupmask(inbox, annot, imapd_userid, &val) && val.len) { /* User may override global config */ int b = config_parse_switch(buf_cstring(&val)); if (b > 0 || (b < 0 && config_getswitch(IMAPOPT_SEARCH_FUZZY_ALWAYS))) { searchargs->fuzzy_depth++; } } else if (config_getswitch(IMAPOPT_SEARCH_FUZZY_ALWAYS)) { /* Use global config */ searchargs->fuzzy_depth++; } buf_free(&val); free(inbox); c = get_search_program(imapd_in, imapd_out, searchargs); if (c == EOF) { eatline(imapd_in, ' '); freesearchargs(searchargs); return; } if (c == IMAP_LITERAL_TOO_LARGE) { prot_printf(imapd_out, "%s NO %s in Search\r\n", tag, error_message(c)); freesearchargs(searchargs); return; } if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Search\r\n", tag); eatline(imapd_in, c); freesearchargs(searchargs); return; } // this refreshes the index, we may be looking at it in our search imapd_check(NULL, 0); if (searchargs->charset == CHARSET_UNKNOWN_CHARSET) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_UNRECOGNIZED_CHARSET)); } else { n = index_search(imapd_index, searchargs, usinguid); int r = cmd_cancelled(/*insearch*/1); if (!r) { snprintf(mytime, sizeof(mytime), "%2.3f", (clock() - start) / (double) CLOCKS_PER_SEC); prot_printf(imapd_out, "%s OK %s (%d msgs in %s secs)\r\n", tag, error_message(IMAP_OK_COMPLETED), n, mytime); } else { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } } freesearchargs(searchargs); } /* * Perform a SORT/UID SORT command */ static void cmd_sort(char *tag, int usinguid) { int c; struct buf arg = BUF_INITIALIZER; struct sortcrit *sortcrit = NULL; struct searchargs *searchargs = NULL; clock_t start = clock(); char mytime[100]; int n; if (backend_current) { /* remote mailbox */ const char *cmd = usinguid ? "UID Sort" : "Sort"; prot_printf(backend_current->out, "%s %s ", tag, cmd); if (!pipe_command(backend_current, 65536)) { pipe_including_tag(backend_current, tag, 0); } return; } /* local mailbox */ searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, &imapd_namespace, imapd_userid, imapd_authstate, imapd_userisadmin || imapd_userisproxyadmin); searchargs->maxargssize_mark = maxargssize_mark; if (imapd_id.quirks & QUIRK_SEARCHFUZZY) searchargs->fuzzy_depth++; /* See if its ESORT */ c = getword(imapd_in, &arg); if (c == EOF) goto error; else if (c == ' ' && !strcasecmp(arg.s, "return")) { /* RFC 5267 */ c = get_search_return_opts(imapd_in, imapd_out, searchargs); if (c != ' ') goto error; } else prot_ungetc(c, imapd_in); c = getsortcriteria(tag, &sortcrit); if (c == EOF) goto error; c = get_search_program(imapd_in, imapd_out, searchargs); if (c == EOF) goto error; if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Sort\r\n", tag); goto error; } n = index_sort(imapd_index, sortcrit, searchargs, usinguid); snprintf(mytime, sizeof(mytime), "%2.3f", (clock() - start) / (double) CLOCKS_PER_SEC); if (CONFIG_TIMING_VERBOSE) { char *s = sortcrit_as_string(sortcrit); syslog(LOG_DEBUG, "SORT (%s) processing time: %d msg in %s sec", s, n, mytime); free(s); } prot_printf(imapd_out, "%s OK %s (%d msgs in %s secs)\r\n", tag, error_message(IMAP_OK_COMPLETED), n, mytime); buf_free(&arg); freesortcrit(sortcrit); freesearchargs(searchargs); return; error: eatline(imapd_in, (c == EOF ? ' ' : c)); buf_free(&arg); freesortcrit(sortcrit); freesearchargs(searchargs); } static void cmd_xstats(char *tag) { int metric; if (backend_current) { /* remote mailbox */ const char *cmd = "Xstats"; prot_printf(backend_current->out, "%s %s ", tag, cmd); if (!pipe_command(backend_current, 65536)) { pipe_including_tag(backend_current, tag, 0); } return; } prot_printf(imapd_out, "* XSTATS"); for (metric = 0 ; metric < XSTATS_NUM_METRICS ; metric++) prot_printf(imapd_out, " %s %u", xstats_names[metric], xstats[metric]); prot_printf(imapd_out, "\r\n"); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); return; } /* * Perform a THREAD/UID THREAD command */ static void cmd_thread(char *tag, int usinguid) { static struct buf arg; int c; int alg; struct searchargs *searchargs; clock_t start = clock(); char mytime[100]; int n; if (backend_current) { /* remote mailbox */ const char *cmd = usinguid ? "UID Thread" : "Thread"; prot_printf(backend_current->out, "%s %s ", tag, cmd); if (!pipe_command(backend_current, 65536)) { pipe_including_tag(backend_current, tag, 0); } return; } /* local mailbox */ /* get algorithm */ c = getword(imapd_in, &arg); if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing algorithm in Thread\r\n", tag); eatline(imapd_in, c); return; } if ((alg = find_thread_algorithm(arg.s)) == -1) { prot_printf(imapd_out, "%s BAD Invalid Thread algorithm %s\r\n", tag, arg.s); eatline(imapd_in, c); return; } searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST, &imapd_namespace, imapd_userid, imapd_authstate, imapd_userisadmin || imapd_userisproxyadmin); searchargs->maxargssize_mark = maxargssize_mark; c = get_search_program(imapd_in, imapd_out, searchargs); if (c == EOF) { eatline(imapd_in, ' '); goto done; } if (c == IMAP_LITERAL_TOO_LARGE) { prot_printf(imapd_out, "%s NO %s in Thread\r\n", tag, error_message(c)); goto done; } if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Thread\r\n", tag); eatline(imapd_in, c); goto done; } n = index_thread(imapd_index, alg, searchargs, usinguid); snprintf(mytime, sizeof(mytime), "%2.3f", (clock() - start) / (double) CLOCKS_PER_SEC); prot_printf(imapd_out, "%s OK %s (%d msgs in %s secs)\r\n", tag, error_message(IMAP_OK_COMPLETED), n, mytime); done: freesearchargs(searchargs); return; } /* * Perform a COPY/UID COPY command */ static void cmd_copy(char *tag, char *sequence, char *name, int usinguid, int ismove) { int r, myrights = 0; char *copyuid = NULL; mbentry_t *mbentry = NULL; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(NULL, NULL, intname, &mbentry); if (!r) myrights = cyrus_acl_myrights(imapd_authstate, mbentry->acl); if (!r && backend_current) { /* remote mailbox -> local or remote mailbox */ /* xxx start of separate proxy-only code (remove when we move to a unified environment) */ struct backend *s = NULL; s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); mboxlist_entry_free(&mbentry); if (!s) { r = IMAP_SERVER_UNAVAILABLE; goto done; } if (s != backend_current) { /* this is the hard case; we have to fetch the messages and append them to the other mailbox */ proxy_copy(tag, sequence, name, myrights, usinguid, s); goto cleanup; } /* xxx end of separate proxy-only code */ /* simply send the COPY to the backend */ prot_printf( backend_current->out, "%s %s %s {" SIZE_T_FMT "+}\r\n%s\r\n", tag, usinguid ? (ismove ? "UID Move" : "UID Copy") : (ismove ? "Move" : "Copy"), sequence, strlen(name), name ); pipe_including_tag(backend_current, tag, 0); goto cleanup; } else if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) { /* local mailbox -> remote mailbox * * fetch the messages and APPEND them to the backend * * xxx completely untested */ struct backend *s = NULL; int res; s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); mboxlist_entry_free(&mbentry); if (!s) r = IMAP_SERVER_UNAVAILABLE; else if (!CAPA(s, CAPA_MULTIAPPEND)) { /* we need MULTIAPPEND for atomicity */ r = IMAP_REMOTE_NO_MULTIAPPEND; } if (r) goto done; assert(!ismove); /* XXX - support proxying moves */ /* start the append */ prot_printf(s->out, "%s Append {" SIZE_T_FMT "+}\r\n%s", tag, strlen(name), name); /* append the messages */ r = index_copy_remote(imapd_index, sequence, usinguid, s->out); if (!r) { /* ok, finish the append; we need the UIDVALIDITY and UIDs to return as part of our COPYUID response code */ char *appenduid, *b; prot_printf(s->out, "\r\n"); res = pipe_until_tag(s, tag, 0); if (res == PROXY_OK) { if (myrights & ACL_READ) { appenduid = strchr(s->last_result.s, '['); /* skip over APPENDUID */ if (appenduid) { appenduid += strlen("[appenduid "); b = strchr(appenduid, ']'); if (b) *b = '\0'; prot_printf(imapd_out, "%s OK [COPYUID %s] %s\r\n", tag, appenduid, error_message(IMAP_OK_COMPLETED)); } else prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } } else { prot_printf(imapd_out, "%s %s", tag, s->last_result.s); } } else { /* abort the append */ prot_printf(s->out, " {0}\r\n"); pipe_until_tag(s, tag, 0); /* report failure */ prot_printf(imapd_out, "%s NO inter-server COPY failed\r\n", tag); } goto cleanup; } /* need permission to delete from source if it's a move */ if (!r && ismove && !(imapd_index->myrights & ACL_EXPUNGE)) r = IMAP_PERMISSION_DENIED; /* local mailbox -> local mailbox */ if (!r) { r = index_copy(imapd_index, sequence, usinguid, intname, ©uid, !config_getswitch(IMAPOPT_SINGLEINSTANCESTORE), &imapd_namespace, (imapd_userisadmin || imapd_userisproxyadmin), ismove, ignorequota); } if (ismove && copyuid && !r) { prot_printf(imapd_out, "* OK [COPYUID %s] %s\r\n", copyuid, error_message(IMAP_OK_COMPLETED)); free(copyuid); copyuid = NULL; } imapd_check(NULL, ismove || usinguid); done: if (r && !(usinguid && r == IMAP_NO_NOSUCHMSG)) { const char *respcode = ""; if (r == IMAP_MAILBOX_NOTSUPPORTED) { respcode = "[CANNOT] "; } else if (r == IMAP_MAILBOX_NONEXISTENT && mboxlist_createmailboxcheck(intname, 0, 0, imapd_userisadmin, imapd_userid, imapd_authstate, NULL, NULL, 0) == 0) { respcode = "[TRYCREATE] "; } prot_printf(imapd_out, "%s NO %s%s\r\n", tag, respcode, error_message(r)); } else if (copyuid) { prot_printf(imapd_out, "%s OK [COPYUID %s] %s\r\n", tag, copyuid, error_message(IMAP_OK_COMPLETED)); free(copyuid); } else { index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } cleanup: mboxlist_entry_free(&mbentry); free(intname); } /* * Perform an EXPUNGE command * sequence == NULL if this isn't a UID EXPUNGE */ static void cmd_expunge(char *tag, char *sequence) { modseq_t old; modseq_t new; int r = 0; if (backend_current) { /* remote mailbox */ if (sequence) { prot_printf(backend_current->out, "%s UID Expunge %s\r\n", tag, sequence); } else { prot_printf(backend_current->out, "%s Expunge\r\n", tag); } pipe_including_tag(backend_current, tag, 0); return; } /* local mailbox */ if (!index_hasrights(imapd_index, ACL_EXPUNGE)) r = IMAP_PERMISSION_DENIED; old = index_highestmodseq(imapd_index); if (!r) r = index_expunge(imapd_index, sequence, 1); /* tell expunges */ if (!r) index_tellchanges(imapd_index, 1, sequence ? 1 : 0, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); return; } new = index_highestmodseq(imapd_index); index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK ", tag); if (new > old) prot_printf(imapd_out, "[HIGHESTMODSEQ " MODSEQ_FMT "] ", new); prot_printf(imapd_out, "%s\r\n", error_message(IMAP_OK_COMPLETED)); } /* * Perform a CREATE command */ static void cmd_create(char *tag, char *name, struct dlist *extargs, int localonly) { int r = 0; int mbtype = MBTYPE_EMAIL; int options = 0; const char *partition = NULL; const char *server = NULL; const char *uniqueid = NULL; struct buf specialuse = BUF_INITIALIZER; struct dlist *use; struct mailbox *mailbox = NULL; char *mailboxid = NULL; mbentry_t *parent = NULL; /* We don't care about trailing hierarchy delimiters. */ if (name[0] && name[strlen(name)-1] == imapd_namespace.hier_sep) { name[strlen(name)-1] = '\0'; } mbname_t *mbname = mbname_from_extname(name, &imapd_namespace, imapd_userid); struct mboxlock *namespacelock = mboxname_usernamespacelock(mbname_intname(mbname)); const char *type = NULL; dlist_getatom(extargs, "PARTITION", &partition); dlist_getatom(extargs, "SERVER", &server); dlist_getatom(extargs, "MAILBOXID", &uniqueid); if (dlist_getatom(extargs, "TYPE", &type)) { if (!strcasecmp(type, "CALENDAR")) mbtype = MBTYPE_CALENDAR; else if (!strcasecmp(type, "COLLECTION")) mbtype = MBTYPE_COLLECTION; else if (!strcasecmp(type, "ADDRESSBOOK")) mbtype = MBTYPE_ADDRESSBOOK; else { r = IMAP_MAILBOX_BADTYPE; goto err; } } use = dlist_getchild(extargs, "USE"); if (use) { /* only user mailboxes can have specialuse, and they must be user toplevel folders */ if (!mbname_userid(mbname) || strarray_size(mbname_boxes(mbname)) != 1) { r = IMAP_MAILBOX_SPECIALUSE; goto err; } /* I would much prefer to create the specialuse annotation FIRST * and do the sanity check on the values, so we can return the * correct error. Sadly, that's a pain - so we compromise by * "normalising" first */ struct dlist *item; char *raw; strarray_t *su = strarray_new(); for (item = use->head; item; item = item->next) { strarray_append(su, dlist_cstring(item)); } raw = strarray_join(su, " "); strarray_free(su); r = specialuse_validate(NULL, imapd_userid, raw, &specialuse, 0); free(raw); if (r) { prot_printf(imapd_out, "%s NO [USEATTR] %s\r\n", tag, error_message(r)); goto done; } if (strstr(buf_cstring(&specialuse), "\\Snoozed")) options |= OPT_IMAP_HAS_ALARMS; } // A non-admin is not allowed to specify the server nor partition on which // to create the mailbox. // // 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 || uniqueid) && !imapd_userisadmin) { if (config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_STANDARD || config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_UNIFIED) { if (!config_getstring(IMAPOPT_PROXYSERVERS)) { r = IMAP_PERMISSION_DENIED; goto err; } } } /* check for INBOX.INBOX creation by broken Apple clients */ const strarray_t *boxes = mbname_boxes(mbname); if (strarray_size(boxes) > 1 && !strcasecmp(strarray_nth(boxes, 0), "INBOX") && !strcasecmp(strarray_nth(boxes, 1), "INBOX")) r = IMAP_MAILBOX_BADNAME; if (r) { err: prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); goto done; } // If the create command does not mandate the mailbox must be created // locally, let's go and find the most appropriate location. if (!localonly) { // If we're running in a Murder, things get more complicated. if (config_mupdate_server) { // Consider your actions on a per type of topology basis. // // First up: Standard / discrete murder topology, with dedicated // imap frontends, or unified -- both allow the IMAP server to either // need to proxy through, or create locally. if ( config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_STANDARD || config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_UNIFIED ) { // The way that we detect whether we're a frontend is by testing // for the proxy servers setting ... :/ if (!config_getstring(IMAPOPT_PROXYSERVERS)) { // Find the parent mailbox, if any. // mboxlist_findparent either supplies the parent // or has a return code of IMAP_MAILBOX_NONEXISTENT. r = mboxlist_findparent(mbname_intname(mbname), &parent); if (r) { if (r != IMAP_MAILBOX_NONEXISTENT) { prot_printf(imapd_out, "%s NO %s (%s:%d)\r\n", tag, error_message(r), __FILE__, __LINE__); goto done; } } if (!server && !partition) { if (!parent) { server = find_free_server(); if (!server) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_SERVER_UNAVAILABLE)); goto done; } } else { server = parent->server; /* DO NOT set the partition: only admins are allowed to do this and the backend will use the partition of the parent by default anyways. partition = parent->partition; */ } } // don't hold the lock locally, we're proxying mboxname_release(&namespacelock); struct backend *s_conn = NULL; s_conn = proxy_findserver( server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in ); if (!s_conn) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_SERVER_UNAVAILABLE)); goto done; } // Huh? if (imapd_userisadmin && supports_referrals) { // "They are not an admin remotely, so let's refer them" -- // - Who is they? // - How did imapd_userisadmin get set all of a sudden? imapd_refer(tag, server, name); referral_kick = 1; return; } if (!CAPA(s_conn, CAPA_MUPDATE)) { // Huh? // "reserve mailbox on MUPDATE" syslog(LOG_WARNING, "backend %s is not advertising any MUPDATE capability (%s:%d)", server, __FILE__, __LINE__); } // why not send a LOCALCREATE to the backend? prot_printf(s_conn->out, "%s CREATE ", tag); prot_printastring(s_conn->out, name); // special use needs extended support, so pass through extargs if (specialuse.len || uniqueid) { prot_printf(s_conn->out, " ("); int printsp = 0; if (specialuse.len) { if (printsp) prot_putc(' ', s_conn->out); printsp = 1; prot_printf(s_conn->out, "USE (%s)", buf_cstring(&specialuse)); } if (partition) { if (printsp) prot_putc(' ', s_conn->out); printsp = 1; prot_printf(s_conn->out, "PARTITION "); prot_printastring(s_conn->out, partition); } if (uniqueid) { if (printsp) prot_putc(' ', s_conn->out); printsp = 1; prot_printf(s_conn->out, "MAILBOXID "); prot_printastring(s_conn->out, uniqueid); } prot_putc(')', s_conn->out); } // Send partition as an atom, since its supported by older servers else if (partition) { prot_putc(' ', s_conn->out); prot_printastring(s_conn->out, partition); } prot_printf(s_conn->out, "\r\n"); int res = pipe_until_tag(s_conn, tag, 0); if (!CAPA(s_conn, CAPA_MUPDATE)) { // Huh? // "do MUPDATE create operations" syslog(LOG_WARNING, "backend %s is not advertising any MUPDATE capability (%s:%d)", server, __FILE__, __LINE__); } /* make sure we've seen the update */ if (ultraparanoid && res == PROXY_OK) kick_mupdate(); imapd_check(s_conn, 0); prot_printf(imapd_out, "%s %s", tag, s_conn->last_result.s); goto done; } else { // (!config_getstring(IMAPOPT_PROXYSERVERS)) // I have a standard murder config but also proxy servers configured; I'm a backend! goto localcreate; } // (!config_getstring(IMAPOPT_PROXYSERVERS)) } // (config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_STANDARD) else if (config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_REPLICATED) { // Everything is local goto localcreate; } // (config_mupdate_config == IMAP_ENUM_MUPDATE_CONFIG_REPLICATED) else { syslog(LOG_ERR, "murder configuration I cannot deal with"); prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_SERVER_UNAVAILABLE)); goto done; } } else { // (config_mupdate_server) // I'm no part of a Murder, *everything* is localcreate goto localcreate; } // (config_mupdate_server) } else { // (!localonly) goto localcreate; } localcreate: // XXX Do we want a config option for this? // find the nearest ancestor to see if we have to fill out the branch r = mboxlist_findparent(mbname_intname(mbname), &parent); if (r == IMAP_MAILBOX_NONEXISTENT) r = 0; mbentry_t mbentry = MBENTRY_INITIALIZER; unsigned flags = MBOXLIST_CREATE_NOTIFY; if (localonly) flags |= MBOXLIST_CREATE_LOCALONLY | MBOXLIST_CREATE_FORCEUSER; if (!r && (parent || !mbname_userid(mbname))) { mbname_t *ancestor = mbname_from_intname(parent ? parent->name : NULL); int oldest = strarray_size(mbname_boxes(ancestor)); int youngest = strarray_size(boxes) - 1; // any missing ancestors? if (youngest > oldest) { // verify that we can create the requested mailbox // before creating its ancestors r = mboxlist_createmailboxcheck(mbname_intname(mbname), mbtype, partition, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate, NULL, NULL, 0); int i; for (i = oldest; !r && i < youngest; i++) { // create the ancestors mbname_push_boxes(ancestor, strarray_nth(boxes, i)); /* Don't create any magic user.foo.INBOX mailboxes */ if (mbname_userid(ancestor) != NULL && strarray_size(mbname_boxes(ancestor)) == 1 && !strcmp(strarray_nth(mbname_boxes(ancestor), 0), "INBOX")) { continue; } mbentry.name = (char *) mbname_intname(ancestor); mbentry.partition = (char *) partition; mbentry.mbtype = mbtype; r = mboxlist_createmailbox(&mbentry, 0/*options*/, 0/*highestmodseq*/, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate, flags, NULL/*mailboxptr*/); if (r) { // If the mailbox already exists this should be fine. // TODO: check acl? if (r == IMAP_MAILBOX_EXISTS) { r = 0; continue; } // XXX should we delete the ancestors we just created? break; } } } mbname_free(&ancestor); } if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); goto done; } // now create the requested mailbox mbentry.name = (char *) mbname_intname(mbname); mbentry.partition = (char *) partition; mbentry.uniqueid = (char *) uniqueid; mbentry.mbtype = mbtype; r = mboxlist_createmailbox(&mbentry, options, 0/*highestmodseq*/, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate, flags, &mailbox); #ifdef USE_AUTOCREATE // Clausing autocreate for the INBOX if (r == IMAP_PERMISSION_DENIED) { if (!strarray_size(mbname_boxes(mbname)) && !strcmpsafe(imapd_userid, mbname_userid(mbname))) { int autocreatequotastorage = config_getint(IMAPOPT_AUTOCREATE_QUOTA); if (autocreatequotastorage > 0) { mbentry.uniqueid = NULL; r = mboxlist_createmailbox(&mbentry, 0/*options*/, 0/*highestmodseq*/, 1/*isadmin*/, imapd_userid, imapd_authstate, MBOXLIST_CREATE_NOTIFY, &mailbox); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); goto done; } int autocreatequotamessage = config_getint(IMAPOPT_AUTOCREATE_QUOTA_MESSAGES); if ((autocreatequotastorage > 0) || (autocreatequotamessage > 0)) { quota_t newquotas[QUOTA_NUMRESOURCES]; int res; for (res = 0; res < QUOTA_NUMRESOURCES; res++) { newquotas[res] = QUOTA_UNLIMITED; } newquotas[QUOTA_STORAGE] = autocreatequotastorage; newquotas[QUOTA_MESSAGE] = autocreatequotamessage; (void) mboxlist_setquotas(mbname_intname(mbname), newquotas, 0, 0); } } } } #endif if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); goto done; } /* Close newly created mailbox before writing annotations */ mailboxid = xstrdup(mailbox_uniqueid(mailbox)); mailbox_close(&mailbox); if (specialuse.len) { r = annotatemore_write(mbname_intname(mbname), "/specialuse", mbname_userid(mbname), &specialuse); if (r) { /* XXX - failure here SHOULD cause a cleanup of the created mailbox */ syslog( LOG_ERR, "IOERROR: failed to write specialuse for %s on %s (%s) (%s:%d)", imapd_userid, mbname_intname(mbname), buf_cstring(&specialuse), __FILE__, __LINE__ ); prot_printf(imapd_out, "%s NO %s (%s:%d)\r\n", tag, error_message(r), __FILE__, __LINE__); goto done; } } index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK [MAILBOXID (%s)] Completed\r\n", tag, mailboxid); imapd_check(NULL, 0); done: mailbox_close(&mailbox); mboxname_release(&namespacelock); mboxlist_entry_free(&parent); buf_free(&specialuse); mbname_free(&mbname); free(mailboxid); } /* Callback for use by cmd_delete */ static int delmbox(const mbentry_t *mbentry, void *rock __attribute__((unused))) { int r; if (!mboxlist_delayed_delete_isenabled()) { r = mboxlist_deletemailbox(mbentry->name, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate, NULL, MBOXLIST_DELETE_KEEP_INTERMEDIARIES); } else if ((imapd_userisadmin || imapd_userisproxyadmin) && mboxname_isdeletedmailbox(mbentry->name, NULL)) { r = mboxlist_deletemailbox(mbentry->name, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate, NULL, MBOXLIST_DELETE_KEEP_INTERMEDIARIES); } else { r = mboxlist_delayed_deletemailbox(mbentry->name, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate, NULL, MBOXLIST_DELETE_KEEP_INTERMEDIARIES); } if (r) { prot_printf(imapd_out, "* NO delete %s: %s\r\n", mbentry->name, error_message(r)); } return 0; } /* * Perform a DELETE command */ static void cmd_delete(char *tag, char *name, int localonly, int force) { int r; mbentry_t *mbentry = NULL; struct mboxevent *mboxevent = NULL; mbname_t *mbname = mbname_from_extname(name, &imapd_namespace, imapd_userid); int delete_user = 0; struct mboxlock *namespacelock = mboxname_usernamespacelock(mbname_intname(mbname)); r = mlookup(NULL, NULL, mbname_intname(mbname), &mbentry); if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) { /* remote mailbox */ struct backend *s = NULL; int res; // don't hold the lock locally, we're proxying mboxname_release(&namespacelock); if (supports_referrals) { imapd_refer(tag, mbentry->server, name); referral_kick = 1; mboxlist_entry_free(&mbentry); mbname_free(&mbname); return; } s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); mboxlist_entry_free(&mbentry); if (!s) r = IMAP_SERVER_UNAVAILABLE; if (!r) { prot_printf(s->out, "%s DELETE {" SIZE_T_FMT "+}\r\n%s\r\n", tag, strlen(name), name); res = pipe_until_tag(s, tag, 0); if (!CAPA(s, CAPA_MUPDATE) && res == PROXY_OK) { /* do MUPDATE delete operations */ } /* make sure we've seen the update */ if (ultraparanoid && res == PROXY_OK) kick_mupdate(); } imapd_check(s, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { /* we're allowed to reference last_result since the noop, if sent, went to a different server */ prot_printf(imapd_out, "%s %s", tag, s->last_result.s); } mbname_free(&mbname); return; } mboxevent = mboxevent_new(EVENT_MAILBOX_DELETE); /* local mailbox */ if (!r) { int isadmin = imapd_userisadmin || imapd_userisproxyadmin; if (mbname_isdeleted(mbname)) { r = mboxlist_deletemailbox(mbname_intname(mbname), isadmin, imapd_userid, imapd_authstate, mboxevent, MBOXLIST_DELETE_LOCALONLY); } else { delete_user = mboxname_isusermailbox(mbname_intname(mbname), 1); int delflags = (1-force) ? MBOXLIST_DELETE_CHECKACL : 0; if (!delete_user && mboxlist_haschildren(mbname_intname(mbname))) { r = IMAP_MAILBOX_HASCHILDREN; } else if (localonly || !mboxlist_delayed_delete_isenabled()) { r = mboxlist_deletemailbox(mbname_intname(mbname), isadmin, imapd_userid, imapd_authstate, mboxevent, delflags|MBOXLIST_DELETE_LOCALONLY); } else { r = mboxlist_delayed_deletemailbox(mbname_intname(mbname), isadmin, imapd_userid, imapd_authstate, mboxevent, delflags); } } } /* send a MailboxDelete event notification */ if (!r) mboxevent_notify(&mboxevent); mboxevent_free(&mboxevent); /* was it a top-level user mailbox? */ /* localonly deletes are only per-mailbox */ if (!r && !localonly && delete_user) { const char *userid = mbname_userid(mbname); if (userid) { r = mboxlist_usermboxtree(userid, NULL, delmbox, NULL, MBOXTREE_INTERMEDIATES); if (!r) r = user_deletedata(mbentry, 1); } } mboxlist_entry_free(&mbentry); if (!r && config_getswitch(IMAPOPT_DELETE_UNSUBSCRIBE)) { mboxlist_changesub(mbname_intname(mbname), imapd_userid, imapd_authstate, /* add */ 0, /* force */ 0, /* notify? */ 1); } mboxname_release(&namespacelock); imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { if (config_mupdate_server) kick_mupdate(); index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } mbname_free(&mbname); } struct renrock { const struct namespace *namespace; int ol; int nl; int rename_user; const char *olduser, *newuser; char *newmailboxname; const char *partition; int found; }; /* Callback for use by cmd_rename */ static int checkmboxname(const mbentry_t *mbentry, void *rock) { struct renrock *text = (struct renrock *)rock; int r; text->found++; if((text->nl + strlen(mbentry->name + text->ol)) >= MAX_MAILBOX_BUFFER) return IMAP_MAILBOX_BADNAME; strcpy(text->newmailboxname + text->nl, mbentry->name + text->ol); /* force create, but don't ignore policy. This is a filthy hack that will go away when we refactor this code */ r = mboxlist_createmailboxcheck(text->newmailboxname, 0, text->partition, 1, imapd_userid, imapd_authstate, NULL, NULL, 2); return r; } /* Callback for use by cmd_rename */ static int renmbox(const mbentry_t *mbentry, void *rock) { struct renrock *text = (struct renrock *)rock; char *oldextname = NULL, *newextname = NULL; int r = 0; uint32_t uidvalidity = mbentry->uidvalidity; if((text->nl + strlen(mbentry->name + text->ol)) >= MAX_MAILBOX_BUFFER) goto done; strcpy(text->newmailboxname + text->nl, mbentry->name + text->ol); /* check if a previous deleted mailbox existed */ mbentry_t *newmbentry = NULL; r = mboxlist_lookup_allow_all(text->newmailboxname, &newmbentry, NULL); /* XXX - otherwise we should probably reject now, but meh, save it for * a real cleanup */ if (!r && (newmbentry->mbtype & MBTYPE_DELETED)) { /* changing the unique id since last time? */ if (strcmpsafe(mbentry->uniqueid, newmbentry->uniqueid)) { /* then the UIDVALIDITY must be higher than before */ if (uidvalidity <= newmbentry->uidvalidity) uidvalidity = newmbentry->uidvalidity+1; } } mboxlist_entry_free(&newmbentry); /* don't notify implied rename in mailbox hierarchy */ r = mboxlist_renamemailbox(mbentry, text->newmailboxname, text->partition, uidvalidity, 1, imapd_userid, imapd_authstate, NULL, 0, 0, text->rename_user, /*keep_intermediaries*/1, 0, 0); if (!r && config_getswitch(IMAPOPT_DELETE_UNSUBSCRIBE)) { mboxlist_changesub(mbentry->name, imapd_userid, imapd_authstate, /* add */ 0, /* force */ 0, /* notify? */ 0); } oldextname = mboxname_to_external(mbentry->name, &imapd_namespace, imapd_userid); newextname = mboxname_to_external(text->newmailboxname, &imapd_namespace, imapd_userid); if(r) { prot_printf(imapd_out, "* NO rename %s %s: %s\r\n", oldextname, newextname, error_message(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(mbentry->name, text->newmailboxname); user_renameacl(text->namespace, text->newmailboxname, text->olduser, text->newuser); } // non-standard output item, but it helps give progress prot_printf(imapd_out, "* OK rename %s %s\r\n", oldextname, newextname); } done: prot_flush(imapd_out); free(oldextname); free(newextname); return r; } /* Callback for use by cmd_rename */ static int checkrenmacl(const mbentry_t *mbentry, void *rock) { const struct auth_state *auth_state = (struct auth_state *) rock; long myrights = cyrus_acl_myrights(auth_state, mbentry->acl); if (myrights & ACL_DELETEMBOX) return IMAP_OK_COMPLETED; return (myrights & ACL_LOOKUP) ? IMAP_PERMISSION_DENIED : IMAP_MAILBOX_NONEXISTENT; } /* * Perform a RENAME command */ static void cmd_rename(char *tag, char *oldname, char *newname, char *location) { int r = 0; char *c; char oldmailboxname[MAX_MAILBOX_BUFFER]; char newmailboxname[MAX_MAILBOX_BUFFER]; char oldmailboxname2[MAX_MAILBOX_BUFFER]; char newmailboxname2[MAX_MAILBOX_BUFFER]; char *oldextname = NULL; char *newextname = NULL; char *oldintname = NULL; char *newintname = NULL; char *olduser = NULL; char *newuser = NULL; int omlen, nmlen; int subcount = 0; /* number of sub-folders found */ int recursive_rename = 1; int rename_user = 0; int mbtype = 0; mbentry_t *mbentry = NULL; struct renrock rock; if (location && !imapd_userisadmin) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_PERMISSION_DENIED)); return; } if (location && strcmp(oldname, newname)) { /* XXX It would be nice to not complain here iff the location * XXX is actually the mailbox's current partition, but we * XXX don't have that info until much later! */ prot_printf(imapd_out, "%s NO Cross-server or cross-partition move w/rename not supported\r\n", tag); return; } oldintname = mboxname_from_external(oldname, &imapd_namespace, imapd_userid); xstrncpy(oldmailboxname, oldintname, MAX_MAILBOX_NAME); free(oldintname); newintname = mboxname_from_external(newname, &imapd_namespace, imapd_userid); xstrncpy(newmailboxname, newintname, MAX_MAILBOX_NAME); free(newintname); olduser = mboxname_to_userid(oldmailboxname); newuser = mboxname_to_userid(newmailboxname); struct mboxlock *oldnamespacelock = NULL; struct mboxlock *newnamespacelock = NULL; if (strcmpsafe(oldmailboxname, newmailboxname) < 0) { oldnamespacelock = mboxname_usernamespacelock(oldmailboxname); newnamespacelock = mboxname_usernamespacelock(newmailboxname); } else { newnamespacelock = mboxname_usernamespacelock(newmailboxname); oldnamespacelock = mboxname_usernamespacelock(oldmailboxname); } /* Keep temporary copy: master is trashed */ strcpy(oldmailboxname2, oldmailboxname); strcpy(newmailboxname2, newmailboxname); r = mlookup(NULL, NULL, oldmailboxname, &mbentry); if (r == IMAP_MAILBOX_NONEXISTENT) { /* Check if the base mailbox is an intermediate */ r = mboxlist_lookup_allow_all(oldmailboxname, &mbentry, 0); if (!r) { if (mbentry->mbtype & (MBTYPE_RESERVE | MBTYPE_DELETED)) { r = IMAP_MAILBOX_NONEXISTENT; } else if (!imapd_userisadmin && (mbentry->mbtype & MBTYPE_INTERMEDIATE)) { /* Make sure we can rename the first child */ r = mboxlist_allmbox(oldmailboxname, checkrenmacl, imapd_authstate, 0); if (r == IMAP_OK_COMPLETED) r = 0; } } } if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); goto done; } mbtype = mbentry->mbtype; if (!r && mbentry->mbtype & MBTYPE_REMOTE) { /* remote mailbox */ struct backend *s = NULL; int res; // don't hold the locks locally, we're proxying mboxname_release(&oldnamespacelock); mboxname_release(&newnamespacelock); s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (!s) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_SERVER_UNAVAILABLE)); goto done; } // Server or partition is going to change if (location) { char *destserver = NULL; char *destpart = NULL; c = strchr(location, '!'); if (c) { destserver = xstrndup(location, c - location); destpart = xstrdup(c + 1); } else { destpart = xstrdup(location); } if (*destpart == '\0') { free(destpart); destpart = NULL; } if (!destserver || !strcmp(destserver, mbentry->server)) { /* same server: proxy a rename */ prot_printf(s->out, "%s RENAME \"%s\" \"%s\" %s\r\n", tag, oldname, newname, location); } else { /* different server: proxy an xfer */ prot_printf(s->out, "%s XFER \"%s\" %s%s%s\r\n", tag, oldname, destserver, destpart ? " " : "", destpart ? destpart : ""); } if (destserver) free(destserver); if (destpart) free(destpart); res = pipe_until_tag(s, tag, 0); /* make sure we've seen the update */ if (ultraparanoid && res == PROXY_OK) kick_mupdate(); } else { // (location) // a simple rename, old name and new name must not be the same if (!strcmp(oldname, newname)) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(IMAP_SERVER_UNAVAILABLE)); goto done; } prot_printf(s->out, "%s RENAME \"%s\" \"%s\"\r\n", tag, oldname, newname ); res = pipe_until_tag(s, tag, 0); /* make sure we've seen the update */ if (ultraparanoid && res == PROXY_OK) kick_mupdate(); } imapd_check(s, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { /* we're allowed to reference last_result since the noop, if sent, went to a different server */ prot_printf(imapd_out, "%s %s", tag, s->last_result.s); } goto done; } /* local mailbox */ if (location && !config_partitiondir(location)) { /* invalid partition, assume its a server (remote destination) */ char *server; char *partition; /* dest partition? */ server = location; partition = strchr(location, '!'); if (partition) *partition++ = '\0'; cmd_xfer(tag, oldname, server, partition); goto done; } /* local rename: it's OK if the mailbox doesn't exist, we'll check * if sub mailboxes can be renamed */ if (r == IMAP_MAILBOX_NONEXISTENT) r = 0; if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); goto done; } /* local destination */ /* if this is my inbox, don't do recursive renames */ if (!strcasecmp(oldname, "inbox")) { /* create the destination mailbox first - if it exists, then that's a reason to error */ mbentry_t newmbentry = MBENTRY_INITIALIZER; newmbentry.name = (char *) newmailboxname; newmbentry.partition = mbentry->partition; newmbentry.mbtype = mbentry->mbtype; unsigned flags = MBOXLIST_CREATE_NOTIFY; r = mboxlist_createmailbox(&newmbentry, 0/*options*/, 0/*highestmodseq*/, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate, flags, NULL/*mailboxptr*/); if (r) goto respond; /* existing imapd index, or open one to copy from */ struct index_state *state = NULL; if (!strcmpnull(index_mboxname(imapd_index), oldmailboxname)) { state = imapd_index; } else { r = index_open(oldmailboxname, NULL, &state); } /* move all the emails to the new folder */ char *copyuid = NULL; if (!r) r = index_copy(state, "1:*", 1 /*usinguid*/, newmailboxname, ©uid, !config_getswitch(IMAPOPT_SINGLEINSTANCESTORE), &imapd_namespace, (imapd_userisadmin || imapd_userisproxyadmin), 1/*ismove*/, 1/*ignorequota*/); free(copyuid); // we don't care, but the API requires it if (state != imapd_index) index_close(&state); goto respond; } /* check if we're an admin renaming a user */ else if (config_getswitch(IMAPOPT_ALLOWUSERMOVES) && mboxname_isusermailbox(oldmailboxname, 1) && mboxname_isusermailbox(newmailboxname, 1) && strcmp(oldmailboxname, newmailboxname) && /* different user */ imapd_userisadmin) { rename_user = 1; } /* if we're renaming something inside of something else, don't recursively rename stuff */ omlen = strlen(oldmailboxname); nmlen = strlen(newmailboxname); if (omlen < nmlen) { if (!strncmp(oldmailboxname, newmailboxname, omlen) && newmailboxname[omlen] == '.') { recursive_rename = 0; } } else { if (!strncmp(oldmailboxname, newmailboxname, nmlen) && oldmailboxname[nmlen] == '.') { recursive_rename = 0; } } oldextname = mboxname_to_external(oldmailboxname, &imapd_namespace, imapd_userid); newextname = mboxname_to_external(newmailboxname, &imapd_namespace, imapd_userid); /* rename all mailboxes matching this */ if (recursive_rename && strcmp(oldmailboxname, newmailboxname)) { int ol = omlen + 1; int nl = nmlen + 1; char ombn[MAX_MAILBOX_BUFFER]; char nmbn[MAX_MAILBOX_BUFFER]; strcpy(ombn, oldmailboxname); strcpy(nmbn, newmailboxname); strcat(ombn, "."); strcat(nmbn, "."); /* setup the rock */ rock.namespace = &imapd_namespace; rock.found = 0; rock.newmailboxname = nmbn; rock.ol = ol; rock.nl = nl; rock.olduser = olduser; rock.newuser = newuser; rock.partition = location; rock.rename_user = rename_user; /* Check mboxnames to ensure we can write them all BEFORE we start */ r = mboxlist_allmbox(ombn, checkmboxname, &rock, 0); subcount = rock.found; } /* attempt to rename the base mailbox */ if (!r) { struct mboxevent *mboxevent = NULL; uint32_t uidvalidity = mbentry ? mbentry->uidvalidity : 0; /* don't send rename notification if we only change the partition */ if (strcmp(oldmailboxname, newmailboxname)) mboxevent = mboxevent_new(EVENT_MAILBOX_RENAME); /* check if a previous deleted mailbox existed */ mbentry_t *newmbentry = NULL; r = mboxlist_lookup_allow_all(newmailboxname, &newmbentry, NULL); /* XXX - otherwise we should probably reject now, but meh, save it for * a real cleanup */ if (!r && (newmbentry->mbtype & MBTYPE_DELETED)) { /* changing the unique id since last time? */ if (!mbentry || strcmpsafe(mbentry->uniqueid, newmbentry->uniqueid)) { /* then the UIDVALIDITY must be higher than before */ if (uidvalidity <= newmbentry->uidvalidity) uidvalidity = newmbentry->uidvalidity+1; } } mboxlist_entry_free(&newmbentry); r = mboxlist_renamemailbox(mbentry, newmailboxname, location ? location : mbentry->partition, 0 /* uidvalidity */, imapd_userisadmin, imapd_userid, imapd_authstate, mboxevent, 0, 0, rename_user, /*keep_intermediaries*/1, 0, 0); /* it's OK to not exist if there are subfolders */ if (r == IMAP_MAILBOX_NONEXISTENT && subcount && !rename_user && mboxname_userownsmailbox(imapd_userid, oldmailboxname) && mboxname_userownsmailbox(imapd_userid, newmailboxname)) { mboxevent_free(&mboxevent); goto submboxes; } /* send a MailboxRename event notification if enabled */ if (!r) mboxevent_notify(&mboxevent); mboxevent_free(&mboxevent); if (!r && config_getswitch(IMAPOPT_DELETE_UNSUBSCRIBE)) { mboxlist_changesub(oldmailboxname, imapd_userid, imapd_authstate, /* add */ 0, /* force */ 0, /* notify? */ 1); } } /* If we're renaming a user, take care of changing quotaroot and ACL */ if (!r && rename_user) { user_copyquotaroot(oldmailboxname, newmailboxname); user_renameacl(&imapd_namespace, newmailboxname, olduser, newuser); if (mbtype & MBTYPE_LEGACY_DIRS) { /* Change seen state, subscriptions and sieve scripts */ user_renamedata(olduser, newuser); } /* XXX report status/progress of meta-data */ } /* rename all mailboxes matching this */ if (!r && recursive_rename) { prot_printf(imapd_out, "* OK rename %s %s\r\n", oldextname, newextname); prot_flush(imapd_out); submboxes: strcat(oldmailboxname, "."); strcat(newmailboxname, "."); /* setup the rock */ rock.namespace = &imapd_namespace; rock.newmailboxname = newmailboxname; rock.ol = omlen + 1; rock.nl = nmlen + 1; rock.olduser = olduser; rock.newuser = 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_allmbox(oldmailboxname, renmbox, &rock, MBOXTREE_INTERMEDIATES); } /* take care of deleting old quotas */ if (!r && rename_user) { user_deletequotaroots(olduser); sync_log_unuser(olduser); } /* take care of intermediaries */ mboxlist_update_intermediaries(oldmailboxname, mbtype, 0); mboxlist_update_intermediaries(newmailboxname, mbtype, 0); respond: imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { if (config_mupdate_server) kick_mupdate(); if (rename_user) sync_log_user(newuser); index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } done: mboxname_release(&oldnamespacelock); mboxname_release(&newnamespacelock); mboxlist_entry_free(&mbentry); free(oldextname); free(newextname); free(olduser); free(newuser); } /* * Perform a RECONSTRUCT command */ static void cmd_reconstruct(const char *tag, const char *name, int recursive) { int r = 0; char quotaroot[MAX_MAILBOX_BUFFER]; mbentry_t *mbentry = NULL; struct mailbox *mailbox = NULL; /* administrators only please */ if (!imapd_userisadmin) r = IMAP_PERMISSION_DENIED; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); if (!r && !strcmpsafe(intname, index_mboxname(imapd_index))) r = IMAP_MAILBOX_LOCKED; if (!r) { r = mlookup(tag, name, intname, &mbentry); } if (r == IMAP_MAILBOX_MOVED) { free(intname); return; } if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) { /* remote mailbox */ imapd_refer(tag, mbentry->server, name); mboxlist_entry_free(&mbentry); free(intname); return; } mboxlist_entry_free(&mbentry); /* local mailbox */ if (!r) { int pid; /* Reconstruct it */ pid = fork(); if (pid == -1) { r = IMAP_SYS_ERROR; } else if (pid == 0) { char buf[4096]; int ret; /* Child - exec reconstruct*/ syslog(LOG_NOTICE, "Reconstructing '%s' (%s) for user '%s'", intname, recursive ? "recursive" : "not recursive", imapd_userid); fclose(stdin); fclose(stdout); fclose(stderr); ret = snprintf(buf, sizeof(buf), "%s/reconstruct", SBIN_DIR); if(ret < 0 || ret >= (int) sizeof(buf)) { /* in child, so fatailing won't disconnect our user */ fatal("reconstruct buffer not sufficiently big", EX_CONFIG); } if(recursive) { execl(buf, buf, "-C", config_filename, "-r", "-f", intname, NULL); } else { execl(buf, buf, "-C", config_filename, intname, NULL); } /* if we are here, we have a problem */ exit(-1); } else { int status; /* Parent, wait on child */ if(waitpid(pid, &status, 0) < 0) r = IMAP_SYS_ERROR; /* Did we fail? */ if(WEXITSTATUS(status) != 0) r = IMAP_SYS_ERROR; } } /* Still in parent, need to re-quota the mailbox*/ /* Find its quota root */ if (!r) r = mailbox_open_irl(intname, &mailbox); if(!r) { if (mailbox_quotaroot(mailbox)) { strcpy(quotaroot, mailbox_quotaroot(mailbox)); } else { strcpy(quotaroot, intname); } mailbox_close(&mailbox); } /* Run quota -f */ if (!r) { int pid; pid = fork(); if(pid == -1) { r = IMAP_SYS_ERROR; } else if(pid == 0) { char buf[4096]; int ret; /* Child - exec reconstruct*/ syslog(LOG_NOTICE, "Regenerating quota roots starting with '%s' for user '%s'", intname, imapd_userid); fclose(stdin); fclose(stdout); fclose(stderr); ret = snprintf(buf, sizeof(buf), "%s/quota", SBIN_DIR); if(ret < 0 || ret >= (int) sizeof(buf)) { /* in child, so fatailing won't disconnect our user */ fatal("quota buffer not sufficiently big", EX_CONFIG); } execl(buf, buf, "-C", config_filename, "-f", quotaroot, NULL); /* if we are here, we have a problem */ exit(-1); } else { int status; /* Parent, wait on child */ if(waitpid(pid, &status, 0) < 0) r = IMAP_SYS_ERROR; /* Did we fail? */ if(WEXITSTATUS(status) != 0) r = IMAP_SYS_ERROR; } } if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } free(intname); } /* number of times the callbacks for findall/findsub have been called */ static int list_callback_calls; /* * Parse LIST command arguments. */ static void getlistargs(char *tag, struct listargs *listargs) { static struct buf reference, buf; int c; /* Check for and parse LIST-EXTENDED selection options */ c = prot_getc(imapd_in); if (c == '(') { listargs->cmd = LIST_CMD_EXTENDED; c = getlistselopts(tag, listargs); if (c <= EOF) { return; } } else prot_ungetc(c, imapd_in); if (!strcmpsafe(imapd_magicplus, "+")) listargs->sel |= LIST_SEL_SUBSCRIBED; else if (!strcasecmpsafe(imapd_magicplus, "+dav")) listargs->sel |= LIST_SEL_DAV; /* Read in reference name */ c = getastring(imapd_in, imapd_out, &reference); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF && !*reference.s) { prot_printf(imapd_out, "%s BAD Missing required argument to List: reference name\r\n", tag); eatline(imapd_in, c); return; } listargs->ref = reference.s; if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing required argument to List: mailbox pattern\r\n", tag); eatline(imapd_in, c); return; } /* Read in mailbox pattern(s) */ c = prot_getc(imapd_in); if (c == '(') { listargs->cmd = LIST_CMD_EXTENDED; for (;;) { c = getastring(imapd_in, imapd_out, &buf); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (*buf.s) strarray_append(&listargs->pat, buf.s); if (c != ' ') break; } if (c != ')') { prot_printf(imapd_out, "%s BAD Invalid syntax in List command\r\n", tag); eatline(imapd_in, c); goto freeargs; } c = prot_getc(imapd_in); } else { prot_ungetc(c, imapd_in); c = getastring(imapd_in, imapd_out, &buf); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing required argument to List: mailbox pattern\r\n", tag); eatline(imapd_in, c); goto freeargs; } strarray_append(&listargs->pat, buf.s); } /* Check for and parse LIST-EXTENDED return options */ if (c == ' ') { listargs->cmd = LIST_CMD_EXTENDED; c = getlistretopts(tag, listargs); if (c <= EOF) { goto freeargs; } } /* check for CRLF */ if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to List\r\n", tag); eatline(imapd_in, c); goto freeargs; } #ifdef USE_AUTOCREATE autocreate_inbox(); #endif // USE_AUTOCREATE return; maxliteral: prot_printf(imapd_out, "%s NO %s in List\r\n", tag, error_message(IMAP_LITERAL_TOO_LARGE)); freeargs: strarray_fini(&listargs->pat); strarray_fini(&listargs->metaitems); return; } /* * Perform a LIST, LSUB, RLIST or RLSUB command */ static void cmd_list(char *tag, struct listargs *listargs) { clock_t start = clock(); char mytime[100]; if (listargs->sel & LIST_SEL_REMOTE) { if (!config_getswitch(IMAPOPT_PROXYD_DISABLE_MAILBOX_REFERRALS)) { supports_referrals = !disable_referrals; } } list_callback_calls = 0; if (listargs->pat.count && !*(listargs->pat.data[0]) && (listargs->cmd == LIST_CMD_LIST)) { /* special case: query top-level hierarchy separator */ prot_printf(imapd_out, "* LIST (\\Noselect) \"%c\" \"\"\r\n", imapd_namespace.hier_sep); } else if (listargs->pat.count && !*(listargs->pat.data[0]) && (listargs->cmd == LIST_CMD_XLIST)) { /* special case: query top-level hierarchy separator */ prot_printf(imapd_out, "* XLIST (\\Noselect) \"%c\" \"\"\r\n", imapd_namespace.hier_sep); } else if ((listargs->cmd == LIST_CMD_LSUB) && (backend_inbox || (backend_inbox = proxy_findinboxserver(imapd_userid)))) { /* remote inbox */ if (list_data_remote(backend_inbox, tag, listargs, NULL)) return; } else { list_data(listargs); } if (global_conversations) { conversations_abort(&global_conversations); global_conversations = NULL; } strarray_fini(&listargs->pat); strarray_fini(&listargs->metaitems); imapd_check((listargs->sel & LIST_SEL_SUBSCRIBED) ? NULL : backend_inbox, 0); snprintf(mytime, sizeof(mytime), "%2.3f", (clock() - start) / (double) CLOCKS_PER_SEC); if ((listargs->sel & LIST_SEL_METADATA) && listargs->metaopts.maxsize && listargs->metaopts.biggest > listargs->metaopts.maxsize) { prot_printf(imapd_out, "%s OK [METADATA LONGENTRIES %u] %s\r\n", tag, (unsigned)listargs->metaopts.biggest, error_message(IMAP_OK_COMPLETED)); } else { prot_printf(imapd_out, "%s OK %s (%s secs", tag, error_message(IMAP_OK_COMPLETED), mytime); if (list_callback_calls) prot_printf(imapd_out, " %u calls", list_callback_calls); prot_printf(imapd_out, ")\r\n"); } } /* * Perform a SUBSCRIBE (add is nonzero) or * UNSUBSCRIBE (add is zero) command */ static void cmd_changesub(char *tag, char *namespace, char *name, int add) { const char *cmd = add ? "Subscribe" : "Unsubscribe"; int r = 0; int force = config_getswitch(IMAPOPT_ALLOWALLSUBSCRIBE); if (backend_inbox || (backend_inbox = proxy_findinboxserver(imapd_userid))) { /* remote INBOX */ if (add) { char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(NULL, NULL, intname, NULL); free(intname); /* Doesn't exist on murder */ } imapd_check(backend_inbox, 0); if (!r) { if (namespace) { prot_printf(backend_inbox->out, "%s %s {" SIZE_T_FMT "+}\r\n%s" " {" SIZE_T_FMT "+}\r\n%s\r\n", tag, cmd, strlen(namespace), namespace, strlen(name), name); } else { prot_printf(backend_inbox->out, "%s %s {" SIZE_T_FMT "+}\r\n%s\r\n", tag, cmd, strlen(name), name); } pipe_including_tag(backend_inbox, tag, 0); } else { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } return; } /* local INBOX */ if (namespace) lcase(namespace); if (!namespace || !strcmp(namespace, "mailbox")) { size_t len = strlen(name); if (force && imapd_namespace.isalt && (((len == strlen(imapd_namespace.prefix[NAMESPACE_USER]) - 1) && !strncmp(name, imapd_namespace.prefix[NAMESPACE_USER], len)) || ((len == strlen(imapd_namespace.prefix[NAMESPACE_SHARED]) - 1) && !strncmp(name, imapd_namespace.prefix[NAMESPACE_SHARED], len)))) { r = 0; } else { char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mboxlist_changesub(intname, imapd_userid, imapd_authstate, add, force, 1); free(intname); } } else if (!strcmp(namespace, "bboard")) { r = add ? IMAP_MAILBOX_NONEXISTENT : 0; } else { prot_printf(imapd_out, "%s BAD Invalid %s subcommand\r\n", tag, cmd); return; } imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s: %s\r\n", tag, cmd, error_message(r)); } else { index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } } /* * Perform a GETACL command */ static void cmd_getacl(const char *tag, const char *name) { int r, access; char *acl; char *rights, *nextid; char *freeme = NULL; mbentry_t *mbentry = NULL; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(tag, name, intname, &mbentry); if (r == IMAP_MAILBOX_MOVED) return; if (!r) { access = cyrus_acl_myrights(imapd_authstate, mbentry->acl); if (!(access & ACL_ADMIN) && !imapd_userisadmin && !mboxname_userownsmailbox(imapd_userid, intname)) { r = (access & ACL_LOOKUP) ? IMAP_PERMISSION_DENIED : IMAP_MAILBOX_NONEXISTENT; } } imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); mboxlist_entry_free(&mbentry); free(intname); return; } prot_printf(imapd_out, "* ACL "); prot_printastring(imapd_out, name); freeme = acl = xstrdupnull(mbentry->acl); while (acl) { rights = strchr(acl, '\t'); if (!rights) break; *rights++ = '\0'; nextid = strchr(rights, '\t'); if (!nextid) break; *nextid++ = '\0'; prot_printf(imapd_out, " "); prot_printastring(imapd_out, acl); prot_printf(imapd_out, " "); prot_printastring(imapd_out, rights); acl = nextid; } prot_printf(imapd_out, "\r\n"); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); free(freeme); mboxlist_entry_free(&mbentry); free(intname); } /* * Perform a LISTRIGHTS command */ static void cmd_listrights(char *tag, char *name, char *identifier) { int r, rights; mbentry_t *mbentry = NULL; struct auth_state *authstate; const char *canon_identifier; int implicit; char rightsdesc[100], optional[33]; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(tag, name, intname, &mbentry); if (r == IMAP_MAILBOX_MOVED) return; if (!r) { rights = cyrus_acl_myrights(imapd_authstate, mbentry->acl); if (!rights && !imapd_userisadmin && !mboxname_userownsmailbox(imapd_userid, intname)) { r = IMAP_MAILBOX_NONEXISTENT; } } mboxlist_entry_free(&mbentry); imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); return; } authstate = auth_newstate(identifier); if (global_authisa(authstate, IMAPOPT_ADMINS)) canon_identifier = identifier; /* don't canonify global admins */ else canon_identifier = canonify_userid(identifier, imapd_userid, NULL); auth_freestate(authstate); if (!canon_identifier) { implicit = 0; } else if (mboxname_userownsmailbox(canon_identifier, intname)) { /* identifier's personal mailbox */ implicit = config_implicitrights; } else if (mboxname_isusermailbox(intname, 1)) { /* anyone can post to an INBOX */ implicit = ACL_POST; } else { implicit = 0; } /* calculate optional rights */ cyrus_acl_masktostr(implicit ^ (canon_identifier ? ACL_FULL : 0), optional); /* build the rights string */ if (implicit) { cyrus_acl_masktostr(implicit, rightsdesc); } else { strcpy(rightsdesc, "\"\""); } if (*optional) { int i, n = strlen(optional); char *p = rightsdesc + strlen(rightsdesc); for (i = 0; i < n; i++) { *p++ = ' '; *p++ = optional[i]; } *p = '\0'; } prot_printf(imapd_out, "* LISTRIGHTS "); prot_printastring(imapd_out, name); (void)prot_putc(' ', imapd_out); prot_printastring(imapd_out, identifier); prot_printf(imapd_out, " %s", rightsdesc); prot_printf(imapd_out, "\r\n%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); free(intname); } static int printmyrights(const char *extname, const mbentry_t *mbentry) { int rights = 0; char str[ACL_MAXSTR]; rights = cyrus_acl_myrights(imapd_authstate, mbentry->acl); /* Add in implicit rights */ if (imapd_userisadmin) { rights |= ACL_LOOKUP|ACL_ADMIN; } else if (mboxname_userownsmailbox(imapd_userid, mbentry->name)) { rights |= config_implicitrights; } if (!(rights & (ACL_LOOKUP|ACL_READ|ACL_INSERT|ACL_CREATE|ACL_DELETEMBOX|ACL_ADMIN))) { return IMAP_MAILBOX_NONEXISTENT; } prot_printf(imapd_out, "* MYRIGHTS "); prot_printastring(imapd_out, extname); prot_printf(imapd_out, " "); prot_printastring(imapd_out, cyrus_acl_masktostr(rights, str)); prot_printf(imapd_out, "\r\n"); return 0; } /* * Perform a MYRIGHTS command */ static void cmd_myrights(const char *tag, const char *name) { mbentry_t *mbentry = NULL; int r; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(tag, name, intname, &mbentry); free(intname); if (r == IMAP_MAILBOX_MOVED) return; if (!r) r = printmyrights(name, mbentry); mboxlist_entry_free(&mbentry); imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); return; } prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } /* * Perform a SETACL command */ static void cmd_setacl(char *tag, const char *name, const char *identifier, const char *rights) { int r; mbentry_t *mbentry = NULL; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); struct mboxlock *namespacelock = mboxname_usernamespacelock(intname); /* is it remote? */ r = mlookup(tag, name, intname, &mbentry); if (r == IMAP_MAILBOX_MOVED) goto done; if (!config_getswitch(IMAPOPT_ALLOWSETACL)) r = IMAP_DISABLED; if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) { /* remote mailbox */ struct backend *s = NULL; int res; // don't hold the lock locally, we're calling remote mboxname_release(&namespacelock); s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (!s) r = IMAP_SERVER_UNAVAILABLE; if (!r && imapd_userisadmin && supports_referrals) { /* They aren't an admin remotely, so let's refer them */ imapd_refer(tag, mbentry->server, name); referral_kick = 1; goto done; } if (!r) { if (rights) { prot_printf(s->out, "%s Setacl {" SIZE_T_FMT "+}\r\n%s" " {" SIZE_T_FMT "+}\r\n%s {" SIZE_T_FMT "+}\r\n%s\r\n", tag, strlen(name), name, strlen(identifier), identifier, strlen(rights), rights); } else { prot_printf(s->out, "%s Deleteacl {" SIZE_T_FMT "+}\r\n%s" " {" SIZE_T_FMT "+}\r\n%s\r\n", tag, strlen(name), name, strlen(identifier), identifier); } res = pipe_until_tag(s, tag, 0); if (!CAPA(s, CAPA_MUPDATE) && res == PROXY_OK) { /* setup new ACL in MUPDATE */ } /* make sure we've seen the update */ if (ultraparanoid && res == PROXY_OK) kick_mupdate(); } imapd_check(s, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { /* we're allowed to reference last_result since the noop, if sent, went to a different server */ prot_printf(imapd_out, "%s %s", tag, s->last_result.s); } goto done; } /* local mailbox */ if (!r) { char *err; /* send BAD response if rights string contains unrecognised chars */ if (rights && *rights) { r = cyrus_acl_checkstr(rights, &err); if (r) { prot_printf(imapd_out, "%s BAD %s\r\n", tag, err); free(err); goto done; } } r = mboxlist_setacl(&imapd_namespace, intname, identifier, rights, imapd_userisadmin || imapd_userisproxyadmin, proxy_userid, imapd_authstate); } imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { if (config_mupdate_server) kick_mupdate(); index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } done: mboxname_release(&namespacelock); free(intname); mboxlist_entry_free(&mbentry); } static void print_quota_used(struct protstream *o, const struct quota *q) { int res; const char *sep = ""; prot_putc('(', o); for (res = 0 ; res < QUOTA_NUMRESOURCES ; res++) { if (q->limits[res] >= 0) { prot_printf(o, "%s%s " QUOTA_T_FMT " " QUOTA_T_FMT, sep, quota_names[res], q->useds[res]/quota_units[res], q->limits[res]); sep = " "; } } prot_putc(')', o); } static void print_quota_limits(struct protstream *o, const struct quota *q) { int res; const char *sep = ""; prot_putc('(', o); for (res = 0 ; res < QUOTA_NUMRESOURCES ; res++) { if (q->limits[res] >= 0) { prot_printf(o, "%s%s " QUOTA_T_FMT, sep, quota_names[res], q->limits[res]); sep = " "; } } prot_putc(')', o); } /* * Callback for (get|set)quota, to ensure that all of the * submailboxes are on the same server. */ static int quota_cb(const mbentry_t *mbentry, void *rock) { const char *servername = (const char *)rock; int r; if (strcmp(servername, mbentry->server)) { /* Not on same server as the root */ r = IMAP_NOT_SINGULAR_ROOT; } else { r = PROXY_OK; } return r; } /* * Perform a GETQUOTA command */ static void cmd_getquota(const char *tag, const char *name) { int r; char quotarootbuf[MAX_MAILBOX_BUFFER]; mbentry_t *mbentry = NULL; struct quota q; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); quota_init(&q, intname); imapd_check(NULL, 0); if (!imapd_userisadmin && !imapd_userisproxyadmin) { r = IMAP_PERMISSION_DENIED; goto done; } r = mlookup(NULL, NULL, intname, &mbentry); if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) { /* remote mailbox */ snprintf(quotarootbuf, sizeof(quotarootbuf), "%s.", intname); r = mboxlist_allmbox(quotarootbuf, quota_cb, (void *)mbentry->server, 0); if (r) goto done; struct backend *s; s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (!s) { r = IMAP_SERVER_UNAVAILABLE; goto done; } imapd_check(s, 0); prot_printf(s->out, "%s Getquota {" SIZE_T_FMT "+}\r\n%s\r\n", tag, strlen(name), name); pipe_including_tag(s, tag, 0); goto done; } /* local mailbox */ r = quota_read_withconversations(&q); if (r) goto done; prot_printf(imapd_out, "* QUOTA "); prot_printastring(imapd_out, name); prot_printf(imapd_out, " "); print_quota_used(imapd_out, &q); prot_printf(imapd_out, "\r\n"); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); done: if (r) prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); mboxlist_entry_free(&mbentry); quota_free(&q); free(intname); } /* * Perform a GETQUOTAROOT command */ static void cmd_getquotaroot(const char *tag, const char *name) { mbentry_t *mbentry = NULL; struct mailbox *mailbox = NULL; int myrights = 0; int r, doclose = 0; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(tag, name, intname, &mbentry); if (r == IMAP_MAILBOX_MOVED) { free(intname); return; } if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) { /* remote mailbox */ struct backend *s; s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (!s) r = IMAP_SERVER_UNAVAILABLE; imapd_check(s, 0); if (!r) { prot_printf(s->out, "%s Getquotaroot {" SIZE_T_FMT "+}\r\n%s\r\n", tag, strlen(name), name); pipe_including_tag(s, tag, 0); } else { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } mboxlist_entry_free(&mbentry); free(intname); return; } mboxlist_entry_free(&mbentry); /* local mailbox */ if (!r) { r = mailbox_open_irl(intname, &mailbox); if (!r) { doclose = 1; myrights = cyrus_acl_myrights(imapd_authstate, mailbox_acl(mailbox)); } } if (!r) { if (!imapd_userisadmin && !(myrights & ACL_READ)) { r = (myrights & ACL_LOOKUP) ? IMAP_PERMISSION_DENIED : IMAP_MAILBOX_NONEXISTENT; } } if (!r) { prot_printf(imapd_out, "* QUOTAROOT "); prot_printastring(imapd_out, name); const char *quotaroot = mailbox_quotaroot(mailbox); if (quotaroot) { struct quota q; char *extname = mboxname_to_external(quotaroot, &imapd_namespace, imapd_userid); prot_printf(imapd_out, " "); prot_printastring(imapd_out, extname); quota_init(&q, quotaroot); r = quota_read_withconversations(&q); if (!r) { prot_printf(imapd_out, "\r\n* QUOTA "); prot_printastring(imapd_out, extname); prot_putc(' ', imapd_out); print_quota_used(imapd_out, &q); } quota_free(&q); free(extname); } prot_printf(imapd_out, "\r\n"); } if (doclose) mailbox_close(&mailbox); free(intname); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); return; } imapd_check(NULL, 0); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } /* * Parse and perform a SETQUOTA command * The command has been parsed up to the resource list */ void cmd_setquota(const char *tag, const char *quotaroot) { quota_t newquotas[QUOTA_NUMRESOURCES]; int res; int c; int force = 0; static struct buf arg; int r; mbentry_t *mbentry = NULL; char *intname = NULL; if (!imapd_userisadmin && !imapd_userisproxyadmin) { /* need to allow proxies so that mailbox moves can set initial quota * roots */ r = IMAP_PERMISSION_DENIED; goto out; } /* are we forcing the creation of a quotaroot by having a leading +? */ if (quotaroot[0] == '+') { force = 1; quotaroot++; } intname = mboxname_from_external(quotaroot, &imapd_namespace, imapd_userid); r = mlookup(NULL, NULL, intname, &mbentry); if (r == IMAP_MAILBOX_NONEXISTENT) r = 0; /* will create a quotaroot anyway */ if (r) goto out; if (mbentry && (mbentry->mbtype & MBTYPE_REMOTE)) { /* remote mailbox */ struct backend *s; char quotarootbuf[MAX_MAILBOX_BUFFER]; snprintf(quotarootbuf, sizeof(quotarootbuf), "%s.", intname); r = mboxlist_allmbox(quotarootbuf, quota_cb, (void *)mbentry->server, 0); if (r) goto out; imapd_check(NULL, 0); s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (!s) { r = IMAP_SERVER_UNAVAILABLE; goto out; } imapd_check(s, 0); prot_printf(s->out, "%s Setquota ", tag); prot_printstring(s->out, quotaroot); prot_putc(' ', s->out); pipe_command(s, 0); pipe_including_tag(s, tag, 0); free(intname); return; } mboxlist_entry_free(&mbentry); /* local mailbox */ /* Now parse the arguments as a setquota_list */ c = prot_getc(imapd_in); if (c != '(') goto badlist; for (res = 0 ; res < QUOTA_NUMRESOURCES ; res++) newquotas[res] = QUOTA_UNLIMITED; for (;;) { /* XXX - limit is actually stored in an int value */ int32_t limit = 0; c = getword(imapd_in, &arg); if ((c == ')') && !arg.s[0]) break; if (c != ' ') goto badlist; res = quota_name_to_resource(arg.s); if (res < 0) { r = IMAP_UNSUPPORTED_QUOTA; goto out; } c = getsint32(imapd_in, &limit); /* note: we accept >= 0 according to RFC 2087, * and also -1 to fix Bug #3559 */ if (limit < -1) goto badlist; newquotas[res] = limit; if (c == ')') break; else if (c != ' ') goto badlist; if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); } c = prot_getc(imapd_in); if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to SETQUOTA\r\n", tag); eatline(imapd_in, c); return; } r = mboxlist_setquotas(intname, newquotas, 0, force); imapd_check(NULL, 0); out: mboxlist_entry_free(&mbentry); free(intname); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); return; } index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); return; badlist: prot_printf(imapd_out, "%s BAD Invalid quota list in Setquota\r\n", tag); eatline(imapd_in, c); free(intname); } #ifdef HAVE_SSL /* * this implements the STARTTLS command, as described in RFC 2595. * one caveat: it assumes that no external layer is currently present. * if a client executes this command, information about the external * layer that was passed on the command line is discarded. this should * be fixed. */ /* imaps - whether this is an imaps transaction or not */ static void cmd_starttls(char *tag, int imaps) { int result; char *localip, *remoteip; if (imapd_starttls_done == 1) { prot_printf(imapd_out, "%s NO TLS already active\r\n", tag); return; } result=tls_init_serverengine("imap", 5, /* depth to verify */ !imaps, /* can client auth? */ NULL); if (result == -1) { syslog(LOG_ERR, "error initializing TLS"); if (imaps == 0) { prot_printf(imapd_out, "%s NO Error initializing TLS\r\n", tag); } else { shut_down(0); } return; } if (imaps == 0) { prot_printf(imapd_out, "%s OK Begin TLS negotiation now\r\n", tag); /* must flush our buffers before starting tls */ prot_flush(imapd_out); } /* tls_start_servertls is going to reset saslprops, discarding the * iplocalport and ipremoteport fields. Preserve them, then put them back * after the call. */ localip = buf_release(&saslprops.iplocalport); remoteip = buf_release(&saslprops.ipremoteport); result=tls_start_servertls(0, /* read */ 1, /* write */ imaps ? 180 : imapd_timeout, &saslprops, &tls_conn); /* put the iplocalport and ipremoteport back */ if (localip) buf_initm(&saslprops.iplocalport, localip, strlen(localip)); if (remoteip) buf_initm(&saslprops.ipremoteport, remoteip, strlen(remoteip)); /* if error */ if (result==-1) { if (imaps == 0) { prot_printf(imapd_out, "%s NO Starttls negotiation failed\r\n", tag); syslog(LOG_NOTICE, "STARTTLS negotiation failed: %s", imapd_clienthost); return; } else { syslog(LOG_NOTICE, "imaps TLS negotiation failed: %s", imapd_clienthost); shut_down(0); } } /* tell SASL about the negotiated layer */ result = saslprops_set_tls(&saslprops, imapd_saslconn); if (result != SASL_OK) { syslog(LOG_NOTICE, "saslprops_set_tls() failed: cmd_starttls()"); if (imaps == 0) { fatal("saslprops_set_tls() failed: cmd_starttls()", EX_TEMPFAIL); } else { shut_down(0); } } /* tell the prot layer about our new layers */ prot_settls(imapd_in, tls_conn); prot_settls(imapd_out, tls_conn); imapd_starttls_done = 1; imapd_tls_required = 0; #if (OPENSSL_VERSION_NUMBER >= 0x0090800fL) imapd_tls_comp = (void *) SSL_get_current_compression(tls_conn); #endif } #else void cmd_starttls(char *tag __attribute__((unused)), int imaps __attribute__((unused))) { fatal("cmd_starttls() executed, but starttls isn't implemented!", EX_SOFTWARE); } #endif // (OPENSSL_VERSION_NUMBER >= 0x0090800fL) static int parse_statusitems(unsigned *statusitemsp, const char **errstr) { static struct buf arg; unsigned statusitems = 0; int c; int hasconv = config_getswitch(IMAPOPT_CONVERSATIONS); c = prot_getc(imapd_in); if (c != '(') goto bad; c = getword(imapd_in, &arg); if (arg.s[0] == '\0') goto bad; for (;;) { if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); lcase(arg.s); if (!strcmp(arg.s, "messages")) { statusitems |= STATUS_MESSAGES; } else if (!strcmp(arg.s, "recent")) { statusitems |= STATUS_RECENT; } else if (!strcmp(arg.s, "uidnext")) { statusitems |= STATUS_UIDNEXT; } else if (!strcmp(arg.s, "uidvalidity")) { statusitems |= STATUS_UIDVALIDITY; } else if (!strcmp(arg.s, "mailboxid")) { statusitems |= STATUS_MAILBOXID; } else if (!strcmp(arg.s, "unseen")) { statusitems |= STATUS_UNSEEN; } else if (!strcmp(arg.s, "size")) { statusitems |= STATUS_SIZE; } else if (!strcmp(arg.s, "highestmodseq")) { statusitems |= STATUS_HIGHESTMODSEQ; } else if (!strcmp(arg.s, "createdmodseq")) { statusitems |= STATUS_CREATEDMODSEQ; } else if (hasconv && !strcmp(arg.s, "xconvexists")) { statusitems |= STATUS_XCONVEXISTS; } else if (hasconv && !strcmp(arg.s, "xconvunseen")) { statusitems |= STATUS_XCONVUNSEEN; } else if (hasconv && !strcmp(arg.s, "xconvmodseq")) { statusitems |= STATUS_XCONVMODSEQ; } else { static char buf[200]; snprintf(buf, 200, "Invalid Status attributes %s", arg.s); *errstr = buf; goto bad; } if (c == ' ') c = getword(imapd_in, &arg); else break; } if (c != ')') { *errstr = "Missing close parenthesis in Status"; goto bad; } c = prot_getc(imapd_in); /* success */ *statusitemsp = statusitems; return c; bad: if (c != EOF) prot_ungetc(c, imapd_in); return EOF; } static int print_statusline(const char *extname, unsigned statusitems, struct statusdata *sd) { int sepchar; prot_printf(imapd_out, "* STATUS "); prot_printastring(imapd_out, extname); prot_printf(imapd_out, " "); sepchar = '('; if (statusitems & STATUS_MESSAGES) { prot_printf(imapd_out, "%cMESSAGES %u", sepchar, sd->messages); sepchar = ' '; } if (statusitems & STATUS_RECENT) { prot_printf(imapd_out, "%cRECENT %u", sepchar, sd->recent); sepchar = ' '; } if (statusitems & STATUS_UIDNEXT) { prot_printf(imapd_out, "%cUIDNEXT %u", sepchar, sd->uidnext); sepchar = ' '; } if (statusitems & STATUS_UIDVALIDITY) { prot_printf(imapd_out, "%cUIDVALIDITY %u", sepchar, sd->uidvalidity); sepchar = ' '; } if (statusitems & STATUS_MAILBOXID) { prot_printf(imapd_out, "%cMAILBOXID (%s)", sepchar, sd->mailboxid); sepchar = ' '; } if (statusitems & STATUS_UNSEEN) { prot_printf(imapd_out, "%cUNSEEN %u", sepchar, sd->unseen); sepchar = ' '; } if (statusitems & STATUS_SIZE) { prot_printf(imapd_out, "%cSIZE %llu", sepchar, sd->size); sepchar = ' '; } if (statusitems & STATUS_CREATEDMODSEQ) { prot_printf(imapd_out, "%cCREATEDMODSEQ " MODSEQ_FMT, sepchar, sd->createdmodseq); sepchar = ' '; } if (statusitems & STATUS_HIGHESTMODSEQ) { prot_printf(imapd_out, "%cHIGHESTMODSEQ " MODSEQ_FMT, sepchar, sd->highestmodseq); sepchar = ' '; } if (statusitems & STATUS_XCONVEXISTS) { prot_printf(imapd_out, "%cXCONVEXISTS %u", sepchar, sd->xconv.threadexists); sepchar = ' '; } if (statusitems & STATUS_XCONVUNSEEN) { prot_printf(imapd_out, "%cXCONVUNSEEN %u", sepchar, sd->xconv.threadunseen); sepchar = ' '; } if (statusitems & STATUS_XCONVMODSEQ) { prot_printf(imapd_out, "%cXCONVMODSEQ " MODSEQ_FMT, sepchar, sd->xconv.threadmodseq); sepchar = ' '; } prot_printf(imapd_out, ")\r\n"); return 0; } static int imapd_statusdata(const mbentry_t *mbentry, unsigned statusitems, struct statusdata *sd) { int r; struct conversations_state *state = NULL; if (!(statusitems & STATUS_CONVITEMS)) goto nonconv; statusitems &= ~STATUS_CONVITEMS; /* strip them for the regular lookup */ /* use the existing state if possible */ state = conversations_get_mbox(mbentry->name); /* otherwise fetch a new one! */ if (!state) { if (global_conversations) { conversations_abort(&global_conversations); global_conversations = NULL; } // we never write anything in STATUS calls, so shared is fine r = conversations_open_mbox(mbentry->name, 1/*shared*/, &state); if (r) { /* maybe the mailbox doesn't even have conversations - just ignore */ goto nonconv; } global_conversations = state; } r = conversation_getstatus(state, CONV_FOLDER_KEY_MBE(state, mbentry), &sd->xconv); if (r) return r; nonconv: /* use the index status if we can so we get the 'alive' Recent count */ if (!strcmpsafe(mbentry->name, index_mboxname(imapd_index)) && imapd_index->mailbox) return index_status(imapd_index, sd); /* fall back to generic lookup */ return status_lookup_mbentry(mbentry, imapd_userid, statusitems, sd); } /* * Parse and perform a STATUS command * The command has been parsed up to the attribute list */ static void cmd_status(char *tag, char *name) { int c; unsigned statusitems = 0; const char *errstr = "Bad status string"; mbentry_t *mbentry = NULL; struct statusdata sdata = STATUSDATA_INIT; int r = 0; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(tag, name, intname, &mbentry); if (r == IMAP_MAILBOX_MOVED) { /* Eat the argument */ eatline(imapd_in, prot_getc(imapd_in)); free(intname); return; } if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) { /* remote mailbox */ if (supports_referrals && config_getswitch(IMAPOPT_PROXYD_ALLOW_STATUS_REFERRAL)) { imapd_refer(tag, mbentry->server, name); /* Eat the argument */ eatline(imapd_in, prot_getc(imapd_in)); } else { struct backend *s; s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (!s) r = IMAP_SERVER_UNAVAILABLE; imapd_check(s, 1); if (!r) { prot_printf(s->out, "%s Status {" SIZE_T_FMT "+}\r\n%s ", tag, strlen(name), name); if (!pipe_command(s, 65536)) { pipe_including_tag(s, tag, 0); } } else { eatline(imapd_in, prot_getc(imapd_in)); prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } } goto done; } /* local mailbox */ c = parse_statusitems(&statusitems, &errstr); if (c == EOF) { prot_printf(imapd_out, "%s BAD %s\r\n", tag, errstr); eatline(imapd_in, c); goto done; } if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Status\r\n", tag); eatline(imapd_in, c); goto done; } /* check permissions */ if (!r) { int myrights = cyrus_acl_myrights(imapd_authstate, mbentry->acl); if (!(myrights & ACL_READ)) { r = (imapd_userisadmin || (myrights & ACL_LOOKUP)) ? IMAP_PERMISSION_DENIED : IMAP_MAILBOX_NONEXISTENT; } } // status of selected mailbox, we need to refresh if (!r && !strcmpsafe(mbentry->name, index_mboxname(imapd_index))) imapd_check(NULL, 1); if (!r) r = imapd_statusdata(mbentry, statusitems, &sdata); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { print_statusline(name, statusitems, &sdata); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } done: if (global_conversations) { conversations_abort(&global_conversations); global_conversations = NULL; } mboxlist_entry_free(&mbentry); free(intname); return; } /* Callback for cmd_namespace to be passed to mboxlist_findall. * For each top-level mailbox found, print a bit of the response * if it is a shared namespace. The rock is used as an integer in * order to ensure the namespace response is correct on a server with * no shared namespace. */ static int namespacedata(struct findall_data *data, void *rock) { if (!data) return 0; int* sawone = (int*) rock; switch (data->mb_category) { case MBNAME_INBOX: case MBNAME_ALTINBOX: case MBNAME_ALTPREFIX: sawone[NAMESPACE_INBOX] = 1; break; case MBNAME_OTHERUSER: sawone[NAMESPACE_USER] = 1; break; case MBNAME_SHARED: sawone[NAMESPACE_SHARED] = 1; break; } return 0; } /* * Print out a response to the NAMESPACE command defined by * RFC 2342. */ static void cmd_namespace(char* tag) { int sawone[3] = {0, 0, 0}; char* pattern; if (SLEEZY_NAMESPACE) { if (strlen(imapd_userid) + 5 >= MAX_MAILBOX_BUFFER) sawone[NAMESPACE_INBOX] = 0; else { char *inbox = mboxname_user_mbox(imapd_userid, NULL); sawone[NAMESPACE_INBOX] = !mboxlist_lookup(inbox, NULL, NULL); free(inbox); } sawone[NAMESPACE_USER] = imapd_userisadmin ? 1 : imapd_namespace.accessible[NAMESPACE_USER]; sawone[NAMESPACE_SHARED] = imapd_userisadmin ? 1 : imapd_namespace.accessible[NAMESPACE_SHARED]; } else { pattern = xstrdup("%"); /* now find all the exciting toplevel namespaces - * we're using internal names here */ mboxlist_findall(NULL, pattern, imapd_userisadmin, imapd_userid, imapd_authstate, namespacedata, (void*) sawone); free(pattern); } prot_printf(imapd_out, "* NAMESPACE"); if (sawone[NAMESPACE_INBOX]) { prot_printf(imapd_out, " ((\"%s\" \"%c\"))", imapd_namespace.prefix[NAMESPACE_INBOX], imapd_namespace.hier_sep); } else { prot_printf(imapd_out, " NIL"); } if (sawone[NAMESPACE_USER]) { prot_printf(imapd_out, " ((\"%s\" \"%c\"))", imapd_namespace.prefix[NAMESPACE_USER], imapd_namespace.hier_sep); } else { prot_printf(imapd_out, " NIL"); } if (sawone[NAMESPACE_SHARED]) { prot_printf(imapd_out, " ((\"%s\" \"%c\"))", imapd_namespace.prefix[NAMESPACE_SHARED], imapd_namespace.hier_sep); } else { prot_printf(imapd_out, " NIL"); } prot_printf(imapd_out, "\r\n"); imapd_check(NULL, 0); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } static int parsecreateargs(struct dlist **extargs) { int c; static struct buf arg, val; struct dlist *res; struct dlist *sub; char *p; const char *name; res = dlist_newkvlist(NULL, "CREATE"); c = prot_getc(imapd_in); if (c == '(') { /* new style RFC 4466 arguments */ do { c = getword(imapd_in, &arg); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); name = ucase(arg.s); if (c != ' ') goto fail; c = prot_getc(imapd_in); if (c == '(') { /* fun - more lists! */ sub = dlist_newlist(res, name); do { c = getword(imapd_in, &val); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); dlist_setatom(sub, name, val.s); } while (c == ' '); if (c != ')') goto fail; c = prot_getc(imapd_in); } else { prot_ungetc(c, imapd_in); c = getword(imapd_in, &val); dlist_setatom(res, name, val.s); } } while (c == ' '); if (c != ')') goto fail; c = prot_getc(imapd_in); } else { prot_ungetc(c, imapd_in); c = getword(imapd_in, &arg); if (c == EOF) goto fail; p = strchr(arg.s, '!'); if (p) { /* with a server */ *p = '\0'; dlist_setatom(res, "SERVER", arg.s); dlist_setatom(res, "PARTITION", p+1); } else { dlist_setatom(res, "PARTITION", arg.s); } } *extargs = res; return c; fail: dlist_free(&res); if (c != EOF) prot_ungetc(c, imapd_in); return EOF; } /* * Parse annotate fetch data. * * This is a generic routine which parses just the annotation data. * Any surrounding command text must be parsed elsewhere, ie, * GETANNOTATION, FETCH. */ static int parse_annotate_fetch_data(const char *tag, int permessage_flag, strarray_t *entries, strarray_t *attribs) { int c; static struct buf arg; c = prot_getc(imapd_in); if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing annotation entry\r\n", tag); goto baddata; } else if (c == '(') { /* entry list */ do { if (permessage_flag) c = getastring(imapd_in, imapd_out, &arg); else c = getqstring(imapd_in, imapd_out, &arg); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing annotation entry\r\n", tag); goto baddata; } /* add the entry to the list */ strarray_append(entries, arg.s); } while (c == ' '); if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close paren in annotation entry list \r\n", tag); goto baddata; } c = prot_getc(imapd_in); } else { /* single entry -- add it to the list */ prot_ungetc(c, imapd_in); if (permessage_flag) c = getastring(imapd_in, imapd_out, &arg); else c = getqstring(imapd_in, imapd_out, &arg); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing annotation entry\r\n", tag); goto baddata; } strarray_append(entries, arg.s); } if (c != ' ' || (c = prot_getc(imapd_in)) == EOF) { prot_printf(imapd_out, "%s BAD Missing annotation attribute(s)\r\n", tag); goto baddata; } if (c == '(') { /* attrib list */ do { if (permessage_flag) c = getastring(imapd_in, imapd_out, &arg); else c = getqstring(imapd_in, imapd_out, &arg); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing annotation attribute(s)\r\n", tag); goto baddata; } /* add the attrib to the list */ strarray_append(attribs, arg.s); } while (c == ' '); if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close paren in " "annotation attribute list\r\n", tag); goto baddata; } c = prot_getc(imapd_in); } else { /* single attrib */ prot_ungetc(c, imapd_in); if (permessage_flag) c = getastring(imapd_in, imapd_out, &arg); else c = getqstring(imapd_in, imapd_out, &arg); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing annotation attribute\r\n", tag); goto baddata; } strarray_append(attribs, arg.s); } return c; baddata: eatline(imapd_in, c); return EOF; maxliteral: prot_printf(imapd_out, "%s NO %s in annotation entry\r\n", tag, error_message(IMAP_LITERAL_TOO_LARGE)); return IMAP_LITERAL_TOO_LARGE; } /* * Parse either a single string or a (bracketed) list of strings. * This is used up to three times in the GETMETADATA command. */ static int parse_metadata_string_or_list(const char *tag, strarray_t *entries, int *is_list) { int c; static struct buf arg; // Assume by default the arguments are a list of entries, // until proven otherwise. if (is_list) *is_list = 1; c = prot_getc(imapd_in); if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing metadata entry\r\n", tag); goto baddata; } else if (c == '\r') { return c; } else if (c == '(') { /* entry list */ do { c = getastring(imapd_in, imapd_out, &arg); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing metadata entry\r\n", tag); goto baddata; } /* add the entry to the list */ strarray_append(entries, arg.s); } while (c == ' '); if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close paren in metadata entry list \r\n", tag); goto baddata; } if (is_list) *is_list = 0; c = prot_getc(imapd_in); } else { /* single entry -- add it to the list */ prot_ungetc(c, imapd_in); c = getastring(imapd_in, imapd_out, &arg); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing metadata entry\r\n", tag); goto baddata; } strarray_append(entries, arg.s); // It is only not a list if there are no wildcards if (!strchr(arg.s, '*') && !strchr(arg.s, '%')) { // Not a list if (is_list) *is_list = 0; } } if (c == ' ' || c == '\r' || c == ')') return c; baddata: eatline(imapd_in, c); return EOF; maxliteral: prot_printf(imapd_out, "%s NO %s in metadata entry\r\n", tag, error_message(IMAP_LITERAL_TOO_LARGE)); return IMAP_LITERAL_TOO_LARGE; } /* * Parse annotate store data. * * This is a generic routine which parses just the annotation data. * Any surrounding command text must be parsed elsewhere, ie, * SETANNOTATION, STORE, APPEND. * * Also parse RFC 5257 per-message annotation store data, which * is almost identical but differs in that entry names and attrib * names are astrings rather than strings, and that the whole set * of data *must* be enclosed in parentheses. */ static int parse_annotate_store_data(const char *tag, int permessage_flag, struct entryattlist **entryatts) { int c, islist = 0; static struct buf entry, attrib, value; struct attvaluelist *attvalues = NULL; *entryatts = NULL; c = prot_getc(imapd_in); if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing annotation entry\r\n", tag); goto baddata; } else if (c == '(') { /* entry list */ islist = 1; } else if (permessage_flag) { prot_printf(imapd_out, "%s BAD Missing paren for annotation entry\r\n", tag); goto baddata; } else { /* single entry -- put the char back */ prot_ungetc(c, imapd_in); } do { /* get entry */ if (permessage_flag) c = getastring(imapd_in, imapd_out, &entry); else c = getqstring(imapd_in, imapd_out, &entry); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing annotation entry\r\n", tag); goto baddata; } /* parse att-value list */ if (c != ' ' || (c = prot_getc(imapd_in)) != '(') { prot_printf(imapd_out, "%s BAD Missing annotation attribute-values list\r\n", tag); goto baddata; } do { /* get attrib */ if (permessage_flag) c = getastring(imapd_in, imapd_out, &attrib); else c = getqstring(imapd_in, imapd_out, &attrib); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing annotation attribute\r\n", tag); goto baddata; } /* get value */ if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing annotation value\r\n", tag); goto baddata; } c = getbnstring(imapd_in, imapd_out, &value); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing annotation value\r\n", tag); goto baddata; } if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); /* add the attrib-value pair to the list */ appendattvalue(&attvalues, attrib.s, &value); } while (c == ' '); if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close paren in annotation " "attribute-values list\r\n", tag); goto baddata; } /* add the entry to the list */ appendentryatt(entryatts, entry.s, attvalues); attvalues = NULL; c = prot_getc(imapd_in); } while (c == ' '); if (islist) { if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close paren in annotation entry list \r\n", tag); goto baddata; } c = prot_getc(imapd_in); } return c; baddata: if (attvalues) freeattvalues(attvalues); eatline(imapd_in, c); return EOF; maxliteral: if (attvalues) freeattvalues(attvalues); prot_printf(imapd_out, "%s NO %s in annotation entry\r\n", tag, error_message(IMAP_LITERAL_TOO_LARGE)); return IMAP_LITERAL_TOO_LARGE; } /* * Parse metadata store data. * * This is a generic routine which parses just the annotation data. * Any surrounding command text must be parsed elsewhere, ie, * SETANNOTATION, STORE, APPEND. */ static int parse_metadata_store_data(const char *tag, struct entryattlist **entryatts) { int c; const char *name; const char *att; static struct buf entry, value; struct attvaluelist *attvalues = NULL; struct entryattlist *entryp; int need_add; *entryatts = NULL; c = prot_getc(imapd_in); if (c != '(') { prot_printf(imapd_out, "%s BAD Missing metadata entry list\r\n", tag); goto baddata; } do { /* get entry */ c = getastring(imapd_in, imapd_out, &entry); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing metadata entry\r\n", tag); goto baddata; } /* DAV code uses case significant metadata entries, so if you log in with +dav, * we make the metadata commands case significant! */ if (strcasecmpsafe(imapd_magicplus, "+dav")) lcase(entry.s); /* get value */ c = getbnstring(imapd_in, imapd_out, &value); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (c == IMAP_LITERAL_TOO_LARGE) goto maxliteral; if (c == EOF) { prot_printf(imapd_out, "%s BAD Missing metadata value\r\n", tag); goto baddata; } if (!strncmp(entry.s, "/private", 8) && (entry.s[8] == '\0' || entry.s[8] == '/')) { att = "value.priv"; name = entry.s + 8; } else if (!strncmp(entry.s, "/shared", 7) && (entry.s[7] == '\0' || entry.s[7] == '/')) { att = "value.shared"; name = entry.s + 7; } else { prot_printf(imapd_out, "%s BAD entry must begin with /shared or /private\r\n", tag); goto baddata; } need_add = 1; for (entryp = *entryatts; entryp; entryp = entryp->next) { if (strcmp(entryp->entry, name)) continue; /* it's a match, have to append! */ appendattvalue(&entryp->attvalues, att, &value); need_add = 0; break; } if (need_add) { appendattvalue(&attvalues, att, &value); appendentryatt(entryatts, name, attvalues); attvalues = NULL; } } while (c == ' '); if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close paren in metadata entry list \r\n", tag); goto baddata; } c = prot_getc(imapd_in); return c; baddata: if (attvalues) freeattvalues(attvalues); eatline(imapd_in, c); return EOF; maxliteral: if (attvalues) freeattvalues(attvalues); prot_printf(imapd_out, "%s NO %s in metadata entry\r\n", tag, error_message(IMAP_LITERAL_TOO_LARGE)); return IMAP_LITERAL_TOO_LARGE; } static void getannotation_response(const char *mboxname, uint32_t uid __attribute__((unused)), const char *entry, struct attvaluelist *attvalues, void *rock __attribute__((unused))) { int sep = '('; struct attvaluelist *l; char *extname = *mboxname ? mboxname_to_external(mboxname, &imapd_namespace, imapd_userid) : xstrdup(""); /* server annotation */ prot_printf(imapd_out, "* ANNOTATION "); prot_printastring(imapd_out, extname); prot_putc(' ', imapd_out); prot_printstring(imapd_out, entry); prot_putc(' ', imapd_out); for (l = attvalues ; l ; l = l->next) { prot_putc(sep, imapd_out); sep = ' '; prot_printstring(imapd_out, l->attrib); prot_putc(' ', imapd_out); prot_printmap(imapd_out, l->value.s, l->value.len); } prot_printf(imapd_out, ")\r\n"); free(extname); } struct annot_fetch_rock { strarray_t *entries; strarray_t *attribs; annotate_fetch_cb_t callback; void *cbrock; }; static int annot_fetch_cb(annotate_state_t *astate, void *rock) { struct annot_fetch_rock *arock = rock; return annotate_state_fetch(astate, arock->entries, arock->attribs, arock->callback, arock->cbrock); } struct annot_store_rock { struct entryattlist *entryatts; }; static int annot_store_cb(annotate_state_t *astate, void *rock) { struct annot_store_rock *arock = rock; return annotate_state_store(astate, arock->entryatts); } /* * Common code used to apply a function to every mailbox which matches * a mailbox pattern, with an annotate_state_t* set up to point to the * mailbox. */ struct apply_rock { annotate_state_t *state; int (*proc)(annotate_state_t *, void *data); void *data; char lastname[MAX_MAILBOX_PATH+1]; unsigned int nseen; unsigned want_dav : 1; }; static int apply_cb(struct findall_data *data, void* rock) { if (!data) return 0; if (!data->is_exactmatch) return 0; struct apply_rock *arock = (struct apply_rock *)rock; annotate_state_t *state = arock->state; int r; if (!arock->want_dav && mbtype_isa(data->mbentry->mbtype) != MBTYPE_EMAIL) { return 0; } strlcpy(arock->lastname, mbname_intname(data->mbname), sizeof(arock->lastname)); // malloc extra-long to have room for pattern shenanigans later /* NOTE: this is a fricking horrible abuse of layers. We'll be passing the * extname less horribly one day */ const char *extname = mbname_extname(data->mbname, &imapd_namespace, imapd_userid); mbentry_t *backdoor = (mbentry_t *)data->mbentry; backdoor->ext_name = xmalloc(strlen(extname)+1); strcpy(backdoor->ext_name, extname); r = annotate_state_set_mailbox_mbe(state, data->mbentry); if (r) return r; r = arock->proc(state, arock->data); arock->nseen++; return r; } static int apply_mailbox_pattern(annotate_state_t *state, const char *pattern, int (*proc)(annotate_state_t *, void *), void *data) { struct apply_rock arock; int r = 0; memset(&arock, 0, sizeof(arock)); arock.state = state; arock.proc = proc; arock.data = data; arock.want_dav = !strcasecmpsafe(imapd_magicplus, "+dav"); r = mboxlist_findall(&imapd_namespace, pattern, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate, apply_cb, &arock); if (!r && !arock.nseen) r = IMAP_MAILBOX_NONEXISTENT; return r; } static int apply_mailbox_array(annotate_state_t *state, const strarray_t *mboxes, int (*proc)(annotate_state_t *, void *), void *rock) { int i; mbentry_t *mbentry = NULL; char *intname = NULL; int r = 0; for (i = 0 ; i < mboxes->count ; i++) { const char *extname = strarray_nth(mboxes, i); intname = mboxname_from_external(extname, &imapd_namespace, imapd_userid); r = mboxlist_lookup(intname, &mbentry, NULL); if (r) break; mbentry->ext_name = xstrdup(extname); r = annotate_state_set_mailbox_mbe(state, mbentry); if (r) break; r = proc(state, rock); if (r) break; mboxlist_entry_free(&mbentry); free(intname); intname = NULL; } mboxlist_entry_free(&mbentry); free(intname); return r; } /* * Perform a GETANNOTATION command * * The command has been parsed up to the entries */ static void cmd_getannotation(const char *tag, char *mboxpat) { int c, r = 0; strarray_t entries = STRARRAY_INITIALIZER; strarray_t attribs = STRARRAY_INITIALIZER; annotate_state_t *astate = NULL; c = parse_annotate_fetch_data(tag, /*permessage_flag*/0, &entries, &attribs); if (c <= EOF) { goto freeargs; } /* check for CRLF */ if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Getannotation\r\n", tag); eatline(imapd_in, c); goto freeargs; } if (config_getswitch(IMAPOPT_ANNOTATION_ENABLE_LEGACY_COMMANDS)) { astate = annotate_state_new(); annotate_state_set_auth(astate, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate); if (!*mboxpat) { r = annotate_state_set_server(astate); if (!r) r = annotate_state_fetch(astate, &entries, &attribs, getannotation_response, NULL); } else { struct annot_fetch_rock arock; arock.entries = &entries; arock.attribs = &attribs; arock.callback = getannotation_response; arock.cbrock = NULL; r = apply_mailbox_pattern(astate, mboxpat, annot_fetch_cb, &arock); } /* we didn't write anything */ annotate_state_abort(&astate); } else { /* nope, sorry */ r = IMAP_PERMISSION_DENIED; } imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } freeargs: strarray_fini(&entries); strarray_fini(&attribs); } static void getmetadata_response(const char *mboxname, uint32_t uid __attribute__((unused)), const char *entry, struct attvaluelist *attvalues, void *rock) { struct getmetadata_options *opts = (struct getmetadata_options *)rock; if (strcmpsafe(mboxname, opts->lastname) || !entry) { if (opts->items.count) { char *extname = NULL; size_t i; if (opts->lastname) extname = mboxname_to_external(opts->lastname, &imapd_namespace, imapd_userid); else extname = xstrdup(""); prot_printf(imapd_out, "* METADATA "); prot_printastring(imapd_out, extname); prot_putc(' ', imapd_out); for (i = 0; i + 1 < opts->items.count; i+=2) { prot_putc(i ? ' ' : '(', imapd_out); const struct buf *key = bufarray_nth(&opts->items, i); prot_printmap(imapd_out, key->s, key->len); prot_putc(' ', imapd_out); const struct buf *val = bufarray_nth(&opts->items, i+1); prot_printmap(imapd_out, val->s, val->len); } prot_printf(imapd_out, ")\r\n"); free(extname); } free(opts->lastname); opts->lastname = xstrdupnull(mboxname); bufarray_fini(&opts->items); } struct attvaluelist *l; struct buf buf = BUF_INITIALIZER; for (l = attvalues ; l ; l = l->next) { /* size check */ if (opts->maxsize && l->value.len >= opts->maxsize) { if (l->value.len > opts->biggest) opts->biggest = l->value.len; continue; } /* check if it's a value we print... */ buf_reset(&buf); if (!strcmp(l->attrib, "value.shared")) buf_appendcstr(&buf, "/shared"); else if (!strcmp(l->attrib, "value.priv")) buf_appendcstr(&buf, "/private"); else continue; buf_appendcstr(&buf, entry); bufarray_append(&opts->items, &buf); bufarray_append(&opts->items, &l->value); } buf_free(&buf); } static int parse_getmetadata_options(const strarray_t *sa, struct getmetadata_options *opts) { int i; int n = 0; struct getmetadata_options dummy = OPTS_INITIALIZER; if (!opts) opts = &dummy; for (i = 0 ; i < sa->count ; i+=2) { const char *option = sa->data[i]; const char *value = sa->data[i+1]; if (!value) return -1; if (!strcasecmp(option, "MAXSIZE")) { char *end = NULL; /* we add one so that it's "less than" maxsize * and zero works but is still true */ opts->maxsize = strtoul(value, &end, 10) + 1; if (!end || *end || end == value) return -1; n++; } else if (!strcasecmp(option, "DEPTH")) { if (!strcmp(value, "0")) opts->depth = 0; else if (!strcmp(value, "1")) opts->depth = 1; else if (!strcasecmp(value, "infinity")) opts->depth = -1; else return -1; n++; } else { return 0; } } return n; } static int _metadata_to_annotate(const strarray_t *entries, strarray_t *newa, strarray_t *newe, const char *tag, int depth) { int i; - int have_shared = 0; - int have_private = 0; /* we need to rewrite the entries and attribs to match the way that * the old annotation system works. */ for (i = 0 ; i < entries->count ; i++) { char *ent = entries->data[i]; char entry[MAX_MAILBOX_NAME+1]; lcase(ent); /* there's no way to perfect this - unfortunately - the old style * syntax doesn't support everything. XXX - will be nice to get * rid of this... */ if (!strncmp(ent, "/private", 8) && (ent[8] == '\0' || ent[8] == '/')) { xstrncpy(entry, ent + 8, MAX_MAILBOX_NAME); - have_private = 1; + strarray_append(newa, "value.priv"); } else if (!strncmp(ent, "/shared", 7) && (ent[7] == '\0' || ent[7] == '/')) { xstrncpy(entry, ent + 7, MAX_MAILBOX_NAME); - have_shared = 1; + strarray_append(newa, "value.shared"); } else { if (tag) prot_printf(imapd_out, "%s BAD entry must begin with /shared or /private\r\n", tag); return IMAP_NO_NOSUCHMSG; } strarray_append(newe, entry); if (depth == 1) { strncat(entry, "/%", MAX_MAILBOX_NAME); strarray_append(newe, entry); } else if (depth == -1) { strncat(entry, "/*", MAX_MAILBOX_NAME); strarray_append(newe, entry); } } - if (have_private) strarray_append(newa, "value.priv"); - if (have_shared) strarray_append(newa, "value.shared"); return 0; } /* * Perform a GETMETADATA command * * The command has been parsed up to the mailbox */ static void cmd_getmetadata(const char *tag) { int c, r = 0; strarray_t lists[3] = { STRARRAY_INITIALIZER, STRARRAY_INITIALIZER, STRARRAY_INITIALIZER }; int is_list[3] = { 1, 1, 1 }; int nlists = 0; strarray_t *options = NULL; strarray_t *mboxes = NULL; strarray_t *entries = NULL; strarray_t newe = STRARRAY_INITIALIZER; strarray_t newa = STRARRAY_INITIALIZER; struct buf arg1 = BUF_INITIALIZER; int mbox_is_pattern = 0; struct getmetadata_options opts = OPTS_INITIALIZER; annotate_state_t *astate = NULL; while (nlists < 3) { c = parse_metadata_string_or_list(tag, &lists[nlists], &is_list[nlists]); if (c <= EOF) goto freeargs; nlists++; if (c == '\r') break; } /* check for CRLF */ if (c == '\r') { c = prot_getc(imapd_in); if (c != '\n') { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Getannotation\r\n", tag); eatline(imapd_in, c); goto freeargs; } } else { // Make sure this line is gone eatline(imapd_in, c); } /* * We have three strings or lists of strings. Now to figure out * what's what. We have two complicating factors. First, due to * a erratum in RFC 5464 and our earlier misreading of the document, * we historically supported specifying the options *after* the * mailbox name. Second, we have for a few months now supported * a non-standard extension where a list of mailbox names could * be supplied instead of just a single one. So we have to apply * some rules. We support the following syntaxes: * * --- no options * mailbox entry * mailbox (entries) * (mailboxes) entry * (mailboxes) (entries) * * --- options in the correct place (per the ABNF in RFC 5464) * (options) mailbox entry * (options) mailbox (entries) * (options) (mailboxes) entry * (options) (mailboxes) (entries) * * --- options in the wrong place (per the examples in RFC 5464) * mailbox (options) entry * mailbox (options) (entries) * (mailboxes) (options) entry * (mailboxes) (options) (entries) */ if (nlists < 2) goto missingargs; entries = &lists[nlists-1]; /* entries always last */ if (nlists == 2) { /* no options */ mboxes = &lists[0]; mbox_is_pattern = is_list[0]; } if (nlists == 3) { /* options, either before or after */ int r0 = (parse_getmetadata_options(&lists[0], NULL) > 0); int r1 = (parse_getmetadata_options(&lists[1], NULL) > 0); switch ((r1<<1)|r0) { case 0: /* neither are valid options */ goto missingargs; case 1: /* (options) (mailboxes) */ options = &lists[0]; mboxes = &lists[1]; mbox_is_pattern = is_list[1]; break; case 2: /* (mailboxes) (options) */ mboxes = &lists[0]; mbox_is_pattern = is_list[0]; options = &lists[1]; break; case 3: /* both appear like valid options */ prot_printf(imapd_out, "%s BAD Too many option lists for Getmetadata\r\n", tag); eatline(imapd_in, c); goto freeargs; } } if (options) parse_getmetadata_options(options, &opts); if (_metadata_to_annotate(entries, &newa, &newe, tag, opts.depth)) goto freeargs; astate = annotate_state_new(); annotate_state_set_auth(astate, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate); if (!mboxes->count || !strcmpsafe(mboxes->data[0], NULL)) { r = annotate_state_set_server(astate); if (!r) r = annotate_state_fetch(astate, &newe, &newa, getmetadata_response, &opts); } else { struct annot_fetch_rock arock; arock.entries = &newe; arock.attribs = &newa; arock.callback = getmetadata_response; arock.cbrock = &opts; if (mbox_is_pattern) r = apply_mailbox_pattern(astate, mboxes->data[0], annot_fetch_cb, &arock); else r = apply_mailbox_array(astate, mboxes, annot_fetch_cb, &arock); } /* we didn't write anything */ annotate_state_abort(&astate); getmetadata_response(NULL, 0, NULL, NULL, &opts); imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else if (opts.maxsize && opts.biggest > opts.maxsize) { prot_printf(imapd_out, "%s OK [METADATA LONGENTRIES %u] %s\r\n", tag, (unsigned)opts.biggest, error_message(IMAP_OK_COMPLETED)); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } freeargs: strarray_fini(&lists[0]); strarray_fini(&lists[1]); strarray_fini(&lists[2]); strarray_fini(&newe); strarray_fini(&newa); buf_free(&arg1); return; missingargs: prot_printf(imapd_out, "%s BAD Missing arguments to Getmetadata\r\n", tag); eatline(imapd_in, c); goto freeargs; } /* * Perform a SETANNOTATION command * * The command has been parsed up to the entry-att list */ static void cmd_setannotation(const char *tag, char *mboxpat) { int c, r = 0; struct entryattlist *entryatts = NULL; annotate_state_t *astate = NULL; c = parse_annotate_store_data(tag, 0, &entryatts); if (c <= EOF) { goto freeargs; } /* check for CRLF */ if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Setannotation\r\n", tag); eatline(imapd_in, c); goto freeargs; } if (config_getswitch(IMAPOPT_ANNOTATION_ENABLE_LEGACY_COMMANDS)) { astate = annotate_state_new(); annotate_state_set_auth(astate, imapd_userisadmin, imapd_userid, imapd_authstate); if (!r) { if (!*mboxpat) { r = annotate_state_set_server(astate); if (!r) r = annotate_state_store(astate, entryatts); } else { struct annot_store_rock arock; arock.entryatts = entryatts; r = apply_mailbox_pattern(astate, mboxpat, annot_store_cb, &arock); } } if (!r) annotate_state_commit(&astate); else annotate_state_abort(&astate); } else { /* nope, sorry */ r = IMAP_PERMISSION_DENIED; } imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } freeargs: if (entryatts) freeentryatts(entryatts); } /* * Perform a SETMETADATA command * * The command has been parsed up to the entry-att list */ static void cmd_setmetadata(const char *tag, char *mboxpat) { int c, r = 0; struct entryattlist *entryatts = NULL; annotate_state_t *astate = NULL; c = parse_metadata_store_data(tag, &entryatts); if (c <= EOF) { goto freeargs; } /* check for CRLF */ if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Setmetadata\r\n", tag); eatline(imapd_in, c); goto freeargs; } astate = annotate_state_new(); annotate_state_set_auth(astate, imapd_userisadmin, imapd_userid, imapd_authstate); if (!r) { if (!*mboxpat) { r = annotate_state_set_server(astate); if (!r) r = annotate_state_store(astate, entryatts); } else { struct annot_store_rock arock; arock.entryatts = entryatts; r = apply_mailbox_pattern(astate, mboxpat, annot_store_cb, &arock); } } if (!r) r = annotate_state_commit(&astate); else annotate_state_abort(&astate); imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } freeargs: if (entryatts) freeentryatts(entryatts); return; } static void cmd_xrunannotator(const char *tag, const char *sequence, int usinguid) { const char *cmd = usinguid ? "UID Xrunannotator" : "Xrunannotator"; clock_t start = clock(); char mytime[100]; int r = 0; if (backend_current) { /* remote mailbox */ prot_printf(backend_current->out, "%s %s %s ", tag, cmd, sequence); if (!pipe_command(backend_current, 65536)) { pipe_including_tag(backend_current, tag, 0); } return; } /* local mailbox */ r = index_run_annotator(imapd_index, sequence, usinguid, &imapd_namespace, imapd_userisadmin); index_release(imapd_index); sync_checkpoint(imapd_in); snprintf(mytime, sizeof(mytime), "%2.3f", (clock() - start) / (double) CLOCKS_PER_SEC); if (r) prot_printf(imapd_out, "%s NO %s (%s sec)\r\n", tag, error_message(r), mytime); else prot_printf(imapd_out, "%s OK %s (%s sec)\r\n", tag, error_message(IMAP_OK_COMPLETED), mytime); } static void cmd_xwarmup(const char *tag) { const char *cmd = "Xwarmup"; clock_t start = clock(); char mytime[100]; struct buf arg = BUF_INITIALIZER; int warmup_flags = 0; seqset_t *uids = NULL; /* We deal with the mboxlist API instead of the index_state API or * mailbox API to avoid the overhead of index_open(), which will * block while reading all the cyrus.index...we want to be * non-blocking */ struct mboxlist_entry *mbentry = NULL; int myrights; int c, r = 0; char *intname = NULL; /* parse arguments: expect '('')' */ c = getastring(imapd_in, imapd_out, &arg); if (c == IMAP_LITERAL_TOO_LARGE) { prot_printf(imapd_out, "%s NO %s in Xwarmup\r\n", tag, error_message(c)); goto out_noprint; } if (c != ' ') { syntax_error: prot_printf(imapd_out, "%s BAD syntax error in %s\r\n", tag, cmd); eatline(imapd_in, c); goto out_noprint; } intname = mboxname_from_external(arg.s, &imapd_namespace, imapd_userid); r = mboxlist_lookup(intname, &mbentry, NULL); if (r) goto out; /* Do a permissions check to avoid server DoS opportunity. But we * only need read permission to warmup a mailbox. Also, be careful * to avoid telling the client about the existence of mailboxes to * which he doesn't have LOOKUP rights. */ r = IMAP_PERMISSION_DENIED; myrights = (mbentry->acl ? cyrus_acl_myrights(imapd_authstate, mbentry->acl) : 0); if (imapd_userisadmin) r = 0; else if (!(myrights & ACL_LOOKUP)) r = IMAP_MAILBOX_NONEXISTENT; else if (myrights & ACL_READ) r = 0; if (r) goto out; if (mbentry->mbtype & MBTYPE_REMOTE) { /* remote mailbox */ struct backend *be; be = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (!be) { r = IMAP_SERVER_UNAVAILABLE; goto out; } prot_printf(be->out, "%s %s %s ", tag, cmd, arg.s); if (!pipe_command(backend_current, 65536)) { pipe_including_tag(backend_current, tag, 0); } goto out; } /* local mailbox */ /* parse the arguments after the mailbox */ c = prot_getc(imapd_in); if (c != '(') goto syntax_error; for (;;) { c = getword(imapd_in, &arg); if (arg.len) { if (!strcasecmp(arg.s, "index")) warmup_flags |= WARMUP_INDEX; else if (!strcasecmp(arg.s, "conversations")) warmup_flags |= WARMUP_CONVERSATIONS; else if (!strcasecmp(arg.s, "annotations")) warmup_flags |= WARMUP_ANNOTATIONS; else if (!strcasecmp(arg.s, "search")) warmup_flags |= WARMUP_SEARCH; else if (!strcasecmp(arg.s, "uids")) { if (c != ' ') goto syntax_error; c = getword(imapd_in, &arg); if (c == EOF) goto syntax_error; if (!imparse_issequence(arg.s)) goto syntax_error; uids = seqset_parse(arg.s, NULL, /*maxval*/0); if (!uids) goto syntax_error; } else if (!strcasecmp(arg.s, "all")) warmup_flags |= WARMUP_ALL; else goto syntax_error; } if (c == ')') break; if (c != ' ') goto syntax_error; } /* we're expecting no more arguments */ c = prot_getc(imapd_in); if (!IS_EOL(c, imapd_in)) goto syntax_error; r = index_warmup(mbentry, warmup_flags, uids); out: snprintf(mytime, sizeof(mytime), "%2.3f", (clock() - start) / (double) CLOCKS_PER_SEC); if (r) prot_printf(imapd_out, "%s NO %s (%s sec)\r\n", tag, error_message(r), mytime); else prot_printf(imapd_out, "%s OK %s (%s sec)\r\n", tag, error_message(IMAP_OK_COMPLETED), mytime); out_noprint: mboxlist_entry_free(&mbentry); free(intname); buf_free(&arg); seqset_free(&uids); } static void cmd_dump(char *tag, char *name, int uid_start) { int r = 0; struct mailbox *mailbox = NULL; /* administrators only please */ if (!imapd_userisadmin) r = IMAP_PERMISSION_DENIED; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); if (!r) r = mailbox_open_irl(intname, &mailbox); if (!r) r = dump_mailbox(tag, mailbox, uid_start, MAILBOX_MINOR_VERSION, imapd_in, imapd_out, imapd_authstate); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } if (mailbox) mailbox_close(&mailbox); free(intname); } static void cmd_undump(char *tag, char *name) { int r = 0; mbname_t *mbname = mbname_from_extname(name, &imapd_namespace, imapd_userid); struct mboxlock *namespacelock = mboxname_usernamespacelock(mbname_intname(mbname)); /* administrators only please */ if (!imapd_userisadmin) r = IMAP_PERMISSION_DENIED; if (!r) r = mlookup(tag, name, mbname_intname(mbname), NULL); if (!r) r = undump_mailbox(mbname_intname(mbname), imapd_in, imapd_out, imapd_authstate); if (r) { prot_printf(imapd_out, "%s NO %s%s\r\n", tag, (r == IMAP_MAILBOX_NONEXISTENT && mboxlist_createmailboxcheck(mbname_intname(mbname), 0, 0, imapd_userisadmin, imapd_userid, imapd_authstate, NULL, NULL, 0) == 0) ? "[TRYCREATE] " : "", error_message(r)); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } mboxname_release(&namespacelock); mbname_free(&mbname); } static int getresult(struct protstream *p, const char *tag) { char buf[4096]; char *str = (char *) buf; while(1) { if (!prot_fgets(str, sizeof(buf), p)) { return IMAP_SERVER_UNAVAILABLE; } if (!strncmp(str, tag, strlen(tag))) { str += strlen(tag); if(!*str) { /* We got a tag, but no response */ return IMAP_SERVER_UNAVAILABLE; } str++; if (!strncasecmp(str, "OK ", 3)) { return 0; } if (!strncasecmp(str, "NO ", 3)) { return IMAP_REMOTE_DENIED; } return IMAP_SERVER_UNAVAILABLE; /* huh? */ } /* skip this line, we don't really care */ } } /* given 2 protstreams and a mailbox, gets the acl and then wipes it */ static int trashacl(struct protstream *pin, struct protstream *pout, char *mailbox) { int i=0, j=0; char tagbuf[128]; int c; /* getword() returns an int */ struct buf cmd, tmp, user; int r = 0; memset(&cmd, 0, sizeof(struct buf)); memset(&tmp, 0, sizeof(struct buf)); memset(&user, 0, sizeof(struct buf)); prot_printf(pout, "ACL0 GETACL {" SIZE_T_FMT "+}\r\n%s\r\n", strlen(mailbox), mailbox); while(1) { c = prot_getc(pin); if (c != '*') { prot_ungetc(c, pin); r = getresult(pin, "ACL0"); break; } c = prot_getc(pin); /* skip SP */ c = getword(pin, &cmd); if (c == EOF) { r = IMAP_SERVER_UNAVAILABLE; break; } if (!strncmp(cmd.s, "ACL", 3)) { while(c != '\n') { /* An ACL response, we should send a DELETEACL command */ c = getastring(pin, pout, &tmp); if (c == EOF) { r = IMAP_SERVER_UNAVAILABLE; goto cleanup; } if(c == '\r') { c = prot_getc(pin); if(c != '\n') { r = IMAP_SERVER_UNAVAILABLE; goto cleanup; } } if(c == '\n') break; /* end of * ACL */ c = getastring(pin, pout, &user); if (c == EOF) { r = IMAP_SERVER_UNAVAILABLE; goto cleanup; } snprintf(tagbuf, sizeof(tagbuf), "ACL%d", ++i); prot_printf(pout, "%s DELETEACL {" SIZE_T_FMT "+}\r\n%s" " {" SIZE_T_FMT "+}\r\n%s\r\n", tagbuf, strlen(mailbox), mailbox, strlen(user.s), user.s); if(c == '\r') { c = prot_getc(pin); if(c != '\n') { r = IMAP_SERVER_UNAVAILABLE; goto cleanup; } } /* if the next character is \n, we'll exit the loop */ } } else { /* skip this line, we don't really care */ eatline(pin, c); } } cleanup: /* Now cleanup after all the DELETEACL commands */ if(!r) { while(j < i) { snprintf(tagbuf, sizeof(tagbuf), "ACL%d", ++j); r = getresult(pin, tagbuf); if (r) break; } } if(r) eatline(pin, c); buf_free(&user); buf_free(&tmp); buf_free(&cmd); return r; } static int dumpacl(struct protstream *pin, struct protstream *pout, const char *mboxname, const char *acl_in) { int r = 0; char tag[128]; int tagnum = 1; char *rights, *nextid; char *acl_safe = acl_in ? xstrdup(acl_in) : NULL; char *acl = acl_safe; while (acl) { rights = strchr(acl, '\t'); if (!rights) break; *rights++ = '\0'; nextid = strchr(rights, '\t'); if (!nextid) break; *nextid++ = '\0'; snprintf(tag, sizeof(tag), "SACL%d", tagnum++); prot_printf(pout, "%s SETACL {" SIZE_T_FMT "+}\r\n%s" " {" SIZE_T_FMT "+}\r\n%s {" SIZE_T_FMT "+}\r\n%s\r\n", tag, strlen(mboxname), mboxname, strlen(acl), acl, strlen(rights), rights); r = getresult(pin, tag); if (r) break; acl = nextid; } if(acl_safe) free(acl_safe); return r; } enum { XFER_MOVING_USER = -1, XFER_DEACTIVATED = 1, XFER_REMOTE_CREATED, XFER_LOCAL_MOVING, XFER_UNDUMPED, }; struct xfer_item { mbentry_t *mbentry; char extname[MAX_MAILBOX_NAME]; struct mailbox *mailbox; int state; struct xfer_item *next; }; struct xfer_header { struct sync_client_state sync_cs; int remoteversion; unsigned long use_replication; char *userid; char *toserver; char *topart; struct seen *seendb; struct xfer_item *items; }; enum mupdate_op { MUPDATE_ACTIVATE, MUPDATE_DEACTIVATE, MUPDATE_DELETE }; static int xfer_mupdate(enum mupdate_op op, const char *mboxname, const char *part, const char *servername, const char *acl) { char buf[MAX_PARTITION_LEN+HOSTNAME_SIZE+2]; int retry = 0; int r = 0; /* no mupdate handle */ if (!mupdate_h) return 0; if (servername && part) snprintf(buf, sizeof(buf), "%s!%s", servername, part); retry: /* make the change */ if (op == MUPDATE_DELETE) r = mupdate_delete(mupdate_h, mboxname); else if (op == MUPDATE_ACTIVATE) r = mupdate_activate(mupdate_h, mboxname, buf, acl); else r = mupdate_deactivate(mupdate_h, mboxname, buf); if (r && !retry) { syslog(LOG_INFO, "MUPDATE: lost connection, retrying"); mupdate_disconnect(&mupdate_h); r = mupdate_connect(config_mupdate_server, NULL, &mupdate_h, NULL); if (r) { syslog(LOG_INFO, "Failed to connect to mupdate '%s'", config_mupdate_server); } else { retry = 1; goto retry; } } return r; } /* nothing you can do about failures, just try to clean up */ static void xfer_cleanup(struct xfer_header *xfer) { struct xfer_item *item, *next; /* remove items */ item = xfer->items; while (item) { next = item->next; mboxlist_entry_free(&item->mbentry); free(item); item = next; } xfer->items = NULL; free(xfer->topart); free(xfer->userid); xfer->topart = xfer->userid = NULL; seen_close(&xfer->seendb); xfer->seendb = NULL; } static void xfer_done(struct xfer_header **xferptr) { struct xfer_header *xfer = *xferptr; syslog(LOG_INFO, "XFER: disconnecting from servers"); /* The sync_cs.backend connection is in backend_cached[], so it will be disconnected and freed in imapd_reset() */ buf_free(&xfer->sync_cs.tagbuf); free(xfer->toserver); xfer_cleanup(xfer); free(xfer); *xferptr = NULL; } static int xfer_init(const char *toserver, struct xfer_header **xferptr) { struct xfer_header *xfer = xzmalloc(sizeof(struct xfer_header)); int r; syslog(LOG_INFO, "XFER: connecting to server '%s'", toserver); xfer->sync_cs.servername = toserver; xfer->sync_cs.flags = SYNC_FLAG_LOGGING | SYNC_FLAG_LOCALONLY; /* Get a connection to the remote backend */ xfer->sync_cs.backend = proxy_findserver(toserver, &imap_protocol, "", &backend_cached, NULL, NULL, imapd_in); if (!xfer->sync_cs.backend) { syslog(LOG_ERR, "Failed to connect to server '%s'", toserver); r = IMAP_SERVER_UNAVAILABLE; goto fail; } struct backend *be = xfer->sync_cs.backend; xfer->sync_cs.clientin = be->in; xfer->remoteversion = backend_version(be); if (CAPA(be, CAPA_REPLICATION)) { syslog(LOG_INFO, "XFER: destination supports replication"); xfer->use_replication = 1; be->in->userdata = be->out->userdata = &xfer->sync_cs.tagbuf; if (CAPA(be, CAPA_SIEVE_MAILBOX)) { syslog(LOG_INFO, "XFER: destination supports #sieve mailbox"); sync_do_enable(&xfer->sync_cs, CAPA_SIEVE_MAILBOX); } } xfer->toserver = xstrdup(toserver); xfer->topart = NULL; xfer->seendb = NULL; /* connect to mupdate server if configured */ if (config_mupdate_server && !mupdate_h) { syslog(LOG_INFO, "XFER: connecting to mupdate '%s'", config_mupdate_server); r = mupdate_connect(config_mupdate_server, NULL, &mupdate_h, NULL); if (r) { syslog(LOG_INFO, "Failed to connect to mupdate '%s'", config_mupdate_server); goto fail; } } *xferptr = xfer; return 0; fail: xfer_done(&xfer); return r; } static int xfer_localcreate(struct xfer_header *xfer) { struct xfer_item *item; int r; syslog(LOG_INFO, "XFER: creating mailboxes on destination"); for (item = xfer->items; item; item = item->next) { struct backend *be = xfer->sync_cs.backend; if (xfer->topart) { /* need to send partition as an atom */ prot_printf(be->out, "LC1 LOCALCREATE {" SIZE_T_FMT "+}\r\n%s %s\r\n", strlen(item->extname), item->extname, xfer->topart); } else { prot_printf(be->out, "LC1 LOCALCREATE {" SIZE_T_FMT "+}\r\n%s\r\n", strlen(item->extname), item->extname); } r = getresult(be->in, "LC1"); if (r) { syslog(LOG_ERR, "Could not move mailbox: %s, LOCALCREATE failed", item->mbentry->name); return r; } item->state = XFER_REMOTE_CREATED; } return 0; } static int xfer_backport_seen_item(struct xfer_item *item, struct seen *seendb) { struct mailbox *mailbox = item->mailbox; seqset_t *outlist = NULL; struct seendata sd = SEENDATA_INITIALIZER; int r; outlist = seqset_init(mailbox->i.last_uid, SEQ_MERGE); struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_EXPUNGED); const message_t *msg; while ((msg = mailbox_iter_step(iter))) { const struct index_record *record = msg_record(msg); if (record->system_flags & FLAG_SEEN) seqset_add(outlist, record->uid, 1); else seqset_add(outlist, record->uid, 0); } mailbox_iter_done(&iter); sd.lastread = mailbox->i.recenttime; sd.lastuid = mailbox->i.recentuid; sd.lastchange = mailbox->i.last_appenddate; sd.seenuids = seqset_cstring(outlist); if (!sd.seenuids) sd.seenuids = xstrdup(""); r = seen_write(seendb, mailbox_uniqueid(mailbox), &sd); seen_freedata(&sd); seqset_free(&outlist); return r; } static int xfer_deactivate(struct xfer_header *xfer) { struct xfer_item *item; int r; syslog(LOG_INFO, "XFER: deactivating mailboxes"); /* Step 3: mupdate.DEACTIVATE(mailbox, newserver) */ for (item = xfer->items; item; item = item->next) { r = xfer_mupdate(MUPDATE_DEACTIVATE, item->mbentry->name, item->mbentry->partition, config_servername, NULL); if (r) { syslog(LOG_ERR, "Could not move mailbox: %s, MUPDATE DEACTIVATE failed", item->mbentry->name); return r; } item->state = XFER_DEACTIVATED; } return 0; } static int xfer_undump(struct xfer_header *xfer) { struct xfer_item *item; int r; mbentry_t *newentry; struct mailbox *mailbox = NULL; syslog(LOG_INFO, "XFER: dumping mailboxes to destination"); for (item = xfer->items; item; item = item->next) { r = mailbox_open_irl(item->mbentry->name, &mailbox); if (r) { syslog(LOG_ERR, "Failed to open mailbox %s for dump_mailbox() %s", item->mbentry->name, error_message(r)); return r; } /* Step 3.5: Set mailbox as MOVING on local server */ /* XXX - this code is awful... need a sane way to manage mbentries */ newentry = mboxlist_entry_create(); newentry->name = xstrdupnull(item->mbentry->name); newentry->acl = xstrdupnull(item->mbentry->acl); newentry->server = xstrdupnull(xfer->toserver); newentry->partition = xstrdupnull(xfer->topart); newentry->mbtype = item->mbentry->mbtype|MBTYPE_MOVING; r = mboxlist_updatelock(newentry, 1); mboxlist_entry_free(&newentry); if (r) { syslog(LOG_ERR, "Could not move mailbox: %s, mboxlist_update() failed %s", item->mbentry->name, error_message(r)); } else item->state = XFER_LOCAL_MOVING; if (!r && xfer->seendb) { /* Backport the user's seendb on-the-fly */ item->mailbox = mailbox; r = xfer_backport_seen_item(item, xfer->seendb); if (r) syslog(LOG_WARNING, "Failed to backport seen state for mailbox '%s'", item->mbentry->name); /* Need to close seendb before dumping Inbox (last item) */ if (!item->next) seen_close(&xfer->seendb); } /* Step 4: Dump local -> remote */ if (!r) { struct backend *be = xfer->sync_cs.backend; prot_printf(be->out, "D01 UNDUMP {" SIZE_T_FMT "+}\r\n%s ", strlen(item->extname), item->extname); r = dump_mailbox(NULL, mailbox, 0, xfer->remoteversion, be->in, be->out, imapd_authstate); if (r) { syslog(LOG_ERR, "Could not move mailbox: %s, dump_mailbox() failed %s", item->mbentry->name, error_message(r)); } } mailbox_close(&mailbox); if (r) return r; struct backend *be = xfer->sync_cs.backend; r = getresult(be->in, "D01"); if (r) { syslog(LOG_ERR, "Could not move mailbox: %s, UNDUMP failed %s", item->mbentry->name, error_message(r)); return r; } /* Step 5: Set ACL on remote */ r = trashacl(be->in, be->out, item->extname); if (r) { syslog(LOG_ERR, "Could not clear remote acl on %s", item->mbentry->name); return r; } r = dumpacl(be->in, be->out, item->extname, item->mbentry->acl); if (r) { syslog(LOG_ERR, "Could not set remote acl on %s", item->mbentry->name); return r; } item->state = XFER_UNDUMPED; } return 0; } static int xfer_addusermbox(const mbentry_t *mbentry, void *rock) { struct xfer_header *xfer = (struct xfer_header *)rock; /* Skip remote mailbox */ if (mbentry->mbtype & MBTYPE_REMOTE) return 0; struct xfer_item *item = xzmalloc(sizeof(struct xfer_item)); item->mbentry = mboxlist_entry_copy(mbentry); char *extname = mboxname_to_external(item->mbentry->name, &imapd_namespace, imapd_userid); xstrncpy(item->extname, extname, sizeof(item->extname)); free(extname); item->mailbox = NULL; item->state = 0; /* and link on to the list (reverse order) */ item->next = xfer->items; xfer->items = item; return 0; } static int xfer_initialsync(struct xfer_header *xfer) { int r; if (xfer->userid) { struct xfer_item *item, *next; syslog(LOG_INFO, "XFER: initial sync of user %s", xfer->userid); r = sync_do_user(&xfer->sync_cs, xfer->userid, xfer->topart); if (r) return r; /* User moves may take a while, do another non-blocking sync */ syslog(LOG_INFO, "XFER: second sync of user %s", xfer->userid); r = sync_do_user(&xfer->sync_cs, xfer->userid, xfer->topart); if (r) return r; /* User may have renamed/deleted a mailbox while syncing, recreate the submailboxes list */ for (item = xfer->items; item; item = next) { next = item->next; mboxlist_entry_free(&item->mbentry); free(item); } xfer->items = NULL; r = mboxlist_usermboxtree(xfer->userid, NULL, xfer_addusermbox, xfer, MBOXTREE_DELETED); } else { struct sync_name_list *mboxname_list = sync_name_list_create(); syslog(LOG_INFO, "XFER: initial sync of mailbox %s", xfer->items->mbentry->name); sync_name_list_add(mboxname_list, xfer->items->mbentry->name); r = sync_do_mailboxes(&xfer->sync_cs, mboxname_list, xfer->topart, xfer->sync_cs.flags); sync_name_list_free(&mboxname_list); } return r; } /* * This is similar to do_folders() from sync_support, but for a single mailbox. * It is needed for xfer_finalsync(), which needs to hold a single exclusive * lock on the mailbox for the duration of this operation. */ static int sync_mailbox(struct xfer_header *xfer, struct mailbox *mailbox, struct sync_folder_list *replica_folders, const char *topart) { int r = 0; struct sync_folder_list *master_folders = NULL; struct sync_reserve_list *reserve_guids = NULL; struct sync_msgid_list *part_list; struct sync_reserve *reserve; struct sync_folder *mfolder, *rfolder; struct sync_annot_list *annots = NULL; modseq_t xconvmodseq = 0; modseq_t raclmodseq = 0; if (!topart) topart = mailbox_partition(mailbox); reserve_guids = sync_reserve_list_create(SYNC_MSGID_LIST_HASH_SIZE); part_list = sync_reserve_partlist(reserve_guids, topart); /* always send mailbox annotations */ r = read_annotations(mailbox, NULL, &annots, /*since_modseq*/0, /*flags*/0); if (r) { syslog(LOG_ERR, "sync_mailbox(): read annotations failed: %s '%s'", mailbox_name(mailbox), error_message(r)); goto cleanup; } /* xconvmodseq */ if (mailbox_has_conversations(mailbox)) { r = mailbox_get_xconvmodseq(mailbox, &xconvmodseq); if (r) { syslog(LOG_ERR, "sync_mailbox(): mailbox get xconvmodseq failed: %s '%s'", mailbox_name(mailbox), error_message(r)); goto cleanup; } } /* raclmodseq */ if (config_getswitch(IMAPOPT_REVERSEACLS)) { raclmodseq = mboxname_readraclmodseq(mailbox_name(mailbox)); } master_folders = sync_folder_list_create(); sync_folder_list_add(master_folders, mailbox_uniqueid(mailbox), mailbox_name(mailbox), mailbox_mbtype(mailbox), mailbox_partition(mailbox), mailbox_acl(mailbox), mailbox->i.options, mailbox->i.uidvalidity, mailbox->i.last_uid, mailbox->i.highestmodseq, mailbox->i.synccrcs, mailbox->i.recentuid, mailbox->i.recenttime, mailbox->i.pop3_last_login, mailbox->i.pop3_show_after, annots, xconvmodseq, raclmodseq, mailbox_foldermodseq(mailbox), /* ispartial */0); annots = NULL; /* list took ownership */ mfolder = master_folders->head; /* when mfolder->mailbox is set, sync_update_mailbox will use it rather * than obtaining its own (short-lived) locks */ mfolder->mailbox = mailbox; uint32_t fromuid = 0; rfolder = sync_folder_lookup(replica_folders, mfolder->uniqueid); if (rfolder) { rfolder->mark = 1; /* does it need a rename? */ if (strcmp(mfolder->name, rfolder->name) || strcmp(topart, rfolder->part)) { /* bail and retry */ syslog(LOG_NOTICE, "XFER: rename %s!%s -> %s!%s during final sync" " - must try XFER again", mfolder->name, mfolder->part, rfolder->name, rfolder->part); r = IMAP_AGAIN; goto cleanup; } fromuid = rfolder->last_uid; } sync_find_reserve_messages(mailbox, fromuid, mailbox->i.last_uid, part_list); reserve = reserve_guids->head; r = sync_reserve_partition(&xfer->sync_cs, reserve->part, replica_folders, reserve->list); if (r) { syslog(LOG_ERR, "sync_mailbox(): reserve partition failed: %s '%s'", mfolder->name, error_message(r)); goto cleanup; } r = sync_do_update_mailbox(&xfer->sync_cs, mfolder, rfolder, topart, reserve_guids); if (r) { syslog(LOG_ERR, "sync_mailbox(): update failed: %s '%s'", mfolder->name, error_message(r)); } cleanup: sync_reserve_list_free(&reserve_guids); sync_folder_list_free(&master_folders); sync_annot_list_free(&annots); return r; } static int xfer_finalsync(struct xfer_header *xfer) { struct sync_name_list *master_quotaroots = sync_name_list_create(); struct sync_folder_list *replica_folders = sync_folder_list_create(); struct sync_folder *rfolder; struct sync_name_list *replica_subs = NULL; struct sync_sieve_list *replica_sieve = NULL; struct sync_seen_list *replica_seen = NULL; struct sync_quota_list *replica_quota = sync_quota_list_create(); const char *cmd; struct dlist *kl = NULL; struct xfer_item *item; struct mailbox *mailbox = NULL; int r; if (xfer->userid) { syslog(LOG_INFO, "XFER: final sync of user %s", xfer->userid); replica_subs = sync_name_list_create(); replica_sieve = sync_sieve_list_create(); replica_seen = sync_seen_list_create(); cmd = "USER"; kl = dlist_setatom(NULL, cmd, xfer->userid); } else { syslog(LOG_INFO, "XFER: final sync of mailbox %s", xfer->items->mbentry->name); cmd = "MAILBOXES"; kl = dlist_newlist(NULL, cmd); dlist_setatom(kl, "MBOXNAME", xfer->items->mbentry->name); } sync_send_lookup(kl, xfer->sync_cs.backend->out); dlist_free(&kl); r = sync_response_parse(&xfer->sync_cs, cmd, replica_folders, replica_subs, replica_sieve, replica_seen, replica_quota); if (r) goto done; for (item = xfer->items; item; item = item->next) { if (mbtype_isa(item->mbentry->mbtype) == MBTYPE_SIEVE && !(xfer->sync_cs.flags & SYNC_FLAG_SIEVE_MAILBOX)) { /* Ignore #sieve mailbox - replicated via *SIEVE* commands */ continue; } r = mailbox_open_iwl(item->mbentry->name, &mailbox); if (!r) r = sync_mailbox_version_check(&mailbox); if (r) { syslog(LOG_ERR, "Failed to open mailbox %s for xfer_final_sync() %s", item->mbentry->name, error_message(r)); goto done; } /* Open cyrus.annotations before we set mailbox to MOVING and change its location to destination server and partition */ r = mailbox_get_annotate_state(mailbox, ANNOTATE_ANY_UID, NULL); if (r) { syslog(LOG_ERR, "Failed to get annotate state for mailbox %s" " for xfer_final_sync() %s", mailbox_name(mailbox), error_message(r)); mailbox_close(&mailbox); goto done; } /* Step 3.5: Set mailbox as MOVING on local server */ /* XXX - this code is awful... need a sane way to manage mbentries */ mbentry_t *newmbentry = mboxlist_entry_create(); newmbentry->name = xstrdupnull(item->mbentry->name); newmbentry->acl = xstrdupnull(item->mbentry->acl); newmbentry->uniqueid = xstrdupnull(item->mbentry->uniqueid); newmbentry->uidvalidity = item->mbentry->uidvalidity; newmbentry->mbtype = item->mbentry->mbtype|MBTYPE_MOVING; newmbentry->server = xstrdupnull(xfer->toserver); newmbentry->partition = xstrdupnull(xfer->topart); r = mboxlist_update(newmbentry, 1); mboxlist_entry_free(&newmbentry); if (r) { syslog(LOG_ERR, "Could not move mailbox: %s, mboxlist_update() failed %s", item->mbentry->name, error_message(r)); } else item->state = XFER_LOCAL_MOVING; /* Step 4: Sync local -> remote */ if (!r) { r = sync_mailbox(xfer, mailbox, replica_folders, xfer->topart); if (r) { syslog(LOG_ERR, "Could not move mailbox: %s, sync_mailbox() failed %s", item->mbentry->name, error_message(r)); } else { const char *quotaroot = mailbox_quotaroot(mailbox); if (quotaroot) sync_name_list_add(master_quotaroots, quotaroot); r = sync_do_annotation(&xfer->sync_cs, mailbox_name(mailbox)); if (r) { syslog(LOG_ERR, "Could not move mailbox: %s," " sync_do_annotation() failed %s", item->mbentry->name, error_message(r)); } } } mailbox_close(&mailbox); if (r) goto done; item->state = XFER_UNDUMPED; } /* Delete folders on replica which no longer exist on master */ for (rfolder = replica_folders->head; rfolder; rfolder = rfolder->next) { if (rfolder->mark) continue; r = sync_do_folder_delete(&xfer->sync_cs, rfolder->name); if (r) { syslog(LOG_ERR, "sync_folder_delete(): failed: %s '%s'", rfolder->name, error_message(r)); goto done; } } /* Handle any mailbox/user metadata */ r = sync_do_user_quota(&xfer->sync_cs, master_quotaroots, replica_quota); if (!r && xfer->userid) { r = sync_do_user_seen(&xfer->sync_cs, xfer->userid, replica_seen); if (!r) r = sync_do_user_sub(&xfer->sync_cs, xfer->userid, replica_subs); if (!r && !(xfer->sync_cs.flags & SYNC_FLAG_SIEVE_MAILBOX)) { r = sync_do_user_sieve(&xfer->sync_cs, xfer->userid, replica_sieve); } } done: sync_name_list_free(&master_quotaroots); sync_folder_list_free(&replica_folders); if (replica_subs) sync_name_list_free(&replica_subs); if (replica_sieve) sync_sieve_list_free(&replica_sieve); if (replica_seen) sync_seen_list_free(&replica_seen); if (replica_quota) sync_quota_list_free(&replica_quota); return r; } static int xfer_reactivate(struct xfer_header *xfer) { struct xfer_item *item; int r; syslog(LOG_INFO, "XFER: reactivating mailboxes"); if (!mupdate_h) return 0; /* 6.5) Kick remote server to correct mupdate entry */ for (item = xfer->items; item; item = item->next) { if (mbtype_isa(item->mbentry->mbtype) == MBTYPE_SIEVE && !(xfer->sync_cs.flags & SYNC_FLAG_SIEVE_MAILBOX)) { /* Don't activate #sieve on remote, remove it from mupdate */ r = xfer_mupdate(MUPDATE_DELETE, item->mbentry->name, NULL, NULL, NULL); if (r) { syslog(LOG_ERR, "MUPDATE: can't delete mailbox entry '%s': %s", item->mbentry->name, error_message(r)); } continue; } struct backend *be = xfer->sync_cs.backend; prot_printf(be->out, "MP1 MUPDATEPUSH {" SIZE_T_FMT "+}\r\n%s\r\n", strlen(item->extname), item->extname); r = getresult(be->in, "MP1"); if (r) { syslog(LOG_ERR, "MUPDATE: can't activate mailbox entry '%s': %s", item->mbentry->name, error_message(r)); } } return 0; } static int xfer_delete(struct xfer_header *xfer) { struct xfer_item *item; int r; syslog(LOG_INFO, "XFER: deleting mailboxes on source"); /* 7) local delete of mailbox * & remove local "remote" mailboxlist entry */ for (item = xfer->items; item; item = item->next) { /* Note that we do not check the ACL, and we don't update MUPDATE */ /* note also that we need to remember to let proxyadmins do this */ /* On a unified system, the subsequent MUPDATE PUSH on the remote should repopulate the local mboxlist entry */ r = mboxlist_deletemailboxlock(item->mbentry->name, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate, NULL, MBOXLIST_DELETE_LOCALONLY|MBOXLIST_DELETE_FORCE); if (r) { syslog(LOG_ERR, "Could not delete local mailbox during move of %s: %s", item->mbentry->name, error_message(r)); /* can't abort now! */ } } return 0; } static void xfer_recover(struct xfer_header *xfer) { struct xfer_item *item; int r; syslog(LOG_INFO, "XFER: recovering"); /* Backout any changes - we stop on first untouched mailbox */ for (item = xfer->items; item && item->state; item = item->next) { switch (item->state) { case XFER_UNDUMPED: case XFER_LOCAL_MOVING: /* Unset mailbox as MOVING on local server */ r = mboxlist_updatelock(item->mbentry, 1); if (r) { syslog(LOG_ERR, "Could not back out MOVING flag during move of %s (%s)", item->mbentry->name, error_message(r)); } GCC_FALLTHROUGH case XFER_REMOTE_CREATED: if (!xfer->use_replication) { /* Delete remote mailbox */ prot_printf(xfer->sync_cs.backend->out, "LD1 LOCALDELETE {" SIZE_T_FMT "+}\r\n%s\r\n", strlen(item->extname), item->extname); r = getresult(xfer->sync_cs.backend->in, "LD1"); if (r) { syslog(LOG_ERR, "Could not back out remote mailbox during move of %s (%s)", item->mbentry->name, error_message(r)); } } GCC_FALLTHROUGH case XFER_DEACTIVATED: /* Tell murder it's back here and active */ r = xfer_mupdate(MUPDATE_ACTIVATE, item->mbentry->name, item->mbentry->partition, config_servername, item->mbentry->acl); if (r) { syslog(LOG_ERR, "Could not back out mupdate during move of %s (%s)", item->mbentry->name, error_message(r)); } } } } static int do_xfer(struct xfer_header *xfer) { int r = 0; if (xfer->use_replication) { /* Initial non-blocking sync */ r = xfer_initialsync(xfer); if (r) return r; } r = xfer_deactivate(xfer); if (!r) { if (xfer->use_replication) { /* Final sync with write locks on mailboxes */ r = xfer_finalsync(xfer); } else { r = xfer_localcreate(xfer); if (!r) r = xfer_undump(xfer); } } if (r) { /* Something failed, revert back to local server */ xfer_recover(xfer); return r; } /* Successful dump of all mailboxes to remote server. * Remove them locally and activate them on remote. * Note - we don't report errors if this fails! */ xfer_delete(xfer); xfer_reactivate(xfer); return 0; } static int xfer_setquotaroot(struct xfer_header *xfer, const char *mboxname) { struct quota q; int r; syslog(LOG_INFO, "XFER: setting quota root %s", mboxname); quota_init(&q, mboxname); r = quota_read(&q, NULL, 0); if (r == IMAP_QUOTAROOT_NONEXISTENT) return 0; if (r) return r; /* note use of + to force the setting of a nonexistent * quotaroot */ char *extname = mboxname_to_external(mboxname, &imapd_namespace, imapd_userid); prot_printf(xfer->sync_cs.backend->out, "Q01 SETQUOTA {" SIZE_T_FMT "+}\r\n+%s ", strlen(extname)+1, extname); free(extname); print_quota_limits(xfer->sync_cs.backend->out, &q); prot_printf(xfer->sync_cs.backend->out, "\r\n"); quota_free(&q); r = getresult(xfer->sync_cs.backend->in, "Q01"); if (r) syslog(LOG_ERR, "Could not move mailbox: %s, " \ "failed setting initial quota root", mboxname); return r; } struct xfer_list { const struct namespace *ns; const char *userid; const char *part; struct xfer_item *mboxes; }; static int xfer_addmbox(struct findall_data *data, void *rock) { if (!data) return 0; if (!data->is_exactmatch) return 0; struct xfer_list *list = (struct xfer_list *) rock; if (list->part && strcmp(data->mbentry->partition, list->part)) { /* Not on specified partition */ return 0; } /* Only add shared mailboxes or user INBOXes */ if (!mbname_localpart(data->mbname) || (!mbname_isdeleted(data->mbname) && !strarray_size(mbname_boxes(data->mbname)))) { const char *extname = mbname_extname(data->mbname, list->ns, list->userid); struct xfer_item *mbox = xzmalloc(sizeof(struct xfer_item)); mbox->mbentry = mboxlist_entry_copy(data->mbentry); xstrncpy(mbox->extname, extname, sizeof(mbox->extname)); if (mbname_localpart(data->mbname)) { /* User INBOX */ mbox->state = XFER_MOVING_USER; } /* Add link on to the list (reverse order) */ mbox->next = list->mboxes; list->mboxes = mbox; } return 0; } static void cmd_xfer(const char *tag, const char *name, const char *toserver, const char *topart) { int r = 0, partial_success = 0, mbox_count = 0; struct xfer_header *xfer = NULL; struct xfer_list list = { &imapd_namespace, imapd_userid, NULL, NULL }; struct xfer_item *item, *next; mbname_t *mbname = mbname_from_extname(name, &imapd_namespace, imapd_userid); /* administrators only please */ /* however, proxys can do this, if their authzid is an admin */ if (!imapd_userisadmin && !imapd_userisproxyadmin) { r = IMAP_PERMISSION_DENIED; goto done; } if (!strcmp(toserver, config_servername)) { r = IMAP_BAD_SERVER; goto done; } /* Build list of users/mailboxes to transfer */ if (config_partitiondir(name)) { /* entire partition */ list.part = name; mboxlist_findall(NULL, "*", 1, NULL, NULL, xfer_addmbox, &list); } else { /* mailbox pattern */ if (mbname_localpart(mbname) && (mbname_isdeleted(mbname) || strarray_size(mbname_boxes(mbname)))) { /* targeted a user submailbox */ r = IMAP_MAILBOX_NOTSUPPORTED; goto done; } /* admin namespace, use original name */ mboxlist_findall(NULL, name, 1, NULL, NULL, xfer_addmbox, &list); } /* bail out if we didn't find anything to do */ if (!list.mboxes) { r = IMAP_MAILBOX_NONEXISTENT; goto done; } r = xfer_init(toserver, &xfer); if (r) goto done; for (item = list.mboxes; item; item = next) { mbentry_t *mbentry = item->mbentry; /* NOTE: Since XFER can only be used by an admin, and we always connect * to the destination backend as an admin, we take advantage of the fact * that admins *always* use a consistent mailbox naming scheme. * So, 'name' should be used in any command we send to a backend, and * 'mbentry->name' is the internal name to be used for mupdate and findall. */ if (mbtype_isa(mbentry->mbtype) == MBTYPE_SIEVE && !(xfer->use_replication & SYNC_FLAG_SIEVE_MAILBOX)) { /* Ignore #sieve mailbox - replicated via *SIEVE* commands */ mboxlist_entry_free(&mbentry); next = item->next; free(item); continue; } r = 0; xfer->topart = xstrdup(topart ? topart : mbentry->partition); /* if we are not moving a user, just move the one mailbox */ if (item->state != XFER_MOVING_USER) { syslog(LOG_INFO, "XFER: mailbox '%s' -> %s!%s", mbentry->name, xfer->toserver, xfer->topart); /* is the selected mailbox the one we're moving? */ if (!strcmpsafe(mbentry->name, index_mboxname(imapd_index))) { r = IMAP_MAILBOX_LOCKED; goto next; } /* we're moving this mailbox */ xfer_addusermbox(mbentry, xfer); mbox_count++; r = do_xfer(xfer); } else { xfer->userid = xstrdupnull(mbname_userid(mbname)); syslog(LOG_INFO, "XFER: user '%s' -> %s!%s", xfer->userid, xfer->toserver, xfer->topart); if (!config_getswitch(IMAPOPT_ALLOWUSERMOVES)) { /* not configured to allow user moves */ r = IMAP_MAILBOX_NOTSUPPORTED; } else if (!strcmp(xfer->userid, imapd_userid)) { /* don't move your own inbox, that could be troublesome */ r = IMAP_MAILBOX_NOTSUPPORTED; } else if (!strncmpsafe(mbentry->name, index_mboxname(imapd_index), strlen(mbentry->name))) { /* selected mailbox is in the namespace we're moving */ r = IMAP_MAILBOX_LOCKED; } if (r) goto next; struct mboxlock *namespacelock = user_namespacelock(xfer->userid); if (!xfer->use_replication) { /* set the quotaroot if needed */ r = xfer_setquotaroot(xfer, mbentry->name); if (r) { mboxname_release(&namespacelock); goto next; } /* backport the seen file if needed */ if (xfer->remoteversion < 12) { r = seen_open(xfer->userid, SEEN_CREATE, &xfer->seendb); if (r) { mboxname_release(&namespacelock); goto next; } } } mbentry_t *inbox_mbentry = NULL; char *inbox = mboxname_user_mbox(xfer->userid, 0); r = mboxlist_lookup_allow_all(inbox, &inbox_mbentry, NULL); free(inbox); if (r) { mboxname_release(&namespacelock); mboxlist_entry_free(&inbox_mbentry); goto next; } r = mboxlist_usermboxtree(xfer->userid, NULL, xfer_addusermbox, xfer, MBOXTREE_DELETED); /* NOTE: mailboxes were added in reverse, so the inbox is * done last */ if (!r) r = do_xfer(xfer); if (!r) { /* this was a successful user move, and we need to delete certain user meta-data (but not seen state!) */ syslog(LOG_INFO, "XFER: deleting user metadata"); user_deletedata(inbox_mbentry, 0); } mboxname_release(&namespacelock); mboxlist_entry_free(&inbox_mbentry); } next: if (r) { if (xfer->userid) prot_printf(imapd_out, "* NO USER %s (%s)\r\n", xfer->userid, error_message(r)); else prot_printf(imapd_out, "* NO MAILBOX \"%s\" (%s)\r\n", item->extname, error_message(r)); } else { partial_success = 1; if (xfer->userid) prot_printf(imapd_out, "* OK USER %s\r\n", xfer->userid); else prot_printf(imapd_out, "* OK MAILBOX \"%s\"\r\n", item->extname); } prot_flush(imapd_out); mboxlist_entry_free(&mbentry); next = item->next; free(item); if (xfer->use_replication && (xfer->userid || mbox_count > 1000)) { /* RESTART after each user or after every 1000 mailboxes */ mbox_count = 0; r = sync_do_restart(&xfer->sync_cs); if (r) goto done; } xfer_cleanup(xfer); if (partial_success) r = 0; } done: mbname_free(&mbname); if (xfer) xfer_done(&xfer); imapd_check(NULL, 0); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } return; } #define SORTGROWSIZE 10 /* * Parse sort criteria */ static int getsortcriteria(char *tag, struct sortcrit **sortcrit) { int c; static struct buf criteria; int nsort, n; int hasconv = config_getswitch(IMAPOPT_CONVERSATIONS); *sortcrit = NULL; c = prot_getc(imapd_in); if (c != '(') goto missingcrit; c = getword(imapd_in, &criteria); if (criteria.s[0] == '\0') goto missingcrit; nsort = 0; n = 0; for (;;) { if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (n >= nsort - 1) { /* leave room for implicit criterion */ /* (Re)allocate an array for sort criteria */ nsort += SORTGROWSIZE; *sortcrit = (struct sortcrit *) xrealloc(*sortcrit, nsort * sizeof(struct sortcrit)); /* Zero out the newly added sortcrit */ memset((*sortcrit)+n, 0, SORTGROWSIZE * sizeof(struct sortcrit)); } lcase(criteria.s); if (!strcmp(criteria.s, "reverse")) { (*sortcrit)[n].flags |= SORT_REVERSE; goto nextcrit; } else if (!strcmp(criteria.s, "arrival")) (*sortcrit)[n].key = SORT_ARRIVAL; else if (!strcmp(criteria.s, "cc")) (*sortcrit)[n].key = SORT_CC; else if (!strcmp(criteria.s, "date")) (*sortcrit)[n].key = SORT_DATE; else if (!strcmp(criteria.s, "displayfrom")) (*sortcrit)[n].key = SORT_DISPLAYFROM; else if (!strcmp(criteria.s, "displayto")) (*sortcrit)[n].key = SORT_DISPLAYTO; else if (!strcmp(criteria.s, "from")) (*sortcrit)[n].key = SORT_FROM; else if (!strcmp(criteria.s, "size")) (*sortcrit)[n].key = SORT_SIZE; else if (!strcmp(criteria.s, "subject")) (*sortcrit)[n].key = SORT_SUBJECT; else if (!strcmp(criteria.s, "to")) (*sortcrit)[n].key = SORT_TO; else if (!strcmp(criteria.s, "annotation")) { const char *userid = NULL; (*sortcrit)[n].key = SORT_ANNOTATION; if (c != ' ') goto missingarg; c = getastring(imapd_in, imapd_out, &criteria); if (c == IMAP_LITERAL_TOO_LARGE) return c; if (c != ' ') goto missingarg; (*sortcrit)[n].args.annot.entry = xstrdup(criteria.s); c = getastring(imapd_in, imapd_out, &criteria); if (c == IMAP_LITERAL_TOO_LARGE) return c; if (c == EOF) goto missingarg; if (!strcmp(criteria.s, "value.shared")) userid = ""; else if (!strcmp(criteria.s, "value.priv")) userid = imapd_userid; else goto missingarg; (*sortcrit)[n].args.annot.userid = xstrdup(userid); } else if (!strcmp(criteria.s, "modseq")) (*sortcrit)[n].key = SORT_MODSEQ; else if (!strcmp(criteria.s, "uid")) (*sortcrit)[n].key = SORT_UID; else if (!strcmp(criteria.s, "hasflag")) { (*sortcrit)[n].key = SORT_HASFLAG; if (c != ' ') goto missingarg; c = getastring(imapd_in, imapd_out, &criteria); if (c == IMAP_LITERAL_TOO_LARGE) return c; if (c == EOF) goto missingarg; (*sortcrit)[n].args.flag.name = xstrdup(criteria.s); } else if (hasconv && !strcmp(criteria.s, "convmodseq")) (*sortcrit)[n].key = SORT_CONVMODSEQ; else if (hasconv && !strcmp(criteria.s, "convexists")) (*sortcrit)[n].key = SORT_CONVEXISTS; else if (hasconv && !strcmp(criteria.s, "convsize")) (*sortcrit)[n].key = SORT_CONVSIZE; else if (hasconv && !strcmp(criteria.s, "hasconvflag")) { (*sortcrit)[n].key = SORT_HASCONVFLAG; if (c != ' ') goto missingarg; c = getastring(imapd_in, imapd_out, &criteria); if (c == IMAP_LITERAL_TOO_LARGE) return c; if (c == EOF) goto missingarg; (*sortcrit)[n].args.flag.name = xstrdup(criteria.s); } else if (!strcmp(criteria.s, "folder")) (*sortcrit)[n].key = SORT_FOLDER; else if (!strcmp(criteria.s, "relevancy")) (*sortcrit)[n].key = SORT_RELEVANCY; else if (!strcmp(criteria.s, "spamscore")) (*sortcrit)[n].key = SORT_SPAMSCORE; else { prot_printf(imapd_out, "%s BAD Invalid Sort criterion %s\r\n", tag, criteria.s); if (c != EOF) prot_ungetc(c, imapd_in); return EOF; } n++; nextcrit: if (c == ' ') c = getword(imapd_in, &criteria); else break; } if ((*sortcrit)[n].flags & SORT_REVERSE && !(*sortcrit)[n].key) { prot_printf(imapd_out, "%s BAD Missing Sort criterion to reverse\r\n", tag); if (c != EOF) prot_ungetc(c, imapd_in); return EOF; } if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close parenthesis in Sort\r\n", tag); if (c != EOF) prot_ungetc(c, imapd_in); return EOF; } /* Terminate the list with the implicit sort criterion */ (*sortcrit)[n++].key = SORT_SEQUENCE; c = prot_getc(imapd_in); return c; missingcrit: prot_printf(imapd_out, "%s BAD Missing Sort criteria\r\n", tag); if (c != EOF) prot_ungetc(c, imapd_in); return EOF; missingarg: prot_printf(imapd_out, "%s BAD Missing argument to Sort criterion %s\r\n", tag, criteria.s); if (c != EOF) prot_ungetc(c, imapd_in); return EOF; } /* * Parse LIST selection options. * The command has been parsed up to and including the opening '('. */ static int getlistselopts(char *tag, struct listargs *args) { int c; static struct buf buf; int allowdeleted = config_getswitch(IMAPOPT_ALLOWDELETED); if ( (c = prot_getc(imapd_in)) == ')') return prot_getc(imapd_in); else prot_ungetc(c, imapd_in); for (;;) { c = getword(imapd_in, &buf); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (!*buf.s) { prot_printf(imapd_out, "%s BAD Invalid syntax in List command\r\n", tag); goto bad; } lcase(buf.s); if (!strcmp(buf.s, "subscribed")) { args->sel |= LIST_SEL_SUBSCRIBED; args->ret |= LIST_RET_SUBSCRIBED; } else if (!strcmp(buf.s, "vendor.cmu-dav")) { args->sel |= LIST_SEL_DAV; } else if (!strcmp(buf.s, "vendor.cmu-include-deleted") && allowdeleted) { args->sel |= LIST_SEL_DELETED; } else if (!strcmp(buf.s, "vendor.fm-include-nonexistent")) { args->sel |= LIST_SEL_INTERMEDIATES; } else if (!strcmp(buf.s, "remote")) { args->sel |= LIST_SEL_REMOTE; } else if (!strcmp(buf.s, "recursivematch")) { args->sel |= LIST_SEL_RECURSIVEMATCH; } else if (!strcmp(buf.s, "special-use")) { args->sel |= LIST_SEL_SPECIALUSE; args->ret |= LIST_RET_SPECIALUSE; } else if (!strcmp(buf.s, "metadata")) { struct getmetadata_options opts = OPTS_INITIALIZER; args->sel |= LIST_SEL_METADATA; args->ret |= LIST_RET_METADATA; strarray_t options = STRARRAY_INITIALIZER; c = parse_metadata_string_or_list(tag, &options, NULL); if (c <= EOF) return c; parse_getmetadata_options(&options, &opts); args->metaopts = opts; strarray_fini(&options); } else { prot_printf(imapd_out, "%s BAD Invalid List selection option \"%s\"\r\n", tag, buf.s); goto bad; } if (c != ' ') break; } if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close parenthesis for List selection options\r\n", tag); goto bad; } if (args->sel & list_select_mod_opts && ! (args->sel & list_select_base_opts)) { prot_printf(imapd_out, "%s BAD Invalid combination of selection options\r\n", tag); goto bad; } return prot_getc(imapd_in); bad: eatline(imapd_in, c); return EOF; } /* * Parse LIST return options. * The command has been parsed up to and including the ' ' before RETURN. */ static int getlistretopts(char *tag, struct listargs *args) { static struct buf buf; int c; c = getword(imapd_in, &buf); if (!*buf.s) { prot_printf(imapd_out, "%s BAD Invalid syntax in List command\r\n", tag); goto bad; } lcase(buf.s); if (strcasecmp(buf.s, "return")) { prot_printf(imapd_out, "%s BAD Unexpected extra argument to List: \"%s\"\r\n", tag, buf.s); goto bad; } if (c != ' ' || (c = prot_getc(imapd_in)) != '(') { prot_printf(imapd_out, "%s BAD Missing return argument list\r\n", tag); goto bad; } if ( (c = prot_getc(imapd_in)) == ')') return prot_getc(imapd_in); else prot_ungetc(c, imapd_in); for (;;) { c = getword(imapd_in, &buf); if (prot_bytes_in(imapd_in) > maxargssize_mark) fatal(error_message(IMAP_ARGS_TOO_LARGE), EX_PROTOCOL); if (!*buf.s) { prot_printf(imapd_out, "%s BAD Invalid syntax in List command\r\n", tag); goto bad; } lcase(buf.s); if (!strcmp(buf.s, "subscribed")) args->ret |= LIST_RET_SUBSCRIBED; else if (!strcmp(buf.s, "children")) args->ret |= LIST_RET_CHILDREN; else if (!strcmp(buf.s, "myrights")) args->ret |= LIST_RET_MYRIGHTS; else if (!strcmp(buf.s, "special-use")) args->ret |= LIST_RET_SPECIALUSE; else if (!strcmp(buf.s, "status")) { const char *errstr = "Bad status string"; args->ret |= LIST_RET_STATUS; c = parse_statusitems(&args->statusitems, &errstr); if (c == EOF) { prot_printf(imapd_out, "%s BAD %s\r\n", tag, errstr); return EOF; } } else if (!strcmp(buf.s, "metadata")) { args->ret |= LIST_RET_METADATA; /* outputs the error for us */ c = parse_metadata_string_or_list(tag, &args->metaitems, NULL); if (c <= EOF) return c; } else { prot_printf(imapd_out, "%s BAD Invalid List return option \"%s\"\r\n", tag, buf.s); goto bad; } if (c != ' ') break; } if (c != ')') { prot_printf(imapd_out, "%s BAD Missing close parenthesis for List return options\r\n", tag); goto bad; } return prot_getc(imapd_in); bad: eatline(imapd_in, c); return EOF; } /* * Parse a string in IMAP date-time format (and some more * obscure legacy formats too) to a time_t. Parses both * date and time parts. See cyrus_parsetime() for formats. * * Returns: the next character read from imapd_in, or * or EOF on error. */ static int getdatetime(time_t *date) { int c; int r; int i = 0; char buf[RFC3501_DATETIME_MAX+1]; c = prot_getc(imapd_in); if (c != '\"') goto baddate; while ((c = prot_getc(imapd_in)) != '\"') { if (i >= RFC3501_DATETIME_MAX) goto baddate; buf[i++] = c; } buf[i] = '\0'; r = time_from_rfc5322(buf, date, DATETIME_FULL); if (r < 0) goto baddate; c = prot_getc(imapd_in); return c; baddate: if (c != EOF) prot_ungetc(c, imapd_in); return EOF; } /* * Append 'section', 'fields', 'trail' to the fieldlist 'l'. */ static void appendfieldlist(struct fieldlist **l, char *section, strarray_t *fields, char *trail, void *d, size_t size) { struct fieldlist **tail = l; while (*tail) tail = &(*tail)->next; *tail = (struct fieldlist *)xmalloc(sizeof(struct fieldlist)); (*tail)->section = xstrdup(section); (*tail)->fields = fields; (*tail)->trail = xstrdup(trail); if(d && size) { (*tail)->rock = xmalloc(size); memcpy((*tail)->rock, d, size); } else { (*tail)->rock = NULL; } (*tail)->next = 0; } /* * Free the fieldlist 'l' */ static void freefieldlist(struct fieldlist *l) { struct fieldlist *n; while (l) { n = l->next; free(l->section); strarray_free(l->fields); free(l->trail); if (l->rock) free(l->rock); free((char *)l); l = n; } } static int set_haschildren(const mbentry_t *mbentry __attribute__((unused)), void *rock) { uint32_t *attributes = (uint32_t *)rock; list_callback_calls++; *attributes |= MBOX_ATTRIBUTE_HASCHILDREN; return CYRUSDB_DONE; } static void specialuse_flags(const mbentry_t *mbentry, struct buf *attrib, int isxlist) { if (!mbentry) return; char *inbox = mboxname_user_mbox(imapd_userid, NULL); int inboxlen = strlen(inbox); /* doesn't match inbox, not xlistable */ if (strncmp(mbentry->name, inbox, inboxlen)) { free(inbox); return; } /* inbox - only print if command is XLIST */ if (mbentry->name[inboxlen] == '\0') { if (isxlist) buf_init_ro_cstr(attrib, "\\Inbox"); } /* subdir */ else if (mbentry->name[inboxlen] == '.') { /* check if there's a special use flag set */ annotatemore_lookup(mbentry->name, "/specialuse", imapd_userid, attrib); } free(inbox); /* otherwise it's actually another user who matches for * the substr. Ok to just print nothing */ } static void printmetadata(const mbentry_t *mbentry, const strarray_t *entries, struct getmetadata_options *opts) { annotate_state_t *astate = annotate_state_new(); strarray_t newa = STRARRAY_INITIALIZER; strarray_t newe = STRARRAY_INITIALIZER; annotate_state_set_auth(astate, imapd_userisadmin || imapd_userisproxyadmin, imapd_userid, imapd_authstate); int r = annotate_state_set_mailbox_mbe(astate, mbentry); if (r) goto done; r = _metadata_to_annotate(entries, &newa, &newe, NULL, opts->depth); if (r) goto done; annotate_state_fetch(astate, &newe, &newa, getmetadata_response, opts); getmetadata_response(NULL, 0, NULL, NULL, opts); done: annotate_state_abort(&astate); } /* Print LIST or LSUB untagged response */ static void list_response(const char *extname, const mbentry_t *mbentry, uint32_t attributes, struct listargs *listargs) { int r; struct statusdata sdata = STATUSDATA_INIT; struct buf specialuse = BUF_INITIALIZER; /* Intermediates don't actually exist */ if (mbentry && (mbentry->mbtype & MBTYPE_INTERMEDIATE)) { attributes |= MBOX_ATTRIBUTE_NONEXISTENT; } if ((attributes & MBOX_ATTRIBUTE_NONEXISTENT)) { if (!(listargs->cmd == LIST_CMD_EXTENDED)) { attributes |= MBOX_ATTRIBUTE_NOSELECT; attributes &= ~MBOX_ATTRIBUTE_NONEXISTENT; } } else if (listargs->scan) { /* SCAN mailbox for content */ if (!strcmpsafe(mbentry->name, index_mboxname(imapd_index))) { /* currently selected mailbox */ if (!index_scan(imapd_index, listargs->scan)) return; /* no matching messages */ } else { /* other local mailbox */ struct index_state *state; struct index_init init; int doclose = 0; memset(&init, 0, sizeof(struct index_init)); init.userid = imapd_userid; init.authstate = imapd_authstate; init.out = imapd_out; r = index_open(mbentry->name, &init, &state); if (!r) doclose = 1; if (!r && index_hasrights(state, ACL_READ)) { r = (imapd_userisadmin || index_hasrights(state, ACL_LOOKUP)) ? IMAP_PERMISSION_DENIED : IMAP_MAILBOX_NONEXISTENT; } if (!r) { if (!index_scan(state, listargs->scan)) { r = -1; /* no matching messages */ } } if (doclose) index_close(&state); if (r) return; } } /* figure out \Has(No)Children if necessary This is mainly used for LIST (SUBSCRIBED) RETURN (CHILDREN) */ uint32_t have_childinfo = MBOX_ATTRIBUTE_HASCHILDREN | MBOX_ATTRIBUTE_HASNOCHILDREN; if ((listargs->ret & LIST_RET_CHILDREN) && !(attributes & have_childinfo)) { if (imapd_namespace.isalt && !strcmp(extname, "INBOX")) { /* don't look inside INBOX under altnamespace, its children aren't children */ } else { char *intname = NULL, *freeme = NULL; /* if we got here via subscribed_cb, mbentry isn't set */ if (mbentry) intname = mbentry->name; else intname = freeme = mboxname_from_external(extname, &imapd_namespace, imapd_userid); mboxlist_mboxtree(intname, set_haschildren, &attributes, MBOXTREE_SKIP_ROOT); if (freeme) free(freeme); } if (!(attributes & MBOX_ATTRIBUTE_HASCHILDREN)) attributes |= MBOX_ATTRIBUTE_HASNOCHILDREN; } if (attributes & (MBOX_ATTRIBUTE_NONEXISTENT | MBOX_ATTRIBUTE_NOSELECT)) { int keep = 0; /* extended get told everything */ if (listargs->cmd == LIST_CMD_EXTENDED) { keep = 1; } /* we have to mention this, it has children */ if (listargs->cmd == LIST_CMD_LSUB) { /* subscribed children need a mention */ if (attributes & MBOX_ATTRIBUTE_CHILDINFO_SUBSCRIBED) keep = 1; /* if mupdate is configured we can't drop out, we might * be a backend and need to report folders that don't * exist on this backend - this is awful and complex * and brittle and should be changed */ if (config_mupdate_server) keep = 1; } else if (attributes & MBOX_ATTRIBUTE_HASCHILDREN) keep = 1; if (!keep) return; } if (listargs->cmd == LIST_CMD_LSUB) { /* \Noselect has a special second meaning with (R)LSUB */ if ( !(attributes & MBOX_ATTRIBUTE_SUBSCRIBED) && attributes & MBOX_ATTRIBUTE_CHILDINFO_SUBSCRIBED) attributes |= MBOX_ATTRIBUTE_NOSELECT | MBOX_ATTRIBUTE_HASCHILDREN; attributes &= ~MBOX_ATTRIBUTE_SUBSCRIBED; } /* As CHILDINFO extended data item is not allowed if the * RECURSIVEMATCH selection option is not specified */ if (!(listargs->sel & LIST_SEL_RECURSIVEMATCH)) { attributes &= ~MBOX_ATTRIBUTE_CHILDINFO_SUBSCRIBED; } /* no inferiors means no children (this basically means the INBOX * in alt namespace mode */ if (attributes & MBOX_ATTRIBUTE_NOINFERIORS) attributes &= ~MBOX_ATTRIBUTE_HASCHILDREN; /* you can't have both! If it's had children, it has children */ if (attributes & MBOX_ATTRIBUTE_HASCHILDREN) attributes &= ~MBOX_ATTRIBUTE_HASNOCHILDREN; /* remove redundant flags */ if (listargs->cmd == LIST_CMD_EXTENDED) { /* \NoInferiors implies \HasNoChildren */ if (attributes & MBOX_ATTRIBUTE_NOINFERIORS) attributes &= ~MBOX_ATTRIBUTE_HASNOCHILDREN; /* \NonExistent implies \Noselect */ if (attributes & MBOX_ATTRIBUTE_NONEXISTENT) attributes &= ~MBOX_ATTRIBUTE_NOSELECT; } if (config_getswitch(IMAPOPT_SPECIALUSEALWAYS) || listargs->cmd == LIST_CMD_XLIST || listargs->ret & LIST_RET_SPECIALUSE) { specialuse_flags(mbentry, &specialuse, listargs->cmd == LIST_CMD_XLIST); } if (listargs->sel & LIST_SEL_SPECIALUSE) { /* check that this IS a specialuse folder */ if (!buf_len(&specialuse)) return; } /* can we read the status data ? */ if ((listargs->ret & LIST_RET_STATUS) && mbentry) { r = imapd_statusdata(mbentry, listargs->statusitems, &sdata); if (r) { /* RFC 5819: the STATUS response MUST NOT be returned and the * LIST response MUST include the \NoSelect attribute. */ attributes |= MBOX_ATTRIBUTE_NOSELECT; } } print_listresponse(listargs->cmd, extname, imapd_namespace.hier_sep, attributes, &specialuse); buf_free(&specialuse); if ((listargs->ret & LIST_RET_STATUS) && !(attributes & MBOX_ATTRIBUTE_NOSELECT)) { /* output the status line now, per RFC 5819 */ if (mbentry) print_statusline(extname, listargs->statusitems, &sdata); } if ((listargs->ret & LIST_RET_MYRIGHTS) && !(attributes & MBOX_ATTRIBUTE_NOSELECT)) { if (mbentry) printmyrights(extname, mbentry); } if ((listargs->ret & LIST_RET_METADATA) && !(attributes & MBOX_ATTRIBUTE_NOSELECT)) { if (mbentry) printmetadata(mbentry, &listargs->metaitems, &listargs->metaopts); } } static void _addsubs(struct list_rock *rock) { if (!rock->subs) return; if (!rock->last_mbentry) return; int i; const char *last_name = rock->last_mbentry->name; int namelen = strlen(last_name); for (i = 0; i < rock->subs->count; i++) { const char *name = strarray_nth(rock->subs, i); if (strncmp(last_name, name, namelen)) continue; else if (!name[namelen]) { if ((rock->last_attributes & MBOX_ATTRIBUTE_NONEXISTENT)) rock->last_attributes |= MBOX_ATTRIBUTE_CHILDINFO_SUBSCRIBED; else rock->last_attributes |= MBOX_ATTRIBUTE_SUBSCRIBED; } else if (name[namelen] == '.') rock->last_attributes |= MBOX_ATTRIBUTE_CHILDINFO_SUBSCRIBED; } } static int perform_output(const char *extname, const mbentry_t *mbentry, struct list_rock *rock) { /* skip non-responsive mailboxes early, so they don't break sub folder detection */ if (!imapd_userisadmin) { int mbtype = mbentry ? mbentry->mbtype : 0; if (mbtype_isa(mbtype) == MBTYPE_NETNEWS) return 0; if ((mbtype & MBTYPE_INTERMEDIATE) && !(rock->listargs->sel & LIST_SEL_INTERMEDIATES)) return 0; if (!(rock->listargs->sel & LIST_SEL_DAV)) { char *intname = NULL, *freeme = NULL; int skip = 0; /* if we got here via subscribed_cb, mbentry isn't set */ if (mbentry) intname = mbentry->name; else { intname = freeme = mboxname_from_external(extname, &imapd_namespace, imapd_userid); } if (mboxname_isnonimapmailbox(intname, mbtype)) skip = 1; free(freeme); if (skip) return 0; } } if (mbentry && (mbentry->mbtype & MBTYPE_REMOTE)) { struct listargs *listargs = rock->listargs; if (hash_lookup(mbentry->server, &rock->server_table)) { /* already proxied to this backend server */ return 0; } if (listargs->scan || (listargs->ret & (LIST_RET_SPECIALUSE | LIST_RET_STATUS | LIST_RET_METADATA))) { /* remote mailbox that we need to fetch metadata from */ struct backend *s; hash_insert(mbentry->server, (void *)0xDEADBEEF, &rock->server_table); s = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (s) { char mytag[128]; proxy_gentag(mytag, sizeof(mytag)); if (listargs->scan) { /* Send SCAN command to backend */ prot_printf(s->out, "%s Scan {%tu+}\r\n%s {%tu+}\r\n%s" " {%tu+}\r\n%s\r\n", mytag, strlen(listargs->ref), listargs->ref, strlen(listargs->pat.data[0]), listargs->pat.data[0], strlen(listargs->scan), listargs->scan); pipe_until_tag(s, mytag, 0); } else { /* Send LIST command to backend */ list_data_remote(s, mytag, listargs, rock->subs); } } return 0; } } if (rock->last_name) { if (extname) { /* same again */ if (!strcmp(rock->last_name, extname)) return 0; size_t extlen = strlen(extname); if (extlen < strlen(rock->last_name) && rock->last_name[extlen] == imapd_namespace.hier_sep && !strncmp(rock->last_name, extname, extlen)) return 0; /* skip duplicate or reversed calls */ } _addsubs(rock); /* check if we need to filter out this mailbox */ if (!(rock->listargs->sel & LIST_SEL_SUBSCRIBED) || (rock->last_attributes & (MBOX_ATTRIBUTE_SUBSCRIBED | MBOX_ATTRIBUTE_CHILDINFO_SUBSCRIBED))) { list_response(rock->last_name, rock->last_mbentry, rock->last_attributes, rock->listargs); } free(rock->last_name); rock->last_name = NULL; mboxlist_entry_free(&rock->last_mbentry); } if (extname) { rock->last_name = xstrdup(extname); if (mbentry) rock->last_mbentry = mboxlist_entry_copy(mbentry); } rock->last_attributes = 0; rock->last_category = 0; return 1; } static void add_intermediates(const char *extname, struct list_rock *lrock) { mbname_t *mbname = mbname_from_extname(extname, &imapd_namespace, imapd_userid); strarray_t inter = STRARRAY_INITIALIZER; /* build a list of "missing" ancestors (youngest to oldest) */ while (strarray_size(mbname_boxes(mbname))) { free(mbname_pop_boxes(mbname)); extname = mbname_extname(mbname, &imapd_namespace, imapd_userid); if (!extname) break; /* root of hierarchy */ if (lrock->last_name && mboxname_is_prefix(lrock->last_name, extname)) break; strarray_push(&inter, extname); } mbname_free(&mbname); /* output the ancestors (oldest to youngest) */ char *ancestor; while ((ancestor = strarray_pop(&inter))) { mbentry_t *mbentry = NULL; if (!mboxlist_lookup_allow_all(ancestor, &mbentry, NULL)) { mbentry->mbtype |= MBTYPE_INTERMEDIATE; /* force \NonExistent */ perform_output(ancestor, mbentry, lrock); } mboxlist_entry_free(&mbentry); free(ancestor); } strarray_fini(&inter); } /* callback for mboxlist_findall * used when the SUBSCRIBED selection option is NOT given */ static int list_cb(struct findall_data *data, void *rockp) { struct list_rock *rock = (struct list_rock *)rockp; // skip anything DELETED unless explicitly asked for if (data && !imapd_userisadmin && (!(rock->listargs->sel & LIST_SEL_DELETED) || !config_getswitch(IMAPOPT_ALLOWDELETED)) && mbname_isdeleted(data->mbname)) return 0; if (!data) { if (!(rock->last_attributes & MBOX_ATTRIBUTE_HASCHILDREN)) rock->last_attributes |= MBOX_ATTRIBUTE_HASNOCHILDREN; perform_output(NULL, NULL, rock); return 0; } size_t last_len = (rock->last_name ? strlen(rock->last_name) : 0); const char *extname = data->extname; int last_name_is_ancestor = rock->last_name && strlen(extname) >= last_len && extname[last_len] == imapd_namespace.hier_sep && !memcmp(rock->last_name, extname, last_len); list_callback_calls++; /* list_response will calculate haschildren/hasnochildren flags later * if they're required but not yet set, but it's a little cheaper to * precalculate them now while we're iterating the mailboxes anyway. */ if (last_name_is_ancestor || (rock->last_name && !data->is_exactmatch && !strcmp(rock->last_name, extname))) rock->last_attributes |= MBOX_ATTRIBUTE_HASCHILDREN; else if (!(rock->last_attributes & MBOX_ATTRIBUTE_HASCHILDREN)) rock->last_attributes |= MBOX_ATTRIBUTE_HASNOCHILDREN; /* do we need to add "missing" intermediates? */ if ((rock->listargs->sel & LIST_SEL_INTERMEDIATES) && ((rock->listargs->sel & LIST_SEL_DAV) || !mbtypes_dav(data->mbentry->mbtype)) && !mboxname_contains_parent(data->extname, rock->last_name)) { add_intermediates(data->extname, rock); } if (!perform_output(data->extname, data->mbentry, rock)) return 0; if (!data->is_exactmatch) rock->last_attributes |= MBOX_ATTRIBUTE_HASCHILDREN | MBOX_ATTRIBUTE_NONEXISTENT; else if (data->mb_category == MBNAME_ALTINBOX) rock->last_attributes |= MBOX_ATTRIBUTE_NOINFERIORS; return 0; } /* callback for mboxlist_findsub * used when SUBSCRIBED but not RECURSIVEMATCH is given */ static int subscribed_cb(struct findall_data *data, void *rockp) { struct list_rock *rock = (struct list_rock *)rockp; if (!data) { perform_output(NULL, NULL, rock); return 0; } size_t last_len = (rock->last_name ? strlen(rock->last_name) : 0); const char *extname = data->extname; int last_name_is_ancestor = rock->last_name && strlen(extname) >= last_len && extname[last_len] == imapd_namespace.hier_sep && !memcmp(rock->last_name, extname, last_len); list_callback_calls++; if (last_name_is_ancestor || (rock->last_name && !data->is_exactmatch && !strcmp(rock->last_name, extname))) rock->last_attributes |= MBOX_ATTRIBUTE_HASCHILDREN; if (data->is_exactmatch) { mbentry_t *mbentry = NULL; mboxlist_lookup(mbname_intname(data->mbname), &mbentry, NULL); perform_output(extname, mbentry, rock); mboxlist_entry_free(&mbentry); rock->last_attributes |= MBOX_ATTRIBUTE_SUBSCRIBED; if (mboxlist_lookup(mbname_intname(data->mbname), NULL, NULL)) rock->last_attributes |= MBOX_ATTRIBUTE_NONEXISTENT; if (data->mb_category == MBNAME_ALTINBOX) rock->last_attributes |= MBOX_ATTRIBUTE_NOINFERIORS; } else if (rock->listargs->cmd == LIST_CMD_LSUB) { /* special case: for LSUB, * mailbox names that match the pattern but aren't subscribed * must also be returned if they have a child mailbox that is * subscribed */ perform_output(extname, data->mbentry, rock); rock->last_attributes |= MBOX_ATTRIBUTE_CHILDINFO_SUBSCRIBED; } return 0; } /* * Takes the "reference name" and "mailbox name" arguments of the LIST command * and returns a "canonical LIST pattern". The caller is responsible for * free()ing the returned string. */ static char *canonical_list_pattern(const char *reference, const char *pattern) { int patlen = strlen(pattern); int reflen = strlen(reference); char *buf = xmalloc(patlen + reflen + 1); buf[0] = '\0'; if (*reference) { if (reference[reflen-1] == imapd_namespace.hier_sep && pattern[0] == imapd_namespace.hier_sep) --reflen; memcpy(buf, reference, reflen); buf[reflen] = '\0'; } strcat(buf, pattern); return buf; } /* * Turns the strings in patterns into "canonical LIST pattern"s. Also * translates any hierarchy separators. */ static void canonical_list_patterns(const char *reference, strarray_t *patterns) { static int ignorereference = 0; int i; /* Ignore the reference argument? (the behavior in 1.5.10 & older) */ if (ignorereference == 0) ignorereference = config_getswitch(IMAPOPT_IGNOREREFERENCE); for (i = 0 ; i < patterns->count ; i++) { char *p = patterns->data[i]; if (!ignorereference || p[0] == imapd_namespace.hier_sep) { strarray_setm(patterns, i, canonical_list_pattern(reference, p)); p = patterns->data[i]; } } } static void list_data_remotesubscriptions(struct listargs *listargs) { /* Need to fetch subscription list from backend_inbox */ struct list_rock rock; char mytag[128]; memset(&rock, 0, sizeof(struct list_rock)); rock.listargs = listargs; rock.subs = strarray_new(); construct_hash_table(&rock.server_table, 10, 1); proxy_gentag(mytag, sizeof(mytag)); if ((listargs->sel & LIST_SEL_SUBSCRIBED) && !(listargs->sel & (LIST_SEL_SPECIALUSE | LIST_SEL_METADATA))) { /* Subscriptions are the only selection criteria. Send client request as-is to backend_inbox. Responses will be piped to the client as we build subs list. */ list_data_remote(backend_inbox, mytag, listargs, rock.subs); /* Don't proxy to backend_inbox again */ hash_insert(backend_inbox->hostname, (void *)0xDEADBEEF, &rock.server_table); } else { /* Multiple selection criteria or need to return subscription info. Just fetch subscriptions without piping responses to the client. If we send entire client request, subscribed mailboxes on non-Inbox servers might be filtered out due to lack of metadata to meet the selection criteria. Note that we end up sending two requests to backend_inbox, but there doesn't appear to be any way around this. */ struct listargs myargs; memcpy(&myargs, listargs, sizeof(struct listargs)); myargs.sel = LIST_SEL_SUBSCRIBED; myargs.ret = 0; list_data_remote(backend_inbox, mytag, &myargs, rock.subs); } /* find */ mboxlist_findallmulti(&imapd_namespace, &listargs->pat, imapd_userisadmin, imapd_userid, imapd_authstate, list_cb, &rock); strarray_free(rock.subs); free_hash_table(&rock.server_table, NULL); if (rock.last_name) free(rock.last_name); } /* callback for mboxlist_findsub * used by list_data_recursivematch */ static int recursivematch_cb(struct findall_data *data, void *rockp) { if (!data) return 0; struct list_rock_recursivematch *rock = (struct list_rock_recursivematch *)rockp; list_callback_calls++; const char *extname = data->extname; /* skip non-responsive mailboxes early, so they don't break sub folder detection */ if (!(imapd_userisadmin || (rock->listargs->sel & LIST_SEL_DAV))) { mbname_t *mbname = (mbname_t *) data->mbname; const char *intname; int r; if (!mbname) { mbname = mbname_from_extname(extname, &imapd_namespace, imapd_userid); } if (mbname) { intname = mbname_intname(mbname); r = mboxname_iscalendarmailbox(intname, 0) || mboxname_isaddressbookmailbox(intname, 0) || mboxname_isdavdrivemailbox(intname, 0) || mboxname_isdavnotificationsmailbox(intname, 0) || mboxname_issievemailbox(intname, 0); if (!data->mbname) mbname_free(&mbname); if (r) return 0; } } struct list_entry *entry = hash_lookup(extname, &rock->table); if (!entry) { entry = xzmalloc(sizeof(struct list_entry)); entry->extname = xstrdupsafe(extname); entry->attributes |= MBOX_ATTRIBUTE_NONEXISTENT; hash_insert(extname, entry, &rock->table); rock->count++; } if (data->is_exactmatch) { entry->attributes |= MBOX_ATTRIBUTE_SUBSCRIBED; if (!data->mbentry) { mboxlist_lookup(mbname_intname(data->mbname), &entry->mbentry, NULL); if (entry->mbentry) entry->attributes &= ~MBOX_ATTRIBUTE_NONEXISTENT; } } else { entry->attributes |= MBOX_ATTRIBUTE_CHILDINFO_SUBSCRIBED | MBOX_ATTRIBUTE_HASCHILDREN; } return 0; } /* callback for hash_enumerate */ static void copy_to_array(const char *key __attribute__((unused)), void *data, void *void_rock) { struct list_entry *entry = (struct list_entry *)data; struct list_rock_recursivematch *rock = (struct list_rock_recursivematch *)void_rock; assert(rock->count > 0); rock->array[--rock->count] = *entry; } /* Comparator for sorting an array of struct list_entry by mboxname. */ static int list_entry_comparator(const void *p1, const void *p2) { const struct list_entry *e1 = (struct list_entry *)p1; const struct list_entry *e2 = (struct list_entry *)p2; return strcmp(e1->extname, e2->extname); } static void free_list_entry(void *rock) { struct list_entry *entry = (struct list_entry *)rock; mboxlist_entry_free(&entry->mbentry); free(entry->extname); free(entry); } static void list_data_recursivematch(struct listargs *listargs) { struct list_rock_recursivematch rock; rock.count = 0; rock.listargs = listargs; construct_hash_table(&rock.table, 100, 1); /* find */ mboxlist_findsubmulti(&imapd_namespace, &listargs->pat, imapd_userisadmin, imapd_userid, imapd_authstate, recursivematch_cb, &rock, 1); if (rock.count) { int i; int entries = rock.count; /* sort */ rock.array = xmalloc(entries * (sizeof(struct list_entry))); hash_enumerate(&rock.table, copy_to_array, &rock); qsort(rock.array, entries, sizeof(struct list_entry), list_entry_comparator); assert(rock.count == 0); /* print */ for (i = 0; i < entries; i++) { if (!rock.array[i].extname) continue; list_response(rock.array[i].extname, rock.array[i].mbentry, rock.array[i].attributes, rock.listargs); } free(rock.array); } free_hash_table(&rock.table, free_list_entry); } /* Retrieves the data and prints the untagged responses for a LIST command. */ static void list_data(struct listargs *listargs) { canonical_list_patterns(listargs->ref, &listargs->pat); /* Check to see if we should only list the personal namespace */ if (!(listargs->cmd == LIST_CMD_EXTENDED) && !strcmp(listargs->pat.data[0], "*") && config_getswitch(IMAPOPT_FOOLSTUPIDCLIENTS)) { strarray_set(&listargs->pat, 0, "INBOX*"); } if ((listargs->ret & LIST_RET_SUBSCRIBED) && (backend_inbox || (backend_inbox = proxy_findinboxserver(imapd_userid)))) { list_data_remotesubscriptions(listargs); } else if (listargs->sel & LIST_SEL_RECURSIVEMATCH) { list_data_recursivematch(listargs); } else { struct list_rock rock; memset(&rock, 0, sizeof(struct list_rock)); rock.listargs = listargs; if (listargs->sel & LIST_SEL_SUBSCRIBED) { mboxlist_findsubmulti(&imapd_namespace, &listargs->pat, imapd_userisadmin, imapd_userid, imapd_authstate, subscribed_cb, &rock, 1); } else { if (config_mupdate_server) { /* In case we proxy to backends due to select/return criteria */ construct_hash_table(&rock.server_table, 10, 1); } /* XXX: is there a cheaper way to figure out \Subscribed? */ if (listargs->ret & LIST_RET_SUBSCRIBED) { rock.subs = mboxlist_sublist(imapd_userid); } mboxlist_findallmulti(&imapd_namespace, &listargs->pat, imapd_userisadmin, imapd_userid, imapd_authstate, list_cb, &rock); if (rock.subs) strarray_free(rock.subs); if (rock.server_table.size) free_hash_table(&rock.server_table, NULL); } if (rock.last_name) free(rock.last_name); } } /* * Retrieves the data and prints the untagged responses for a LIST command in * the case of a remote inbox. */ static int list_data_remote(struct backend *be, char *tag, struct listargs *listargs, strarray_t *subs) { if ((listargs->cmd == LIST_CMD_EXTENDED) && !CAPA(be, CAPA_LISTEXTENDED)) { /* client wants to use extended list command but backend doesn't * support it */ prot_printf(imapd_out, "%s NO Backend server does not support LIST-EXTENDED\r\n", tag); return IMAP_MAILBOX_NOTSUPPORTED; } /* print tag, command and list selection options */ if (listargs->cmd == LIST_CMD_LSUB) { prot_printf(be->out, "%s Lsub ", tag); } else if (listargs->cmd == LIST_CMD_XLIST) { prot_printf(be->out, "%s Xlist ", tag); } else { prot_printf(be->out, "%s List ", tag); uint32_t select_mask = listargs->sel; if (be != backend_inbox) { /* don't send subscribed selection options to non-Inbox backend */ select_mask &= ~(LIST_SEL_SUBSCRIBED | LIST_SEL_RECURSIVEMATCH); } /* print list selection options */ if (select_mask) { const char *select_opts[] = { /* XXX MUST be in same order as LIST_SEL_* bitmask */ "subscribed", "remote", "recursivematch", "special-use", "vendor.cmu-dav", "metadata", NULL }; char c = '('; int i; for (i = 0; select_opts[i]; i++) { unsigned opt = (1 << i); if (!(select_mask & opt)) continue; prot_printf(be->out, "%c%s", c, select_opts[i]); c = ' '; if (opt == LIST_SEL_METADATA) { /* print metadata options */ prot_puts(be->out, " (depth "); if (listargs->metaopts.depth < 0) { prot_puts(be->out, "infinity"); } else { prot_printf(be->out, "%d", listargs->metaopts.depth); } if (listargs->metaopts.maxsize) { prot_printf(be->out, " maxsize %zu", listargs->metaopts.maxsize); } (void)prot_putc(')', be->out); } } prot_puts(be->out, ") "); } } /* print reference argument */ prot_printf(be->out, "{%tu+}\r\n%s ", strlen(listargs->ref), listargs->ref); /* print mailbox pattern(s) */ if (listargs->pat.count > 1) { char **p; char c = '('; for (p = listargs->pat.data ; *p ; p++) { prot_printf(be->out, "%c{%tu+}\r\n%s", c, strlen(*p), *p); c = ' '; } (void)prot_putc(')', be->out); } else { prot_printf(be->out, "{%tu+}\r\n%s", strlen(listargs->pat.data[0]), listargs->pat.data[0]); } /* print list return options */ if (listargs->ret && listargs->cmd == LIST_CMD_EXTENDED) { const char *return_opts[] = { /* XXX MUST be in same order as LIST_RET_* bitmask */ "subscribed", "children", "special-use", "status ", "myrights", "metadata ", NULL }; char c = '('; int i, j; prot_puts(be->out, " return "); for (i = 0; return_opts[i]; i++) { unsigned opt = (1 << i); if (!(listargs->ret & opt)) continue; prot_printf(be->out, "%c%s", c, return_opts[i]); c = ' '; if (opt == LIST_RET_STATUS) { /* print status items */ const char *status_items[] = { /* XXX MUST be in same order as STATUS_* bitmask */ "messages", "recent", "uidnext", "uidvalidity", "unseen", "uniqueid", "size", "highestmodseq", "xconvexists", "xconvunseen", "xconvmodseq", "createdmodseq", "sharedseen", NULL }; c = '('; for (j = 0; status_items[j]; j++) { if (!(listargs->statusitems & (1 << j))) continue; prot_printf(be->out, "%c%s", c, status_items[j]); c = ' '; } (void)prot_putc(')', be->out); } else if (opt == LIST_RET_METADATA) { /* print metadata items */ int n = strarray_size(&listargs->metaitems); c = '('; for (j = 0; j < n; j++) { prot_printf(be->out, "%c\"%s\"", c, strarray_nth(&listargs->metaitems, j)); c = ' '; } (void)prot_putc(')', be->out); } } (void)prot_putc(')', be->out); } prot_printf(be->out, "\r\n"); pipe_lsub(be, imapd_userid, tag, 0, listargs, subs); return 0; } /* 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("imap", config_servername, NULL, buf_cstringnull_ifempty(&saslprops.iplocalport), buf_cstringnull_ifempty(&saslprops.ipremoteport), NULL, 0, conn); if(ret != SASL_OK) return ret; secprops = mysasl_secprops(0); 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 = saslprops_set_tls(&saslprops, *conn); } else { ret = sasl_setprop(*conn, SASL_SSF_EXTERNAL, &extprops_ssf); } if(ret != SASL_OK) return ret; /* End TLS/SSL Info */ return SASL_OK; } static void cmd_mupdatepush(char *tag, char *name) { int r = 0, retry = 0; mbentry_t *mbentry = NULL; char buf[MAX_PARTITION_LEN + HOSTNAME_SIZE + 2]; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); if (!imapd_userisadmin) { r = IMAP_PERMISSION_DENIED; goto done; } if (!config_mupdate_server) { r = IMAP_SERVER_UNAVAILABLE; goto done; } r = mlookup(tag, name, intname, &mbentry); if (r) goto done; /* Push mailbox to mupdate server */ if (!mupdate_h) { syslog(LOG_INFO, "XFER: connecting to mupdate '%s'", config_mupdate_server); r = mupdate_connect(config_mupdate_server, NULL, &mupdate_h, NULL); retry = 1; if (r) { syslog(LOG_INFO, "Failed to connect to mupdate '%s'", config_mupdate_server); goto done; } } snprintf(buf, sizeof(buf), "%s!%s", config_servername, mbentry->partition); retry: r = mupdate_activate(mupdate_h, intname, buf, mbentry->acl); if (r && !retry) { syslog(LOG_INFO, "MUPDATE: lost connection, retrying"); mupdate_disconnect(&mupdate_h); r = mupdate_connect(config_mupdate_server, NULL, &mupdate_h, NULL); if (r) { syslog(LOG_INFO, "Failed to connect to mupdate '%s'", config_mupdate_server); } else { retry = 1; goto retry; } } done: mboxlist_entry_free(&mbentry); free(intname); if (r) { prot_printf(imapd_out, "%s NO %s\r\n", tag, error_message(r)); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } } #ifdef HAVE_SSL enum { URLAUTH_ALG_HMAC_SHA1 = 0 /* HMAC-SHA1 */ }; static void cmd_urlfetch(char *tag) { struct mboxkey *mboxkey_db; int c, r, doclose; static struct buf arg, param; struct imapurl url; struct index_state *state; uint32_t msgno; mbentry_t *mbentry = NULL; time_t now = time(NULL); unsigned extended, params; prot_printf(imapd_out, "* URLFETCH"); do { char *intname = NULL; extended = params = 0; /* See if its an extended URLFETCH */ c = prot_getc(imapd_in); if (c == '(') extended = 1; else prot_ungetc(c, imapd_in); c = getastring(imapd_in, imapd_out, &arg); if (c == IMAP_LITERAL_TOO_LARGE) { prot_printf(imapd_out, "%s NO %s in Urlfetch\r\n", tag, error_message(c)); return; } (void)prot_putc(' ', imapd_out); prot_printstring(imapd_out, arg.s); if (extended) { while (c == ' ') { c = getword(imapd_in, ¶m); ucase(param.s); if (!strcmp(param.s, "BODY")) { if (params & (URLFETCH_BODY | URLFETCH_BINARY)) goto badext; params |= URLFETCH_BODY; } else if (!strcmp(param.s, "BINARY")) { if (params & (URLFETCH_BODY | URLFETCH_BINARY)) goto badext; params |= URLFETCH_BINARY; } else if (!strcmp(param.s, "BODYPARTSTRUCTURE")) { if (params & URLFETCH_BODYPARTSTRUCTURE) goto badext; params |= URLFETCH_BODYPARTSTRUCTURE; } else { goto badext; } } if (c != ')') goto badext; c = prot_getc(imapd_in); } doclose = 0; r = imapurl_fromURL(&url, arg.s); /* validate the URL */ if (r || !url.user || !url.server || !url.mailbox || !url.uid || (url.section && !*url.section) || (url.urlauth.access && !(url.urlauth.mech && url.urlauth.token))) { /* missing info */ r = IMAP_BADURL; } else if (strcmp(url.server, config_servername)) { /* wrong server */ r = IMAP_BADURL; } else if (url.urlauth.expire && url.urlauth.expire < mktime(gmtime(&now))) { /* expired */ r = IMAP_BADURL; } else if (url.urlauth.access) { /* check mechanism & authorization */ int authorized = 0; if (!strcasecmp(url.urlauth.mech, "INTERNAL")) { if (!strncasecmp(url.urlauth.access, "submit+", 7) && global_authisa(imapd_authstate, IMAPOPT_SUBMITSERVERS)) { /* authorized submit server */ authorized = 1; } else if (!strncasecmp(url.urlauth.access, "user+", 5) && !strcmp(url.urlauth.access+5, imapd_userid)) { /* currently authorized user */ authorized = 1; } else if (!strcasecmp(url.urlauth.access, "authuser") && strcmp(imapd_userid, "anonymous")) { /* any non-anonymous authorized user */ authorized = 1; } else if (!strcasecmp(url.urlauth.access, "anonymous")) { /* anyone */ authorized = 1; } } if (!authorized) r = IMAP_BADURL; } if (r) goto err; intname = mboxname_from_external(url.mailbox, &imapd_namespace, url.user); r = mlookup(NULL, NULL, intname, &mbentry); if (r) goto err; if ((mbentry->mbtype & MBTYPE_REMOTE)) { /* remote mailbox */ struct backend *be; be = proxy_findserver(mbentry->server, &imap_protocol, proxy_userid, &backend_cached, &backend_current, &backend_inbox, imapd_in); if (!be) { r = IMAP_SERVER_UNAVAILABLE; } else { /* XXX proxy command to backend */ } free(url.freeme); mboxlist_entry_free(&mbentry); free(intname); continue; } mboxlist_entry_free(&mbentry); /* local mailbox */ if (url.urlauth.token) { /* validate the URLAUTH token */ /* yes, this is evil, in-place conversion from hex * to binary */ if (hex_to_bin(url.urlauth.token, 0, (unsigned char *) url.urlauth.token) < 1) { r = IMAP_BADURL; break; } /* first byte is the algorithm used to create token */ switch (url.urlauth.token[0]) { case URLAUTH_ALG_HMAC_SHA1: { const char *key; size_t keylen; unsigned char vtoken[EVP_MAX_MD_SIZE]; unsigned int vtoken_len; r = mboxkey_open(url.user, 0, &mboxkey_db); if (r) break; r = mboxkey_read(mboxkey_db, intname, &key, &keylen); if (r) break; HMAC(EVP_sha1(), key, keylen, (unsigned char *) arg.s, url.urlauth.rump_len, vtoken, &vtoken_len); mboxkey_close(mboxkey_db); if (memcmp(vtoken, url.urlauth.token+1, vtoken_len)) { r = IMAP_BADURL; } break; } default: r = IMAP_BADURL; break; } } if (r) goto err; if (!strcmpnull(index_mboxname(imapd_index), intname)) { state = imapd_index; } else { /* not the currently selected mailbox, so try to open it */ r = index_open(intname, NULL, &state); if (!r) doclose = 1; if (!r && !url.urlauth.access && !(state->myrights & ACL_READ)) { r = (imapd_userisadmin || (state->myrights & ACL_LOOKUP)) ? IMAP_PERMISSION_DENIED : IMAP_MAILBOX_NONEXISTENT; } } if (r) goto err; if (url.uidvalidity && (state->mailbox->i.uidvalidity != url.uidvalidity)) { r = IMAP_BADURL; } else if (!url.uid || !(msgno = index_finduid(state, url.uid)) || (index_getuid(state, msgno) != url.uid)) { r = IMAP_BADURL; } else { r = index_urlfetch(state, msgno, params, url.section, url.start_octet, url.octet_count, imapd_out, UINT32_MAX, NULL); } err: if (doclose) index_close(&state); free(url.freeme); if (r) prot_printf(imapd_out, " NIL"); free(intname); } while (c == ' '); prot_printf(imapd_out, "\r\n"); if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to URLFETCH\r\n", tag); eatline(imapd_in, c); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } return; badext: prot_printf(imapd_out, " NIL\r\n"); prot_printf(imapd_out, "%s BAD Invalid extended URLFETCH parameters\r\n", tag); eatline(imapd_in, c); } #define MBOX_KEY_LEN 16 /* 128 bits */ static void cmd_genurlauth(char *tag) { struct mboxkey *mboxkey_db; int first = 1; int c, r; static struct buf arg1, arg2; struct imapurl url; char newkey[MBOX_KEY_LEN]; char *urlauth = NULL; const char *key; size_t keylen; unsigned char token[EVP_MAX_MD_SIZE+1]; /* +1 for algorithm */ unsigned int token_len; mbentry_t *mbentry = NULL; time_t now = time(NULL); r = mboxkey_open(imapd_userid, MBOXKEY_CREATE, &mboxkey_db); if (r) { prot_printf(imapd_out, "%s NO Cannot open mailbox key db for %s: %s\r\n", tag, imapd_userid, error_message(r)); return; } do { char *intname = NULL; c = getastring(imapd_in, imapd_out, &arg1); if (c == IMAP_LITERAL_TOO_LARGE) { prot_printf(imapd_out, "%s NO %s in Genurlauth\r\n", tag, error_message(c)); return; } if (c != ' ') { prot_printf(imapd_out, "%s BAD Missing required argument to Genurlauth\r\n", tag); eatline(imapd_in, c); return; } c = getword(imapd_in, &arg2); if (strcasecmp(arg2.s, "INTERNAL")) { prot_printf(imapd_out, "%s BAD Unknown auth mechanism to Genurlauth %s\r\n", tag, arg2.s); eatline(imapd_in, c); return; } r = imapurl_fromURL(&url, arg1.s); /* validate the URL */ if (r || !url.user || !url.server || !url.mailbox || !url.uid || (url.section && !*url.section) || !url.urlauth.access) { r = IMAP_BADURL; } else if (strcmp(url.user, imapd_userid)) { /* not using currently authorized user's namespace */ r = IMAP_BADURL; } else if (strcmp(url.server, config_servername)) { /* wrong server */ r = IMAP_BADURL; } else if (url.urlauth.expire && url.urlauth.expire < mktime(gmtime(&now))) { /* already expired */ r = IMAP_BADURL; } if (r) goto err; intname = mboxname_from_external(url.mailbox, &imapd_namespace, imapd_userid); r = mlookup(NULL, NULL, intname, &mbentry); if (r) { prot_printf(imapd_out, "%s BAD Poorly specified URL to Genurlauth %s\r\n", tag, arg1.s); eatline(imapd_in, c); free(url.freeme); free(intname); return; } if (mbentry->mbtype & MBTYPE_REMOTE) { /* XXX proxy to backend */ mboxlist_entry_free(&mbentry); free(url.freeme); free(intname); continue; } mboxlist_entry_free(&mbentry); /* lookup key */ r = mboxkey_read(mboxkey_db, intname, &key, &keylen); if (r) { syslog(LOG_ERR, "DBERROR: error fetching mboxkey: %s", cyrusdb_strerror(r)); } else if (!key) { /* create a new key */ RAND_bytes((unsigned char *) newkey, MBOX_KEY_LEN); key = newkey; keylen = MBOX_KEY_LEN; r = mboxkey_write(mboxkey_db, intname, key, keylen); if (r) { syslog(LOG_ERR, "DBERROR: error writing new mboxkey: %s", cyrusdb_strerror(r)); } } if (r) { err: eatline(imapd_in, c); prot_printf(imapd_out, "%s NO Error authorizing %s: %s\r\n", tag, arg1.s, cyrusdb_strerror(r)); free(url.freeme); free(intname); return; } /* first byte is the algorithm used to create token */ token[0] = URLAUTH_ALG_HMAC_SHA1; HMAC(EVP_sha1(), key, keylen, (unsigned char *) arg1.s, strlen(arg1.s), token+1, &token_len); token_len++; urlauth = xrealloc(urlauth, strlen(arg1.s) + 10 + 2 * (EVP_MAX_MD_SIZE+1) + 1); strcpy(urlauth, arg1.s); strcat(urlauth, ":internal:"); bin_to_hex(token, token_len, urlauth+strlen(urlauth), BH_LOWER); if (first) { prot_printf(imapd_out, "* GENURLAUTH"); first = 0; } (void)prot_putc(' ', imapd_out); prot_printstring(imapd_out, urlauth); free(intname); free(url.freeme); } while (c == ' '); if (!first) prot_printf(imapd_out, "\r\n"); if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to GENURLAUTH\r\n", tag); eatline(imapd_in, c); } else { prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } free(urlauth); mboxkey_close(mboxkey_db); } static void cmd_resetkey(char *tag, char *name, char *mechanism __attribute__((unused))) /* XXX we don't support any external mechanisms, so we ignore it */ { int r; if (name) { /* delete key for specified mailbox */ struct mboxkey *mboxkey_db; mbentry_t *mbentry = NULL; char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(NULL, NULL, intname, &mbentry); if (r) { prot_printf(imapd_out, "%s NO Error removing key: %s\r\n", tag, error_message(r)); free(intname); return; } if (mbentry->mbtype & MBTYPE_REMOTE) { /* XXX proxy to backend */ mboxlist_entry_free(&mbentry); free(intname); return; } mboxlist_entry_free(&mbentry); r = mboxkey_open(imapd_userid, MBOXKEY_CREATE, &mboxkey_db); if (!r) { r = mboxkey_write(mboxkey_db, intname, NULL, 0); mboxkey_close(mboxkey_db); } if (r) { prot_printf(imapd_out, "%s NO Error removing key: %s\r\n", tag, cyrusdb_strerror(r)); } else { prot_printf(imapd_out, "%s OK [URLMECH INTERNAL] key removed\r\n", tag); } free(intname); } else { /* delete ALL keys */ /* XXX what do we do about multiple backends? */ r = mboxkey_delete_user(imapd_userid); if (r) { prot_printf(imapd_out, "%s NO Error removing keys: %s\r\n", tag, cyrusdb_strerror(r)); } else { prot_printf(imapd_out, "%s OK All keys removed\r\n", tag); } } } #endif /* HAVE_SSL */ #ifdef HAVE_ZLIB static void cmd_compress(char *tag, char *alg) { if (imapd_compress_done) { prot_printf(imapd_out, "%s BAD [COMPRESSIONACTIVE] DEFLATE active via COMPRESS\r\n", tag); } #if defined(HAVE_SSL) && (OPENSSL_VERSION_NUMBER >= 0x0090800fL) else if (imapd_tls_comp) { prot_printf(imapd_out, "%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) else if (strcasecmp(alg, "DEFLATE")) { prot_printf(imapd_out, "%s NO Unknown COMPRESS algorithm: %s\r\n", tag, alg); } else if (ZLIB_VERSION[0] != zlibVersion()[0]) { prot_printf(imapd_out, "%s NO Error initializing %s (incompatible zlib version)\r\n", tag, alg); } else { prot_printf(imapd_out, "%s OK %s active\r\n", tag, alg); /* enable (de)compression for the prot layer */ prot_setcompress(imapd_in); prot_setcompress(imapd_out); imapd_compress_done = 1; } } #endif /* HAVE_ZLIB */ static void cmd_enable(char *tag) { static struct buf arg; int c; unsigned new_capa = 0; /* RFC 5161 says that enable while selected is actually bogus, * but it's no skin off our nose to support it, so don't * bother checking */ do { c = getword(imapd_in, &arg); if (!arg.s[0]) { prot_printf(imapd_out, "\r\n%s BAD Missing required argument to Enable\r\n", tag); eatline(imapd_in, c); return; } if (!strcasecmp(arg.s, "condstore")) new_capa |= CAPA_CONDSTORE; else if (!strcasecmp(arg.s, "qresync")) new_capa |= CAPA_QRESYNC | CAPA_CONDSTORE; } while (c == ' '); /* check for CRLF */ if (!IS_EOL(c, imapd_in)) { prot_printf(imapd_out, "%s BAD Unexpected extra arguments to Enable\r\n", tag); eatline(imapd_in, c); return; } int started = 0; if (!(client_capa & CAPA_CONDSTORE) && (new_capa & CAPA_CONDSTORE)) { if (!started) prot_printf(imapd_out, "* ENABLED"); started = 1; prot_printf(imapd_out, " CONDSTORE"); } if (!(client_capa & CAPA_QRESYNC) && (new_capa & CAPA_QRESYNC)) { if (!started) prot_printf(imapd_out, "* ENABLED"); started = 1; prot_printf(imapd_out, " QRESYNC"); } if (started) prot_printf(imapd_out, "\r\n"); /* track the new capabilities */ client_capa |= new_capa; prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } static void cmd_xkillmy(const char *tag, const char *cmdname) { char *cmd = xstrdup(cmdname); char *p; /* normalise to imapd conventions */ if (Uislower(cmd[0])) cmd[0] = toupper((unsigned char) cmd[0]); for (p = cmd+1; *p; p++) { if (Uisupper(*p)) *p = tolower((unsigned char) *p); } proc_killusercmd(imapd_userid, cmd, SIGUSR2); free(cmd); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } static void cmd_xforever(const char *tag) { unsigned n = 1; int r = 0; while (!r) { sleep(1); prot_printf(imapd_out, "* FOREVER %u\r\n", n++); prot_flush(imapd_out); r = cmd_cancelled(/*insearch*/0); } prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(r)); } static void cmd_xmeid(const char *tag, const char *id) { mboxevent_set_client_id(id); prot_printf(imapd_out, "%s OK %s\r\n", tag, error_message(IMAP_OK_COMPLETED)); } /***************************** server-side sync *****************************/ static void cmd_syncapply(const char *tag, struct dlist *kin, struct sync_reserve_list *reserve_list) { struct sync_state sync_state = { imapd_userid, imapd_userisadmin || imapd_userisproxyadmin, imapd_authstate, &imapd_namespace, imapd_out, 0 /* flags */ }; if (sync_sieve_mailbox_enabled) { sync_state.flags |= SYNC_FLAG_SIEVE_MAILBOX; } /* administrators only please */ if (!imapd_userisadmin) { syslog(LOG_ERR, "SYNCERROR: invalid user %s trying to sync", imapd_userid); prot_printf(imapd_out, "%s NO only administrators may use sync commands\r\n", tag); return; } const char *resp = sync_apply(kin, reserve_list, &sync_state); if (sync_state.flags & SYNC_FLAG_SIEVE_MAILBOX) { sync_sieve_mailbox_enabled = 1; } // chaining! index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s %s\r\n", tag, resp); /* Reset inactivity timer in case we spent a long time processing data */ prot_resettimeout(imapd_in); } static void cmd_syncget(const char *tag, struct dlist *kin) { struct sync_state sync_state = { imapd_userid, imapd_userisadmin || imapd_userisproxyadmin, imapd_authstate, &imapd_namespace, imapd_out, 0 /* flags */ }; if (sync_sieve_mailbox_enabled) { sync_state.flags |= SYNC_FLAG_SIEVE_MAILBOX; } /* administrators only please */ if (!imapd_userisadmin) { syslog(LOG_ERR, "SYNCERROR: invalid user %s trying to sync", imapd_userid); prot_printf(imapd_out, "%s NO only administrators may use sync commands\r\n", tag); return; } const char *resp = sync_get(kin, &sync_state); prot_printf(imapd_out, "%s %s\r\n", tag, resp); /* Reset inactivity timer in case we spent a long time processing data */ prot_resettimeout(imapd_in); } /* partition_list is simple linked list of names used by cmd_syncrestart */ struct partition_list { struct partition_list *next; char *name; }; static struct partition_list * partition_list_add(char *name, struct partition_list *pl) { struct partition_list *p; /* Is name already on list? */ for (p=pl; p; p = p->next) { if (!strcmp(p->name, name)) return(pl); } /* Add entry to start of list and return new list */ p = xzmalloc(sizeof(struct partition_list)); p->next = pl; p->name = xstrdup(name); return(p); } static void partition_list_free(struct partition_list *current) { while (current) { struct partition_list *next = current->next; free(current->name); free(current); current = next; } } static void cmd_syncrestart(const char *tag, struct sync_reserve_list **reserve_listp, int re_alloc) { struct sync_reserve *res; struct sync_reserve_list *l = *reserve_listp; struct sync_msgid *msg; int hash_size = l->hash_size; struct partition_list *p, *pl = NULL; for (res = l->head; res; res = res->next) { for (msg = res->list->head; msg; msg = msg->next) { if (!msg->fname) continue; pl = partition_list_add(res->part, pl); unlink(msg->fname); } } sync_reserve_list_free(reserve_listp); /* Remove all /sync./ directories referred to above */ for (p=pl; p ; p = p->next) { static char buf[MAX_MAILBOX_PATH]; snprintf(buf, MAX_MAILBOX_PATH, "%s/sync./%lu", config_partitiondir(p->name), (unsigned long)getpid()); rmdir(buf); if (config_getswitch(IMAPOPT_ARCHIVE_ENABLED)) { /* and the archive partition too */ snprintf(buf, MAX_MAILBOX_PATH, "%s/sync./%lu", config_archivepartitiondir(p->name), (unsigned long)getpid()); rmdir(buf); } } partition_list_free(pl); if (re_alloc) { *reserve_listp = sync_reserve_list_create(hash_size); prot_printf(imapd_out, "%s OK Restarting\r\n", tag); } else *reserve_listp = NULL; } static void cmd_syncrestore(const char *tag, struct dlist *kin, struct sync_reserve_list *reserve_list) { struct sync_state sync_state = { imapd_userid, imapd_userisadmin || imapd_userisproxyadmin, imapd_authstate, &imapd_namespace, imapd_out, 0 /* flags */ }; if (sync_sieve_mailbox_enabled) { sync_state.flags |= SYNC_FLAG_SIEVE_MAILBOX; } /* administrators only please */ if (!imapd_userisadmin) { syslog(LOG_ERR, "SYNCERROR: invalid user %s trying to sync", imapd_userid); prot_printf(imapd_out, "%s NO only administrators may use sync commands\r\n", tag); return; } const char *resp = sync_restore(kin, reserve_list, &sync_state); index_release(imapd_index); sync_checkpoint(imapd_in); prot_printf(imapd_out, "%s %s\r\n", tag, resp); /* Reset inactivity timer in case we spent a long time processing data */ prot_resettimeout(imapd_in); } static void cmd_xapplepushservice(const char *tag, struct applepushserviceargs *applepushserviceargs) { int r = 0; strarray_t notif_mailboxes = STRARRAY_INITIALIZER; int i; mbentry_t *mbentry = NULL; const char *aps_topic = config_getstring(IMAPOPT_APS_TOPIC); if (!aps_topic) { syslog(LOG_ERR, "aps_topic not configured, can't complete XAPPLEPUSHSERVICE response"); prot_printf(imapd_out, "%s NO Server configuration error\r\n", tag); return; } if (!buf_len(&applepushserviceargs->aps_account_id)) { prot_printf(imapd_out, "%s NO Missing APNS account ID\r\n", tag); return; } if (!buf_len(&applepushserviceargs->aps_device_token)) { prot_printf(imapd_out, "%s NO Missing APNS device token\r\n", tag); return; } if (!buf_len(&applepushserviceargs->aps_subtopic)) { prot_printf(imapd_out, "%s NO Missing APNS sub-topic\r\n", tag); return; } // v1 is inbox-only, so override the mailbox list if (applepushserviceargs->aps_version == 1) { strarray_truncate(&applepushserviceargs->mailboxes, 0); strarray_push(&applepushserviceargs->mailboxes, "INBOX"); applepushserviceargs->aps_version = 1; } else { // 2 is the most we support applepushserviceargs->aps_version = 2; } for (i = 0; i < strarray_size(&applepushserviceargs->mailboxes); i++) { const char *name = strarray_nth(&applepushserviceargs->mailboxes, i); char *intname = mboxname_from_external(name, &imapd_namespace, imapd_userid); r = mlookup(tag, name, intname, &mbentry); if (!r && mbtype_isa(mbentry->mbtype) == MBTYPE_EMAIL) { strarray_push(¬if_mailboxes, name); if (applepushserviceargs->aps_version >= 2) { prot_puts(imapd_out, "* XAPPLEPUSHSERVICE \"mailbox\" "); prot_printstring(imapd_out, name); prot_puts(imapd_out, "\r\n"); } } mboxlist_entry_free(&mbentry); free(intname); } prot_printf(imapd_out, "* XAPPLEPUSHSERVICE \"aps-version\" \"%d\" \"aps-topic\" \"%s\"\r\n", applepushserviceargs->aps_version, aps_topic); prot_printf(imapd_out, "%s OK XAPPLEPUSHSERVICE completed.\r\n", tag); struct mboxevent *mboxevent = mboxevent_new(EVENT_APPLEPUSHSERVICE); mboxevent_set_applepushservice(mboxevent, applepushserviceargs, ¬if_mailboxes, imapd_userid); mboxevent_notify(&mboxevent); mboxevent_free(&mboxevent); buf_free(&applepushserviceargs->aps_account_id); buf_free(&applepushserviceargs->aps_device_token); buf_free(&applepushserviceargs->aps_subtopic); strarray_fini(&applepushserviceargs->mailboxes); strarray_fini(¬if_mailboxes); }