Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
918 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/changes/next/3360-apply-message-write-failure-desync b/changes/next/3360-apply-message-write-failure-desync
new file mode 100644
index 000000000..b37844955
--- /dev/null
+++ b/changes/next/3360-apply-message-write-failure-desync
@@ -0,0 +1,11 @@
+Description:
+
+Bug fix for #3360
+
+Config changes:
+
+debug_writefail_guid
+
+Upgrade instructions:
+
+None required
diff --git a/imap/dlist.c b/imap/dlist.c
index 8a3a975b4..2aa8ba2d9 100644
--- a/imap/dlist.c
+++ b/imap/dlist.c
@@ -1,1649 +1,1674 @@
/* dlist.c - list protocol for dump and sync
*
* Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <config.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <syslog.h>
#include <string.h>
#include <sys/wait.h>
#include <dirent.h>
#include "global.h"
#include "assert.h"
#include "mboxlist.h"
#include "mailbox.h"
#include "quota.h"
#include "xmalloc.h"
#include "seen.h"
#include "mboxname.h"
#include "map.h"
#include "imapd.h"
#include "message.h"
#include "util.h"
#include "prot.h"
/* generated headers are not necessarily in current directory */
#include "imap/imap_err.h"
#include "dlist.h"
/* Parse routines */
static const char *lastkey = NULL;
static void printfile(struct protstream *out, const struct dlist *dl)
{
struct stat sbuf;
FILE *f;
unsigned long size;
struct message_guid guid2;
const char *msg_base = NULL;
size_t msg_len = 0;
assert(dlist_isfile(dl));
f = fopen(dl->sval, "r");
if (!f) {
syslog(LOG_ERR, "IOERROR: Failed to read file %s", dl->sval);
prot_printf(out, "NIL");
return;
}
if (fstat(fileno(f), &sbuf) == -1) {
syslog(LOG_ERR, "IOERROR: Failed to stat file %s", dl->sval);
prot_printf(out, "NIL");
fclose(f);
return;
}
size = sbuf.st_size;
if (size != dl->nval) {
syslog(LOG_ERR, "IOERROR: Size mismatch %s (%lu != " MODSEQ_FMT ")",
dl->sval, size, dl->nval);
prot_printf(out, "NIL");
fclose(f);
return;
}
map_refresh(fileno(f), 1, &msg_base, &msg_len, sbuf.st_size,
"new message", 0);
message_guid_generate(&guid2, msg_base, msg_len);
if (!message_guid_equal(&guid2, dl->gval)) {
syslog(LOG_ERR, "IOERROR: GUID mismatch %s",
dl->sval);
prot_printf(out, "NIL");
fclose(f);
map_free(&msg_base, &msg_len);
return;
}
prot_printf(out, "%%{");
prot_printastring(out, dl->part);
prot_printf(out, " ");
prot_printastring(out, message_guid_encode(dl->gval));
prot_printf(out, " %lu}\r\n", size);
prot_write(out, msg_base, msg_len);
fclose(f);
map_free(&msg_base, &msg_len);
}
/* XXX - these two functions should be out in append.c or reserve.c
* or something more general */
EXPORTED const char *dlist_reserve_path(const char *part, int isarchive, int isbackup,
const struct message_guid *guid)
{
static char buf[MAX_MAILBOX_PATH];
const char *base = NULL;
/* part must be a configured partition name on this server */
if (isbackup) {
base = config_backupstagingpath();
}
else {
if (isarchive) base = config_archivepartitiondir(part);
if (!base) base = config_partitiondir(part);
}
/* we expect to have a base at this point, so let's assert that */
assert(base != NULL);
snprintf(buf, MAX_MAILBOX_PATH, "%s/sync./%lu/%s",
base, (unsigned long)getpid(),
message_guid_encode(guid));
/* gotta make sure we can create files */
if (cyrus_mkdir(buf, 0755)) {
/* it's going to fail later, but at least this will help */
syslog(LOG_ERR, "IOERROR: failed to create %s/sync./%lu/ for reserve: %m",
base, (unsigned long)getpid());
}
return buf;
}
static int reservefile(struct protstream *in, const char *part,
struct message_guid *guid, unsigned long size,
int isbackup, const char **fname)
{
+ static struct message_guid debug_writefail_guid = MESSAGE_GUID_INITIALIZER;
FILE *file;
char buf[8192+1];
int r = 0;
+ if (debug_writefail_guid.status == GUID_UNKNOWN) {
+ const char *guidstr = config_getstring(IMAPOPT_DEBUG_WRITEFAIL_GUID);
+ if (guidstr) {
+ if (!message_guid_decode(&debug_writefail_guid, guidstr)) {
+ xsyslog(LOG_DEBUG, "debug_writefail_guid: ignoring invalid guid",
+ "guid=<%s>", guidstr);
+ message_guid_set_null(&debug_writefail_guid);
+ }
+ }
+ else {
+ message_guid_set_null(&debug_writefail_guid);
+ }
+ }
+
/* XXX - write to a temporary file then move in to place! */
*fname = dlist_reserve_path(part, /*isarchive*/0, isbackup, guid);
/* remove any duplicates if they're still here */
unlink(*fname);
file = fopen(*fname, "w+");
if (!file) {
syslog(LOG_ERR,
"IOERROR: failed to upload file %s", message_guid_encode(guid));
r = IMAP_IOERROR;
- /* Note: we still read the file's data from the wire,
- * to avoid losing protocol sync */
}
+ else if (debug_writefail_guid.status == GUID_NONNULL
+ && message_guid_equal(&debug_writefail_guid, guid)) {
+ /* no error, but pretend the disk is full */
+ fclose(file);
+ file = NULL;
+ errno = ENOSPC;
+ syslog(LOG_ERR, "IOERROR: failed to upload file %s (simulated)",
+ message_guid_encode(guid));
+ r = IMAP_IOERROR;
+ }
+ /* Note: in the case of error we still read the file's data from the wire,
+ * to avoid losing protocol sync */
/* XXX - calculate sha1 on the fly? */
while (size) {
size_t n = prot_read(in, buf, size > 8192 ? 8192 : size);
if (!n) {
syslog(LOG_ERR,
"IOERROR: reading message: unexpected end of file");
r = IMAP_IOERROR;
break;
}
size -= n;
if (!file) continue;
if (fwrite(buf, 1, n, file) != n) {
syslog(LOG_ERR, "IOERROR: writing to file '%s': %m", *fname);
r = IMAP_IOERROR;
break;
}
}
if (r)
goto error;
/* Make sure that message flushed to disk just incase mmap has problems */
fflush(file);
if (ferror(file)) {
r = IMAP_IOERROR;
goto error;
}
if (fsync(fileno(file)) < 0) {
syslog(LOG_ERR, "IOERROR: fsyncing file '%s': %m", *fname);
r = IMAP_IOERROR;
goto error;
}
fclose(file);
return 0;
error:
if (file) {
fclose(file);
unlink(*fname);
*fname = NULL;
}
return r;
}
/* DLIST STUFF */
EXPORTED void dlist_stitch(struct dlist *parent, struct dlist *child)
{
assert(!child->next);
if (parent->tail) {
parent->tail->next = child;
parent->tail = child;
}
else {
parent->head = parent->tail = child;
}
}
EXPORTED void dlist_unstitch(struct dlist *parent, struct dlist *child)
{
struct dlist *prev = NULL;
struct dlist *replace = NULL;
/* find old record */
for (replace = parent->head; replace; replace = replace->next) {
if (replace == child) break;
prev = replace;
}
assert(replace);
if (prev) prev->next = child->next;
else parent->head = child->next;
if (parent->tail == child) parent->tail = prev;
child->next = NULL;
}
static struct dlist *dlist_child(struct dlist *dl, const char *name)
{
struct dlist *i = xzmalloc(sizeof(struct dlist));
if (name) i->name = xstrdup(name);
i->type = DL_NIL;
if (dl)
dlist_stitch(dl, i);
return i;
}
static void _dlist_free_children(struct dlist *dl)
{
struct dlist *next;
struct dlist *i;
if (!dl) return;
i = dl->head;
while (i) {
next = i->next;
dlist_free(&i);
i = next;
}
dl->head = dl->tail = NULL;
}
static void _dlist_clean(struct dlist *dl)
{
if (!dl) return;
/* remove any children */
_dlist_free_children(dl);
/* clean out values */
free(dl->part);
dl->part = NULL;
free(dl->sval);
dl->sval = NULL;
free(dl->gval);
dl->gval = NULL;
dl->nval = 0;
}
void dlist_makeatom(struct dlist *dl, const char *val)
{
if (!dl) return;
_dlist_clean(dl);
if (val) {
dl->type = DL_ATOM;
dl->sval = xstrdup(val);
dl->nval = strlen(val);
}
else
dl->type = DL_NIL;
}
void dlist_makeflag(struct dlist *dl, const char *val)
{
if (!dl) return;
_dlist_clean(dl);
if (val) {
dl->type = DL_FLAG;
dl->sval = xstrdup(val);
dl->nval = strlen(val);
}
else
dl->type = DL_NIL;
}
void dlist_makenum32(struct dlist *dl, uint32_t val)
{
if (!dl) return;
_dlist_clean(dl);
dl->type = DL_NUM;
dl->nval = val;
}
void dlist_makenum64(struct dlist *dl, bit64 val)
{
if (!dl) return;
_dlist_clean(dl);
dl->type = DL_NUM;
dl->nval = val;
}
void dlist_makedate(struct dlist *dl, time_t val)
{
if (!dl) return;
_dlist_clean(dl);
dl->type = DL_DATE;
dl->nval = val;
}
void dlist_makehex64(struct dlist *dl, bit64 val)
{
if (!dl) return;
_dlist_clean(dl);
dl->type = DL_HEX;
dl->nval = val;
}
void dlist_makeguid(struct dlist *dl, const struct message_guid *guid)
{
if (!dl) return;
_dlist_clean(dl);
if (guid) {
dl->type = DL_GUID,
dl->gval = xzmalloc(sizeof(struct message_guid));
message_guid_copy(dl->gval, guid);
}
else
dl->type = DL_NIL;
}
void dlist_makefile(struct dlist *dl,
const char *part, const struct message_guid *guid,
unsigned long size, const char *fname)
{
if (!dl) return;
_dlist_clean(dl);
if (part && guid && fname) {
dl->type = DL_FILE;
dl->gval = xzmalloc(sizeof(struct message_guid));
message_guid_copy(dl->gval, guid);
dl->sval = xstrdup(fname);
dl->nval = size;
dl->part = xstrdup(part);
}
else
dl->type = DL_NIL;
}
EXPORTED void dlist_makemap(struct dlist *dl, const char *val, size_t len)
{
if (!dl) return;
_dlist_clean(dl);
if (val) {
dl->type = DL_BUF;
/* WARNING - DO NOT replace this with xstrndup - the
* data may be binary, and xstrndup does not copy
* binary data correctly - but we still want to NULL
* terminate for non-binary data */
dl->sval = xmalloc(len+1);
memcpy(dl->sval, val, len);
dl->sval[len] = '\0'; /* make it string safe too */
dl->nval = len;
}
else
dl->type = DL_NIL;
}
EXPORTED struct dlist *dlist_newkvlist(struct dlist *parent, const char *name)
{
struct dlist *dl = dlist_child(parent, name);
dl->type = DL_KVLIST;
return dl;
}
EXPORTED struct dlist *dlist_newlist(struct dlist *parent, const char *name)
{
struct dlist *dl = dlist_child(parent, name);
dl->type = DL_ATOMLIST;
return dl;
}
EXPORTED struct dlist *dlist_newpklist(struct dlist *parent, const char *name)
{
struct dlist *dl = dlist_child(parent, name);
dl->type = DL_ATOMLIST;
dl->nval = 1;
return dl;
}
EXPORTED struct dlist *dlist_setatom(struct dlist *parent, const char *name, const char *val)
{
struct dlist *dl = dlist_child(parent, name);
dlist_makeatom(dl, val);
return dl;
}
EXPORTED struct dlist *dlist_setflag(struct dlist *parent, const char *name, const char *val)
{
struct dlist *dl = dlist_child(parent, name);
dlist_makeflag(dl, val);
return dl;
}
EXPORTED struct dlist *dlist_setnum64(struct dlist *parent, const char *name, bit64 val)
{
struct dlist *dl = dlist_child(parent, name);
dlist_makenum64(dl, val);
return dl;
}
EXPORTED struct dlist *dlist_setnum32(struct dlist *parent, const char *name, uint32_t val)
{
struct dlist *dl = dlist_child(parent, name);
dlist_makenum32(dl, val);
return dl;
}
EXPORTED struct dlist *dlist_setdate(struct dlist *parent, const char *name, time_t val)
{
struct dlist *dl = dlist_child(parent, name);
dlist_makedate(dl, val);
return dl;
}
EXPORTED struct dlist *dlist_sethex64(struct dlist *parent, const char *name, bit64 val)
{
struct dlist *dl = dlist_child(parent, name);
dlist_makehex64(dl, val);
return dl;
}
EXPORTED struct dlist *dlist_setmap(struct dlist *parent, const char *name,
const char *val, size_t len)
{
struct dlist *dl = dlist_child(parent, name);
dlist_makemap(dl, val, len);
return dl;
}
EXPORTED struct dlist *dlist_setguid(struct dlist *parent, const char *name,
const struct message_guid *guid)
{
struct dlist *dl = dlist_child(parent, name);
dlist_makeguid(dl, guid);
return dl;
}
EXPORTED struct dlist *dlist_setfile(struct dlist *parent, const char *name,
const char *part, const struct message_guid *guid,
size_t size, const char *fname)
{
struct dlist *dl = dlist_child(parent, name);
dlist_makefile(dl, part, guid, size, fname);
return dl;
}
static struct dlist *dlist_updatechild(struct dlist *parent, const char *name)
{
struct dlist *dl = dlist_getchild(parent, name);
if (!dl) dl = dlist_child(parent, name);
return dl;
}
struct dlist *dlist_updateatom(struct dlist *parent, const char *name, const char *val)
{
struct dlist *dl = dlist_updatechild(parent, name);
dlist_makeatom(dl, val);
return dl;
}
struct dlist *dlist_updateflag(struct dlist *parent, const char *name, const char *val)
{
struct dlist *dl = dlist_updatechild(parent, name);
dlist_makeflag(dl, val);
return dl;
}
struct dlist *dlist_updatenum64(struct dlist *parent, const char *name, bit64 val)
{
struct dlist *dl = dlist_updatechild(parent, name);
dlist_makenum64(dl, val);
return dl;
}
struct dlist *dlist_updatenum32(struct dlist *parent, const char *name, uint32_t val)
{
struct dlist *dl = dlist_updatechild(parent, name);
dlist_makenum32(dl, val);
return dl;
}
struct dlist *dlist_updatedate(struct dlist *parent, const char *name, time_t val)
{
struct dlist *dl = dlist_updatechild(parent, name);
dlist_makedate(dl, val);
return dl;
}
struct dlist *dlist_updatehex64(struct dlist *parent, const char *name, bit64 val)
{
struct dlist *dl = dlist_updatechild(parent, name);
dlist_makehex64(dl, val);
return dl;
}
struct dlist *dlist_updatemap(struct dlist *parent, const char *name,
const char *val, size_t len)
{
struct dlist *dl = dlist_updatechild(parent, name);
dlist_makemap(dl, val, len);
return dl;
}
struct dlist *dlist_updateguid(struct dlist *parent, const char *name,
const struct message_guid *guid)
{
struct dlist *dl = dlist_updatechild(parent, name);
dlist_makeguid(dl, guid);
return dl;
}
struct dlist *dlist_updatefile(struct dlist *parent, const char *name,
const char *part, const struct message_guid *guid,
size_t size, const char *fname)
{
struct dlist *dl = dlist_updatechild(parent, name);
dlist_makefile(dl, part, guid, size, fname);
return dl;
}
EXPORTED void dlist_print(const struct dlist *dl, int printkeys,
struct protstream *out)
{
struct dlist *di;
if (printkeys) {
prot_printastring(out, dl->name);
prot_putc(' ', out);
}
switch (dl->type) {
case DL_NIL:
prot_printf(out, "NIL");
break;
case DL_ATOM:
prot_printastring(out, dl->sval);
break;
case DL_FLAG:
prot_printf(out, "%s", dl->sval);
break;
case DL_NUM:
case DL_DATE: /* for now, we will format it later */
prot_printf(out, "%llu", dl->nval);
break;
case DL_FILE:
printfile(out, dl);
break;
case DL_BUF:
if (strlen(dl->sval) == dl->nval)
prot_printastring(out, dl->sval);
else
prot_printliteral(out, dl->sval, dl->nval);
break;
case DL_GUID:
prot_printf(out, "%s", message_guid_encode(dl->gval));
break;
case DL_HEX:
{
char buf[17];
snprintf(buf, 17, "%016llx", dl->nval);
prot_printf(out, "%s", buf);
}
break;
case DL_KVLIST:
prot_printf(out, "%%(");
for (di = dl->head; di; di = di->next) {
dlist_print(di, 1, out);
if (di->next) {
prot_printf(out, " ");
}
}
prot_printf(out, ")");
break;
case DL_ATOMLIST:
prot_printf(out, "(");
for (di = dl->head; di; di = di->next) {
dlist_print(di, dl->nval, out);
if (di->next)
prot_printf(out, " ");
}
prot_printf(out, ")");
break;
}
}
EXPORTED void dlist_printbuf(const struct dlist *dl, int printkeys, struct buf *outbuf)
{
struct protstream *outstream;
outstream = prot_writebuf(outbuf);
dlist_print(dl, printkeys, outstream);
prot_flush(outstream);
prot_free(outstream);
}
EXPORTED void dlist_unlink_files(struct dlist *dl)
{
struct dlist *i;
if (!dl) return;
for (i = dl->head; i; i = i->next) {
dlist_unlink_files(i);
}
if (dl->type != DL_FILE) return;
if (!dl->sval) return;
syslog(LOG_DEBUG, "%s: unlinking %s", __func__, dl->sval);
unlink(dl->sval);
}
EXPORTED void dlist_free(struct dlist **dlp)
{
if (!*dlp) return;
_dlist_clean(*dlp);
free((*dlp)->name);
free(*dlp);
*dlp = NULL;
}
struct dlist_stack_node {
const struct dlist *dl;
int printkeys;
struct dlist_stack_node *next;
};
struct dlist_print_iter {
int printkeys;
struct dlist_stack_node *parent;
const struct dlist *next;
};
EXPORTED struct dlist_print_iter *dlist_print_iter_new(const struct dlist *dl, int printkeys)
{
struct dlist_print_iter *iter = xzmalloc(sizeof *iter);
iter->printkeys = printkeys;
iter->next = dl;
return iter;
}
EXPORTED const char *dlist_print_iter_step(struct dlist_print_iter *iter, struct buf *outbuf)
{
/* already finished */
if (!iter->next) return NULL;
buf_reset(outbuf);
/* Bundle short steps together to minimise call overhead.
* Note that outbuf can grow significantly longer than this limit, if a
* single item in the dlist is very long (e.g. a message), but then it
* won't bundle up more than that.
*/
while (iter->next != NULL && buf_len(outbuf) < 1024) {
const struct dlist *curr = iter->next;
struct dlist_stack_node *parent = NULL;
int descend = 0;
/* output */
switch (curr->type) {
case DL_KVLIST:
case DL_ATOMLIST:
// XXX should use equiv to "prot_printastring" for curr->name
if (iter->printkeys)
buf_printf(outbuf, "%s ", curr->name);
buf_appendcstr(outbuf, curr->type == DL_KVLIST ? "%(" : "(");
if (curr->head) {
descend = 1;
}
else {
buf_putc(outbuf, ')');
if (curr->next)
buf_putc(outbuf, ' ');
}
break;
default:
dlist_printbuf(curr, iter->printkeys, outbuf);
if (curr->next)
buf_putc(outbuf, ' ');
break;
}
/* increment */
if (descend) {
parent = xmalloc(sizeof *parent);
parent->printkeys = iter->printkeys;
parent->dl = curr;
parent->next = iter->parent;
iter->parent = parent;
iter->next = curr->head;
// XXX can this always be 1? we know an atom list here is non-empty
iter->printkeys = curr->type == DL_KVLIST ? 1 : curr->nval;
}
else if (curr->next) {
iter->next = curr->next;
}
else if (iter->parent) {
/* multiple parents might be ending at the same point
* don't mistake one parent ending for end of entire tree
*/
do {
buf_putc(outbuf, ')');
parent = iter->parent;
iter->parent = iter->parent->next;
iter->next = parent->dl->next;
iter->printkeys = parent->printkeys;
free(parent);
if (iter->next) {
/* found an unfinished dlist, stop closing parents */
buf_putc(outbuf, ' ');
break;
}
} while (iter->parent);
}
else {
iter->next = NULL;
}
}
/* and return */
return buf_cstringnull(outbuf);
}
EXPORTED void dlist_print_iter_free(struct dlist_print_iter **iterp)
{
struct dlist_print_iter *iter = *iterp;
struct dlist_stack_node *tmp = NULL;
*iterp = NULL;
while (iter->parent) {
tmp = iter->parent;
iter->parent = iter->parent->next;
free(tmp);
}
free(iter);
}
struct dlistsax_state {
const char *base;
const char *p;
const char *end;
dlistsax_cb_t *proc;
int depth;
struct dlistsax_data d;
struct buf buf;
struct buf gbuf;
};
#ifdef HAVE_DECLARE_OPTIMIZE
static int _parseqstring(struct dlistsax_state *s, struct buf *buf)
__attribute__((optimize("-O3")));
static int _parseliteral(struct dlistsax_state *s, struct buf *buf)
__attribute__((optimize("-O3")));
static int _parseitem(struct dlistsax_state *s, struct buf *buf)
__attribute__((optimize("-O3")));
#endif
static int _parseqstring(struct dlistsax_state *s, struct buf *buf)
{
buf->len = 0;
/* get over the first quote */
if (*s->p++ != '"') return IMAP_INVALID_IDENTIFIER;
while (s->p < s->end) {
/* found the end quote */
if (*s->p == '"') {
s->p++;
return 0;
}
/* backslash just quotes the next char, no matter what it is */
if (*s->p == '\\') {
s->p++;
if (s->p == s->end) break;
/* fall through */
}
buf_putc(buf, *s->p++);
}
return IMAP_INVALID_IDENTIFIER;
}
static int _parseliteral(struct dlistsax_state *s, struct buf *buf)
{
size_t len = 0;
if (*s->p++ != '{') return IMAP_INVALID_IDENTIFIER;
while (s->p < s->end) {
if (cyrus_isdigit(*s->p)) {
len = (len * 10) + (*s->p++ - '0');
continue;
}
// skip literal+ if present
if (*s->p == '+' && (s->p + 1 < s->end))
s->p++;
// we'd better be at the end of the literal
if (*s->p == '}') {
if (s->p + 3 + len >= s->end) break;
if (s->p[1] != '\r') break;
if (s->p[2] != '\n') break;
buf_truncate(buf, 0);
buf_appendmap(buf, s->p + 3, len);
s->p += len + 3;
return 0;
}
break;
}
return IMAP_INVALID_IDENTIFIER;
}
static int _parseitem(struct dlistsax_state *s, struct buf *buf)
{
const char *sp;
/* this is much faster than setmap because it doesn't
* do a reset and check the MMAP flag */
buf_truncate(buf, 0);
switch (*s->p) {
case '"':
return _parseqstring(s, buf);
case '{':
return _parseliteral(s, buf);
default:
sp = memchr(s->p, ' ', s->end - s->p);
if (!sp) sp = s->end;
while (sp[-1] == ')' && sp > s->p) sp--;
buf_appendmap(buf, s->p, sp - s->p);
s->p = sp;
if (buf->len == 3 && buf->s[0] == 'N' && buf->s[1] == 'I' && buf->s[2] == 'L')
return IMAP_ZERO_LENGTH_LITERAL; // this is kinda bogus, but...
return 0; /* this could be the last thing, so end is OK */
}
}
static int _parsesax(struct dlistsax_state *s, int parsekey)
{
int r = 0;
s->depth++;
/* handle the key if wanted */
struct buf *backdoor = (struct buf *)(&s->d.kbuf);
if (parsekey) {
r = _parseitem(s, backdoor);
if (r) return r;
if (s->p >= s->end) return IMAP_INVALID_IDENTIFIER;
if (*s->p == ' ') s->p++;
else return IMAP_INVALID_IDENTIFIER;
}
else {
backdoor->len = 0;
}
if (s->p >= s->end) return IMAP_INVALID_IDENTIFIER;
/* check what sort of value we have */
if (*s->p == '(') {
r = s->proc(DLISTSAX_LISTSTART, &s->d);
if (r) return r;
s->p++;
if (s->p >= s->end) return IMAP_INVALID_IDENTIFIER;
while (*s->p != ')') {
r = _parsesax(s, 0);
if (r) return r;
if (*s->p == ')') break;
if (*s->p == ' ') s->p++;
else return IMAP_INVALID_IDENTIFIER;
if (s->p >= s->end) return IMAP_INVALID_IDENTIFIER;
}
r = s->proc(DLISTSAX_LISTEND, &s->d);
if (r) return r;
s->p++;
}
else if (*s->p == '%') {
s->p++;
if (s->p >= s->end) return IMAP_INVALID_IDENTIFIER;
/* no whitespace allowed here */
if (*s->p == '(') {
r = s->proc(DLISTSAX_KVLISTSTART, &s->d);
if (r) return r;
s->p++;
if (s->p >= s->end) return IMAP_INVALID_IDENTIFIER;
while (*s->p != ')') {
r = _parsesax(s, 1);
if (r) return r;
if (*s->p == ')') break;
if (*s->p == ' ') s->p++;
else return IMAP_INVALID_IDENTIFIER;
if (s->p >= s->end) return IMAP_INVALID_IDENTIFIER;
}
r = s->proc(DLISTSAX_KVLISTEND, &s->d);
if (r) return r;
s->p++;
}
else {
/* unknown percent type */
return IMAP_INVALID_IDENTIFIER;
}
}
else {
r = _parseitem(s, &s->buf);
if (r == IMAP_ZERO_LENGTH_LITERAL)
s->d.data = NULL; // NIL
else if (r) return r;
else
s->d.data = buf_cstring(&s->buf);
r = s->proc(DLISTSAX_STRING, &s->d);
s->d.data = NULL; // zero out for next call
if (r) return r;
}
s->depth--;
/* success */
return 0;
}
EXPORTED int dlist_parsesax(const char *base, size_t len, int parsekey,
dlistsax_cb_t *proc, void *rock)
{
static struct dlistsax_state state;
int r;
state.base = base;
state.p = base;
state.end = base + len;
state.proc = proc;
state.d.rock = rock;
r = _parsesax(&state, parsekey);
if (r) return r;
if (state.p < state.end)
return IMAP_IOERROR;
return 0;
}
static char next_nonspace(struct protstream *in, char c)
{
while (Uisspace(c)) c = prot_getc(in);
return c;
}
EXPORTED int dlist_parse(struct dlist **dlp, int parsekey, int isbackup,
struct protstream *in)
{
struct dlist *dl = NULL;
static struct buf kbuf;
static struct buf vbuf;
int c;
/* handle the key if wanted */
if (parsekey) {
c = getastring(in, NULL, &kbuf);
c = next_nonspace(in, c);
}
else {
buf_setcstr(&kbuf, "");
c = prot_getc(in);
}
/* connection dropped? */
if (c == EOF) goto fail;
/* check what sort of value we have */
if (c == '(') {
dl = dlist_newlist(NULL, kbuf.s);
c = next_nonspace(in, ' ');
while (c != ')') {
struct dlist *di = NULL;
prot_ungetc(c, in);
c = dlist_parse(&di, 0, isbackup, in);
if (di) dlist_stitch(dl, di);
c = next_nonspace(in, c);
if (c == EOF) goto fail;
}
c = prot_getc(in);
}
else if (c == '%') {
/* no whitespace allowed here */
c = prot_getc(in);
if (c == '(') {
dl = dlist_newkvlist(NULL, kbuf.s);
c = next_nonspace(in, ' ');
while (c != ')') {
struct dlist *di = NULL;
prot_ungetc(c, in);
c = dlist_parse(&di, 1, isbackup, in);
if (di) dlist_stitch(dl, di);
c = next_nonspace(in, c);
if (c == EOF) goto fail;
}
}
else if (c == '{') {
struct message_guid tmp_guid;
static struct buf pbuf, gbuf;
unsigned size = 0;
const char *fname;
c = getastring(in, NULL, &pbuf);
if (c != ' ') goto fail;
c = getastring(in, NULL, &gbuf);
if (c != ' ') goto fail;
c = getuint32(in, &size);
if (c != '}') goto fail;
c = prot_getc(in);
if (c == '\r') c = prot_getc(in);
if (c != '\n') goto fail;
if (!message_guid_decode(&tmp_guid, gbuf.s)) goto fail;
if (reservefile(in, pbuf.s, &tmp_guid, size, isbackup, &fname)) goto fail;
dl = dlist_setfile(NULL, kbuf.s, pbuf.s, &tmp_guid, size, fname);
/* file literal */
}
else {
/* unknown percent type */
goto fail;
}
c = prot_getc(in);
}
else if (c == '{') {
prot_ungetc(c, in);
/* could be binary in a literal */
c = getbastring(in, NULL, &vbuf);
dl = dlist_setmap(NULL, kbuf.s, vbuf.s, vbuf.len);
}
else if (c == '\\') { /* special case for flags */
prot_ungetc(c, in);
c = getastring(in, NULL, &vbuf);
dl = dlist_setflag(NULL, kbuf.s, vbuf.s);
}
else {
prot_ungetc(c, in);
c = getnastring(in, NULL, &vbuf);
dl = dlist_setatom(NULL, kbuf.s, vbuf.s);
}
/* success */
*dlp = dl;
return c;
fail:
dlist_free(&dl);
return EOF;
}
EXPORTED int dlist_parse_asatomlist(struct dlist **dlp, int parsekey,
struct protstream *in)
{
int c = dlist_parse(dlp, parsekey, 0, in);
/* make a list with one item */
if (*dlp && !dlist_isatomlist(*dlp)) {
struct dlist *tmp = dlist_newlist(NULL, "");
dlist_stitch(tmp, *dlp);
*dlp = tmp;
}
return c;
}
EXPORTED int dlist_parsemap(struct dlist **dlp, int parsekey, int isbackup,
const char *base, unsigned len)
{
struct protstream *stream;
int c;
struct dlist *dl = NULL;
stream = prot_readmap(base, len);
prot_setisclient(stream, 1); /* don't sync literals */
c = dlist_parse(&dl, parsekey, isbackup, stream);
prot_free(stream);
if (c != EOF) {
dlist_free(&dl);
return IMAP_IOERROR; /* failed to slurp entire buffer */
}
*dlp = dl;
return 0;
}
EXPORTED struct dlist *dlist_getchild(struct dlist *dl, const char *name)
{
struct dlist *i;
if (!dl) return NULL;
for (i = dl->head; i; i = i->next) {
if (i->name && !strcmp(name, i->name))
return i;
}
lastkey = name;
return NULL;
}
EXPORTED struct dlist *dlist_getchildn(struct dlist *dl, int num)
{
struct dlist *i;
if (!dl) return NULL;
for (i = dl->head; i && num; i = i->next)
num--;
return i;
}
/* duplicate the parent list as a new list, and then move @num
* of the children from the parent onto the new list */
EXPORTED struct dlist *dlist_splice(struct dlist *dl, int num)
{
struct dlist *ret = dlist_newlist(NULL, dl->name);
/* clone exact type */
ret->type = dl->type;
ret->nval = dl->nval;
if (num > 0) {
struct dlist *end = dlist_getchildn(dl, num - 1);
/* take the start of the list */
ret->head = dl->head;
/* leave the end (if any) */
if (end) {
ret->tail = end;
dl->head = end->next;
end->next = NULL;
}
else {
ret->tail = dl->tail;
dl->head = NULL;
dl->tail = NULL;
}
}
return ret;
}
EXPORTED void dlist_splat(struct dlist *parent, struct dlist *child)
{
struct dlist *prev = NULL;
struct dlist *replace;
/* find old record */
for (replace = parent->head; replace; replace = replace->next) {
if (replace == child) break;
prev = replace;
}
assert(replace);
if (child->head) {
/* stitch in children */
if (prev) prev->next = child->head;
else parent->head = child->head;
if (child->next) child->tail->next = child->next;
else parent->tail = child->tail;
}
else {
/* just remove the record */
if (prev) prev->next = child->next;
else parent->head = child->next;
if (!child->next) parent->tail = prev;
}
/* remove the node itself, carefully blanking out
* the now unlinked children */
child->head = NULL;
child->tail = NULL;
dlist_free(&child);
}
struct dlist *dlist_getkvchild_bykey(struct dlist *dl,
const char *key, const char *val)
{
struct dlist *i;
struct dlist *tmp;
if (!dl) return NULL;
for (i = dl->head; i; i = i->next) {
tmp = dlist_getchild(i, key);
if (tmp && !strcmp(tmp->sval, val))
return i;
}
return NULL;
}
int dlist_toatom(struct dlist *dl, const char **valp)
{
const char *str;
size_t len;
if (!dl) return 0;
/* atom can be NULL */
if (dl->type == DL_NIL) {
*valp = NULL;
return 1;
}
/* tomap always adds a trailing \0 */
if (!dlist_tomap(dl, &str, &len))
return 0;
/* got NULLs? */
if (dl->type == DL_BUF && strlen(str) != len)
return 0;
if (valp) *valp = str;
return 1;
}
HIDDEN int dlist_tomap(struct dlist *dl, const char **valp, size_t *lenp)
{
char tmp[30];
if (!dl) return 0;
switch (dl->type) {
case DL_NUM:
case DL_DATE:
snprintf(tmp, 30, "%llu", dl->nval);
dlist_makeatom(dl, tmp);
break;
case DL_HEX:
snprintf(tmp, 30, "%016llx", dl->nval);
dlist_makeatom(dl, tmp);
break;
case DL_GUID:
dlist_makeatom(dl, message_guid_encode(dl->gval));
break;
case DL_ATOM:
case DL_FLAG:
case DL_BUF:
case DL_NIL:
break;
default:
return 0;
}
if (valp) *valp = dl->sval;
if (lenp) *lenp = dl->nval;
return 1;
}
/* ensure value is exactly one number */
static int dlist_tonum64(struct dlist *dl, bit64 *valp)
{
const char *end;
bit64 newval;
if (!dl) return 0;
switch (dl->type) {
case DL_ATOM:
case DL_BUF:
if (parsenum(dl->sval, &end, dl->nval, &newval))
return 0;
if (end - dl->sval != (int)dl->nval)
return 0;
/* successfully parsed - switch to a numeric value */
dlist_makenum64(dl, newval);
break;
case DL_NUM:
case DL_HEX:
case DL_DATE:
break;
default:
return 0;
}
if (valp) *valp = dl->nval;
return 1;
}
EXPORTED int dlist_tonum32(struct dlist *dl, uint32_t *valp)
{
bit64 v;
if (dlist_tonum64(dl, &v)) {
if (valp) *valp = (uint32_t)v;
return 1;
}
return 0;
}
int dlist_todate(struct dlist *dl, time_t *valp)
{
bit64 v;
if (dlist_tonum64(dl, &v)) {
if (valp) *valp = (time_t)v;
dl->type = DL_DATE;
return 1;
}
return 0;
}
static int dlist_tohex64(struct dlist *dl, bit64 *valp)
{
const char *end = NULL;
bit64 newval;
if (!dl) return 0;
switch (dl->type) {
case DL_ATOM:
case DL_BUF:
if (parsehex(dl->sval, &end, dl->nval, &newval))
return 0;
if (end - dl->sval != (int)dl->nval)
return 0;
/* successfully parsed - switch to a numeric value */
dlist_makehex64(dl, newval);
break;
case DL_NUM:
case DL_HEX:
case DL_DATE:
dl->type = DL_HEX;
break;
default:
return 0;
}
if (valp) *valp = dl->nval;
return 1;
}
EXPORTED int dlist_toguid(struct dlist *dl, struct message_guid **valp)
{
struct message_guid tmpguid;
if (!dl) return 0;
switch (dl->type) {
case DL_ATOM:
case DL_BUF:
if (dl->nval != 40)
return 0;
if (!message_guid_decode(&tmpguid, dl->sval))
return 0;
/* successfully parsed - switch to guid value */
dlist_makeguid(dl, &tmpguid);
break;
case DL_GUID:
break;
default:
return 0;
}
if (valp) *valp = dl->gval;
return 1;
}
EXPORTED int dlist_tofile(struct dlist *dl,
const char **partp, struct message_guid **guidp,
unsigned long *sizep, const char **fnamep)
{
if (!dlist_isfile(dl)) return 0;
if (guidp) *guidp = dl->gval;
if (sizep) *sizep = dl->nval;
if (fnamep) *fnamep = dl->sval;
if (partp) *partp = dl->part;
return 1;
}
EXPORTED int dlist_isatomlist(const struct dlist *dl)
{
if (!dl) return 0;
return (dl->type == DL_ATOMLIST);
}
int dlist_iskvlist(const struct dlist *dl)
{
if (!dl) return 0;
return (dl->type == DL_KVLIST);
}
int dlist_isfile(const struct dlist *dl)
{
if (!dl) return 0;
return (dl->type == DL_FILE);
}
/* XXX - these ones aren't const, because they can change
* things... */
int dlist_isnum(struct dlist *dl)
{
bit64 tmp;
if (!dl) return 0;
/* see if it can be parsed as a number */
return dlist_tonum64(dl, &tmp);
}
/* XXX - these ones aren't const, because they can change
* things... */
EXPORTED int dlist_ishex64(struct dlist *dl)
{
bit64 tmp;
if (!dl) return 0;
/* see if it can be parsed as a number */
return dlist_tohex64(dl, &tmp);
}
/* XXX - these ones aren't const, because they can change
* things... */
int dlist_isguid(struct dlist *dl)
{
struct message_guid *tmp = NULL;
if (!dl) return 0;
return dlist_toguid(dl, &tmp);
}
/* XXX - this stuff is all shitty, rationalise later */
EXPORTED bit64 dlist_num(struct dlist *dl)
{
bit64 v;
if (!dl) return 0;
if (dlist_tonum64(dl, &v))
return v;
return 0;
}
/* XXX - this stuff is all shitty, rationalise later */
EXPORTED const char *dlist_cstring(struct dlist *dl)
{
static char zerochar = '\0';
if (dl) {
const char *res = NULL;
dlist_toatom(dl, &res);
if (res) return res;
}
return &zerochar;
}
EXPORTED int dlist_getatom(struct dlist *parent, const char *name, const char **valp)
{
struct dlist *child = dlist_getchild(parent, name);
return dlist_toatom(child, valp);
}
EXPORTED int dlist_getnum32(struct dlist *parent, const char *name, uint32_t *valp)
{
struct dlist *child = dlist_getchild(parent, name);
return dlist_tonum32(child, valp);
}
EXPORTED int dlist_getnum64(struct dlist *parent, const char *name, bit64 *valp)
{
struct dlist *child = dlist_getchild(parent, name);
return dlist_tonum64(child, valp);
}
EXPORTED int dlist_getdate(struct dlist *parent, const char *name, time_t *valp)
{
struct dlist *child = dlist_getchild(parent, name);
return dlist_todate(child, valp);
}
EXPORTED int dlist_gethex64(struct dlist *parent, const char *name, bit64 *valp)
{
struct dlist *child = dlist_getchild(parent, name);
return dlist_tohex64(child, valp);
}
EXPORTED int dlist_getguid(struct dlist *parent, const char *name,
struct message_guid **valp)
{
struct dlist *child = dlist_getchild(parent, name);
return dlist_toguid(child, valp);
}
EXPORTED int dlist_getmap(struct dlist *parent, const char *name,
const char **valp, size_t *lenp)
{
struct dlist *child = dlist_getchild(parent, name);
return dlist_tomap(child, valp, lenp);
}
EXPORTED int dlist_getbuf(struct dlist *parent, const char *name,
struct buf *value)
{
const char *v = NULL;
size_t l = 0;
if (dlist_getmap(parent, name, &v, &l)) {
buf_init_ro(value, v, l);
return 1;
}
return 0;
}
int dlist_getfile(struct dlist *parent, const char *name,
const char **partp,
struct message_guid **guidp,
unsigned long *sizep,
const char **fnamep)
{
struct dlist *child = dlist_getchild(parent, name);
return dlist_tofile(child, partp, guidp, sizep, fnamep);
}
EXPORTED int dlist_getlist(struct dlist *dl, const char *name, struct dlist **valp)
{
struct dlist *i = dlist_getchild(dl, name);
if (!i) return 0;
*valp = i;
return 1;
}
EXPORTED const char *dlist_lastkey(void)
{
return lastkey;
}
diff --git a/imap/imapd.c b/imap/imapd.c
index a9dbd6a18..b8ba8e10a 100644
--- a/imap/imapd.c
+++ b/imap/imapd.c
@@ -1,14627 +1,14633 @@
/*
* Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <config.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sysexits.h>
#include <syslog.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <stdbool.h>
#include <errno.h>
#include <sasl/sasl.h>
#ifdef HAVE_SSL
#include <openssl/hmac.h>
#include <openssl/rand.h>
#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 maxsize = 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 */
struct backend **backend_cached = NULL;
/* 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;
#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 */
{ "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_xconvsort(char *tag, int updates);
static void cmd_xconvmultisort(char *tag);
static void cmd_xconvmeta(const char *tag);
static void cmd_xconvfetch(const char *tag);
static int do_xconvfetch(struct dlist *cidlist,
modseq_t ifchangedsince,
struct fetchargs *fetchargs);
static void cmd_xsnippets(char *tag);
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 get_snippetargs(struct snippetargs **sap);
static void free_snippetargs(struct snippetargs **sap);
static int getsortcriteria(char *tag, struct sortcrit **sortcrit);
static int getdatetime(time_t *date);
static int parse_windowargs(const char *tag, struct windowargs **, int);
static void free_windowargs(struct windowargs *wa);
static void appendfieldlist(struct fieldlist **l, char *section,
strarray_t *fields, char *trail,
void *d, size_t size);
static void freefieldlist(struct fieldlist *l);
void freestrlist(struct strlist *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;
int bytes_in = 0;
int bytes_out = 0;
proc_cleanup();
/* close backend connections */
i = 0;
while (backend_cached && backend_cached[i]) {
proxy_downserver(backend_cached[i]);
if (backend_cached[i]->last_result.s) {
free(backend_cached[i]->last_result.s);
}
free(backend_cached[i]);
i++;
}
if (backend_cached) free(backend_cached);
backend_cached = NULL;
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=<%d> bytes_out=<%d>",
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);
maxsize = config_getint(IMAPOPT_MAXMESSAGESIZE) * 1024;
if (!maxsize) maxsize = 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);
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;
int bytes_in = 0;
int bytes_out = 0;
in_shutdown = 1;
proc_cleanup();
i = 0;
while (backend_cached && backend_cached[i]) {
proxy_downserver(backend_cached[i]);
if (backend_cached[i]->last_result.s) {
free(backend_cached[i]->last_result.s);
}
free(backend_cached[i]);
i++;
}
if (backend_cached) free(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=<%d> bytes_out=<%d>",
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 Fatal error: %s\r\n", 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);
/* 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;
/* 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 extraargs;
+ 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 extraargs;
+ 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 extraargs;
+ 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, "Xconvfetch")) {
cmd_xconvfetch(tag.s);
/* XXX prometheus_increment(CYRUS_IMAP_XCONVFETCH_TOTAL); */
}
else if (!strcmp(cmd.s, "Xconvmultisort")) {
if (c != ' ') goto missingargs;
if (!imapd_index && !backend_current) goto nomailbox;
cmd_xconvmultisort(tag.s);
/* XXX prometheus_increment(CYRUS_IMAP_XCONVMULTISORT_TOTAL); */
}
else if (!strcmp(cmd.s, "Xconvsort")) {
if (c != ' ') goto missingargs;
if (!imapd_index && !backend_current) goto nomailbox;
cmd_xconvsort(tag.s, 0);
/* XXX prometheus_increment(CYRUS_IMAP_XCONVSORT_TOTAL); */
}
else if (!strcmp(cmd.s, "Xconvupdates")) {
if (c != ' ') goto missingargs;
if (!imapd_index && !backend_current) goto nomailbox;
cmd_xconvsort(tag.s, 1);
/* XXX prometheus_increment(CYRUS_IMAP_XCONVUPDATES_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, "Xconvmeta")) {
cmd_xconvmeta(tag.s);
}
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, "Xsnippets")) {
if (c != ' ') goto missingargs;
if (!imapd_index && !backend_current) goto nomailbox;
cmd_xsnippets(tag.s);
/* XXX prometheus_increment(CYRUS_IMAP_XSNIPPETS_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);
if (!mboxname) mboxname = "<none>";
cmdtime_endtimer(&cmdtime, &nettime);
if (cmdtime >= commandmintimerd) {
syslog(LOG_NOTICE, "cmdtimer: '%s' '%s' '%s' '%f' '%f' '%f'",
imapd_userid ? imapd_userid : "<none>", cmdname, 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:
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:
prot_printf(imapd_out,
"%s BAD Unexpected extra arguments to %s\r\n", tag.s, cmd.s);
eatline(imapd_in, c);
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);
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;
/* 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 != ' ') {
prot_printf(imapd_out,
"%s BAD Invalid/missing field name in Id\r\n",
tag);
eatline(imapd_in, c);
return;
}
/* get field value */
c = getnstring(imapd_in, imapd_out, &arg);
if (c != ' ' && c != ')') {
prot_printf(imapd_out,
"%s BAD Invalid/missing value 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);
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", maxsize);
}
/*
* 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 *p, int c, size_t maxsize,
unsigned *size, int *binary, const char **parseerr)
{
int isnowait = 0;
uint32_t num;
/* 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 == '+') {
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(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(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);
if (init.vanishedlist) 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(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(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 != ' ' && 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);
/* 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 (!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) {
eatline(imapd_in, c);
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(curstage->f, cur_name, maxsize, &size,
&(curstage->binary), &parseerr, &url);
if (r) goto done;
}
else {
/* Read size from literal */
r = getliteralsize(arg.s, c, maxsize, &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:
if (r) {
eatline(imapd_in, c);
} else {
/* we should be looking at the end of the line */
if (!IS_EOL(c, imapd_in)) {
parseerr = "junk after literal";
r = IMAP_PROTOCOL_ERROR;
eatline(imapd_in, c);
}
}
/* 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;
}
if (!r) {
r = append_fromstage(&appendstate, &body, curstage->stage,
curstage->internaldate, /*createdmodseq*/0,
&curstage->flags, 0,
&curstage->annotations);
}
if (body) {
/* Note: either the calls to message_parse_binary_file()
* or append_fromstage() above, may create a 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_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 (;;) {
ucase(arg.s);
if (!strcmp(arg.s, "CONDSTORE")) {
client_capa |= CAPA_CONDSTORE;
}
else if ((client_capa & CAPA_QRESYNC) &&
!strcmp(arg.s, "QRESYNC")) {
char *p;
if (c != ' ') goto badqresync;
c = prot_getc(imapd_in);
if (c != '(') goto badqresync;
c = getastring(imapd_in, imapd_out, &arg);
v->uidvalidity = strtoul(arg.s, &p, 10);
if (*p || !v->uidvalidity || v->uidvalidity == ULONG_MAX) goto badqresync;
if (c != ' ') 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 (%lu " 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));
if (init.vanishedlist) seqset_free(init.vanishedlist);
init.vanishedlist = NULL;
if (doclose) index_close(&imapd_index);
free(intname);
return;
}
if (index_hasrights(imapd_index, ACL_EXPUNGE))
warn_about_quota(imapd_index->mailbox->quotaroot);
index_select(imapd_index, &init);
if (init.vanishedlist) seqset_free(init.vanishedlist);
init.vanishedlist = NULL;
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 (;;) {
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) {
eatline(imapd_in, c);
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);
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 ]<x.y> 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);
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);
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))) {
// annoyingly, this codepath COULD be called from xconv* commands, but it never is,
// in reality, so it's safe leaving this as shared
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 void do_one_xconvmeta(struct conversations_state *state,
conversation_id_t cid,
conversation_t *conv,
struct dlist *itemlist)
{
struct dlist *item = dlist_newpklist(NULL, "");
struct dlist *fl;
assert(conv);
assert(itemlist);
for (fl = itemlist->head; fl; fl = fl->next) {
const char *key = dlist_cstring(fl);
/* xxx - parse to a fetchitems? */
if (!strcasecmp(key, "MODSEQ"))
dlist_setnum64(item, "MODSEQ", conv->modseq);
else if (!strcasecmp(key, "EXISTS"))
dlist_setnum32(item, "EXISTS", conv->exists);
else if (!strcasecmp(key, "UNSEEN"))
dlist_setnum32(item, "UNSEEN", conv->unseen);
else if (!strcasecmp(key, "SIZE"))
dlist_setnum32(item, "SIZE", conv->size);
else if (!strcasecmp(key, "COUNT")) {
struct dlist *flist = dlist_newlist(item, "COUNT");
fl = fl->next;
if (dlist_isatomlist(fl)) {
struct dlist *tmp;
for (tmp = fl->head; tmp; tmp = tmp->next) {
const char *lookup = dlist_cstring(tmp);
int i = strarray_find_case(state->counted_flags, lookup, 0);
if (i >= 0) {
dlist_setflag(flist, "FLAG", lookup);
dlist_setnum32(flist, "COUNT", conv->counts[i]);
}
}
}
}
else if (!strcasecmp(key, "SENDERS")) {
conv_sender_t *sender;
struct dlist *slist = dlist_newlist(item, "SENDERS");
for (sender = conv->senders; sender; sender = sender->next) {
struct dlist *sli = dlist_newlist(slist, "");
dlist_setatom(sli, "NAME", sender->name);
dlist_setatom(sli, "ROUTE", sender->route);
dlist_setatom(sli, "MAILBOX", sender->mailbox);
dlist_setatom(sli, "DOMAIN", sender->domain);
}
}
/* XXX - maybe rename FOLDERCOUNTS or something? */
else if (!strcasecmp(key, "FOLDEREXISTS")) {
struct dlist *flist = dlist_newlist(item, "FOLDEREXISTS");
conv_folder_t *folder;
fl = fl->next;
if (dlist_isatomlist(fl)) {
struct dlist *tmp;
for (tmp = fl->head; tmp; tmp = tmp->next) {
const char *extname = dlist_cstring(tmp);
char *intname = mboxname_from_external(extname, &imapd_namespace, imapd_userid);
folder = conversation_find_folder(state, conv, intname);
free(intname);
dlist_setatom(flist, "MBOXNAME", extname);
/* ok if it's not there */
dlist_setnum32(flist, "EXISTS", folder ? folder->exists : 0);
}
}
}
else if (!strcasecmp(key, "FOLDERUNSEEN")) {
struct dlist *flist = dlist_newlist(item, "FOLDERUNSEEN");
conv_folder_t *folder;
fl = fl->next;
if (dlist_isatomlist(fl)) {
struct dlist *tmp;
for (tmp = fl->head; tmp; tmp = tmp->next) {
const char *extname = dlist_cstring(tmp);
char *intname = mboxname_from_external(extname, &imapd_namespace, imapd_userid);
folder = conversation_find_folder(state, conv, intname);
free(intname);
dlist_setatom(flist, "MBOXNAME", extname);
/* ok if it's not there */
dlist_setnum32(flist, "UNSEEN", folder ? folder->unseen : 0);
}
}
}
else {
dlist_setatom(item, key, NULL); /* add a NIL response */
}
}
prot_printf(imapd_out, "* XCONVMETA %s ", conversation_id_encode(cid));
dlist_print(item, 0, imapd_out);
prot_printf(imapd_out, "\r\n");
dlist_free(&item);
}
static void do_xconvmeta(const char *tag,
struct conversations_state *state,
struct dlist *cidlist,
struct dlist *itemlist)
{
conversation_id_t cid;
struct dlist *dl;
int r;
for (dl = cidlist->head; dl; dl = dl->next) {
const char *cidstr = dlist_cstring(dl);
conversation_t *conv = NULL;
if (!conversation_id_decode(&cid, cidstr) || !cid) {
prot_printf(imapd_out, "%s BAD Invalid CID %s\r\n", tag, cidstr);
return;
}
r = conversation_load(state, cid, &conv);
if (r) {
prot_printf(imapd_out, "%s BAD Failed to read %s\r\n", tag, cidstr);
conversation_free(conv);
return;
}
if (conv && conv->exists)
do_one_xconvmeta(state, cid, conv, itemlist);
conversation_free(conv);
}
prot_printf(imapd_out, "%s OK Completed\r\n", tag);
}
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));
}
}
/*
* Parse and perform a XCONVMETA command.
*/
void cmd_xconvmeta(const char *tag)
{
int r;
int c = ' ';
struct conversations_state *state = NULL;
struct dlist *cidlist = NULL;
struct dlist *itemlist = NULL;
if (backend_current) {
/* remote mailbox */
prot_printf(backend_current->out, "%s XCONVMETA ", tag);
if (!pipe_command(backend_current, 65536)) {
pipe_including_tag(backend_current, tag, 0);
}
return;
}
if (!config_getswitch(IMAPOPT_CONVERSATIONS)) {
prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag);
eatline(imapd_in, c);
goto done;
}
c = dlist_parse_asatomlist(&cidlist, 0, imapd_in);
if (c != ' ') {
prot_printf(imapd_out, "%s BAD Failed to parse CID list\r\n", tag);
eatline(imapd_in, c);
goto done;
}
c = dlist_parse_asatomlist(&itemlist, 0, imapd_in);
if (!IS_EOL(c, imapd_in)) {
prot_printf(imapd_out, "%s BAD Failed to parse item list\r\n", tag);
eatline(imapd_in, c);
goto done;
}
// this one is OK, xconvmeta doesn't do an expunge
r = conversations_open_user(imapd_userid, 1/*shared*/, &state);
if (r) {
prot_printf(imapd_out, "%s BAD failed to open db: %s\r\n",
tag, error_message(r));
goto done;
}
do_xconvmeta(tag, state, cidlist, itemlist);
done:
dlist_free(&itemlist);
dlist_free(&cidlist);
conversations_commit(&state);
}
/*
* Parse and perform a XCONVFETCH command.
*/
void cmd_xconvfetch(const char *tag)
{
int c = ' ';
struct fetchargs fetchargs;
int r;
clock_t start = clock();
modseq_t ifchangedsince = 0;
char mytime[100];
struct dlist *cidlist = NULL;
struct dlist *item;
if (backend_current) {
/* remote mailbox */
prot_printf(backend_current->out, "%s XCONVFETCH ", tag);
if (!pipe_command(backend_current, 65536)) {
pipe_including_tag(backend_current, tag, 0);
}
return;
}
if (!config_getswitch(IMAPOPT_CONVERSATIONS)) {
prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag);
eatline(imapd_in, c);
return;
}
/* local mailbox */
memset(&fetchargs, 0, sizeof(struct fetchargs));
c = dlist_parse_asatomlist(&cidlist, 0, imapd_in);
if (c != ' ')
goto syntax_error;
/* check CIDs */
for (item = cidlist->head; item; item = item->next) {
if (!dlist_ishex64(item)) {
prot_printf(imapd_out, "%s BAD Invalid CID\r\n", tag);
eatline(imapd_in, c);
goto freeargs;
}
}
c = getmodseq(imapd_in, &ifchangedsince);
if (c != ' ')
goto syntax_error;
r = parse_fetch_args(tag, "Xconvfetch", 0, &fetchargs);
if (r)
goto freeargs;
fetchargs.fetchitems |= (FETCH_UIDVALIDITY|FETCH_FOLDER);
fetchargs.namespace = &imapd_namespace;
fetchargs.userid = imapd_userid;
r = do_xconvfetch(cidlist, ifchangedsince, &fetchargs);
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 Completed (%s sec)\r\n",
tag, mytime);
}
freeargs:
dlist_free(&cidlist);
fetchargs_fini(&fetchargs);
return;
syntax_error:
prot_printf(imapd_out, "%s BAD Syntax error\r\n", tag);
eatline(imapd_in, c);
dlist_free(&cidlist);
fetchargs_fini(&fetchargs);
}
static int xconvfetch_lookup(struct conversations_state *statep,
conversation_id_t cid,
modseq_t ifchangedsince,
hash_table *wanted_cids,
strarray_t *folder_list)
{
const char *key = conversation_id_encode(cid);
conversation_t *conv = NULL;
conv_folder_t *folder;
int r;
r = conversation_load(statep, cid, &conv);
if (r) return r;
if (!conv)
goto out;
if (!conv->exists)
goto out;
/* output the metadata for this conversation */
{
struct dlist *dl = dlist_newlist(NULL, "");
dlist_setatom(dl, "", "MODSEQ");
do_one_xconvmeta(statep, cid, conv, dl);
dlist_free(&dl);
}
if (ifchangedsince >= conv->modseq)
goto out;
hash_insert(key, (void *)1, wanted_cids);
for (folder = conv->folders; folder; folder = folder->next) {
/* no contents */
if (!folder->exists)
continue;
/* finally, something worth looking at */
strarray_add(folder_list, strarray_nth(statep->folder_names, folder->number));
}
out:
conversation_free(conv);
return 0;
}
static int do_xconvfetch(struct dlist *cidlist,
modseq_t ifchangedsince,
struct fetchargs *fetchargs)
{
struct conversations_state *state = NULL;
int r = 0;
struct index_state *index_state = NULL;
struct dlist *dl;
hash_table wanted_cids = HASH_TABLE_INITIALIZER;
strarray_t folder_list = STRARRAY_INITIALIZER;
struct index_init init;
int i;
// this one expunges each mailbox it enters, so we need to lock exclusively
r = conversations_open_user(imapd_userid, 0/*shared*/, &state);
if (r) goto out;
construct_hash_table(&wanted_cids, 1024, 0);
for (dl = cidlist->head; dl; dl = dl->next) {
r = xconvfetch_lookup(state, dlist_num(dl), ifchangedsince,
&wanted_cids, &folder_list);
if (r) goto out;
}
/* unchanged, woot */
if (!folder_list.count)
goto out;
fetchargs->cidhash = &wanted_cids;
memset(&init, 0, sizeof(struct index_init));
init.userid = imapd_userid;
init.authstate = imapd_authstate;
init.out = imapd_out;
for (i = 0; i < folder_list.count; i++) {
const char *mboxname = folder_list.data[i];
r = index_open(mboxname, &init, &index_state);
if (r == IMAP_MAILBOX_NONEXISTENT)
continue;
if (r)
goto out;
index_checkflags(index_state, 0, 0);
/* make sure \Deleted messages are expunged. Will also lock the
* mailbox state and read any new information */
r = index_expunge(index_state, NULL, 1);
if (!r)
index_fetchresponses(index_state, NULL, /*usinguid*/1,
fetchargs, NULL);
index_close(&index_state);
if (r) goto out;
}
r = 0;
out:
index_close(&index_state);
conversations_commit(&state);
free_hash_table(&wanted_cids, NULL);
strarray_fini(&folder_list);
return r;
}
#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);
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) {
eatline(imapd_in, c);
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 (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);
/* 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 (!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;
}
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);
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);
}
/*
* Perform a XCONVSORT or XCONVUPDATES command
*/
void cmd_xconvsort(char *tag, int updates)
{
int c;
struct sortcrit *sortcrit = NULL;
struct searchargs *searchargs = NULL;
struct windowargs *windowargs = NULL;
struct index_init init;
struct index_state *oldstate = NULL;
struct conversations_state *cstate = NULL;
clock_t start = clock();
char mytime[100];
int r;
if (backend_current) {
/* remote mailbox */
const char *cmd = "Xconvsort";
prot_printf(backend_current->out, "%s %s ", tag, cmd);
if (!pipe_command(backend_current, 65536)) {
pipe_including_tag(backend_current, tag, 0);
}
return;
}
assert(imapd_index);
if (!config_getswitch(IMAPOPT_CONVERSATIONS)) {
prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag);
eatline(imapd_in, ' ');
return;
}
c = getsortcriteria(tag, &sortcrit);
if (c == EOF) goto error;
if (c != ' ') {
prot_printf(imapd_out, "%s BAD Missing window args in XConvSort\r\n",
tag);
goto error;
}
c = parse_windowargs(tag, &windowargs, updates);
if (c != ' ')
goto error;
/* open the conversations state first - we don't care if it fails,
* because that probably just means it's already open */
// this codepath might expunge, so we can't open shared
conversations_open_mbox(index_mboxname(imapd_index), 0/*shared*/, &cstate);
if (updates) {
/* in XCONVUPDATES, need to force a re-read from scratch into
* a new index, because we ask for deleted messages */
oldstate = imapd_index;
imapd_index = NULL;
memset(&init, 0, sizeof(struct index_init));
init.userid = imapd_userid;
init.authstate = imapd_authstate;
init.out = imapd_out;
init.want_expunged = 1;
r = index_open(index_mboxname(oldstate), &init, &imapd_index);
if (r) {
prot_printf(imapd_out, "%s NO %s\r\n", tag,
error_message(r));
goto error;
}
index_checkflags(imapd_index, 0, 0);
}
/* need index loaded to even parse searchargs! */
searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST,
&imapd_namespace, imapd_userid, imapd_authstate,
imapd_userisadmin || imapd_userisproxyadmin);
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 Xconvsort\r\n", tag);
goto error;
}
if (updates)
r = index_convupdates(imapd_index, sortcrit, searchargs, windowargs);
else
r = index_convsort(imapd_index, sortcrit, searchargs, windowargs);
if (oldstate) {
index_close(&imapd_index);
imapd_index = oldstate;
}
if (r < 0) {
prot_printf(imapd_out, "%s NO %s\r\n", tag,
error_message(r));
goto error;
}
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, "XCONVSORT (%s) processing time %s sec",
s, mytime);
free(s);
}
prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag,
error_message(IMAP_OK_COMPLETED), mytime);
out:
if (cstate) conversations_commit(&cstate);
freesortcrit(sortcrit);
freesearchargs(searchargs);
free_windowargs(windowargs);
return;
error:
if (cstate) conversations_commit(&cstate);
if (oldstate) {
if (imapd_index) index_close(&imapd_index);
imapd_index = oldstate;
}
eatline(imapd_in, (c == EOF ? ' ' : c));
goto out;
}
/*
* Perform a XCONVMULTISORT command. This is like XCONVSORT but returns
* search results from multiple folders. It still requires a selected
* mailbox, for two reasons:
*
* a) it's a useful shorthand for choosing what the current
* conversations scope is, and
*
* b) the code to parse a search program currently relies on a selected
* mailbox.
*
* Unlike ESEARCH it doesn't take folder names for scope, instead the
* search scope is implicitly the current conversation scope. This is
* implemented more or less by accident because both the Sphinx index
* and the conversations database are hardcoded to be per-user.
*/
static void cmd_xconvmultisort(char *tag)
{
int c;
struct sortcrit *sortcrit = NULL;
struct searchargs *searchargs = NULL;
struct windowargs *windowargs = NULL;
struct conversations_state *cstate = NULL;
clock_t start = clock();
char mytime[100];
int r;
if (backend_current) {
/* remote mailbox */
const char *cmd = "Xconvmultisort";
prot_printf(backend_current->out, "%s %s ", tag, cmd);
if (!pipe_command(backend_current, 65536)) {
pipe_including_tag(backend_current, tag, 0);
}
return;
}
assert(imapd_index);
if (!config_getswitch(IMAPOPT_CONVERSATIONS)) {
prot_printf(imapd_out, "%s BAD Unrecognized command\r\n", tag);
eatline(imapd_in, ' ');
return;
}
c = getsortcriteria(tag, &sortcrit);
if (c == EOF) goto error;
if (c != ' ') {
prot_printf(imapd_out, "%s BAD Missing window args in XConvMultiSort\r\n",
tag);
goto error;
}
c = parse_windowargs(tag, &windowargs, /*updates*/0);
if (c != ' ')
goto error;
/* open the conversations state first - we don't care if it fails,
* because that probably just means it's already open */
// this codepath might expunge, so we can't open shared
conversations_open_mbox(index_mboxname(imapd_index), 0/*shared*/, &cstate);
/* need index loaded to even parse searchargs! */
searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST,
&imapd_namespace, imapd_userid, imapd_authstate,
imapd_userisadmin || imapd_userisproxyadmin);
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 XconvMultiSort\r\n", tag);
goto error;
}
r = index_convmultisort(imapd_index, sortcrit, searchargs, windowargs);
if (r < 0) {
prot_printf(imapd_out, "%s NO %s\r\n", tag,
error_message(r));
goto error;
}
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, "XCONVMULTISORT (%s) processing time %s sec",
s, mytime);
free(s);
}
prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag,
error_message(IMAP_OK_COMPLETED), mytime);
out:
if (cstate) conversations_commit(&cstate);
freesortcrit(sortcrit);
freesearchargs(searchargs);
free_windowargs(windowargs);
return;
error:
if (cstate) conversations_commit(&cstate);
eatline(imapd_in, (c == EOF ? ' ' : c));
goto out;
}
static void cmd_xsnippets(char *tag)
{
int c;
struct searchargs *searchargs = NULL;
struct snippetargs *snippetargs = NULL;
clock_t start = clock();
char mytime[100];
int r;
if (backend_current) {
/* remote mailbox */
const char *cmd = "Xsnippets";
prot_printf(backend_current->out, "%s %s ", tag, cmd);
if (!pipe_command(backend_current, 65536)) {
pipe_including_tag(backend_current, tag, 0);
}
return;
}
assert(imapd_index);
c = get_snippetargs(&snippetargs);
if (c == EOF) {
prot_printf(imapd_out, "%s BAD Syntax error in snippet arguments\r\n", tag);
goto error;
}
if (c != ' ') {
prot_printf(imapd_out,
"%s BAD Unexpected arguments in Xsnippets\r\n", tag);
goto error;
}
/* need index loaded to even parse searchargs! */
searchargs = new_searchargs(tag, GETSEARCH_CHARSET_FIRST,
&imapd_namespace, imapd_userid, imapd_authstate,
imapd_userisadmin || imapd_userisproxyadmin);
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 Xsnippets\r\n", tag);
goto error;
}
r = index_snippets(imapd_index, snippetargs, searchargs);
if (r < 0) {
prot_printf(imapd_out, "%s NO %s\r\n", tag,
error_message(r));
goto error;
}
snprintf(mytime, sizeof(mytime), "%2.3f",
(clock() - start) / (double) CLOCKS_PER_SEC);
prot_printf(imapd_out, "%s OK %s (in %s secs)\r\n", tag,
error_message(IMAP_OK_COMPLETED), mytime);
out:
freesearchargs(searchargs);
free_snippetargs(&snippetargs);
return;
error:
eatline(imapd_in, (c == EOF ? ' ' : c));
goto out;
}
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);
c = get_search_program(imapd_in, imapd_out, searchargs);
if (c == EOF) {
eatline(imapd_in, ' ');
freesearchargs(searchargs);
return;
}
if (!IS_EOL(c, imapd_in)) {
prot_printf(imapd_out,
"%s BAD Unexpected extra arguments to Thread\r\n", tag);
eatline(imapd_in, c);
freesearchargs(searchargs);
return;
}
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);
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;
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,
&copyuid, !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 = 0;
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;
*/
}
}
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:
#if 0 /* XXX Do we want a config option for this?
XXX Regardless, will will have to update Cassandane tests */
// 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;
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));
r = mboxlist_createmailbox(mbname_intname(ancestor),
mbtype, partition,
imapd_userisadmin
|| imapd_userisproxyadmin,
imapd_userid, imapd_authstate,
localonly, localonly, 0, 1, NULL);
if (r) {
// 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;
}
#endif
// now create the requested mailbox
r = mboxlist_createmailbox_opts(
mbname_intname(mbname), // const char *name
mbtype, // int mbtype
partition, // const char *partition
imapd_userisadmin || imapd_userisproxyadmin, // int isadmin
imapd_userid, // const char *userid
imapd_authstate, // struct auth_state *auth_state
options, // options
localonly, // int localonly
localonly, // int forceuser
0, // int dbonly
1, // int notify
uniqueid, // const char *uniqueid
&mailbox // struct mailbox **mailboxptr
);
#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) {
r = mboxlist_createmailbox(
mbname_intname(mbname),
0,
partition,
1,
imapd_userid,
imapd_authstate,
0,
0,
0,
1,
&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_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:
mboxname_release(&namespacelock);
mailbox_close(&mailbox);
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;
r = mlookup(NULL, NULL, mbname_intname(mbname), &mbentry);
if (!r && (mbentry->mbtype & MBTYPE_REMOTE)) {
/* remote mailbox */
struct backend *s = NULL;
int res;
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;
}
mboxlist_entry_free(&mbentry);
struct mboxlock *namespacelock = mboxname_usernamespacelock(mbname_intname(mbname));
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(userid, 1);
}
}
mboxname_release(&namespacelock);
if (!r && config_getswitch(IMAPOPT_DELETE_UNSUBSCRIBE)) {
mboxlist_changesub(mbname_intname(mbname), imapd_userid, imapd_authstate,
/* add */ 0, /* force */ 0, /* notify? */ 1);
}
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;
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")) {
recursive_rename = 0;
}
/* 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, ACL,
seen state, subscriptions and sieve scripts */
if (!r && rename_user) {
user_copyquotaroot(oldmailboxname, newmailboxname);
user_renameacl(&imapd_namespace, newmailboxname, olduser, newuser);
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 ACLs, subscriptions, seen state and quotas */
if (!r && rename_user) {
/* user_deletedata takes care of logging the unuser */
user_deletedata(olduser, 1);
}
/* take care of intermediaries */
mboxlist_update_intermediaries(oldmailboxname, mbtype, 0);
mboxlist_update_intermediaries(newmailboxname, mbtype, 0);
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) {
strcpy(quotaroot, mailbox->quotaroot);
} 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) {
eatline(imapd_in, c);
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 == 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 (*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 == 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) {
eatline(imapd_in, c);
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;
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);
/* 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;
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:
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;
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);
}
}
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);
if (mailbox->quotaroot) {
struct quota q;
char *extname = mboxname_to_external(mailbox->quotaroot, &imapd_namespace, imapd_userid);
prot_printf(imapd_out, " ");
prot_printastring(imapd_out, extname);
quota_init(&q, mailbox->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;
}
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 (;;) {
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, mbentry->name, &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, 0);
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 */
imapd_check(NULL, 0);
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;
}
}
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);
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);
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 (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 == 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 (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 == EOF) {
prot_printf(imapd_out,
"%s BAD Missing annotation attribute\r\n", tag);
goto baddata;
}
strarray_append(attribs, arg.s);
}
return c;
baddata:
if (c != EOF) prot_ungetc(c, imapd_in);
return EOF;
}
/*
* 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 (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 == 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:
if (c != EOF) prot_ungetc(c, imapd_in);
return EOF;
}
/*
* 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 (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 == 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 == EOF) {
prot_printf(imapd_out,
"%s BAD Missing annotation value\r\n", tag);
goto baddata;
}
/* 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);
if (c != EOF) prot_ungetc(c, imapd_in);
return EOF;
}
/*
* 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 (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 (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 annotation entry list \r\n",
tag);
goto baddata;
}
c = prot_getc(imapd_in);
return c;
baddata:
if (attvalues) freeattvalues(attvalues);
if (c != EOF) prot_ungetc(c, imapd_in);
return EOF;
}
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;
};
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;
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;
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) {
eatline(imapd_in, c);
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;
}
else if (!strncmp(ent, "/shared", 7) &&
(ent[7] == '\0' || ent[7] == '/')) {
xstrncpy(entry, ent + 7, MAX_MAILBOX_NAME);
have_shared = 1;
}
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]);
nlists++;
if (c == '\r' || c == EOF)
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) {
eatline(imapd_in, c);
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) {
eatline(imapd_in, c);
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;
struct seqset *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 <mboxname> '('<warmup-items>')' */
c = getastring(imapd_in, imapd_out, &arg);
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);
if (uids) seqset_free(uids);
}
static void free_snippetargs(struct snippetargs **sap)
{
while (*sap) {
struct snippetargs *sa = *sap;
*sap = sa->next;
free(sa->mboxname);
free(sa->uids.data);
free(sa);
}
}
static int get_snippetargs(struct snippetargs **sap)
{
int c;
struct snippetargs **prevp = sap;
struct snippetargs *sa = NULL;
struct buf arg = BUF_INITIALIZER;
uint32_t uid;
char *intname = NULL;
c = prot_getc(imapd_in);
if (c != '(') goto syntax_error;
for (;;) {
c = prot_getc(imapd_in);
if (c == ')') break;
if (c != '(') goto syntax_error;
c = getastring(imapd_in, imapd_out, &arg);
if (c != ' ') goto syntax_error;
intname = mboxname_from_external(buf_cstring(&arg), &imapd_namespace, imapd_userid);
/* allocate a new snippetargs */
sa = xzmalloc(sizeof(struct snippetargs));
sa->mboxname = xstrdup(intname);
/* append to the list */
*prevp = sa;
prevp = &sa->next;
c = getuint32(imapd_in, &sa->uidvalidity);
if (c != ' ') goto syntax_error;
c = prot_getc(imapd_in);
if (c != '(') break;
for (;;) {
c = getuint32(imapd_in, &uid);
if (c != ' ' && c != ')') goto syntax_error;
if (sa->uids.count + 1 > sa->uids.alloc) {
sa->uids.alloc += 64;
sa->uids.data = xrealloc(sa->uids.data,
sizeof(uint32_t) * sa->uids.alloc);
}
sa->uids.data[sa->uids.count++] = uid;
if (c == ')') break;
}
c = prot_getc(imapd_in);
if (c != ')') goto syntax_error;
}
c = prot_getc(imapd_in);
if (c != ' ') goto syntax_error;
out:
free(intname);
buf_free(&arg);
return c;
syntax_error:
free_snippetargs(sap);
c = EOF;
goto out;
}
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);
/* administrators only please */
if (!imapd_userisadmin)
r = IMAP_PERMISSION_DENIED;
struct mboxlock *namespacelock = mboxname_usernamespacelock(mbname_intname(mbname));
if (!r) r = mlookup(tag, name, mbname_intname(mbname), NULL);
if (!r) r = undump_mailbox(mbname_intname(mbname), imapd_in, imapd_out, imapd_authstate);
mboxname_release(&namespacelock);
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));
}
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;
};
static int xfer_mupdate(int isactivate,
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;
snprintf(buf, sizeof(buf), "%s!%s", servername, part);
retry:
/* make the change */
if (isactivate)
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 (be->capability & CAPA_REPLICATION) {
syslog(LOG_INFO, "XFER: destination supports replication");
xfer->use_replication = 1;
be->in->userdata = be->out->userdata = &xfer->sync_cs.tagbuf;
}
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;
struct seqset *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, &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(0, item->mbentry->name, item->mbentry->partition,
config_servername, item->mbentry->acl);
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_update(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->part;
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, 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, error_message(r));
goto cleanup;
}
}
/* raclmodseq */
if (config_getswitch(IMAPOPT_REVERSEACLS)) {
raclmodseq = mboxname_readraclmodseq(mailbox->name);
}
master_folders = sync_folder_list_create();
sync_folder_list_add(master_folders,
mailbox->uniqueid, mailbox->name,
mailbox->mbtype,
mailbox->part,
mailbox->acl,
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,
/* 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) {
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, 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 {
if (mailbox->quotaroot)
sync_name_list_add(master_quotaroots, mailbox->quotaroot);
r = sync_do_annotation(&xfer->sync_cs, mailbox->name);
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) 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) {
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)
{
mbentry_t *newentry = NULL;
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) {
/* Set mailbox as DELETED on local server
(need to also reset to local partition,
otherwise mailbox can not be opened for deletion) */
/* 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(item->mbentry->server);
newentry->partition = xstrdupnull(item->mbentry->partition);
newentry->mbtype = MBTYPE_DELETED;
r = mboxlist_update(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));
}
/* 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);
if (r) {
syslog(LOG_ERR,
"Could not delete local mailbox during move of %s",
item->mbentry->name);
/* 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_update(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(1, 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\r\n",
mboxname);
return r;
}
struct xfer_list {
const struct namespace *ns;
const char *userid;
const char *part;
short allow_usersubs;
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, targeted user submailboxes, or user INBOXes */
if (!mbname_localpart(data->mbname) || list->allow_usersubs ||
(!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) && !list->allow_usersubs) {
/* 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, 0, 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 */
list.allow_usersubs = 1;
}
mboxlist_findall(NULL, mbname_intname(mbname), 1, NULL, NULL, xfer_addmbox, &list);
}
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.
*/
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) goto next;
/* backport the seen file if needed */
if (xfer->remoteversion < 12) {
r = seen_open(xfer->userid, SEEN_CREATE, &xfer->seendb);
if (r) 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 */
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(xfer->userid, 0);
}
mboxname_release(&namespacelock);
}
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->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 (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 != ' ') goto missingarg;
(*sortcrit)[n].args.annot.entry = xstrdup(criteria.s);
c = getastring(imapd_in, imapd_out, &criteria);
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 == 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 == 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;
}
static int parse_windowargs(const char *tag,
struct windowargs **wa,
int updates)
{
struct windowargs windowargs;
struct buf arg = BUF_INITIALIZER;
struct buf ext_folder = BUF_INITIALIZER;
int c;
memset(&windowargs, 0, sizeof(windowargs));
c = prot_getc(imapd_in);
if (c == EOF)
goto out;
if (c != '(') {
/* no window args at all */
prot_ungetc(c, imapd_in);
goto out;
}
for (;;)
{
c = prot_getc(imapd_in);
if (c == EOF)
goto out;
if (c == ')')
break; /* end of window args */
prot_ungetc(c, imapd_in);
c = getword(imapd_in, &arg);
if (!arg.len)
goto syntax_error;
if (!strcasecmp(arg.s, "CONVERSATIONS"))
windowargs.conversations = 1;
else if (!strcasecmp(arg.s, "POSITION")) {
if (updates)
goto syntax_error;
if (c != ' ')
goto syntax_error;
c = prot_getc(imapd_in);
if (c != '(')
goto syntax_error;
c = getuint32(imapd_in, &windowargs.position);
if (c != ' ')
goto syntax_error;
c = getuint32(imapd_in, &windowargs.limit);
if (c != ')')
goto syntax_error;
c = prot_getc(imapd_in);
if (windowargs.position == 0)
goto syntax_error;
}
else if (!strcasecmp(arg.s, "ANCHOR")) {
if (updates)
goto syntax_error;
if (c != ' ')
goto syntax_error;
c = prot_getc(imapd_in);
if (c != '(')
goto syntax_error;
c = getuint32(imapd_in, &windowargs.anchor);
if (c != ' ')
goto syntax_error;
c = getuint32(imapd_in, &windowargs.offset);
if (c != ' ')
goto syntax_error;
c = getuint32(imapd_in, &windowargs.limit);
if (c != ')')
goto syntax_error;
c = prot_getc(imapd_in);
if (windowargs.anchor == 0)
goto syntax_error;
}
else if (!strcasecmp(arg.s, "MULTIANCHOR")) {
if (updates)
goto syntax_error;
if (c != ' ')
goto syntax_error;
c = prot_getc(imapd_in);
if (c != '(')
goto syntax_error;
c = getuint32(imapd_in, &windowargs.anchor);
if (c != ' ')
goto syntax_error;
c = getastring(imapd_in, imapd_out, &ext_folder);
if (c != ' ')
goto syntax_error;
c = getuint32(imapd_in, &windowargs.offset);
if (c != ' ')
goto syntax_error;
c = getuint32(imapd_in, &windowargs.limit);
if (c != ')')
goto syntax_error;
c = prot_getc(imapd_in);
if (windowargs.anchor == 0)
goto syntax_error;
}
else if (!strcasecmp(arg.s, "CHANGEDSINCE")) {
if (!updates)
goto syntax_error;
windowargs.changedsince = 1;
if (c != ' ')
goto syntax_error;
c = prot_getc(imapd_in);
if (c != '(')
goto syntax_error;
c = getmodseq(imapd_in, &windowargs.modseq);
if (c != ' ')
goto syntax_error;
c = getuint32(imapd_in, &windowargs.uidnext);
if (c != ')')
goto syntax_error;
c = prot_getc(imapd_in);
} else if (!strcasecmp(arg.s, "UPTO")) {
if (!updates)
goto syntax_error;
if (c != ' ')
goto syntax_error;
c = prot_getc(imapd_in);
if (c != '(')
goto syntax_error;
c = getuint32(imapd_in, &windowargs.upto);
if (c != ')')
goto syntax_error;
c = prot_getc(imapd_in);
if (windowargs.upto == 0)
goto syntax_error;
}
else
goto syntax_error;
if (c == ')')
break;
if (c != ' ')
goto syntax_error;
}
c = prot_getc(imapd_in);
if (c != ' ')
goto syntax_error;
out:
/* these two are mutually exclusive */
if (windowargs.anchor && windowargs.position)
goto syntax_error;
/* changedsince is mandatory for XCONVUPDATES
* and illegal for XCONVSORT */
if (!!updates != windowargs.changedsince)
goto syntax_error;
if (ext_folder.len) {
windowargs.anchorfolder = mboxname_from_external(buf_cstring(&ext_folder),
&imapd_namespace,
imapd_userid);
}
*wa = xmemdup(&windowargs, sizeof(windowargs));
buf_free(&ext_folder);
buf_free(&arg);
return c;
syntax_error:
free(windowargs.anchorfolder);
buf_free(&ext_folder);
prot_printf(imapd_out, "%s BAD Syntax error in window arguments\r\n", tag);
if (c != EOF) prot_ungetc(c, imapd_in);
return EOF;
}
static void free_windowargs(struct windowargs *wa)
{
if (!wa)
return;
free(wa->anchorfolder);
free(wa);
}
/*
* 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 (!*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);
parse_getmetadata_options(&options, &opts);
args->metaopts = opts;
strarray_fini(&options);
if (c == EOF) return EOF;
} 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:
if (c != EOF) prot_ungetc(c, imapd_in);
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 (!*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 EOF;
}
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:
if (c != EOF) prot_ungetc(c, imapd_in);
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 == 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_iscalendarmailbox(intname, mbtype) ||
mboxname_isaddressbookmailbox(intname, mbtype) ||
mboxname_isdavdrivemailbox(intname, mbtype) ||
mboxname_issubmissionmailbox(intname, mbtype) ||
mboxname_ispushsubscriptionmailbox(intname, mbtype) ||
mboxname_isdavnotificationsmailbox(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) ||
!(data->mbentry->mbtype & MBTYPES_DAV)) &&
!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);
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 bsearch_compare_mbox(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);
(void)prot_putc(' ', imapd_out);
prot_printstring(imapd_out, arg.s);
if (extended) {
while (c == ' ') {
c = getword(imapd_in, &param);
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 != ' ') {
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 /* local_only */
};
/* 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);
// 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 /* local_only */
};
/* 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 <partition>/sync./<pid> 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 /* local_only */
};
/* 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 && mbentry->mbtype == 0) {
strarray_push(&notif_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,
&notif_mailboxes, imapd_userid);
mboxevent_notify(&mboxevent);
mboxevent_free(&mboxevent);
buf_release(&applepushserviceargs->aps_account_id);
buf_release(&applepushserviceargs->aps_device_token);
buf_release(&applepushserviceargs->aps_subtopic);
strarray_fini(&applepushserviceargs->mailboxes);
strarray_fini(&notif_mailboxes);
}
diff --git a/imap/sync_support.c b/imap/sync_support.c
index 3666b401e..0aa8fa5b5 100644
--- a/imap/sync_support.c
+++ b/imap/sync_support.c
@@ -1,7599 +1,7665 @@
/* sync_support.c -- Cyrus synchronization support functions
*
* Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <config.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sysexits.h>
#include <syslog.h>
#include <string.h>
#include <sys/wait.h>
#include <errno.h>
#include <dirent.h>
#include <utime.h>
#include <limits.h>
#include "assert.h"
#include "bsearch.h"
#include "global.h"
#include "imap_proxy.h"
#include "mboxlist.h"
#include "mailbox.h"
#include "quota.h"
#include "xmalloc.h"
#include "seen.h"
#include "mboxname.h"
#include "map.h"
#include "imapd.h"
#include "message.h"
#include "util.h"
#include "user.h"
#include "prot.h"
#include "dlist.h"
#include "xstrlcat.h"
#include "strarray.h"
#include "ptrarray.h"
#include "sievedir.h"
#ifdef USE_CALALARMD
#include "caldav_alarm.h"
#endif
#ifdef USE_SIEVE
#include "sieve/sieve_interface.h"
#endif
/* generated headers are not necessarily in current directory */
#include "imap/imap_err.h"
#include "message_guid.h"
#include "sync_support.h"
#include "sync_log.h"
static int opt_force = 0; // FIXME
struct sync_client_state rightnow_sync_cs;
/* protocol definitions */
static char *imap_sasl_parsesuccess(char *str, const char **status);
static void imap_postcapability(struct backend *s);
struct protocol_t imap_csync_protocol =
{ "imap", "imap", TYPE_STD,
{ { { 1, NULL },
{ "C01 CAPABILITY", NULL, "C01 ", imap_postcapability,
CAPAF_MANY_PER_LINE,
{ { "AUTH", CAPA_AUTH },
{ "STARTTLS", CAPA_STARTTLS },
// FIXME doesn't work with compress at the moment for some reason
// { "COMPRESS=DEFLATE", CAPA_COMPRESS },
// FIXME do we need these ones?
// { "IDLE", CAPA_IDLE },
// { "MUPDATE", CAPA_MUPDATE },
// { "MULTIAPPEND", CAPA_MULTIAPPEND },
// { "RIGHTS=kxte", CAPA_ACLRIGHTS },
// { "LIST-EXTENDED", CAPA_LISTEXTENDED },
{ "SASL-IR", CAPA_SASL_IR },
{ "X-REPLICATION", CAPA_REPLICATION },
{ NULL, 0 } } },
{ "S01 STARTTLS", "S01 OK", "S01 NO", 0 },
{ "A01 AUTHENTICATE", 0, 0, "A01 OK", "A01 NO", "+ ", "*",
&imap_sasl_parsesuccess, AUTO_CAPA_AUTH_OK },
{ "Z01 COMPRESS DEFLATE", "* ", "Z01 OK" },
{ "N01 NOOP", "* ", "N01 OK" },
{ "Q01 LOGOUT", "* ", "Q01 " } } }
};
struct protocol_t csync_protocol =
{ "csync", "csync", TYPE_STD,
{ { { 1, "* OK" },
{ NULL, NULL, "* OK", NULL,
CAPAF_ONE_PER_LINE|CAPAF_SKIP_FIRST_WORD,
{ { "SASL", CAPA_AUTH },
{ "STARTTLS", CAPA_STARTTLS },
{ "COMPRESS=DEFLATE", CAPA_COMPRESS },
{ NULL, 0 } } },
{ "STARTTLS", "OK", "NO", 1 },
{ "AUTHENTICATE", USHRT_MAX, 0, "OK", "NO", "+ ", "*", NULL, 0 },
{ "COMPRESS DEFLATE", NULL, "OK" },
{ "NOOP", NULL, "OK" },
{ "EXIT", NULL, "OK" } } }
};
/* parse_success api is undocumented but my current understanding
* is that the caller expects it to return a pointer to the position
* within str at which base64 encoded "success data" can be found.
* status is for passing back other status data (if required) to
* the original caller.
*
* in the case of what we're doing here, there is no base64 encoded
* 'success data', but there is a capability string that we want to
* save. so we grab the capability string (including the []s) and
* chuck that in status, and then we return NULL to indicate the
* lack of base64 data.
*/
static char *imap_sasl_parsesuccess(char *str, const char **status)
{
syslog(LOG_DEBUG, "imap_sasl_parsesuccess(): input is: %s", str);
if (NULL == status) return NULL; /* nothing useful we can do */
const char *prelude = "A01 OK "; // FIXME don't hardcode this, get it from sasl_cmd->ok
const size_t prelude_len = strlen(prelude);
const char *capability = "[CAPABILITY ";
const size_t capability_len = strlen(capability);
char *start, *end;
if (strncmp(str, prelude, prelude_len)) {
/* this isn't the string we expected */
syslog(LOG_INFO, "imap_sasl_parsesuccess(): unexpected initial string contents: %s", str);
return NULL;
}
start = str + prelude_len;
if (strncmp(start, capability, capability_len)) {
/* this isn't a capability string */
syslog(LOG_INFO, "imap_sasl_parsesuccess(): str does not contain a capability string: %s", str);
return NULL;
}
end = start + capability_len;
while (*end != ']' && *end != '\0') {
end++;
}
if (*end == '\0') {
/* didn't find end of capability string */
syslog(LOG_INFO, "imap_sasl_parsesuccess(): did not find end of capability string: %s", str);
return NULL;
}
/* we want to keep the ], but crop the rest off */
*++end = '\0';
/* status gets the capability string */
syslog(LOG_DEBUG, "imap_sasl_parsesuccess(): found capability string: %s", start);
*status = start;
/* there's no base64 data, so return NULL */
return NULL;
}
static void imap_postcapability(struct backend *s)
{
if (CAPA(s, CAPA_SASL_IR)) {
/* server supports initial response in AUTHENTICATE command */
s->prot->u.std.sasl_cmd.maxlen = USHRT_MAX;
}
}
static const char *_synclock_name(const char *hostname, const char *userid)
{
const char *p;
static struct buf buf = BUF_INITIALIZER;
if (!userid) userid = ""; // no userid == global lock
buf_setcstr(&buf, "*S*");
for (p = hostname; *p; p++) {
switch(*p) {
case '.':
buf_putc(&buf, '^');
break;
default:
buf_putc(&buf, *p);
break;
}
}
buf_putc(&buf, '*');
for (p = userid; *p; p++) {
switch(*p) {
case '.':
buf_putc(&buf, '^');
break;
default:
buf_putc(&buf, *p);
break;
}
}
return buf_cstring(&buf);
}
static struct mboxlock *sync_lock(struct sync_client_state *sync_cs,
const char *userid)
{
const char *name = _synclock_name(sync_cs->servername, userid);
struct mboxlock *lock = NULL;
int r = mboxname_lock(name, &lock, LOCK_EXCLUSIVE);
return r ? NULL : lock;
}
/* channel-based configuration */
EXPORTED const char *sync_get_config(const char *channel, const char *val)
{
const char *response = NULL;
if (channel) {
char name[MAX_MAILBOX_NAME]; /* crazy long, but hey */
snprintf(name, MAX_MAILBOX_NAME, "%s_%s", channel, val);
response = config_getoverflowstring(name, NULL);
}
if (!response) {
/* get the core value */
if (!strcmp(val, "sync_host"))
response = config_getstring(IMAPOPT_SYNC_HOST);
else if (!strcmp(val, "sync_authname"))
response = config_getstring(IMAPOPT_SYNC_AUTHNAME);
else if (!strcmp(val, "sync_password"))
response = config_getstring(IMAPOPT_SYNC_PASSWORD);
else if (!strcmp(val, "sync_realm"))
response = config_getstring(IMAPOPT_SYNC_REALM);
else if (!strcmp(val, "sync_port"))
response = config_getstring(IMAPOPT_SYNC_PORT);
else if (!strcmp(val, "sync_shutdown_file"))
response = config_getstring(IMAPOPT_SYNC_SHUTDOWN_FILE);
else if (!strcmp(val, "sync_cache_db_path"))
response = config_getstring(IMAPOPT_SYNC_CACHE_DB_PATH);
else
fatal("unknown config variable requested", EX_SOFTWARE);
}
return response;
}
EXPORTED int sync_get_durationconfig(const char *channel, const char *val, int defunit)
{
int response = -1;
if (channel) {
const char *result = NULL;
char name[MAX_MAILBOX_NAME]; /* crazy long, but hey */
snprintf(name, MAX_MAILBOX_NAME, "%s_%s", channel, val);
result = config_getoverflowstring(name, NULL);
if (result)
config_parseduration(result, defunit, &response);
}
if (response == -1) {
if (!strcmp(val, "sync_repeat_interval"))
response = config_getduration(IMAPOPT_SYNC_REPEAT_INTERVAL, defunit);
}
return response;
}
EXPORTED int sync_get_switchconfig(const char *channel, const char *val)
{
int response = -1;
if (channel) {
const char *result = NULL;
char name[MAX_MAILBOX_NAME]; /* crazy long, but hey */
snprintf(name, sizeof(name), "%s_%s", channel, val);
result = config_getoverflowstring(name, NULL);
if (result) response = atoi(result);
}
if (response == -1) {
if (!strcmp(val, "sync_try_imap"))
response = config_getswitch(IMAPOPT_SYNC_TRY_IMAP);
}
return response;
}
/* Parse routines */
char *sync_encode_options(int options)
{
static char buf[4];
int i = 0;
if (options & OPT_POP3_NEW_UIDL)
buf[i++] = 'P';
if (options & OPT_IMAP_SHAREDSEEN)
buf[i++] = 'S';
if (options & OPT_IMAP_DUPDELIVER)
buf[i++] = 'D';
if (options & OPT_IMAP_HAS_ALARMS)
buf[i++] = 'A';
buf[i] = '\0';
return buf;
}
int sync_parse_options(const char *options)
{
int res = 0;
const char *p = options;
if (!options) return 0;
while (*p) {
switch(*p) {
case 'P':
res |= OPT_POP3_NEW_UIDL;
break;
case 'S':
res |= OPT_IMAP_SHAREDSEEN;
break;
case 'D':
res |= OPT_IMAP_DUPDELIVER;
break;
case 'A':
res |= OPT_IMAP_HAS_ALARMS;
break;
}
p++;
}
return res;
}
/* Get a simple line (typically error text) */
static int sync_getline(struct protstream *in, struct buf *buf)
{
int c;
buf_reset(buf);
for (;;) {
c = prot_getc(in);
if (c == EOF || (c == '\r') || (c == '\n')) {
/* Munch optional LF after CR */
if (c == '\r' && ((c = prot_getc(in)) != EOF && c != '\n')) {
prot_ungetc(c, in);
c = '\r';
}
buf_cstring(buf);
return c;
}
if (buf->len > config_maxword)
fatal("word too long", EX_IOERR);
buf_putc(buf, c);
}
return c;
}
/* ====================================================================== */
struct sync_msgid_list *sync_msgid_list_create(int hash_size)
{
struct sync_msgid_list *l = xzmalloc(sizeof (struct sync_msgid_list));
/* Pick a sensible default if no size given */
if (hash_size == 0)
hash_size = 256;
l->head = NULL;
l->tail = NULL;
l->hash_size = hash_size;
l->hash = xzmalloc(hash_size * sizeof(struct sync_msgid *));
l->count = 0;
l->toupload = 0;
return(l);
}
struct sync_msgid *sync_msgid_insert(struct sync_msgid_list *l,
const struct message_guid *guid)
{
struct sync_msgid *msgid;
int offset;
if (message_guid_isnull(guid))
return NULL;
offset = message_guid_hash(guid, l->hash_size);
/* do we already have it? Don't add it again */
for (msgid = l->hash[offset] ; msgid ; msgid = msgid->hash_next) {
if (message_guid_equal(&msgid->guid, guid))
return msgid;
}
msgid = xzmalloc(sizeof(struct sync_msgid));
msgid->need_upload = 1;
message_guid_copy(&msgid->guid, guid);
l->count++;
l->toupload++;
if (l->tail)
l->tail = l->tail->next = msgid;
else
l->head = l->tail = msgid;
/* Insert at start of list */
msgid->hash_next = l->hash[offset];
l->hash[offset] = msgid;
return msgid;
}
void sync_msgid_remove(struct sync_msgid_list *l,
const struct message_guid *guid)
{
int offset = message_guid_hash(guid, l->hash_size);
struct sync_msgid *msgid;
if (message_guid_isnull(guid)) return;
for (msgid = l->hash[offset] ; msgid ; msgid = msgid->hash_next) {
if (message_guid_equal(&msgid->guid, guid)) {
message_guid_set_null(&msgid->guid);
return;
}
}
}
void sync_msgid_list_free(struct sync_msgid_list **lp)
{
struct sync_msgid_list *l = *lp;
struct sync_msgid *current, *next;
current = l->head;
while (current) {
next = current->next;
free(current->fname);
free(current);
current = next;
}
free(l->hash);
free(l);
*lp = NULL;
}
struct sync_msgid *sync_msgid_lookup(const struct sync_msgid_list *l,
const struct message_guid *guid)
{
int offset = message_guid_hash(guid, l->hash_size);
struct sync_msgid *msgid;
if (message_guid_isnull(guid))
return(NULL);
for (msgid = l->hash[offset] ; msgid ; msgid = msgid->hash_next) {
if (message_guid_equal(&msgid->guid, guid))
return(msgid);
}
return(NULL);
}
struct sync_reserve_list *sync_reserve_list_create(int hash_size)
{
struct sync_reserve_list *l = xmalloc(sizeof(struct sync_reserve_list));
l->head = NULL;
l->tail = NULL;
l->hash_size = hash_size;
return l;
}
struct sync_msgid_list *sync_reserve_partlist(struct sync_reserve_list *l,
const char *part)
{
struct sync_reserve *item;
for (item = l->head; item; item = item->next) {
if (!strcmp(item->part, part))
return item->list;
}
/* not found, create it */
item = xmalloc(sizeof(struct sync_reserve));
item->part = xstrdup(part);
item->next = NULL;
item->list = sync_msgid_list_create(l->hash_size);
if (l->tail)
l->tail = l->tail->next = item;
else
l->tail = l->head = item;
return item->list;
}
void sync_reserve_list_free(struct sync_reserve_list **lp)
{
struct sync_reserve_list *l = *lp;
struct sync_reserve *current, *next;
current = l->head;
while (current) {
next = current->next;
sync_msgid_list_free(&current->list);
free(current->part);
free(current);
current = next;
}
free(l);
*lp = NULL;
}
/* ====================================================================== */
struct sync_folder_list *sync_folder_list_create(void)
{
struct sync_folder_list *l = xzmalloc(sizeof (struct sync_folder_list));
l->head = NULL;
l->tail = NULL;
l->count = 0;
return(l);
}
struct sync_folder *sync_folder_list_add(struct sync_folder_list *l,
const char *uniqueid, const char *name,
uint32_t mbtype,
const char *part, const char *acl,
uint32_t options,
uint32_t uidvalidity,
uint32_t last_uid,
modseq_t highestmodseq,
struct synccrcs synccrcs,
uint32_t recentuid,
time_t recenttime,
time_t pop3_last_login,
time_t pop3_show_after,
struct sync_annot_list *annots,
modseq_t xconvmodseq,
modseq_t raclmodseq,
modseq_t foldermodseq,
int ispartial)
{
struct sync_folder *result = xzmalloc(sizeof(struct sync_folder));
if (l->tail)
l->tail = l->tail->next = result;
else
l->head = l->tail = result;
l->count++;
result->next = NULL;
result->mailbox = NULL;
result->uniqueid = xstrdupnull(uniqueid);
result->name = xstrdupnull(name);
result->mbtype = mbtype;
result->part = xstrdupnull(part);
result->acl = xstrdupnull(acl);
result->uidvalidity = uidvalidity;
result->last_uid = last_uid;
result->highestmodseq = highestmodseq;
result->options = options;
result->synccrcs = synccrcs;
result->recentuid = recentuid;
result->recenttime = recenttime;
result->pop3_last_login = pop3_last_login;
result->pop3_show_after = pop3_show_after;
result->annots = annots; /* NOTE: not a copy! */
result->xconvmodseq = xconvmodseq;
result->raclmodseq = raclmodseq;
result->foldermodseq = foldermodseq;
result->ispartial = ispartial;
result->mark = 0;
result->reserve = 0;
return(result);
}
struct sync_folder *sync_folder_lookup(struct sync_folder_list *l,
const char *uniqueid)
{
struct sync_folder *p;
for (p = l->head; p; p = p->next) {
if (!strcmp(p->uniqueid, uniqueid))
return p;
}
return NULL;
}
void sync_folder_list_free(struct sync_folder_list **lp)
{
struct sync_folder_list *l = *lp;
struct sync_folder *current, *next;
if (!l) return;
current = l->head;
while (current) {
next = current->next;
free(current->uniqueid);
free(current->name);
free(current->part);
free(current->acl);
sync_annot_list_free(&current->annots);
free(current);
current = next;
}
free(l);
*lp = NULL;
}
/* ====================================================================== */
struct sync_rename_list *sync_rename_list_create(void)
{
struct sync_rename_list *l = xzmalloc(sizeof(struct sync_rename_list));
l->head = NULL;
l->tail = NULL;
l->count = 0;
l->done = 0;
return(l);
}
struct sync_rename *sync_rename_list_add(struct sync_rename_list *l,
const char *uniqueid, const char *oldname,
const char *newname, const char *partition,
unsigned uidvalidity)
{
struct sync_rename *result
= xzmalloc(sizeof(struct sync_rename));
if (l->tail)
l->tail = l->tail->next = result;
else
l->head = l->tail = result;
l->count++;
result->next = NULL;
result->uniqueid = xstrdupnull(uniqueid);
result->oldname = xstrdupnull(oldname);
result->newname = xstrdupnull(newname);
result->part = xstrdupnull(partition);
result->uidvalidity = uidvalidity;
result->done = 0;
return result;
}
struct sync_rename *sync_rename_lookup(struct sync_rename_list *l,
const char *oldname)
{
struct sync_rename *p;
for (p = l->head; p; p = p->next) {
if (!strcmp(p->oldname, oldname))
return p;
}
return NULL;
}
void sync_rename_list_free(struct sync_rename_list **lp)
{
struct sync_rename_list *l = *lp;
struct sync_rename *current, *next;
if (!l) return;
current = l->head;
while (current) {
next = current->next;
free(current->uniqueid);
free(current->oldname);
free(current->newname);
free(current->part);
free(current);
current = next;
}
free(l);
*lp = NULL;
}
/* ====================================================================== */
struct sync_quota_list *sync_quota_list_create(void)
{
struct sync_quota_list *l = xzmalloc(sizeof(struct sync_quota_list));
l->head = NULL;
l->tail = NULL;
l->count = 0;
l->done = 0;
return(l);
}
struct sync_quota *sync_quota_list_add(struct sync_quota_list *l,
const char *root)
{
struct sync_quota *result
= xzmalloc(sizeof(struct sync_quota));
int res;
if (l->tail)
l->tail = l->tail->next = result;
else
l->head = l->tail = result;
l->count++;
result->next = NULL;
result->root = xstrdup(root);
for (res = 0 ; res < QUOTA_NUMRESOURCES ; res++)
result->limits[res] = QUOTA_UNLIMITED;
result->done = 0;
return result;
}
struct sync_quota *sync_quota_lookup(struct sync_quota_list *l,
const char *name)
{
struct sync_quota *p;
for (p = l->head; p; p = p->next) {
if (!strcmp(p->root, name))
return p;
}
return NULL;
}
void sync_quota_list_free(struct sync_quota_list **lp)
{
struct sync_quota_list *l = *lp;
struct sync_quota *current, *next;
if (!l) return;
current = l->head;
while (current) {
next = current->next;
free(current->root);
free(current);
current = next;
}
free(l);
*lp = NULL;
}
void sync_encode_quota_limits(struct dlist *kl, const quota_t limits[QUOTA_NUMRESOURCES])
{
int res;
/*
* For backwards compatibility, we encode the STORAGE limit as LIMIT
* and we always report it even if it's QUOTA_UNLIMITED. This is
* kinda screwed up but should work. For QUOTA_UNLIMITED < 0, we
* send a very large unsigned number across the wire, and parse it
* back as QUOTA_UNLIMITED at the other end. Spit and string.
*/
dlist_setnum32(kl, "LIMIT", limits[QUOTA_STORAGE]);
for (res = 0 ; res < QUOTA_NUMRESOURCES ; res++) {
if (limits[res] >= 0)
dlist_setnum32(kl, quota_names[res], limits[res]);
}
}
void sync_decode_quota_limits(/*const*/ struct dlist *kl, quota_t limits[QUOTA_NUMRESOURCES])
{
uint32_t limit = 0;
int res;
for (res = 0 ; res < QUOTA_NUMRESOURCES ; res++)
limits[res] = QUOTA_UNLIMITED;
/* For backwards compatibility */
if (dlist_getnum32(kl, "LIMIT", &limit)) {
if (limit == UINT_MAX)
limits[QUOTA_STORAGE] = -1;
else
limits[QUOTA_STORAGE] = limit;
}
for (res = 0 ; res < QUOTA_NUMRESOURCES ; res++) {
if (dlist_getnum32(kl, quota_names[res], &limit))
limits[res] = limit;
}
}
/* ====================================================================== */
struct sync_sieve_list *sync_sieve_list_create(void)
{
struct sync_sieve_list *l = xzmalloc(sizeof (struct sync_sieve_list));
l->head = NULL;
l->tail = NULL;
l->count = 0;
return l;
}
void sync_sieve_list_add(struct sync_sieve_list *l, const char *name,
time_t last_update, struct message_guid *guidp,
int active)
{
struct sync_sieve *item = xzmalloc(sizeof(struct sync_sieve));
item->name = xstrdup(name);
item->last_update = last_update;
item->active = active;
message_guid_copy(&item->guid, guidp);
item->mark = 0;
if (l->tail)
l->tail = l->tail->next = item;
else
l->head = l->tail = item;
l->count++;
}
struct sync_sieve *sync_sieve_lookup(struct sync_sieve_list *l, const char *name)
{
struct sync_sieve *p;
for (p = l->head; p; p = p->next) {
if (!strcmp(p->name, name))
return p;
}
return NULL;
}
void sync_sieve_list_set_active(struct sync_sieve_list *l, const char *name)
{
struct sync_sieve *item;
for (item = l->head; item; item = item->next) {
if (!strcmp(item->name, name)) {
item->active = 1;
break;
}
}
}
void sync_sieve_list_free(struct sync_sieve_list **lp)
{
struct sync_sieve_list *l = *lp;
struct sync_sieve *current, *next;
current = l->head;
while (current) {
next = current->next;
free(current->name);
free(current);
current = next;
}
free(l);
*lp = NULL;
}
static int list_cb(const char *sievedir,
const char *name, struct stat *sbuf,
const char *link_target __attribute__((unused)),
void *rock)
{
struct sync_sieve_list *list = (struct sync_sieve_list *) rock;
/* calculate the sha1 on the fly, relatively cheap */
struct buf *buf = sievedir_get_script(sievedir, name);
if (buf && buf_len(buf)) {
struct message_guid guid;
message_guid_generate(&guid, buf_base(buf), buf_len(buf));
sync_sieve_list_add(list, name, sbuf->st_mtime, &guid, 0);
buf_destroy(buf);
}
return SIEVEDIR_OK;
}
struct sync_sieve_list *sync_sieve_list_generate(const char *userid)
{
struct sync_sieve_list *list = sync_sieve_list_create();
const char *sieve_path = user_sieve_path(userid);
const char *active = sievedir_get_active(sieve_path);
sievedir_foreach(sieve_path, SIEVEDIR_SCRIPTS_ONLY, &list_cb, list);
if (active) {
char target[SIEVEDIR_MAX_NAME_LEN];
struct message_guid guid;
message_guid_set_null(&guid);
snprintf(target, sizeof(target), "%s%s", active, BYTECODE_SUFFIX);
sync_sieve_list_add(list, target, 0, &guid, 1);
}
return list;
}
char *sync_sieve_read(const char *userid, const char *name, uint32_t *sizep)
{
const char *sieve_path = user_sieve_path(userid);
struct buf *buf = sievedir_get_script(sieve_path, name);
char *result = NULL;
if (buf) {
if (sizep) *sizep = buf_len(buf);
result = buf_release(buf);
buf_destroy(buf);
}
else if (sizep)
*sizep = 0;
return result;
}
int sync_sieve_upload(const char *userid, const char *name,
time_t last_update, const char *content,
size_t len)
{
const char *sieve_path = user_sieve_path(userid);
char tmpname[2048];
char newname[2048];
char *ext;
FILE *file;
int r = 0;
struct stat sbuf;
struct utimbuf utimbuf;
ext = strrchr(name, '.');
if (ext && !strcmp(ext, ".bc")) {
/* silently ignore attempts to upload compiled bytecode */
return 0;
}
if (stat(sieve_path, &sbuf) == -1 && errno == ENOENT) {
if (cyrus_mkdir(sieve_path, 0755) == -1) return IMAP_IOERROR;
if (mkdir(sieve_path, 0755) == -1 && errno != EEXIST) {
syslog(LOG_ERR, "Failed to create %s:%m", sieve_path);
return IMAP_IOERROR;
}
}
snprintf(tmpname, sizeof(tmpname), "%s/sync_tmp-%lu",
sieve_path, (unsigned long)getpid());
snprintf(newname, sizeof(newname), "%s/%s", sieve_path, name);
if ((file=fopen(tmpname, "w")) == NULL) {
return IMAP_IOERROR;
}
/* XXX - error handling */
fwrite(content, 1, len, file);
if ((fflush(file) != 0) || (fsync(fileno(file)) < 0))
r = IMAP_IOERROR;
fclose(file);
utimbuf.actime = time(NULL);
utimbuf.modtime = last_update;
if (!r && (utime(tmpname, &utimbuf) < 0))
r = IMAP_IOERROR;
if (!r && (rename(tmpname, newname) < 0))
r = IMAP_IOERROR;
#ifdef USE_SIEVE
if (!r) {
r = sieve_rebuild(newname, NULL, /*force*/ 1, NULL);
if (r == SIEVE_PARSE_ERROR || r == SIEVE_FAIL)
r = IMAP_SYNC_BADSIEVE;
}
#endif
sync_log_sieve(userid);
return r;
}
int sync_sieve_activate(const char *userid, const char *bcname)
{
const char *sieve_path = user_sieve_path(userid);
char target[2048];
int r;
#ifdef USE_SIEVE
snprintf(target, sizeof(target), "%s/%s", sieve_path, bcname);
sieve_rebuild(NULL, target, 0, NULL);
#endif
snprintf(target, sizeof(target), "%.*s",
(int) strlen(bcname) - BYTECODE_SUFFIX_LEN, bcname);
r = sievedir_activate_script(sieve_path, target);
if (r) return r;
sync_log_sieve(userid);
return 0;
}
int sync_sieve_deactivate(const char *userid)
{
const char *sieve_path = user_sieve_path(userid);
int r = sievedir_deactivate_script(sieve_path);
if (r) return r;
sync_log_sieve(userid);
return(0);
}
int sync_sieve_delete(const char *userid, const char *script)
{
const char *sieve_path = user_sieve_path(userid);
char name[2048];
snprintf(name, sizeof(name), "%.*s",
(int) strlen(script) - SCRIPT_SUFFIX_LEN, script);
/* XXX Do we NOT care about errors? */
if (sievedir_script_isactive(sieve_path, name)) {
sievedir_deactivate_script(sieve_path);
}
sievedir_delete_script(sieve_path, name);
sync_log_sieve(userid);
return(0);
}
/* ====================================================================== */
struct sync_name_list *sync_name_list_create(void)
{
struct sync_name_list *l = xzmalloc(sizeof (struct sync_name_list));
l->head = NULL;
l->tail = NULL;
l->count = 0;
l->marked = 0;
return l;
}
struct sync_name *sync_name_list_add(struct sync_name_list *l,
const char *name)
{
struct sync_name * item = sync_name_lookup(l, name);
if (item) return item;
item = xzmalloc(sizeof(struct sync_name));
if (l->tail)
l->tail = l->tail->next = item;
else
l->head = l->tail = item;
l->count++;
item->next = NULL;
item->name = xstrdup(name);
item->mark = 0;
return item;
}
struct sync_name *sync_name_lookup(struct sync_name_list *l,
const char *name)
{
struct sync_name *p;
for (p = l->head; p; p = p->next)
if (!strcmp(p->name, name))
return p;
return NULL;
}
void sync_name_list_free(struct sync_name_list **lp)
{
struct sync_name *current, *next;
current = (*lp)->head;
while (current) {
next = current->next;
free(current->name);
free(current);
current = next;
}
free(*lp);
*lp = NULL;
}
/* ====================================================================== */
struct sync_seen_list *sync_seen_list_create(void)
{
struct sync_seen_list *l = xzmalloc(sizeof (struct sync_seen_list));
l->head = NULL;
l->tail = NULL;
l->count = 0;
return l;
}
struct sync_seen *sync_seen_list_add(struct sync_seen_list *l,
const char *uniqueid, time_t lastread,
unsigned lastuid, time_t lastchange,
const char *seenuids)
{
struct sync_seen *item = xzmalloc(sizeof(struct sync_seen));
if (l->tail)
l->tail = l->tail->next = item;
else
l->head = l->tail = item;
l->count++;
item->next = NULL;
item->uniqueid = xstrdup(uniqueid);
item->sd.lastread = lastread;
item->sd.lastuid = lastuid;
item->sd.lastchange = lastchange;
item->sd.seenuids = xstrdup(seenuids);
item->mark = 0;
return item;
}
struct sync_seen *sync_seen_list_lookup(struct sync_seen_list *l,
const char *uniqueid)
{
struct sync_seen *p;
for (p = l->head; p; p = p->next)
if (!strcmp(p->uniqueid, uniqueid))
return p;
return NULL;
}
void sync_seen_list_free(struct sync_seen_list **lp)
{
struct sync_seen *current, *next;
current = (*lp)->head;
while (current) {
next = current->next;
free(current->uniqueid);
seen_freedata(&current->sd);
free(current);
current = next;
}
free(*lp);
*lp = NULL;
}
/* ====================================================================== */
struct sync_annot_list *sync_annot_list_create(void)
{
struct sync_annot_list *l = xzmalloc(sizeof (struct sync_annot_list));
l->head = NULL;
l->tail = NULL;
l->count = 0;
return(l);
}
void sync_annot_list_add(struct sync_annot_list *l,
const char *entry, const char *userid,
const struct buf *value,
modseq_t modseq)
{
struct sync_annot *item = xzmalloc(sizeof(struct sync_annot));
item->entry = xstrdupnull(entry);
item->userid = xstrdupnull(userid);
buf_copy(&item->value, value);
item->mark = 0;
item->modseq = modseq;
if (l->tail)
l->tail = l->tail->next = item;
else
l->head = l->tail = item;
l->count++;
}
void sync_annot_list_free(struct sync_annot_list **lp)
{
struct sync_annot_list *l = *lp;
struct sync_annot *current, *next;
if (!l)
return;
current = l->head;
while (current) {
next = current->next;
free(current->entry);
free(current->userid);
buf_free(&current->value);
free(current);
current = next;
}
free(l);
*lp = NULL;
}
/* ====================================================================== */
struct sync_action_list *sync_action_list_create(void)
{
struct sync_action_list *l = xzmalloc(sizeof (struct sync_action_list));
l->head = NULL;
l->tail = NULL;
l->count = 0;
return(l);
}
void sync_action_list_add(struct sync_action_list *l,
const char *name, const char *user)
{
struct sync_action *current;
if (!name && !user) return;
for (current = l->head ; current ; current = current->next) {
if ((!name || (current->name && !strcmp(current->name, name))) &&
(!user || (current->user && !strcmp(current->user, user)))) {
current->active = 1; /* Make sure active */
return;
} else {
/* name and/or user don't match current: no match possible */
}
}
current = xzmalloc(sizeof(struct sync_action));
current->next = NULL;
current->name = xstrdupnull(name);
current->user = xstrdupnull(user);
current->active = 1;
if (l->tail)
l->tail = l->tail->next = current;
else
l->head = l->tail = current;
l->count++;
}
void sync_action_list_free(struct sync_action_list **lp)
{
struct sync_action_list *l = *lp;
struct sync_action *current, *next;
current = l->head;
while (current) {
next = current->next;
if (current->name) free(current->name);
if (current->user) free(current->user);
free(current);
current = next;
}
free(l);
*lp = NULL;
}
/* ====================================================================== */
static int read_one_annot(const char *mailbox __attribute__((unused)),
uint32_t uid __attribute__((unused)),
const char *entry,
const char *userid,
const struct buf *value,
const struct annotate_metadata *mdata,
void *rock)
{
struct sync_annot_list **salp = (struct sync_annot_list **)rock;
if (!*salp)
*salp = sync_annot_list_create();
sync_annot_list_add(*salp, entry, userid, value, mdata->modseq);
return 0;
}
/*
* Read all the annotations in the local annotations database
* for the message given by @mailbox and @record, returning them
* as a new sync_annot_list. The caller should free the new
* list with sync_annot_list_free().
* If record is NULL, return the mailbox annotations
* If since_modseq is greated than zero, return annotations
* add or changed since modseq (exclusively since_modseq).
* If flags is set to ANNOTATE_TOMBSTONES, also return
* deleted annotations. Deleted annotations have a zero value.
*
* Returns: non-zero on error,
* resulting sync_annot_list in *@resp
*/
int read_annotations(const struct mailbox *mailbox,
const struct index_record *record,
struct sync_annot_list **resp,
modseq_t since_modseq __attribute__((unused)),
int flags)
{
*resp = NULL;
return annotatemore_findall(mailbox->name, record ? record->uid : 0,
/* all entries*/"*", /*XXX since_modseq*/0,
read_one_annot, (void *)resp, flags);
}
/*
* Encode the given list of annotations @sal as a dlist
* structure with the given @parent.
*/
void encode_annotations(struct dlist *parent,
struct mailbox *mailbox,
const struct index_record *record,
const struct sync_annot_list *sal)
{
const struct sync_annot *sa;
struct dlist *annots = NULL;
struct dlist *aa;
if (sal) {
for (sa = sal->head ; sa ; sa = sa->next) {
if (!annots)
annots = dlist_newlist(parent, "ANNOTATIONS");
aa = dlist_newkvlist(annots, NULL);
dlist_setatom(aa, "ENTRY", sa->entry);
dlist_setatom(aa, "USERID", sa->userid);
dlist_setnum64(aa, "MODSEQ", sa->modseq);
dlist_setmap(aa, "VALUE", sa->value.s, sa->value.len);
}
}
if (record && record->cid && mailbox->i.minor_version >= 13) {
if (!annots)
annots = dlist_newlist(parent, "ANNOTATIONS");
aa = dlist_newkvlist(annots, NULL);
dlist_setatom(aa, "ENTRY", IMAP_ANNOT_NS "thrid");
dlist_setatom(aa, "USERID", "");
dlist_setnum64(aa, "MODSEQ", 0);
dlist_sethex64(aa, "VALUE", record->cid);
}
if (record && record->savedate && mailbox->i.minor_version >= 15) {
if (!annots)
annots = dlist_newlist(parent, "ANNOTATIONS");
aa = dlist_newkvlist(annots, NULL);
dlist_setatom(aa, "ENTRY", IMAP_ANNOT_NS "savedate");
dlist_setatom(aa, "USERID", "");
dlist_setnum64(aa, "MODSEQ", 0);
dlist_setnum32(aa, "VALUE", record->savedate);
}
if (record && record->createdmodseq && mailbox->i.minor_version >= 16) {
if (!annots)
annots = dlist_newlist(parent, "ANNOTATIONS");
aa = dlist_newkvlist(annots, NULL);
dlist_setatom(aa, "ENTRY", IMAP_ANNOT_NS "createdmodseq");
dlist_setatom(aa, "USERID", "");
dlist_setnum64(aa, "MODSEQ", 0);
dlist_setnum64(aa, "VALUE", record->createdmodseq);
}
}
/*
* Decode the given list of encoded annotations @annots and create
* a new sync_annot_list in *@salp, which the caller should free
* with sync_annot_list_free().
*
* Returns: zero on success or Cyrus error code.
*/
int decode_annotations(/*const*/struct dlist *annots,
struct sync_annot_list **salp,
struct mailbox *mailbox,
struct index_record *record)
{
struct dlist *aa;
const char *entry;
const char *userid;
modseq_t modseq;
*salp = NULL;
if (strcmp(annots->name, "ANNOTATIONS"))
return IMAP_PROTOCOL_BAD_PARAMETERS;
for (aa = annots->head ; aa ; aa = aa->next) {
struct buf value = BUF_INITIALIZER;
if (!*salp)
*salp = sync_annot_list_create();
if (!dlist_getatom(aa, "ENTRY", &entry))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(aa, "USERID", &userid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum64(aa, "MODSEQ", &modseq))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getbuf(aa, "VALUE", &value))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!strcmp(entry, IMAP_ANNOT_NS "thrid") &&
record && mailbox->i.minor_version >= 13) {
const char *p = buf_cstring(&value);
parsehex(p, &p, 16, &record->cid);
}
else if (!strcmp(entry, IMAP_ANNOT_NS "savedate") &&
record && mailbox->i.minor_version >= 15) {
const char *p = buf_cstring(&value);
bit64 newval;
parsenum(p, &p, 0, &newval);
record->savedate = newval;
}
else if (!strcmp(entry, IMAP_ANNOT_NS "createdmodseq") &&
record && mailbox->i.minor_version >= 16) {
const char *p = buf_cstring(&value);
bit64 newval;
parsenum(p, &p, 0, &newval);
record->createdmodseq = newval;
}
else if (!strcmp(entry, IMAP_ANNOT_NS "basethrid") && record) {
/* this might double-apply the annotation, but oh well. It does mean that
* basethrid is paired in here when we do a comparison against new values
* from the replica later! */
const char *p = buf_cstring(&value);
parsehex(p, &p, 16, &record->basecid);
/* XXX - check on p? */
/* "basethrid" is special, since it is written during mailbox
* appends and rewrites, using whatever modseq the index_record
* has at this moment. This might differ from the modseq we
* just parsed here, causing master and replica annotations
* to get out of sync.
* The fix is to set the basecid field both on the index
* record *and* adding the annotation to the annotation list.
* That way the local modseq of basethrid always gets over-
* written by whoever wins to be master of this annotation */
sync_annot_list_add(*salp, entry, userid, &value, modseq);
}
else {
sync_annot_list_add(*salp, entry, userid, &value, modseq);
}
buf_free(&value);
}
return 0;
}
/*
* Merge a local and remote list of annotations, and apply the resulting
* list of annotations to the local annotation database, storing new values
* or deleting old values as necessary. Manages its own annotations
* transaction.
* Record may be null, to process mailbox annotations.
*/
static int diff_annotation(const struct sync_annot *a,
const struct sync_annot *b,
int diff_value)
{
int diff = 0;
if (!a && !b) return 0;
if (a)
diff--;
if (b)
diff++;
if (!diff)
diff = strcmpnull(a->entry, b->entry);
if (!diff)
diff = strcmpnull(a->userid, b->userid);
if (!diff && diff_value)
diff = buf_cmp(&a->value, &b->value);
return diff;
}
int diff_annotations(const struct sync_annot_list *local_annots,
const struct sync_annot_list *remote_annots)
{
const struct sync_annot *local = (local_annots ? local_annots->head : NULL);
const struct sync_annot *remote = (remote_annots ? remote_annots->head : NULL);
while (local || remote) {
int r = diff_annotation(local, remote, 1);
if (r) return r;
if (local) local = local->next;
if (remote) remote = remote->next;
}
return 0;
}
int apply_annotations(struct mailbox *mailbox,
const struct index_record *record,
const struct sync_annot_list *local_annots,
const struct sync_annot_list *remote_annots,
int local_wins, int *hadsnoozed)
{
const struct sync_annot *local = (local_annots ? local_annots->head : NULL);
const struct sync_annot *remote = (remote_annots ? remote_annots->head : NULL);
const struct sync_annot *chosen;
static const struct buf novalue = BUF_INITIALIZER;
const struct buf *value;
int r = 0;
int diff;
annotate_state_t *astate = NULL;
if (record) {
r = mailbox_get_annotate_state(mailbox, record->uid, &astate);
}
else {
astate = annotate_state_new();
r = annotate_state_set_mailbox(astate, mailbox);
}
if (r) goto out;
/*
* We rely here on the database scan order resulting in lists
* of annotations that are ordered lexically on entry then userid.
* We walk over both lists at once, choosing an annotation from
* either the local list only (diff < 0), the remote list only
* (diff > 0), or both lists (diff == 0).
*/
while (local || remote) {
diff = diff_annotation(local, remote, 0);
chosen = 0;
if (diff < 0) {
chosen = local;
value = (local_wins ? &local->value : &novalue);
if (hadsnoozed && !strcmpsafe(chosen->entry, IMAP_ANNOT_NS "snoozed") && buf_len(value))
*hadsnoozed = 1;
local = local->next;
}
else if (diff > 0) {
chosen = remote;
value = (local_wins ? &novalue : &remote->value);
if (hadsnoozed && !strcmpsafe(chosen->entry, IMAP_ANNOT_NS "snoozed") && buf_len(value))
*hadsnoozed = 1;
remote = remote->next;
}
else {
chosen = remote;
value = (local_wins ? &local->value : &remote->value);
if (hadsnoozed && !strcmpsafe(chosen->entry, IMAP_ANNOT_NS "snoozed") && buf_len(value))
*hadsnoozed = 1;
diff = buf_cmp(&local->value, &remote->value);
local = local->next;
remote = remote->next;
if (!diff)
continue; /* same value, skip */
}
/* Replicate the modseq of this record from master */
struct annotate_metadata mdata = {
chosen->modseq, /* modseq */
0 /* flags - is determined by value */
};
r = annotate_state_writemdata(astate, chosen->entry,
chosen->userid, value, &mdata);
if (r)
break;
}
out:
if (record) {
#ifdef USE_CALALARMD
if (mailbox->mbtype & MBTYPE_CALENDAR) {
// NOTE: this is because we don't pass the annotations through
// with the record as we create it, so we can't update the alarm
// database properly. Instead, we don't set anything when we append
// by checking for .silentupdate, and instead update the database by touching
// the alarm AFTER writing the record.
caldav_alarm_sync_nextcheck(mailbox, record);
}
#endif
}
else {
/* need to manage our own txn for the global db */
if (!r)
r = annotate_state_commit(&astate);
else
annotate_state_abort(&astate);
}
/* else, the struct mailbox manages it for us */
return r;
}
/* =========================================================================== */
void sync_print_flags(struct dlist *kl,
struct mailbox *mailbox,
const struct index_record *record)
{
int flag;
struct dlist *fl = dlist_newlist(kl, "FLAGS");
if (record->system_flags & FLAG_DELETED)
dlist_setflag(fl, "FLAG", "\\Deleted");
if (record->system_flags & FLAG_ANSWERED)
dlist_setflag(fl, "FLAG", "\\Answered");
if (record->system_flags & FLAG_FLAGGED)
dlist_setflag(fl, "FLAG", "\\Flagged");
if (record->system_flags & FLAG_DRAFT)
dlist_setflag(fl, "FLAG", "\\Draft");
if (record->internal_flags & FLAG_INTERNAL_EXPUNGED)
dlist_setflag(fl, "FLAG", "\\Expunged");
if (record->system_flags & FLAG_SEEN)
dlist_setflag(fl, "FLAG", "\\Seen");
/* print user flags in mailbox order */
for (flag = 0; flag < MAX_USER_FLAGS; flag++) {
if (!mailbox->flagname[flag])
continue;
if (!(record->user_flags[flag/32] & (1<<(flag&31))))
continue;
dlist_setflag(fl, "FLAG", mailbox->flagname[flag]);
}
}
int sync_getflags(struct dlist *kl,
struct mailbox *mailbox,
struct index_record *record)
{
struct dlist *ki;
int userflag;
for (ki = kl->head; ki; ki = ki->next) {
char *s = xstrdup(ki->sval);
if (s[0] == '\\') {
/* System flags */
lcase(s);
if (!strcmp(s, "\\seen")) {
record->system_flags |= FLAG_SEEN;
} else if (!strcmp(s, "\\expunged")) {
record->internal_flags |= FLAG_INTERNAL_EXPUNGED;
} else if (!strcmp(s, "\\answered")) {
record->system_flags |= FLAG_ANSWERED;
} else if (!strcmp(s, "\\flagged")) {
record->system_flags |= FLAG_FLAGGED;
} else if (!strcmp(s, "\\deleted")) {
record->system_flags |= FLAG_DELETED;
} else if (!strcmp(s, "\\draft")) {
record->system_flags |= FLAG_DRAFT;
} else {
syslog(LOG_ERR, "Unknown system flag: %s", s);
}
}
else {
if (mailbox_user_flag(mailbox, s, &userflag, /*allow all*/2)) {
syslog(LOG_ERR, "Unable to record user flag: %s", s);
free(s);
return IMAP_IOERROR;
}
record->user_flags[userflag/32] |= 1<<(userflag&31);
}
free(s);
}
return 0;
}
int parse_upload(struct dlist *kr, struct mailbox *mailbox,
struct index_record *record,
struct sync_annot_list **salp)
{
struct dlist *fl;
struct message_guid *tmpguid;
int r;
memset(record, 0, sizeof(struct index_record));
if (!dlist_getnum32(kr, "UID", &record->uid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum64(kr, "MODSEQ", &record->modseq))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kr, "LAST_UPDATED", &record->last_updated))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getlist(kr, "FLAGS", &fl))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kr, "INTERNALDATE", &record->internaldate))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum32(kr, "SIZE", &record->size))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getguid(kr, "GUID", &tmpguid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
record->guid = *tmpguid;
/* parse the flags */
r = sync_getflags(fl, mailbox, record);
if (r) return r;
/* the ANNOTATIONS list is optional too */
if (salp && dlist_getlist(kr, "ANNOTATIONS", &fl))
r = decode_annotations(fl, salp, mailbox, record);
return r;
}
/* NOTE - we don't prot_flush here, as we always send an OK at the
* end of a response anyway */
void sync_send_response(struct dlist *kl, struct protstream *out)
{
prot_printf(out, "* ");
dlist_print(kl, 1, out);
prot_printf(out, "\r\n");
}
static const char *sync_gentag(struct buf *tag)
{
static unsigned cmdcnt = 0;
buf_reset(tag);
buf_printf(tag, "S%d", cmdcnt++);
return buf_cstring(tag);
}
/* these are one-shot commands for get and apply, so flush the stream
* after sending */
void sync_send_apply(struct dlist *kl, struct protstream *out)
{
if (out->userdata) {
/* IMAP flavor (w/ tag) */
prot_printf(out, "%s SYNC", sync_gentag((struct buf *) out->userdata));
}
prot_printf(out, "APPLY ");
dlist_print(kl, 1, out);
prot_printf(out, "\r\n");
prot_flush(out);
}
void sync_send_lookup(struct dlist *kl, struct protstream *out)
{
if (out->userdata) {
/* IMAP flavor (w/ tag) */
prot_printf(out, "%s SYNC", sync_gentag((struct buf *) out->userdata));
}
prot_printf(out, "GET ");
dlist_print(kl, 1, out);
prot_printf(out, "\r\n");
prot_flush(out);
}
void sync_send_restart(struct protstream *out)
{
if (out->userdata) {
/* IMAP flavor (w/ tag) */
prot_printf(out, "%s SYNC", sync_gentag((struct buf *) out->userdata));
}
prot_printf(out, "RESTART\r\n");
prot_flush(out);
}
void sync_send_restore(struct dlist *kl, struct protstream *out)
{
if (out->userdata) {
/* IMAP flavor (w/ tag) */
prot_printf(out, "%s SYNC", sync_gentag((struct buf *) out->userdata));
}
prot_printf(out, "RESTORE ");
dlist_print(kl, 1, out);
prot_printf(out, "\r\n");
prot_flush(out);
}
+/*
+ * Copied from imapparse.c:eatline(), and extended to also eat
+ * dlist file literals
+ * XXX potentially dedup back into original eatline
+ */
+void sync_eatline(struct protstream *pin, int c)
+{
+ for (;;) {
+ if (c == '\n') return;
+
+ /* Several of the parser helper functions return EOF
+ even if an unexpected character (other than EOF) is received.
+ We need to confirm that the stream is actually at EOF. */
+ if (c == EOF && (prot_IS_EOF(pin) || prot_IS_ERROR(pin))) return;
+
+ /* see if it's a literal */
+ if (c == '{') {
+ c = prot_getc(pin);
+ uint64_t size = 0;
+ while (cyrus_isdigit(c)) {
+ if (size > 429496729 || (size == 429496729 && (c > '5')))
+ break; /* don't fatal, just drop out of literal parsing */
+ size = size * 10 + c - '0';
+ c = prot_getc(pin);
+ }
+ if (c != '+') continue;
+ c = prot_getc(pin);
+ if (c != '}') continue;
+ c = prot_getc(pin);
+ /* optional \r */
+ if (c == '\r') c = prot_getc(pin);
+ if (c != '\n') continue;
+ /* successful literal, consume it */
+ while (size--) {
+ c = prot_getc(pin);
+ if (c == EOF) return;
+ }
+ }
+ else if (c == '%') {
+ /* replication file literal */
+ static struct buf discard = BUF_INITIALIZER;
+ uint32_t size = 0;
+
+ c = prot_getc(pin);
+ if (c != '{') continue;
+ c = getastring(pin, NULL, &discard); /* partition */
+ if (c != ' ') continue;
+ c = getastring(pin, NULL, &discard); /* guid */
+ if (c != ' ') continue;
+ c = getuint32(pin, &size);
+ if (c != '}') continue;
+ c = prot_getc(pin);
+ /* optional \r */
+ if (c == '\r') c = prot_getc(pin);
+ if (c != '\n') continue;
+ /* successful file literal, consume it */
+ while (size--) {
+ c = prot_getc(pin);
+ if (c == EOF) return;
+ }
+ }
+
+ c = prot_getc(pin);
+ }
+}
+
struct dlist *sync_parseline(struct protstream *in)
{
struct dlist *dl = NULL;
int c;
c = dlist_parse(&dl, 1, 0, in);
/* end line - or fail */
if (c == '\r') c = prot_getc(in);
if (c == '\n') return dl;
dlist_free(&dl);
- eatline(in, c);
+ sync_eatline(in, c);
return NULL;
}
static int sync_send_file(struct mailbox *mailbox,
const char *topart,
const struct index_record *record,
struct sync_msgid_list *part_list,
struct dlist *kupload)
{
struct sync_msgid *msgid;
const char *fname;
/* we'll trust that it exists - if not, we'll bail later,
* but right now we're under locks, so be fast */
fname = mailbox_record_fname(mailbox, record);
if (!fname) return IMAP_MAILBOX_BADNAME;
msgid = sync_msgid_insert(part_list, &record->guid);
/* already uploaded, great */
if (!msgid->need_upload)
return 0;
dlist_setfile(kupload, "MESSAGE", topart, &record->guid, record->size, fname);
/* note that we will be sending it, so it doesn't need to be
* sent again */
msgid->size = record->size;
if (!msgid->fname) msgid->fname = xstrdup(fname);
msgid->need_upload = 0;
msgid->is_archive = (record->internal_flags & FLAG_INTERNAL_ARCHIVED) ? 1 : 0;
part_list->toupload--;
return 0;
}
static int sync_prepare_dlists(struct mailbox *mailbox,
struct sync_folder *local,
struct sync_folder *remote,
const char *topart,
struct sync_msgid_list *part_list,
struct dlist *kl, struct dlist *kupload,
int printrecords, int fullannots, int sendsince)
{
struct sync_annot_list *annots = NULL;
struct mailbox_iter *iter = NULL;
modseq_t xconvmodseq = 0;
int r = 0;
int ispartial = local ? local->ispartial : 0;
if (!topart) topart = mailbox->part;
dlist_setatom(kl, "UNIQUEID", mailbox->uniqueid);
dlist_setatom(kl, "MBOXNAME", mailbox->name);
if (mailbox->mbtype)
dlist_setatom(kl, "MBOXTYPE", mboxlist_mbtype_to_string(mailbox->mbtype));
if (ispartial) {
/* send a zero to make older Cyrus happy */
dlist_setnum32(kl, "SYNC_CRC", 0);
/* calculated partial values */
dlist_setnum32(kl, "LAST_UID", local->last_uid);
dlist_setnum64(kl, "HIGHESTMODSEQ", local->highestmodseq);
/* create synthetic values for the other fields */
dlist_setnum32(kl, "RECENTUID", remote ? remote->recentuid : 0);
dlist_setdate(kl, "RECENTTIME", remote ? remote->recenttime : 0);
dlist_setdate(kl, "LAST_APPENDDATE", 0);
dlist_setdate(kl, "POP3_LAST_LOGIN", remote ? remote->pop3_last_login : 0);
dlist_setdate(kl, "POP3_SHOW_AFTER", remote ? remote->pop3_show_after : 0);
if (remote && remote->xconvmodseq)
dlist_setnum64(kl, "XCONVMODSEQ", remote->xconvmodseq);
if (remote && remote->raclmodseq)
dlist_setnum64(kl, "RACLMODSEQ", remote->raclmodseq);
}
else {
struct synccrcs synccrcs = mailbox_synccrcs(mailbox, /*force*/0);
dlist_setnum32(kl, "SYNC_CRC", synccrcs.basic);
dlist_setnum32(kl, "SYNC_CRC_ANNOT", synccrcs.annot);
dlist_setnum32(kl, "LAST_UID", mailbox->i.last_uid);
dlist_setnum64(kl, "HIGHESTMODSEQ", mailbox->i.highestmodseq);
dlist_setnum32(kl, "RECENTUID", mailbox->i.recentuid);
dlist_setdate(kl, "RECENTTIME", mailbox->i.recenttime);
dlist_setdate(kl, "LAST_APPENDDATE", mailbox->i.last_appenddate);
dlist_setdate(kl, "POP3_LAST_LOGIN", mailbox->i.pop3_last_login);
dlist_setdate(kl, "POP3_SHOW_AFTER", mailbox->i.pop3_show_after);
if (mailbox_has_conversations(mailbox)) {
r = mailbox_get_xconvmodseq(mailbox, &xconvmodseq);
if (!r && xconvmodseq)
dlist_setnum64(kl, "XCONVMODSEQ", xconvmodseq);
}
modseq_t raclmodseq = mboxname_readraclmodseq(mailbox->name);
if (raclmodseq)
dlist_setnum64(kl, "RACLMODSEQ", raclmodseq);
}
dlist_setnum32(kl, "UIDVALIDITY", mailbox->i.uidvalidity);
dlist_setatom(kl, "PARTITION", topart);
dlist_setatom(kl, "ACL", mailbox->acl);
dlist_setatom(kl, "OPTIONS", sync_encode_options(mailbox->i.options));
if (mailbox->quotaroot)
dlist_setatom(kl, "QUOTAROOT", mailbox->quotaroot);
if (mailbox->i.createdmodseq)
dlist_setnum64(kl, "CREATEDMODSEQ", mailbox->i.createdmodseq);
if (mailbox->foldermodseq)
dlist_setnum64(kl, "FOLDERMODSEQ", mailbox->foldermodseq);
/* always send mailbox annotations */
r = read_annotations(mailbox, NULL, &annots, 0, 0);
if (r) goto done;
encode_annotations(kl, mailbox, NULL, annots);
sync_annot_list_free(&annots);
if (sendsince && remote && remote->highestmodseq) {
dlist_setnum64(kl, "SINCE_MODSEQ", remote->highestmodseq);
dlist_setnum32(kl, "SINCE_CRC", remote->synccrcs.basic);
dlist_setnum32(kl, "SINCE_CRC_ANNOT", remote->synccrcs.annot);
}
if (printrecords) {
const message_t *msg;
struct dlist *rl = dlist_newlist(kl, "RECORD");
modseq_t modseq = remote ? remote->highestmodseq : 0;
iter = mailbox_iter_init(mailbox, modseq, 0);
while ((msg = mailbox_iter_step(iter))) {
const struct index_record *record = msg_record(msg);
modseq_t since_modseq = fullannots ? 0 : modseq;
/* stop early for partial sync */
modseq_t mymodseq = record->modseq;
if (ispartial) {
if (record->uid > local->last_uid)
break;
/* something from past the modseq that we're sending now */
if (mymodseq > local->highestmodseq) {
/* we will send this one later */
if (remote && record->uid <= remote->last_uid)
continue;
/* falsify modseq for now, we will resync this message later */
mymodseq = local->highestmodseq;
}
}
/* start off thinking we're sending the file too */
int send_file = 1;
/* does it exist at the other end? Don't send it */
if (remote && record->uid <= remote->last_uid)
send_file = 0;
/* if we're not uploading messages... don't send file */
if (!part_list || !kupload)
send_file = 0;
/* if we don't HAVE the file we can't send it */
if (record->internal_flags & FLAG_INTERNAL_UNLINKED)
send_file = 0;
if (send_file) {
r = sync_send_file(mailbox, topart,
record, part_list, kupload);
if (r) goto done;
}
struct dlist *il = dlist_newkvlist(rl, "RECORD");
dlist_setnum32(il, "UID", record->uid);
dlist_setnum64(il, "MODSEQ", mymodseq);
dlist_setdate(il, "LAST_UPDATED", record->last_updated);
sync_print_flags(il, mailbox, record);
dlist_setdate(il, "INTERNALDATE", record->internaldate);
dlist_setnum32(il, "SIZE", record->size);
dlist_setatom(il, "GUID", message_guid_encode(&record->guid));
r = read_annotations(mailbox, record, &annots, since_modseq,
/*XXX ANNOTATE_TOMBSTONES*/0);
if (r) goto done;
encode_annotations(il, mailbox, record, annots);
sync_annot_list_free(&annots);
}
}
done:
mailbox_iter_done(&iter);
return r;
}
int sync_parse_response(const char *cmd, struct protstream *in,
struct dlist **klp)
{
static struct buf response; /* BSS */
static struct buf errmsg;
struct dlist *kl = NULL;
int c;
if ((c = getword(in, &response)) == EOF) {
xsyslog(LOG_ERR, "IOERROR: zero length response",
"command=<%s> prot_error=<%s>",
cmd, prot_error(in));
return IMAP_PROTOCOL_ERROR;
}
if (c != ' ') goto parse_err;
kl = dlist_newlist(NULL, cmd);
while (!strcmp(response.s, "*")) {
struct dlist *item = sync_parseline(in);
if (!item) goto parse_err;
dlist_stitch(kl, item);
if ((c = getword(in, &response)) == EOF)
goto parse_err;
}
if (in->userdata) {
/* check IMAP response tag */
if (strcmp(response.s, buf_cstring((struct buf *) in->userdata)))
goto parse_err;
/* first word was IMAP response tag - get response token */
if ((c = getword(in, &response)) == EOF)
return IMAP_PROTOCOL_ERROR;
if (c != ' ') goto parse_err;
}
if (!strcmp(response.s, "OK")) {
if (klp) *klp = kl;
else dlist_free(&kl);
eatline(in, c);
return 0;
}
if (!strcmp(response.s, "NO")) {
dlist_free(&kl);
sync_getline(in, &errmsg);
syslog(LOG_ERR, "%s received NO response: %s", cmd, errmsg.s);
/* Slight hack to transform certain error strings into equivalent
* imap_err value so that caller has some idea of cause. Match
* this to the logic at sync_response() */
if (!strncmp(errmsg.s, "IMAP_INVALID_USER ",
strlen("IMAP_INVALID_USER ")))
return IMAP_INVALID_USER;
else if (!strncmp(errmsg.s, "IMAP_MAILBOX_NONEXISTENT ",
strlen("IMAP_MAILBOX_NONEXISTENT ")))
return IMAP_MAILBOX_NONEXISTENT;
else if (!strncmp(errmsg.s, "IMAP_MAILBOX_LOCKED ",
strlen("IMAP_MAILBOX_LOCKED ")))
return IMAP_MAILBOX_LOCKED;
else if (!strncmp(errmsg.s, "IMAP_MAILBOX_MOVED ",
strlen("IMAP_MAILBOX_MOVED ")))
return IMAP_MAILBOX_MOVED;
else if (!strncmp(errmsg.s, "IMAP_MAILBOX_NOTSUPPORTED ",
strlen("IMAP_MAILBOX_NOTSUPPORTED ")))
return IMAP_MAILBOX_NOTSUPPORTED;
else if (!strncmp(errmsg.s, "IMAP_SYNC_CHECKSUM ",
strlen("IMAP_SYNC_CHECKSUM ")))
return IMAP_SYNC_CHECKSUM;
else if (!strncmp(errmsg.s, "IMAP_SYNC_CHANGED ",
strlen("IMAP_SYNC_CHANGED ")))
return IMAP_SYNC_CHANGED;
else if (!strncmp(errmsg.s, "IMAP_SYNC_BADSIEVE ",
strlen("IMAP_SYNC_BADSIEVE ")))
return IMAP_SYNC_BADSIEVE;
else if (!strncmp(errmsg.s, "IMAP_PROTOCOL_ERROR ",
strlen("IMAP_PROTOCOL_ERROR ")))
return IMAP_PROTOCOL_ERROR;
else if (!strncmp(errmsg.s, "IMAP_PROTOCOL_BAD_PARAMETERS ",
strlen("IMAP_PROTOCOL_BAD_PARAMETERS ")))
return IMAP_PROTOCOL_BAD_PARAMETERS;
else
return IMAP_REMOTE_DENIED;
}
parse_err:
dlist_free(&kl);
sync_getline(in, &errmsg);
xsyslog(LOG_ERR, "IOERROR: received bad response",
"command=<%s> response=<%s> errmsg=<%s>",
cmd, response.s, errmsg.s);
return IMAP_PROTOCOL_ERROR;
}
int sync_append_copyfile(struct mailbox *mailbox,
struct index_record *record,
const struct sync_annot_list *annots,
const struct sync_msgid_list *part_list)
{
const char *destname;
struct message_guid tmp_guid;
struct sync_msgid *item;
int r = 0;
message_guid_copy(&tmp_guid, &record->guid);
item = sync_msgid_lookup(part_list, &record->guid);
if (!item || !item->fname)
r = IMAP_IOERROR;
else
r = message_parse(item->fname, record);
if (r) {
/* deal with unlinked master records */
if (record->internal_flags & FLAG_INTERNAL_EXPUNGED) {
/* no need to set 'needs cleanup' here, it's already expunged */
record->internal_flags |= FLAG_INTERNAL_UNLINKED;
goto just_write;
}
xsyslog(LOG_ERR, "IOERROR: parse failed",
"guid=<%s> error=<%s>",
message_guid_encode(&record->guid),
error_message(r));
return r;
}
/* record->guid was rewritten in the parse, see if it changed */
if (!message_guid_equal(&tmp_guid, &record->guid)) {
xsyslog(LOG_ERR, "IOERROR: guid mismatch on parse",
"filename=<%s> guid=<%s>",
item->fname, message_guid_encode(&record->guid));
return IMAP_IOERROR;
}
/* put back to archive if original was archived, gain single instance store */
if (item->is_archive)
record->internal_flags |= FLAG_INTERNAL_ARCHIVED;
/* push it to archive if it should be archived now anyway */
if (mailbox_should_archive(mailbox, record, NULL))
record->internal_flags |= FLAG_INTERNAL_ARCHIVED;
destname = mailbox_record_fname(mailbox, record);
cyrus_mkdir(destname, 0755);
r = mailbox_copyfile(item->fname, destname, 0);
if (r) {
xsyslog(LOG_ERR, "IOERROR: copy file failed",
"filename=<%s> destination=<%s>",
item->fname, destname);
return r;
}
just_write:
/* Never apply GUID limits when replicating or repairing */
record->ignorelimits = 1;
r = mailbox_append_index_record(mailbox, record);
if (r) return r;
int hadsnoozed = 0;
/* apply the remote annotations */
r = apply_annotations(mailbox, record, NULL, annots, 0, &hadsnoozed);
if (r) {
syslog(LOG_ERR, "Failed to apply annotations: %s",
error_message(r));
}
if (!r && hadsnoozed) {
record->silentupdate = 1;
record->internal_flags |= FLAG_INTERNAL_SNOOZED;
r = mailbox_rewrite_index_record(mailbox, record);
}
return r;
}
/* ==================================================================== */
int sync_mailbox_version_check(struct mailbox **mailboxp)
{
int r = 0;
if ((*mailboxp)->i.minor_version < 10) {
/* index records will definitely not have guids! */
r = IMAP_MAILBOX_NOTSUPPORTED;
goto done;
}
/* scan index records to ensure they have guids. version 10 index records
* have this field, but it might have never been initialised.
* XXX this might be overkill for versions > 10, but let's be cautious */
struct mailbox_iter *iter = mailbox_iter_init((*mailboxp), 0, 0);
const message_t *msg;
while ((msg = mailbox_iter_step(iter))) {
const struct index_record *record = msg_record(msg);
if (message_guid_isnull(&record->guid)) {
syslog(LOG_WARNING, "%s: missing guid for record %u -- needs 'reconstruct -G'?",
(*mailboxp)->name, record->recno);
r = IMAP_MAILBOX_NOTSUPPORTED;
break;
}
}
mailbox_iter_done(&iter);
done:
if (r) {
syslog(LOG_DEBUG, "%s: %s failed version check: %s",
__func__, (*mailboxp)->name, error_message(r));
mailbox_close(mailboxp);
}
return r;
}
/* ======================= server-side sync =========================== */
static void reserve_folder(const char *part, const char *mboxname,
struct sync_msgid_list *part_list)
{
struct mailbox *mailbox = NULL;
int r;
struct sync_msgid *item;
const char *mailbox_msg_path, *stage_msg_path;
int num_reserved;
redo:
num_reserved = 0;
/* Open and lock mailbox */
r = mailbox_open_irl(mboxname, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
if (r) return;
struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_UNLINKED);
const message_t *msg;
while ((msg = mailbox_iter_step(iter))) {
const struct index_record *record = msg_record(msg);
/* do we need it? */
item = sync_msgid_lookup(part_list, &record->guid);
if (!item)
continue;
/* have we already found it? */
if (!item->need_upload)
continue;
/* Attempt to reserve this message */
mailbox_msg_path = mailbox_record_fname(mailbox, record);
stage_msg_path = dlist_reserve_path(part, record->internal_flags & FLAG_INTERNAL_ARCHIVED,
0, &record->guid);
/* check that the sha1 of the file on disk is correct */
struct index_record record2;
memset(&record2, 0, sizeof(struct index_record));
r = message_parse(mailbox_msg_path, &record2);
if (r) {
xsyslog(LOG_ERR, "IOERROR: parse failed",
"filename=<%s>",
mailbox_msg_path);
continue;
}
if (!message_guid_equal(&record->guid, &record2.guid)) {
xsyslog(LOG_ERR, "IOERROR: guid mismatch on parse",
"filename=<%s>",
mailbox_msg_path);
continue;
}
if (mailbox_copyfile(mailbox_msg_path, stage_msg_path, 0) != 0) {
xsyslog(LOG_ERR, "IOERROR: link failed",
"mailbox_msg_path=<%s> stage_msg_path=<%s>",
mailbox_msg_path, stage_msg_path);
continue;
}
item->size = record->size;
item->fname = xstrdup(stage_msg_path); /* track the correct location */
item->is_archive = (record->internal_flags & FLAG_INTERNAL_ARCHIVED) ? 1 : 0;
item->need_upload = 0;
part_list->toupload--;
num_reserved++;
/* already found everything, drop out */
if (!part_list->toupload) break;
/* arbitrary batch size */
if (num_reserved >= 1024) {
mailbox_iter_done(&iter);
mailbox_close(&mailbox);
goto redo;
}
}
mailbox_iter_done(&iter);
mailbox_close(&mailbox);
}
int sync_apply_reserve(struct dlist *kl,
struct sync_reserve_list *reserve_list,
struct sync_state *sstate)
{
struct message_guid *tmpguid;
struct sync_name_list *folder_names = sync_name_list_create();
struct sync_msgid_list *part_list;
struct sync_msgid *item;
struct sync_name *folder;
mbentry_t *mbentry = NULL;
const char *partition = NULL;
struct dlist *ml;
struct dlist *gl;
struct dlist *i;
struct dlist *kout = NULL;
if (!dlist_getatom(kl, "PARTITION", &partition)) goto parse_err;
if (!dlist_getlist(kl, "MBOXNAME", &ml)) goto parse_err;
if (!dlist_getlist(kl, "GUID", &gl)) goto parse_err;
part_list = sync_reserve_partlist(reserve_list, partition);
for (i = gl->head; i; i = i->next) {
if (!dlist_toguid(i, &tmpguid))
goto parse_err;
sync_msgid_insert(part_list, tmpguid);
}
/* need a list so we can mark items */
for (i = ml->head; i; i = i->next) {
sync_name_list_add(folder_names, i->sval);
}
for (folder = folder_names->head; folder; folder = folder->next) {
if (!part_list->toupload) break;
if (mboxlist_lookup(folder->name, &mbentry, 0))
continue;
if (strcmp(mbentry->partition, partition)) {
mboxlist_entry_free(&mbentry);
continue; /* try folders on the same partition first! */
}
mboxlist_entry_free(&mbentry);
reserve_folder(partition, folder->name, part_list);
folder->mark = 1;
}
/* if we have other folders, check them now */
for (folder = folder_names->head; folder; folder = folder->next) {
if (!part_list->toupload) break;
if (folder->mark)
continue;
reserve_folder(partition, folder->name, part_list);
folder->mark = 1;
}
/* check if we missed any */
kout = dlist_newlist(NULL, "MISSING");
for (i = gl->head; i; i = i->next) {
if (!dlist_toguid(i, &tmpguid))
goto parse_err;
item = sync_msgid_lookup(part_list, tmpguid);
if (item->need_upload)
dlist_setguid(kout, "GUID", tmpguid);
}
if (kout->head)
sync_send_response(kout, sstate->pout);
dlist_free(&kout);
sync_name_list_free(&folder_names);
mboxlist_entry_free(&mbentry);
return 0;
parse_err:
dlist_free(&kout);
sync_name_list_free(&folder_names);
mboxlist_entry_free(&mbentry);
return IMAP_PROTOCOL_BAD_PARAMETERS;
}
/* ====================================================================== */
int sync_apply_unquota(struct dlist *kin,
struct sync_state *sstate __attribute__((unused)))
{
return mboxlist_unsetquota(kin->sval);
}
int sync_apply_quota(struct dlist *kin,
struct sync_state *sstate __attribute__((unused)))
{
const char *root;
quota_t limits[QUOTA_NUMRESOURCES];
if (!dlist_getatom(kin, "ROOT", &root))
return IMAP_PROTOCOL_BAD_PARAMETERS;
sync_decode_quota_limits(kin, limits);
modseq_t modseq = 0;
dlist_getnum64(kin, "MODSEQ", &modseq);
return mboxlist_setquotas(root, limits, modseq, 1);
}
/* ====================================================================== */
static int sync_mailbox_compare_update(struct mailbox *mailbox,
struct dlist *kr, int doupdate,
struct sync_msgid_list *part_list)
{
struct index_record mrecord;
struct dlist *ki;
struct sync_annot_list *mannots = NULL;
struct sync_annot_list *rannots = NULL;
int r;
int i;
int has_append = 0;
struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, 0);
const message_t *msg = mailbox_iter_step(iter);
const struct index_record *rrecord = msg ? msg_record(msg) : NULL;
for (ki = kr->head; ki; ki = ki->next) {
sync_annot_list_free(&mannots);
sync_annot_list_free(&rannots);
r = parse_upload(ki, mailbox, &mrecord, &mannots);
if (r) {
syslog(LOG_ERR, "SYNCERROR: failed to parse uploaded record");
return IMAP_PROTOCOL_ERROR;
}
/* n.b. we assume the records in kr are in ascending uid order.
* stuff will probably fail in interesting ways if they're ever not.
*/
while (rrecord && rrecord->uid < mrecord.uid) {
/* read another record */
msg = mailbox_iter_step(iter);
rrecord = msg ? msg_record(msg) : NULL;
}
/* found a match, check for updates */
if (rrecord && rrecord->uid == mrecord.uid) {
/* if they're both EXPUNGED then ignore everything else */
if (mrecord.internal_flags & FLAG_INTERNAL_EXPUNGED &&
rrecord->internal_flags & FLAG_INTERNAL_EXPUNGED)
continue;
/* GUID mismatch is an error straight away, it only ever happens if we
* had a split brain - and it will take a full sync to sort out the mess */
if (!message_guid_equal(&mrecord.guid, &rrecord->guid)) {
syslog(LOG_ERR, "SYNCNOTICE: guid mismatch %s %u",
mailbox->name, mrecord.uid);
r = IMAP_SYNC_CHECKSUM;
goto out;
}
/* higher modseq on the replica is an error */
if (rrecord->modseq > mrecord.modseq) {
if (opt_force) {
syslog(LOG_NOTICE, "forcesync: higher modseq on replica %s %u (" MODSEQ_FMT " > " MODSEQ_FMT ")",
mailbox->name, mrecord.uid, rrecord->modseq, mrecord.modseq);
}
else {
syslog(LOG_ERR, "SYNCNOTICE: higher modseq on replica %s %u (" MODSEQ_FMT " > " MODSEQ_FMT ")",
mailbox->name, mrecord.uid, rrecord->modseq, mrecord.modseq);
r = IMAP_SYNC_CHECKSUM;
goto out;
}
}
/* if it's already expunged on the replica, but alive on the master,
* that's bad */
if (!(mrecord.internal_flags & FLAG_INTERNAL_EXPUNGED) &&
(rrecord->internal_flags & FLAG_INTERNAL_EXPUNGED)) {
syslog(LOG_ERR, "SYNCNOTICE: expunged on replica %s %u",
mailbox->name, mrecord.uid);
r = IMAP_SYNC_CHECKSUM;
goto out;
}
/* skip out on the first pass */
if (!doupdate) continue;
struct index_record copy = *rrecord;
copy.cid = mrecord.cid;
copy.basecid = mrecord.basecid;
copy.modseq = mrecord.modseq;
copy.last_updated = mrecord.last_updated;
copy.internaldate = mrecord.internaldate;
copy.savedate = mrecord.savedate;
copy.createdmodseq = mrecord.createdmodseq;
copy.system_flags = mrecord.system_flags;
/* FLAG_INTERNAL_EXPUNGED is a syncable flag, but it's internal.
* The `internal_flags` contain replica's internal_flags for
* non-EXPUNGED, but master's internal_flags for EXPUNGED */
copy.internal_flags = rrecord->internal_flags & ~FLAG_INTERNAL_EXPUNGED;
copy.internal_flags |= mrecord.internal_flags & FLAG_INTERNAL_EXPUNGED;
for (i = 0; i < MAX_USER_FLAGS/32; i++)
copy.user_flags[i] = mrecord.user_flags[i];
r = read_annotations(mailbox, &copy, &rannots, rrecord->modseq,
/*XXX ANNOTATE_TOMBSTONES*/0);
if (r) {
syslog(LOG_ERR, "Failed to read local annotations %s %u: %s",
mailbox->name, rrecord->recno, error_message(r));
goto out;
}
int hadsnoozed = 0;
r = apply_annotations(mailbox, &copy, rannots, mannots, 0, &hadsnoozed);
if (r) {
syslog(LOG_ERR, "Failed to write merged annotations %s %u: %s",
mailbox->name, rrecord->recno, error_message(r));
goto out;
}
if (hadsnoozed) copy.internal_flags |= FLAG_INTERNAL_SNOOZED;
else copy.internal_flags &= ~FLAG_INTERNAL_SNOOZED;
copy.silentupdate = 1;
copy.ignorelimits = 1;
r = mailbox_rewrite_index_record(mailbox, &copy);
if (r) {
xsyslog(LOG_ERR, "IOERROR: rewrite record failed",
"mboxname=<%s> recno=<%u>",
mailbox->name, rrecord->recno);
goto out;
}
}
/* not found and less than LAST_UID, bogus */
else if (mrecord.uid <= mailbox->i.last_uid) {
/* Expunged, just skip it */
if (!(mrecord.internal_flags & FLAG_INTERNAL_EXPUNGED)) {
r = IMAP_SYNC_CHECKSUM;
goto out;
}
}
/* after LAST_UID, it's an append, that's OK */
else {
/* skip out on the first pass */
if (!doupdate) continue;
mrecord.silentupdate = 1;
r = sync_append_copyfile(mailbox, &mrecord, mannots, part_list);
if (r) {
xsyslog(LOG_ERR, "IOERROR: append file failed",
"mboxname=<%s> uid=<%d>",
mailbox->name, mrecord.uid);
goto out;
}
has_append = 1;
}
}
if (has_append)
sync_log_append(mailbox->name);
r = 0;
out:
mailbox_iter_done(&iter);
sync_annot_list_free(&mannots);
sync_annot_list_free(&rannots);
return r;
}
int sync_apply_mailbox(struct dlist *kin,
struct sync_reserve_list *reserve_list,
struct sync_state *sstate)
{
struct sync_msgid_list *part_list;
/* fields from the request */
const char *uniqueid;
const char *partition;
const char *mboxname;
const char *mboxtype = NULL; /* optional */
uint32_t mbtype;
uint32_t last_uid;
modseq_t highestmodseq;
uint32_t recentuid;
time_t recenttime;
time_t last_appenddate;
time_t pop3_last_login;
time_t pop3_show_after = 0; /* optional */
uint32_t uidvalidity;
modseq_t foldermodseq = 0;
const char *acl;
const char *options_str;
struct synccrcs synccrcs = { 0, 0 };
uint32_t options;
/* optional fields */
modseq_t xconvmodseq = 0;
modseq_t raclmodseq = 0;
modseq_t createdmodseq = 0;
/* previous state markers */
modseq_t since_modseq = 0;
struct synccrcs since_crcs = { 0, 0 };
struct mailbox *mailbox = NULL;
struct dlist *kr;
struct dlist *ka = NULL;
int r;
struct sync_annot_list *mannots = NULL;
struct sync_annot_list *rannots = NULL;
annotate_state_t *astate = NULL;
if (!dlist_getatom(kin, "UNIQUEID", &uniqueid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "MBOXNAME", &mboxname))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum64(kin, "HIGHESTMODSEQ", &highestmodseq))
return IMAP_PROTOCOL_BAD_PARAMETERS;
dlist_getnum64(kin, "CREATEDMODSEQ", &createdmodseq);
dlist_getatom(kin, "MBOXTYPE", &mboxtype);
mbtype = mboxlist_string_to_mbtype(mboxtype);
if (mbtype & (MBTYPE_INTERMEDIATE|MBTYPE_DELETED)) {
// XXX - make sure what's already there is either nothing or compatible...
mbentry_t *newmbentry = NULL;
newmbentry = mboxlist_entry_create();
newmbentry->name = xstrdupnull(mboxname);
newmbentry->mbtype = mbtype;
newmbentry->uniqueid = xstrdupnull(uniqueid);
newmbentry->foldermodseq = highestmodseq;
newmbentry->createdmodseq = createdmodseq;
r = mboxlist_update(newmbentry, /*localonly*/1);
mboxlist_entry_free(&newmbentry);
return r;
}
if (!dlist_getatom(kin, "PARTITION", &partition))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum32(kin, "LAST_UID", &last_uid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum32(kin, "RECENTUID", &recentuid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kin, "RECENTTIME", &recenttime))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kin, "LAST_APPENDDATE", &last_appenddate))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kin, "POP3_LAST_LOGIN", &pop3_last_login))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum32(kin, "UIDVALIDITY", &uidvalidity))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "ACL", &acl))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "OPTIONS", &options_str))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getlist(kin, "RECORD", &kr))
return IMAP_PROTOCOL_BAD_PARAMETERS;
/* optional */
dlist_getlist(kin, "ANNOTATIONS", &ka);
dlist_getdate(kin, "POP3_SHOW_AFTER", &pop3_show_after);
dlist_getnum64(kin, "XCONVMODSEQ", &xconvmodseq);
dlist_getnum64(kin, "RACLMODSEQ", &raclmodseq);
dlist_getnum64(kin, "FOLDERMODSEQ", &foldermodseq);
/* Get the CRCs */
dlist_getnum32(kin, "SYNC_CRC", &synccrcs.basic);
dlist_getnum32(kin, "SYNC_CRC_ANNOT", &synccrcs.annot);
/* Get the previous state for this delta */
dlist_getnum64(kin, "SINCE_MODSEQ", &since_modseq);
dlist_getnum32(kin, "SINCE_CRC", &since_crcs.basic);
dlist_getnum32(kin, "SINCE_CRC_ANNOT", &since_crcs.annot);
options = sync_parse_options(options_str);
r = mailbox_open_iwl(mboxname, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
if (r == IMAP_MAILBOX_NONEXISTENT) {
struct mboxlock *namespacelock = mboxname_usernamespacelock(mboxname);
// try again under lock
r = mailbox_open_iwl(mboxname, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
if (r == IMAP_MAILBOX_NONEXISTENT) { // did we win a race?
char *oldname = mboxlist_find_uniqueid(uniqueid, NULL, NULL);
// if they have the same name it's probably an intermediate being
// promoted. Intermediates, the gift that keeps on giving.
if (oldname && strcmp(oldname, mboxname)) {
syslog(LOG_ERR, "SYNCNOTICE: failed to create mailbox %s with uniqueid %s (already used by %s)",
mboxname, uniqueid, oldname);
free(oldname);
r = IMAP_MAILBOX_MOVED;
}
else {
r = mboxlist_createsync(mboxname, mbtype, partition,
sstate->userid, sstate->authstate,
options, uidvalidity, createdmodseq,
highestmodseq, foldermodseq, acl,
uniqueid, sstate->local_only, 0, &mailbox);
}
/* set a highestmodseq of 0 so ALL changes are future
* changes and get applied */
if (!r) mailbox->i.highestmodseq = 0;
}
mboxname_release(&namespacelock);
}
if (r) {
syslog(LOG_ERR, "Failed to open mailbox %s to update: %s",
mboxname, error_message(r));
goto done;
}
// immediate bail if we have an old state to compare
if (since_modseq) {
struct synccrcs mycrcs = mailbox_synccrcs(mailbox, 0);
if (since_modseq != mailbox->i.highestmodseq ||
!mailbox_crceq(since_crcs, mycrcs)) {
syslog(LOG_ERR, "SYNCNOTICE: mailbox sync mismatch %s "
"hms (m=%llu,r=%llu) crcs (m=%u/%u,r=%u/%u)",
mailbox->name,
since_modseq, mailbox->i.highestmodseq,
since_crcs.basic, since_crcs.annot,
mycrcs.basic, mycrcs.annot);
r = IMAP_SYNC_CHANGED;
goto done;
}
}
if (mailbox->mbtype != mbtype) {
syslog(LOG_ERR, "INVALID MAILBOX TYPE %s (%d, %d)", mailbox->name, mailbox->mbtype, mbtype);
/* is this even possible? */
r = IMAP_MAILBOX_BADTYPE;
goto done;
}
part_list = sync_reserve_partlist(reserve_list, mailbox->part);
/* hold the annotate state open */
r = mailbox_get_annotate_state(mailbox, ANNOTATE_ANY_UID, &astate);
if (r) goto done;
/* and make it hold a transaction open */
annotate_state_begin(astate);
if (strcmp(mailbox->uniqueid, uniqueid)) {
if (opt_force) {
syslog(LOG_NOTICE, "forcesync: fixing uniqueid %s (%s => %s)",
mboxname, mailbox->uniqueid, uniqueid);
free(mailbox->uniqueid);
mailbox->uniqueid = xstrdup(uniqueid);
mailbox->header_dirty = 1;
}
else {
syslog(LOG_ERR, "SYNCNOTICE: Mailbox uniqueid changed %s (%s => %s) - retry",
mboxname, mailbox->uniqueid, uniqueid);
r = IMAP_MAILBOX_MOVED;
goto done;
}
}
/* skip out now, it's going to mismatch for sure! */
if (highestmodseq < mailbox->i.highestmodseq) {
if (opt_force) {
syslog(LOG_NOTICE, "forcesync: higher modseq on replica %s - "
MODSEQ_FMT " < " MODSEQ_FMT,
mboxname, highestmodseq, mailbox->i.highestmodseq);
}
else {
syslog(LOG_ERR, "higher modseq on replica %s - "
MODSEQ_FMT " < " MODSEQ_FMT,
mboxname, highestmodseq, mailbox->i.highestmodseq);
r = IMAP_SYNC_CHECKSUM;
goto done;
}
}
/* skip out now, it's going to mismatch for sure! */
if (uidvalidity < mailbox->i.uidvalidity) {
if (opt_force) {
syslog(LOG_NOTICE, "forcesync: higher uidvalidity on replica %s - %u < %u",
mboxname, uidvalidity, mailbox->i.uidvalidity);
}
else {
syslog(LOG_ERR, "higher uidvalidity on replica %s - %u < %u",
mboxname, uidvalidity, mailbox->i.uidvalidity);
r = IMAP_SYNC_CHECKSUM;
goto done;
}
}
/* skip out now, it's going to mismatch for sure! */
if (last_uid < mailbox->i.last_uid) {
if (opt_force) {
syslog(LOG_NOTICE, "forcesync: higher last_uid on replica %s - %u < %u",
mboxname, last_uid, mailbox->i.last_uid);
}
else {
syslog(LOG_ERR, "higher last_uid on replica %s - %u < %u",
mboxname, last_uid, mailbox->i.last_uid);
r = IMAP_SYNC_CHECKSUM;
goto done;
}
}
/* skip out now, it's going to mismatch for sure! */
/* 0 is the default case, and should always be overwritten with the real value */
if (createdmodseq > mailbox->i.createdmodseq && mailbox->i.createdmodseq != 0) {
syslog(LOG_NOTICE, "SYNCNOTICE: lower createdmodseq on replica %s - %llu > %llu",
mboxname, createdmodseq, mailbox->i.createdmodseq);
}
/* NOTE - this is optional */
if (mailbox_has_conversations(mailbox) && xconvmodseq) {
modseq_t ourxconvmodseq = 0;
r = mailbox_get_xconvmodseq(mailbox, &ourxconvmodseq);
if (r) {
syslog(LOG_ERR, "Failed to read xconvmodseq for %s: %s",
mboxname, error_message(r));
goto done;
}
/* skip out now, it's going to mismatch for sure! */
if (xconvmodseq < ourxconvmodseq) {
if (opt_force) {
syslog(LOG_NOTICE, "forcesync: higher xconvmodseq on replica %s - %llu < %llu",
mboxname, xconvmodseq, ourxconvmodseq);
}
else {
syslog(LOG_ERR, "higher xconvmodseq on replica %s - %llu < %llu",
mboxname, xconvmodseq, ourxconvmodseq);
r = IMAP_SYNC_CHECKSUM;
goto done;
}
}
}
r = sync_mailbox_compare_update(mailbox, kr, 0, part_list);
if (r) goto done;
/* now we're committed to writing something no matter what happens! */
mailbox_index_dirty(mailbox);
mailbox->silentchanges = 1;
/* always take the ACL from the master, it's not versioned */
r = mboxlist_sync_setacls(mboxname, acl, foldermodseq ? foldermodseq : highestmodseq);
if (!r) r = mailbox_set_acl(mailbox, acl);
if (r) goto done;
/* take all mailbox (not message) annotations - aka metadata,
* they're not versioned either */
if (ka)
decode_annotations(ka, &mannots, mailbox, NULL);
r = read_annotations(mailbox, NULL, &rannots, 0, 0);
if (!r) r = apply_annotations(mailbox, NULL, rannots, mannots, 0, NULL);
if (r) {
syslog(LOG_ERR, "syncerror: annotations failed to apply to %s",
mailbox->name);
goto done;
}
r = sync_mailbox_compare_update(mailbox, kr, 1, part_list);
if (r) {
abort();
return r;
}
if (!opt_force) {
assert(mailbox->i.last_uid <= last_uid);
}
mailbox->i.last_uid = last_uid;
mailbox->i.recentuid = recentuid;
mailbox->i.highestmodseq = highestmodseq;
mailbox->i.recenttime = recenttime;
mailbox->i.last_appenddate = last_appenddate;
mailbox->i.pop3_last_login = pop3_last_login;
mailbox->i.pop3_show_after = pop3_show_after;
mailbox->i.createdmodseq = createdmodseq;
/* only alter the syncable options */
mailbox->i.options = (options & MAILBOX_OPTIONS_MASK) |
(mailbox->i.options & ~MAILBOX_OPTIONS_MASK);
/* always set the highestmodseq */
mboxname_setmodseq(mailbox->name, highestmodseq, mailbox->mbtype, /*flags*/0);
/* this happens rarely, so let us know */
if (mailbox->i.uidvalidity != uidvalidity) {
syslog(LOG_NOTICE, "%s uidvalidity changed, updating %u => %u",
mailbox->name, mailbox->i.uidvalidity, uidvalidity);
/* make sure nothing new gets created with a lower value */
mailbox->i.uidvalidity = mboxname_setuidvalidity(mailbox->name, uidvalidity);
}
if (mailbox_has_conversations(mailbox)) {
r = mailbox_update_xconvmodseq(mailbox, xconvmodseq, opt_force);
}
if (config_getswitch(IMAPOPT_REVERSEACLS) && raclmodseq) {
mboxname_setraclmodseq(mailbox->name, raclmodseq);
}
done:
sync_annot_list_free(&mannots);
sync_annot_list_free(&rannots);
/* check the CRC too */
if (!r && !mailbox_crceq(synccrcs, mailbox_synccrcs(mailbox, 0))) {
/* try forcing a recalculation */
if (!mailbox_crceq(synccrcs, mailbox_synccrcs(mailbox, 1)))
r = IMAP_SYNC_CHECKSUM;
}
mailbox_close(&mailbox);
return r;
}
/* ====================================================================== */
static int getannotation_cb(const char *mailbox,
uint32_t uid __attribute__((unused)),
const char *entry, const char *userid,
const struct buf *value,
const struct annotate_metadata *mdata __attribute__((unused)),
void *rock)
{
struct protstream *pout = (struct protstream *)rock;
struct dlist *kl;
kl = dlist_newkvlist(NULL, "ANNOTATION");
dlist_setatom(kl, "MBOXNAME", mailbox);
dlist_setatom(kl, "ENTRY", entry);
dlist_setatom(kl, "USERID", userid);
dlist_setmap(kl, "VALUE", value->s, value->len);
sync_send_response(kl, pout);
dlist_free(&kl);
return 0;
}
int sync_get_annotation(struct dlist *kin, struct sync_state *sstate)
{
const char *mboxname = kin->sval;
return annotatemore_findall(mboxname, 0, "*", /*modseq*/0,
&getannotation_cb, (void *) sstate->pout,
/*flags*/0);
}
static void print_quota(struct quota *q, struct protstream *pout)
{
struct dlist *kl;
kl = dlist_newkvlist(NULL, "QUOTA");
dlist_setatom(kl, "ROOT", q->root);
sync_encode_quota_limits(kl, q->limits);
dlist_setnum64(kl, "MODSEQ", q->modseq);
sync_send_response(kl, pout);
dlist_free(&kl);
}
static int quota_work(const char *root, struct protstream *pout)
{
struct quota q;
quota_init(&q, root);
if (!quota_read(&q, NULL, 0))
print_quota(&q, pout);
quota_free(&q);
return 0;
}
int sync_get_quota(struct dlist *kin, struct sync_state *sstate)
{
return quota_work(kin->sval, sstate->pout);
}
struct mbox_rock {
struct protstream *pout;
struct sync_name_list *qrl;
};
static int sync_mailbox_byentry(const mbentry_t *mbentry, void *rock)
{
struct mbox_rock *mrock = (struct mbox_rock *) rock;
struct sync_name_list *qrl = mrock->qrl;
struct mailbox *mailbox = NULL;
struct dlist *kl = dlist_newkvlist(NULL, "MAILBOX");
annotate_state_t *astate = NULL;
int r = 0;
if (!mbentry) goto out;
if (mbentry->mbtype & MBTYPE_DELETED) goto out;
if (mbentry->mbtype & MBTYPE_INTERMEDIATE) {
dlist_setatom(kl, "UNIQUEID", mbentry->uniqueid);
dlist_setatom(kl, "MBOXNAME", mbentry->name);
dlist_setatom(kl, "MBOXTYPE", mboxlist_mbtype_to_string(mbentry->mbtype));
dlist_setnum32(kl, "SYNC_CRC", 0);
// this stuff should be optional, but old sync_client will barf without it
dlist_setnum32(kl, "LAST_UID", 0);
dlist_setnum64(kl, "HIGHESTMODSEQ", 0);
dlist_setnum32(kl, "RECENTUID", 0);
dlist_setdate(kl, "RECENTTIME", 0);
dlist_setdate(kl, "LAST_APPENDDATE", 0);
dlist_setdate(kl, "POP3_LAST_LOGIN", 0);
dlist_setdate(kl, "POP3_SHOW_AFTER", 0);
// standard fields
dlist_setnum32(kl, "UIDVALIDITY", mbentry->uidvalidity);
dlist_setatom(kl, "PARTITION", mbentry->partition);
dlist_setatom(kl, "ACL", mbentry->acl);
dlist_setatom(kl, "OPTIONS", sync_encode_options(0));
dlist_setnum64(kl, "CREATEDMODSEQ", mbentry->createdmodseq);
dlist_setnum64(kl, "FOLDERMODSEQ", mbentry->foldermodseq);
// send the intermediate response
sync_send_response(kl, mrock->pout);
goto out;
}
r = mailbox_open_irl(mbentry->name, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
/* doesn't exist? Probably not finished creating or removing yet */
if (r == IMAP_MAILBOX_NONEXISTENT ||
r == IMAP_MAILBOX_RESERVED) {
r = 0;
goto out;
}
if (r) goto out;
/* hold the annotate state open */
r = mailbox_get_annotate_state(mailbox, ANNOTATE_ANY_UID, &astate);
if (r) goto out;
/* and make it hold a transaction open */
annotate_state_begin(astate);
if (qrl && mailbox->quotaroot)
sync_name_list_add(qrl, mailbox->quotaroot);
r = sync_prepare_dlists(mailbox, NULL, NULL, NULL, NULL, kl, NULL, 0,
/*XXX fullannots*/1, 0);
if (!r) sync_send_response(kl, mrock->pout);
out:
mailbox_close(&mailbox);
dlist_free(&kl);
return r;
}
static int sync_mailbox_byuniqueid(const char *uniqueid, void *rock)
{
char *name = mboxlist_find_uniqueid(uniqueid, NULL, NULL);
mbentry_t *mbentry = NULL;
int r = mboxlist_lookup_allow_all(name, &mbentry, NULL);
if (!r) r = sync_mailbox_byentry(mbentry, rock);
mboxlist_entry_free(&mbentry);
free(name);
return r;
}
int sync_get_fullmailbox(struct dlist *kin, struct sync_state *sstate)
{
struct mailbox *mailbox = NULL;
struct dlist *kl = dlist_newkvlist(NULL, "MAILBOX");
int r;
r = mailbox_open_irl(kin->sval, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
if (r) goto out;
r = sync_prepare_dlists(mailbox, NULL, NULL, NULL, NULL, kl, NULL, 1,
/*XXX fullannots*/1, 0);
if (r) goto out;
sync_send_response(kl, sstate->pout);
out:
dlist_free(&kl);
mailbox_close(&mailbox);
return r;
}
int sync_get_mailboxes(struct dlist *kin, struct sync_state *sstate)
{
struct dlist *ki;
struct mbox_rock mrock = { sstate->pout, NULL };
for (ki = kin->head; ki; ki = ki->next) {
mbentry_t *mbentry = NULL;
int r = mboxlist_lookup_allow_all(ki->sval, &mbentry, NULL);
if (!r) sync_mailbox_byentry(mbentry, &mrock);
mboxlist_entry_free(&mbentry);
}
return 0;
}
int sync_get_uniqueids(struct dlist *kin, struct sync_state *sstate)
{
struct dlist *ki;
struct mbox_rock mrock = { sstate->pout, NULL };
for (ki = kin->head; ki; ki = ki->next)
sync_mailbox_byuniqueid(ki->sval, &mrock);
return 0;
}
/* ====================================================================== */
static int print_seen(const char *uniqueid, struct seendata *sd, void *rock)
{
struct dlist *kl;
struct protstream *pout = (struct protstream *) rock;
kl = dlist_newkvlist(NULL, "SEEN");
dlist_setatom(kl, "UNIQUEID", uniqueid);
dlist_setdate(kl, "LASTREAD", sd->lastread);
dlist_setnum32(kl, "LASTUID", sd->lastuid);
dlist_setdate(kl, "LASTCHANGE", sd->lastchange);
dlist_setatom(kl, "SEENUIDS", sd->seenuids);
sync_send_response(kl, pout);
dlist_free(&kl);
return 0;
}
static int user_getseen(const char *userid, struct protstream *pout)
{
struct seen *seendb = NULL;
/* no SEEN DB is OK, just return */
if (seen_open(userid, SEEN_SILENT, &seendb))
return 0;
seen_foreach(seendb, print_seen, pout);
seen_close(&seendb);
return 0;
}
static int user_getsub(const char *userid, struct protstream *pout)
{
struct dlist *kl = dlist_newlist(NULL, "LSUB");
strarray_t *sublist = mboxlist_sublist(userid);
int i;
for (i = 0; i < sublist->count; i++) {
const char *name = strarray_nth(sublist, i);
dlist_setatom(kl, "MBOXNAME", name);
}
if (kl->head)
sync_send_response(kl, pout);
dlist_free(&kl);
strarray_free(sublist);
return 0;
}
static int user_getsieve(const char *userid, struct protstream *pout)
{
struct sync_sieve_list *sieve_list;
struct sync_sieve *sieve;
struct dlist *kl;
sieve_list = sync_sieve_list_generate(userid);
if (!sieve_list) return 0;
for (sieve = sieve_list->head; sieve; sieve = sieve->next) {
kl = dlist_newkvlist(NULL, "SIEVE");
dlist_setatom(kl, "FILENAME", sieve->name);
dlist_setdate(kl, "LAST_UPDATE", sieve->last_update);
dlist_setatom(kl, "GUID", message_guid_encode(&sieve->guid));
dlist_setnum32(kl, "ISACTIVE", sieve->active ? 1 : 0);
sync_send_response(kl, pout);
dlist_free(&kl);
}
sync_sieve_list_free(&sieve_list);
return 0;
}
static int user_meta(const char *userid, struct protstream *pout)
{
user_getseen(userid, pout);
user_getsub(userid, pout);
user_getsieve(userid, pout);
return 0;
}
int sync_get_meta(struct dlist *kin, struct sync_state *sstate)
{
return user_meta(kin->sval, sstate->pout);
}
int sync_get_user(struct dlist *kin, struct sync_state *sstate)
{
int r;
struct sync_name_list *quotaroots;
struct sync_name *qr;
const char *userid = kin->sval;
struct mbox_rock mrock;
quotaroots = sync_name_list_create();
mrock.qrl = quotaroots;
mrock.pout = sstate->pout;
r = mboxlist_usermboxtree(userid, NULL, sync_mailbox_byentry, &mrock, MBOXTREE_DELETED|MBOXTREE_INTERMEDIATES);
if (r) goto bail;
for (qr = quotaroots->head; qr; qr = qr->next) {
r = quota_work(qr->name, sstate->pout);
if (r) goto bail;
}
r = user_meta(userid, sstate->pout);
if (r) goto bail;
bail:
sync_name_list_free(&quotaroots);
return r;
}
/* ====================================================================== */
int sync_apply_unmailbox(struct dlist *kin, struct sync_state *sstate)
{
const char *mboxname = kin->sval;
struct mboxlock *namespacelock = mboxname_usernamespacelock(mboxname);
/* Delete with admin privileges */
int delflags = MBOXLIST_DELETE_FORCE | MBOXLIST_DELETE_SILENT;
if (sstate->local_only) delflags |= MBOXLIST_DELETE_LOCALONLY;
int r = mboxlist_deletemailbox(mboxname, sstate->userisadmin,
sstate->userid, sstate->authstate,
NULL, delflags);
mboxname_release(&namespacelock);
return r;
}
int sync_apply_rename(struct dlist *kin, struct sync_state *sstate)
{
const char *oldmboxname;
const char *newmboxname;
const char *partition;
uint32_t uidvalidity = 0;
mbentry_t *mbentry = NULL;
int r;
if (!dlist_getatom(kin, "OLDMBOXNAME", &oldmboxname))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "NEWMBOXNAME", &newmboxname))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "PARTITION", &partition))
return IMAP_PROTOCOL_BAD_PARAMETERS;
/* optional */
dlist_getnum32(kin, "UIDVALIDITY", &uidvalidity);
struct mboxlock *oldlock = NULL;
struct mboxlock *newlock = NULL;
/* make sure we grab these locks in a stable order! */
if (strcmpsafe(oldmboxname, newmboxname) < 0) {
oldlock = mboxname_usernamespacelock(oldmboxname);
newlock = mboxname_usernamespacelock(newmboxname);
}
else {
// doesn't hurt to double lock, it's refcounted
newlock = mboxname_usernamespacelock(newmboxname);
oldlock = mboxname_usernamespacelock(oldmboxname);
}
r = mboxlist_lookup_allow_all(oldmboxname, &mbentry, 0);
if (!r) r = mboxlist_renamemailbox(mbentry, newmboxname, partition,
uidvalidity, 1, sstate->userid,
sstate->authstate, NULL, sstate->local_only, 1, 1,
1/*keep_intermediaries*/,
0/*move_subscription*/,
1/*silent*/);
mboxlist_entry_free(&mbentry);
mboxname_release(&oldlock);
mboxname_release(&newlock);
return r;
}
int sync_apply_changesub(struct dlist *kin, struct sync_state *sstate)
{
const char *mboxname;
const char *userid;
int add;
/* SUB or UNSUB */
add = strcmp(kin->name, "SUB") ? 0 : 1;
if (!dlist_getatom(kin, "MBOXNAME", &mboxname))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "USERID", &userid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
return mboxlist_changesub(mboxname, userid, sstate->authstate, add, add, 0);
}
/* ====================================================================== */
int sync_apply_annotation(struct dlist *kin, struct sync_state *sstate)
{
struct entryattlist *entryatts = NULL;
struct attvaluelist *attvalues = NULL;
const char *mboxname = NULL;
const char *entry = NULL;
const char *mapval = NULL;
size_t maplen = 0;
struct buf value = BUF_INITIALIZER;
const char *userid = NULL;
char *name = NULL;
struct mailbox *mailbox = NULL;
annotate_state_t *astate = NULL;
int r;
if (!dlist_getatom(kin, "MBOXNAME", &mboxname))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "ENTRY", &entry))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "USERID", &userid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getmap(kin, "VALUE", &mapval, &maplen))
return IMAP_PROTOCOL_BAD_PARAMETERS;
buf_init_ro(&value, mapval, maplen);
r = mailbox_open_iwl(mboxname, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
if (r) goto done;
appendattvalue(&attvalues,
*userid ? "value.priv" : "value.shared",
&value);
appendentryatt(&entryatts, entry, attvalues);
astate = annotate_state_new();
annotate_state_set_auth(astate,
sstate->userisadmin, userid, sstate->authstate);
r = annotate_state_set_mailbox(astate, mailbox);
if (r) goto done;
r = annotate_state_store(astate, entryatts);
done:
if (!r)
r = annotate_state_commit(&astate);
else
annotate_state_abort(&astate);
mailbox_close(&mailbox);
buf_free(&value);
freeentryatts(entryatts);
free(name);
return r;
}
int sync_apply_unannotation(struct dlist *kin, struct sync_state *sstate)
{
struct entryattlist *entryatts = NULL;
struct attvaluelist *attvalues = NULL;
const char *mboxname = NULL;
const char *entry = NULL;
const char *userid = NULL;
struct buf empty = BUF_INITIALIZER;
char *name = NULL;
struct mailbox *mailbox = NULL;
annotate_state_t *astate = NULL;
int r;
if (!dlist_getatom(kin, "MBOXNAME", &mboxname))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "ENTRY", &entry))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "USERID", &userid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
r = mailbox_open_iwl(mboxname, &mailbox);
if (!r)
r = sync_mailbox_version_check(&mailbox);
if (r)
goto done;
appendattvalue(&attvalues,
*userid ? "value.priv" : "value.shared",
&empty);
appendentryatt(&entryatts, entry, attvalues);
astate = annotate_state_new();
annotate_state_set_auth(astate,
sstate->userisadmin, userid, sstate->authstate);
r = annotate_state_set_mailbox(astate, mailbox);
if (r) goto done;
r = annotate_state_store(astate, entryatts);
done:
if (!r)
r = annotate_state_commit(&astate);
else
annotate_state_abort(&astate);
mailbox_close(&mailbox);
freeentryatts(entryatts);
free(name);
return r;
}
int sync_apply_sieve(struct dlist *kin,
struct sync_state *sstate __attribute__((unused)))
{
const char *userid;
const char *filename;
time_t last_update;
const char *content;
size_t len;
if (!dlist_getatom(kin, "USERID", &userid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "FILENAME", &filename))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kin, "LAST_UPDATE", &last_update))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getmap(kin, "CONTENT", &content, &len))
return IMAP_PROTOCOL_BAD_PARAMETERS;
return sync_sieve_upload(userid, filename, last_update, content, len);
}
int sync_apply_unsieve(struct dlist *kin,
struct sync_state *sstate __attribute__((unused)))
{
const char *userid;
const char *filename;
if (!dlist_getatom(kin, "USERID", &userid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "FILENAME", &filename))
return IMAP_PROTOCOL_BAD_PARAMETERS;
return sync_sieve_delete(userid, filename);
}
int sync_apply_activate_sieve(struct dlist *kin,
struct sync_state *sstate __attribute((unused)))
{
const char *userid;
const char *filename;
if (!dlist_getatom(kin, "USERID", &userid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "FILENAME", &filename))
return IMAP_PROTOCOL_BAD_PARAMETERS;
return sync_sieve_activate(userid, filename);
}
int sync_apply_unactivate_sieve(struct dlist *kin,
struct sync_state *sstate __attribute__((unused)))
{
const char *userid;
if (!dlist_getatom(kin, "USERID", &userid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
return sync_sieve_deactivate(userid);
}
int sync_apply_seen(struct dlist *kin,
struct sync_state *sstate __attribute__((unused)))
{
int r;
struct seen *seendb = NULL;
struct seendata sd = SEENDATA_INITIALIZER;
const char *seenuids;
const char *userid;
const char *uniqueid;
if (!dlist_getatom(kin, "USERID", &userid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "UNIQUEID", &uniqueid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kin, "LASTREAD", &sd.lastread))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum32(kin, "LASTUID", &sd.lastuid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kin, "LASTCHANGE", &sd.lastchange))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "SEENUIDS", &seenuids))
return IMAP_PROTOCOL_BAD_PARAMETERS;
sd.seenuids = xstrdup(seenuids);
r = seen_open(userid, SEEN_CREATE, &seendb);
if (r) return r;
r = seen_write(seendb, uniqueid, &sd);
seen_close(&seendb);
seen_freedata(&sd);
return r;
}
EXPORTED int addmbox_cb(const mbentry_t *mbentry, void *rock)
{
strarray_t *list = (strarray_t *)rock;
strarray_append(list, mbentry->name);
return 0;
}
int sync_apply_unuser(struct dlist *kin, struct sync_state *sstate)
{
const char *userid = kin->sval;
int r = 0;
int i;
/* nothing to do if there's no userid */
if (!userid || !userid[0]) {
xsyslog(LOG_WARNING, "ignoring attempt to sync_apply_unuser() without userid",
NULL);
return 0;
}
struct mboxlock *namespacelock = user_namespacelock(userid);
/* Nuke subscriptions */
/* ignore failures here - the subs file gets deleted soon anyway */
strarray_t *list = mboxlist_sublist(userid);
for (i = 0; i < list->count; i++) {
const char *name = strarray_nth(list, i);
mboxlist_changesub(name, userid, sstate->authstate, 0, 0, 0);
}
strarray_truncate(list, 0);
r = mboxlist_usermboxtree(userid, NULL, addmbox_cb, list, 0);
if (r) goto done;
/* delete in reverse so INBOX is last */
int delflags = MBOXLIST_DELETE_FORCE;
if (sstate->local_only) delflags |= MBOXLIST_DELETE_LOCALONLY;
for (i = list->count; i; i--) {
const char *name = strarray_nth(list, i-1);
r = mboxlist_deletemailbox(name, sstate->userisadmin,
sstate->userid, sstate->authstate,
NULL, delflags);
if (r) goto done;
}
r = user_deletedata(userid, 1);
done:
mboxname_release(&namespacelock);
strarray_free(list);
return r;
}
/* ====================================================================== */
int sync_get_sieve(struct dlist *kin, struct sync_state *sstate)
{
struct dlist *kl;
const char *userid;
const char *filename;
uint32_t size;
char *sieve;
if (!dlist_getatom(kin, "USERID", &userid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "FILENAME", &filename))
return IMAP_PROTOCOL_BAD_PARAMETERS;
sieve = sync_sieve_read(userid, filename, &size);
if (!sieve)
return IMAP_MAILBOX_NONEXISTENT;
kl = dlist_newkvlist(NULL, "SIEVE");
dlist_setatom(kl, "USERID", userid);
dlist_setatom(kl, "FILENAME", filename);
dlist_setmap(kl, "CONTENT", sieve, size);
sync_send_response(kl, sstate->pout);
dlist_free(&kl);
free(sieve);
return 0;
}
/* NOTE - can't lock a mailbox here, because it could deadlock,
* so just pick the file out from under the hood */
int sync_get_message(struct dlist *kin, struct sync_state *sstate)
{
const char *mboxname;
const char *partition;
const char *uniqueid;
const char *guid;
uint32_t uid;
const char *fname;
struct dlist *kl;
struct message_guid tmp_guid;
struct stat sbuf;
if (!dlist_getatom(kin, "MBOXNAME", &mboxname))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "PARTITION", &partition))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "UNIQUEID", &uniqueid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "GUID", &guid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum32(kin, "UID", &uid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!message_guid_decode(&tmp_guid, guid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
fname = mboxname_datapath(partition, mboxname, uniqueid, uid);
if (stat(fname, &sbuf) == -1) {
fname = mboxname_archivepath(partition, mboxname, uniqueid, uid);
if (stat(fname, &sbuf) == -1)
return IMAP_MAILBOX_NONEXISTENT;
}
kl = dlist_setfile(NULL, "MESSAGE", partition, &tmp_guid, sbuf.st_size, fname);
sync_send_response(kl, sstate->pout);
dlist_free(&kl);
return 0;
}
int sync_apply_expunge(struct dlist *kin,
struct sync_state *sstate __attribute__((unused)))
{
const char *mboxname;
const char *uniqueid;
struct dlist *ul;
struct dlist *ui;
struct mailbox *mailbox = NULL;
int r = 0;
if (!dlist_getatom(kin, "MBOXNAME", &mboxname))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kin, "UNIQUEID", &uniqueid))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getlist(kin, "UID", &ul))
return IMAP_PROTOCOL_BAD_PARAMETERS;
r = mailbox_open_iwl(mboxname, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
if (r) goto done;
/* don't want to expunge the wrong mailbox! */
if (strcmp(mailbox->uniqueid, uniqueid)) {
r = IMAP_MAILBOX_MOVED;
goto done;
}
for (ui = ul->head; ui; ui = ui->next) {
struct index_record oldrecord;
r = mailbox_find_index_record(mailbox, dlist_num(ui), &oldrecord);
if (r) continue; /* skip */
oldrecord.internal_flags |= FLAG_INTERNAL_EXPUNGED;
oldrecord.silentupdate = 1; /* so the next sync will succeed */
oldrecord.ignorelimits = 1;
r = mailbox_rewrite_index_record(mailbox, &oldrecord);
if (r) goto done;
}
done:
mailbox_close(&mailbox);
return r;
}
int sync_apply_message(struct dlist *kin,
struct sync_reserve_list *reserve_list,
struct sync_state *sstate __attribute((unused)))
{
struct sync_msgid_list *part_list;
struct dlist *ki;
struct sync_msgid *msgid;
for (ki = kin->head; ki; ki = ki->next) {
struct message_guid *guid;
const char *part;
size_t size;
const char *fname;
/* XXX - complain more? */
if (!dlist_tofile(ki, &part, &guid, (unsigned long *) &size, &fname))
continue;
part_list = sync_reserve_partlist(reserve_list, part);
msgid = sync_msgid_insert(part_list, guid);
if (!msgid->need_upload)
continue;
msgid->size = size;
if (!msgid->fname) msgid->fname = xstrdup(fname);
msgid->need_upload = 0;
part_list->toupload--;
}
return 0;
}
/* ====================================================================== */
int sync_restore_mailbox(struct dlist *kin,
struct sync_reserve_list *reserve_list,
struct sync_state *sstate)
{
/* fields from the request, all but mboxname are optional */
const char *mboxname;
const char *uniqueid = NULL;
const char *partition = NULL;
const char *mboxtype = NULL;
const char *acl = NULL;
const char *options_str = NULL;
modseq_t highestmodseq = 0;
uint32_t uidvalidity = 0;
struct dlist *kr = NULL;
struct dlist *ka = NULL;
modseq_t xconvmodseq = 0;
modseq_t createdmodseq = 0;
modseq_t foldermodseq = 0;
/* derived fields */
uint32_t options = 0;
uint32_t mbtype = 0;
struct mailbox *mailbox = NULL;
struct sync_msgid_list *part_list;
annotate_state_t *astate = NULL;
struct dlist *ki;
int has_append = 0;
int is_new_mailbox = 0;
int r;
if (!dlist_getatom(kin, "MBOXNAME", &mboxname)) {
syslog(LOG_DEBUG, "%s: missing MBOXNAME", __func__);
return IMAP_PROTOCOL_BAD_PARAMETERS;
}
/* optional */
dlist_getatom(kin, "PARTITION", &partition);
dlist_getatom(kin, "ACL", &acl);
dlist_getatom(kin, "OPTIONS", &options_str);
dlist_getlist(kin, "RECORD", &kr);
dlist_getlist(kin, "ANNOTATIONS", &ka);
dlist_getatom(kin, "MBOXTYPE", &mboxtype);
dlist_getnum64(kin, "XCONVMODSEQ", &xconvmodseq);
/* derived */
options = sync_parse_options(options_str);
mbtype = mboxlist_string_to_mbtype(mboxtype);
/* XXX sanely handle deletedprefix mboxnames */
/* If the mboxname being restored already exists, then restored messages
* are appended to it. The UNIQUEID, HIGHESTMODSEQ, UIDVALIDITY and
* MBOXTYPE fields in the dlist, and the UID, MODSEQ and LAST_UPDATED fields
* in the restored message records, are ignored entirely.
*
* If the mboxname does not exist, we create it. If UNIQUEID, HIGHESTMODSEQ
* and UIDVALIDITY were provided, we try to preserve them, and if we can, we
* also try to preserve the UID, MODSEQ and LAST_UPDATED fields of the
* restored messages. This is useful when e.g. rebuilding a server from a
* backup, and wanting clients' IMAP states to match.
*
* If UNIQUEID, HIGHESTMODSEQ or UIDVALIDITY are not provided, we don't try
* to preserve them. We create the mailbox, but then append the restored
* messages to it as if it already existed (new UID et al).
*/
/* open/create mailbox */
r = mailbox_open_iwl(mboxname, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
syslog(LOG_DEBUG, "%s: mailbox_open_iwl %s: %s",
__func__, mboxname, error_message(r));
if (r == IMAP_MAILBOX_NONEXISTENT) {
dlist_getatom(kin, "UNIQUEID", &uniqueid);
dlist_getnum64(kin, "HIGHESTMODSEQ", &highestmodseq);
dlist_getnum32(kin, "UIDVALIDITY", &uidvalidity);
dlist_getnum64(kin, "CREATEDMODSEQ", &createdmodseq);
dlist_getnum64(kin, "FOLDERMODSEQ", &foldermodseq);
/* if any of these three weren't set, disregard the others too */
if (!uniqueid || !highestmodseq || !uidvalidity) {
uniqueid = NULL;
highestmodseq = 0;
uidvalidity = 0;
}
struct mboxlock *namespacelock = mboxname_usernamespacelock(mboxname);
// try again under lock
r = mailbox_open_iwl(mboxname, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
if (r == IMAP_MAILBOX_NONEXISTENT) { // did we win a race?
r = mboxlist_createsync(mboxname, mbtype, partition,
sstate->userid, sstate->authstate,
options, uidvalidity, createdmodseq,
highestmodseq, foldermodseq, acl,
uniqueid, sstate->local_only, 0, &mailbox);
syslog(LOG_DEBUG, "%s: mboxlist_createsync %s: %s",
__func__, mboxname, error_message(r));
is_new_mailbox = 1;
}
mboxname_release(&namespacelock);
}
if (r) {
syslog(LOG_ERR, "Failed to open mailbox %s to restore: %s",
mboxname, error_message(r));
return r;
}
/* XXX what if we've opened a deleted mailbox? */
/* XXX verify mailbox is suitable? */
/* make sure mailbox types match */
if (mailbox->mbtype != mbtype) {
syslog(LOG_ERR, "restore mailbox %s: mbtype mismatch (%d, %d)",
mailbox->name, mailbox->mbtype, mbtype);
r = IMAP_MAILBOX_BADTYPE;
goto bail;
}
part_list = sync_reserve_partlist(reserve_list, mailbox->part);
/* hold the annotate state open */
r = mailbox_get_annotate_state(mailbox, ANNOTATE_ANY_UID, &astate);
syslog(LOG_DEBUG, "%s: mailbox_get_annotate_state %s: %s",
__func__, mailbox->name, error_message(r));
if (r) goto bail;
/* and make it hold a transaction open */
annotate_state_begin(astate);
/* XXX do we need to hold the conversation state open? */
/* restore mailbox annotations */
if (ka) {
struct sync_annot_list *restore_annots = NULL;
struct sync_annot_list *mailbox_annots = NULL;
r = decode_annotations(ka, &restore_annots, mailbox, NULL);
if (!r) r = read_annotations(mailbox, NULL, &mailbox_annots, 0, 0);
if (!r) r = apply_annotations(mailbox, NULL,
mailbox_annots, restore_annots,
!is_new_mailbox, NULL);
if (r)
syslog(LOG_WARNING,
"restore mailbox %s: unable to apply mailbox annotations: %s",
mailbox->name, error_message(r));
/* keep going on annotations failure*/
r = 0;
sync_annot_list_free(&restore_annots);
sync_annot_list_free(&mailbox_annots);
}
/* n.b. undocumented assumption here and in sync_apply_mailbox
* that records will be provided sorted by ascending uid */
for (ki = kr->head; ki; ki = ki->next) {
struct sync_annot_list *annots = NULL;
struct index_record record = {0};
/* XXX skip if the guid is already in this folder? */
r = parse_upload(ki, mailbox, &record, &annots);
syslog(LOG_DEBUG, "%s: parse_upload %s: %s",
__func__, mailbox->name, error_message(r));
if (r) goto bail;
/* generate a uid if we can't reuse a provided one */
if (!uidvalidity || record.uid <= mailbox->i.last_uid)
record.uid = mailbox->i.last_uid + 1;
/* reuse a provided modseq/last_updated if safe */
if (highestmodseq && record.modseq && record.modseq <= mailbox->i.highestmodseq)
record.silentupdate = 1;
r = sync_append_copyfile(mailbox, &record, annots, part_list);
has_append = 1;
sync_annot_list_free(&annots);
if (r) goto bail;
}
r = mailbox_commit(mailbox);
syslog(LOG_DEBUG, "%s: mailbox_commit %s: %s",
__func__, mailbox->name, error_message(r));
if (r) {
syslog(LOG_ERR, "%s: mailbox_commit(%s): %s",
__func__, mailbox->name, error_message(r));
}
if (!r && has_append)
sync_log_append(mailbox->name);
mailbox_close(&mailbox);
return r;
bail:
mailbox_abort(mailbox);
mailbox_close(&mailbox);
return r;
}
/* ====================================================================== */
static const char *sync_response(int r)
{
const char *resp;
switch (r) {
case 0:
resp = "OK success";
break;
case IMAP_INVALID_USER:
resp = "NO IMAP_INVALID_USER No Such User";
break;
case IMAP_MAILBOX_NONEXISTENT:
resp = "NO IMAP_MAILBOX_NONEXISTENT No Such Mailbox";
break;
case IMAP_MAILBOX_LOCKED:
resp = "NO IMAP_MAILBOX_LOCKED Mailbox locked";
break;
case IMAP_MAILBOX_MOVED:
resp = "NO IMAP_MAILBOX_MOVED Mailbox exists with another name or uniqueid";
break;
case IMAP_MAILBOX_NOTSUPPORTED:
resp = "NO IMAP_MAILBOX_NOTSUPPORTED Operation is not supported on mailbox";
break;
case IMAP_SYNC_CHECKSUM:
resp = "NO IMAP_SYNC_CHECKSUM Checksum Failure";
break;
case IMAP_SYNC_CHANGED:
resp = "NO IMAP_SYNC_CHANGED Changed since last sync";
break;
case IMAP_SYNC_BADSIEVE:
resp = "NO IMAP_SYNC_BADSIEVE Sieve script compilation failure";
break;
case IMAP_PROTOCOL_ERROR:
resp = "NO IMAP_PROTOCOL_ERROR Protocol error";
break;
case IMAP_PROTOCOL_BAD_PARAMETERS:
resp = "NO IMAP_PROTOCOL_BAD_PARAMETERS";
// XXX resp = "NO IMAP_PROTOCOL_BAD_PARAMETERS near %s\r\n", dlist_lastkey());
break;
default:
resp = "NO Unknown error";
}
return resp;
}
/* ======================= client-side sync =========================== */
/* Routines relevant to reserve operation */
/* Find the messages that we will want to upload from this mailbox,
* flag messages that are already available at the server end */
int sync_find_reserve_messages(struct mailbox *mailbox,
uint32_t fromuid,
uint32_t touid,
struct sync_msgid_list *part_list)
{
struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_UNLINKED);
mailbox_iter_startuid(iter, fromuid+1); /* only read new records */
const message_t *msg;
while ((msg = mailbox_iter_step(iter))) {
const struct index_record *record = msg_record(msg);
sync_msgid_insert(part_list, &record->guid);
if (record->uid >= touid) break;
}
mailbox_iter_done(&iter);
return 0;
}
static int calculate_intermediate_state(struct mailbox *mailbox,
modseq_t frommodseq,
uint32_t fromuid,
uint32_t batchsize,
uint32_t *touidp,
modseq_t *tomodseqp)
{
modseq_t tomodseq = mailbox->i.highestmodseq;
uint32_t touid = fromuid;
uint32_t seen = 0;
struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, ITER_SKIP_UNLINKED);
mailbox_iter_startuid(iter, fromuid+1); /* only read new records */
const message_t *msg;
while ((msg = mailbox_iter_step(iter))) {
const struct index_record *record = msg_record(msg);
if (seen < batchsize) {
touid = record->uid;
}
else if (record->modseq <= tomodseq)
tomodseq = record->modseq - 1;
seen++;
}
mailbox_iter_done(&iter);
/* no need to batch if there are fewer than batchsize records */
if (seen <= batchsize)
return 0;
/* must have found at least one record past the end to do a partial batch,
* and we need a highestmodseq at least one less than that records so that
* it can successfully sync */
if (tomodseq > frommodseq && tomodseq < mailbox->i.highestmodseq) {
*tomodseqp = tomodseq;
*touidp = touid;
return 1;
}
/* can't find an intermediate modseq */
return 0;
}
static int find_reserve_all(struct sync_name_list *mboxname_list,
const char *topart,
struct sync_folder_list *master_folders,
struct sync_folder_list *replica_folders,
struct sync_reserve_list *reserve_list,
uint32_t batchsize)
{
struct sync_name *mbox;
struct sync_folder *rfolder;
struct sync_msgid_list *part_list;
struct mailbox *mailbox = NULL;
int r = 0;
/* Find messages we want to upload that are available on server */
for (mbox = mboxname_list->head; mbox; mbox = mbox->next) {
mbentry_t *mbentry = NULL;
if (mboxlist_lookup_allow_all(mbox->name, &mbentry, NULL))
continue;
if (mbentry->mbtype & MBTYPE_INTERMEDIATE) {
struct synccrcs synccrcs = {0, 0};
sync_folder_list_add(master_folders, mbentry->uniqueid, mbentry->name,
mbentry->mbtype,
mbentry->partition, mbentry->acl, 0,
mbentry->uidvalidity, 0,
0, synccrcs,
0, 0,
0, 0,
NULL, 0,
0, mbentry->foldermodseq, 0);
mboxlist_entry_free(&mbentry);
continue;
}
mboxlist_entry_free(&mbentry);
r = mailbox_open_irl(mbox->name, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
/* Quietly skip over folders which have been deleted since we
started working (but record fact in case caller cares) */
if (r == IMAP_MAILBOX_NONEXISTENT) {
r = 0;
continue;
}
if (r) {
xsyslog(LOG_ERR, "IOERROR: mailbox open failed",
"mboxname=<%s> error=<%s>",
mbox->name, error_message(r));
goto bail;
}
modseq_t xconvmodseq = 0;
if (mailbox_has_conversations(mailbox)) {
r = mailbox_get_xconvmodseq(mailbox, &xconvmodseq);
if (r) {
xsyslog(LOG_ERR, "IOERROR: get xconvmodseq failed",
"mboxname=<%s> error=<%s>",
mbox->name, error_message(r));
goto bail;
}
}
modseq_t raclmodseq = mboxname_readraclmodseq(mbox->name);
/* mailbox is open from here, no exiting without closing it! */
rfolder = sync_folder_lookup(replica_folders, mailbox->uniqueid);
uint32_t fromuid = rfolder ? rfolder->last_uid : 0;
uint32_t touid = mailbox->i.last_uid;
modseq_t tomodseq = mailbox->i.highestmodseq;
int ispartial = 0;
if (batchsize && touid - fromuid > batchsize) {
/* see if we actually need to calculate an intermediate state */
modseq_t frommodseq = rfolder ? rfolder->highestmodseq : 0;
/* is there an intermediate modseq available and enough records to make a batch? */
ispartial = calculate_intermediate_state(mailbox, frommodseq, fromuid,
batchsize, &touid, &tomodseq);
if (ispartial) {
syslog(LOG_DEBUG, "doing partial sync: %s (%u/%u/%u) (%llu/%llu/%llu)",
mailbox->name, fromuid, touid, mailbox->i.last_uid,
frommodseq, tomodseq, mailbox->i.highestmodseq);
}
}
sync_folder_list_add(master_folders, mailbox->uniqueid, mailbox->name,
mailbox->mbtype,
mailbox->part, mailbox->acl, mailbox->i.options,
mailbox->i.uidvalidity, touid,
tomodseq, mailbox->i.synccrcs,
mailbox->i.recentuid, mailbox->i.recenttime,
mailbox->i.pop3_last_login,
mailbox->i.pop3_show_after, NULL, xconvmodseq,
raclmodseq, mailbox->foldermodseq, ispartial);
part_list = sync_reserve_partlist(reserve_list, topart ? topart : mailbox->part);
sync_find_reserve_messages(mailbox, fromuid, touid, part_list);
mailbox_close(&mailbox);
}
bail:
mailbox_close(&mailbox);
return r;
}
static int mark_missing (struct dlist *kin,
struct sync_msgid_list *part_list)
{
struct dlist *kl = kin->head;
struct dlist *ki;
struct message_guid tmp_guid;
struct sync_msgid *msgid;
/* no missing at all, good */
if (!kl) return 0;
if (strcmp(kl->name, "MISSING")) {
syslog(LOG_ERR, "SYNCERROR: Illegal response to RESERVE: %s",
kl->name);
return IMAP_PROTOCOL_BAD_PARAMETERS;
}
/* unmark each missing item */
for (ki = kl->head; ki; ki = ki->next) {
if (!message_guid_decode(&tmp_guid, ki->sval)) {
syslog(LOG_ERR, "SYNCERROR: reserve: failed to parse GUID %s",
ki->sval);
return IMAP_PROTOCOL_BAD_PARAMETERS;
}
/* afraid we will need this after all */
msgid = sync_msgid_lookup(part_list, &tmp_guid);
if (msgid && !msgid->need_upload) {
msgid->need_upload = 1;
part_list->toupload++;
}
}
return 0;
}
int sync_reserve_partition(struct sync_client_state *sync_cs, char *partition,
struct sync_folder_list *replica_folders,
struct sync_msgid_list *part_list)
{
const char *cmd = "RESERVE";
struct sync_msgid *msgid = part_list->head;
struct sync_folder *folder;
struct dlist *kl = NULL;
struct dlist *kin = NULL;
struct dlist *ki;
int r = 0;
if (!replica_folders->head)
return 0; /* nowhere to reserve */
while (msgid) {
int n = 0;
if (!part_list->toupload)
goto done; /* got them all */
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "PARTITION", partition);
ki = dlist_newlist(kl, "MBOXNAME");
for (folder = replica_folders->head; folder; folder = folder->next)
dlist_setatom(ki, "MBOXNAME", folder->name);
ki = dlist_newlist(kl, "GUID");
for (; msgid; msgid = msgid->next) {
if (!msgid->need_upload) continue;
if (n > 8192) break;
dlist_setatom(ki, "GUID", message_guid_encode(&msgid->guid));
/* we will re-add the "need upload" if we get a MISSING response */
msgid->need_upload = 0;
part_list->toupload--;
n++;
}
sync_send_apply(kl, sync_cs->backend->out);
r = sync_parse_response(cmd, sync_cs->backend->in, &kin);
if (r) goto done;
r = mark_missing(kin, part_list);
if (r) goto done;
dlist_free(&kl);
dlist_free(&kin);
}
done:
dlist_free(&kl);
dlist_free(&kin);
return r;
}
static int reserve_messages(struct sync_client_state *sync_cs,
struct sync_name_list *mboxname_list,
const char *topart,
struct sync_folder_list *master_folders,
struct sync_folder_list *replica_folders,
struct sync_reserve_list *reserve_list,
uint32_t batchsize)
{
struct sync_reserve *reserve;
int r;
r = find_reserve_all(mboxname_list, topart, master_folders,
replica_folders, reserve_list, batchsize);
if (r) return r;
for (reserve = reserve_list->head; reserve; reserve = reserve->next) {
r = sync_reserve_partition(sync_cs, reserve->part,
replica_folders, reserve->list);
if (r) return r;
}
return 0;
}
static struct db *sync_getcachedb(struct sync_client_state *sync_cs)
{
if (sync_cs->cachedb) return sync_cs->cachedb;
const char *dbtype = config_getstring(IMAPOPT_SYNC_CACHE_DB);
if (!dbtype) return NULL;
const char *dbpath = sync_get_config(sync_cs->channel, "sync_cache_db_path");
if (!dbpath) return NULL;
int flags = CYRUSDB_CREATE;
int r = cyrusdb_open(dbtype, dbpath, flags, &sync_cs->cachedb);
if (r) {
syslog(LOG_ERR, "DBERROR: failed to open sync cache db %s: %s",
dbpath, cyrusdb_strerror(r));
}
return sync_cs->cachedb;
}
static int sync_readcache(struct sync_client_state *sync_cs, const char *mboxname,
struct dlist **klp)
{
struct db *db = sync_getcachedb(sync_cs);
if (!db) return 0;
const char *base;
size_t len;
int r = cyrusdb_fetch(db, mboxname, strlen(mboxname), &base, &len, /*tid*/NULL);
if (r) return r;
dlist_parsemap(klp, 0, 0, base, len);
// we need the name so the parser can parse it
if (*klp) (*klp)->name = xstrdup("MAILBOX");
return 0;
}
// NOTE: this is destructive of kl - it removes the RECORD section!
// this is always safe because of where we call it
static int sync_cache(struct sync_client_state *sync_cs, const char *mboxname,
struct dlist *kl)
{
struct db *db = sync_getcachedb(sync_cs);
if (!db) return 0;
struct dlist *ritem = dlist_getchild(kl, "RECORD");
if (ritem) {
dlist_unstitch(kl, ritem);
dlist_free(&ritem);
}
struct buf buf = BUF_INITIALIZER;
dlist_printbuf(kl, 0, &buf);
int r = cyrusdb_store(db, mboxname, strlen(mboxname),
buf_base(&buf), buf_len(&buf), /*tid*/NULL);
buf_free(&buf);
return r;
}
static void sync_uncache(struct sync_client_state *sync_cs, const char *mboxname)
{
struct db *db = sync_getcachedb(sync_cs);
if (!db) return;
cyrusdb_delete(db, mboxname, strlen(mboxname), /*tid*/NULL, /*force*/1);
}
static int sync_kl_parse(struct dlist *kin,
struct sync_folder_list *folder_list,
struct sync_name_list *sub_list,
struct sync_sieve_list *sieve_list,
struct sync_seen_list *seen_list,
struct sync_quota_list *quota_list)
{
struct dlist *kl;
for (kl = kin->head; kl; kl = kl->next) {
if (!strcmp(kl->name, "SIEVE")) {
struct message_guid guid;
const char *filename = NULL;
const char *guidstr = NULL;
time_t modtime = 0;
uint32_t active = 0;
if (!sieve_list) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kl, "FILENAME", &filename)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kl, "LAST_UPDATE", &modtime)) return IMAP_PROTOCOL_BAD_PARAMETERS;
dlist_getatom(kl, "GUID", &guidstr); /* optional */
if (guidstr) {
if (!message_guid_decode(&guid, guidstr)) return IMAP_PROTOCOL_BAD_PARAMETERS;
}
else {
message_guid_set_null(&guid);
}
dlist_getnum32(kl, "ISACTIVE", &active); /* optional */
sync_sieve_list_add(sieve_list, filename, modtime, &guid, active);
}
else if (!strcmp(kl->name, "QUOTA")) {
const char *root = NULL;
struct sync_quota *sq;
if (!quota_list) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kl, "ROOT", &root)) return IMAP_PROTOCOL_BAD_PARAMETERS;
sq = sync_quota_list_add(quota_list, root);
sync_decode_quota_limits(kl, sq->limits);
}
else if (!strcmp(kl->name, "LSUB")) {
struct dlist *i;
if (!sub_list) return IMAP_PROTOCOL_BAD_PARAMETERS;
for (i = kl->head; i; i = i->next) {
sync_name_list_add(sub_list, i->sval);
}
}
else if (!strcmp(kl->name, "SEEN")) {
const char *uniqueid = NULL;
time_t lastread = 0;
uint32_t lastuid = 0;
time_t lastchange = 0;
const char *seenuids = NULL;
if (!seen_list) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kl, "UNIQUEID", &uniqueid)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kl, "LASTREAD", &lastread)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum32(kl, "LASTUID", &lastuid)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kl, "LASTCHANGE", &lastchange)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kl, "SEENUIDS", &seenuids)) return IMAP_PROTOCOL_BAD_PARAMETERS;
sync_seen_list_add(seen_list, uniqueid, lastread,
lastuid, lastchange, seenuids);
}
else if (!strcmp(kl->name, "MAILBOX")) {
const char *uniqueid = NULL;
const char *mboxname = NULL;
const char *mboxtype = NULL;
const char *part = NULL;
const char *acl = NULL;
const char *options = NULL;
modseq_t highestmodseq = 0;
uint32_t uidvalidity = 0;
uint32_t last_uid = 0;
struct synccrcs synccrcs = { 0, 0 };
uint32_t recentuid = 0;
time_t recenttime = 0;
time_t pop3_last_login = 0;
time_t pop3_show_after = 0;
struct dlist *al = NULL;
struct sync_annot_list *annots = NULL;
modseq_t xconvmodseq = 0;
modseq_t raclmodseq = 0;
modseq_t foldermodseq = 0;
if (!folder_list) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kl, "UNIQUEID", &uniqueid)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kl, "MBOXNAME", &mboxname)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kl, "PARTITION", &part)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kl, "ACL", &acl)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getatom(kl, "OPTIONS", &options)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum64(kl, "HIGHESTMODSEQ", &highestmodseq)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum32(kl, "UIDVALIDITY", &uidvalidity)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum32(kl, "LAST_UID", &last_uid)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getnum32(kl, "RECENTUID", &recentuid)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kl, "RECENTTIME", &recenttime)) return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getdate(kl, "POP3_LAST_LOGIN", &pop3_last_login)) return IMAP_PROTOCOL_BAD_PARAMETERS;
/* optional */
dlist_getdate(kl, "POP3_SHOW_AFTER", &pop3_show_after);
dlist_getatom(kl, "MBOXTYPE", &mboxtype);
dlist_getnum32(kl, "SYNC_CRC", &synccrcs.basic);
dlist_getnum32(kl, "SYNC_CRC_ANNOT", &synccrcs.annot);
dlist_getnum64(kl, "XCONVMODSEQ", &xconvmodseq);
dlist_getnum64(kl, "RACLMODSEQ", &raclmodseq);
dlist_getnum64(kl, "FOLDERMODSEQ", &foldermodseq);
if (dlist_getlist(kl, "ANNOTATIONS", &al))
decode_annotations(al, &annots, NULL, NULL);
sync_folder_list_add(folder_list, uniqueid, mboxname,
mboxlist_string_to_mbtype(mboxtype),
part, acl,
sync_parse_options(options),
uidvalidity, last_uid,
highestmodseq, synccrcs,
recentuid, recenttime,
pop3_last_login,
pop3_show_after, annots,
xconvmodseq, raclmodseq,
foldermodseq, /*ispartial*/0);
}
else {
return IMAP_PROTOCOL_BAD_PARAMETERS;
}
}
return 0;
}
int sync_response_parse(struct sync_client_state *sync_cs, const char *cmd,
struct sync_folder_list *folder_list,
struct sync_name_list *sub_list,
struct sync_sieve_list *sieve_list,
struct sync_seen_list *seen_list,
struct sync_quota_list *quota_list)
{
struct dlist *kin = NULL;
int r;
r = sync_parse_response(cmd, sync_cs->backend->in, &kin);
/* Unpleasant: translate remote access error into "please reset me" */
if (r == IMAP_MAILBOX_NONEXISTENT)
return 0;
if (r) return r;
r = sync_kl_parse(kin, folder_list, sub_list,
sieve_list, seen_list, quota_list);
if (r)
syslog(LOG_ERR, "SYNCERROR: %s: Invalid response %s", cmd, dlist_lastkey());
else {
// do we have mailboxes to cache?
struct dlist *kl = NULL;
for (kl = kin->head; kl; kl = kl->next) {
if (strcmp(kl->name, "MAILBOX")) continue;
const char *mboxname = NULL;
if (!dlist_getatom(kl, "MBOXNAME", &mboxname)) continue;
sync_cache(sync_cs, mboxname, kl);
}
}
dlist_free(&kin);
return r;
}
static int folder_rename(struct sync_client_state *sync_cs,
const char *oldname, const char *newname,
const char *partition, unsigned uidvalidity)
{
const char *cmd = (sync_cs->flags & SYNC_FLAG_LOCALONLY) ? "LOCAL_RENAME" : "RENAME";
struct dlist *kl;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s -> %s (%s)\n", cmd, oldname, newname, partition);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s -> %s (%s)", cmd, oldname, newname, partition);
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "OLDMBOXNAME", oldname);
dlist_setatom(kl, "NEWMBOXNAME", newname);
dlist_setatom(kl, "PARTITION", partition);
dlist_setnum32(kl, "UIDVALIDITY", uidvalidity);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
int r = sync_parse_response(cmd, sync_cs->backend->in, NULL);
// this means that newname won't be cached, but we'll cache it next sync
sync_uncache(sync_cs, oldname);
return r;
}
int sync_do_folder_delete(struct sync_client_state *sync_cs, const char *mboxname)
{
const char *cmd =
(sync_cs->flags & SYNC_FLAG_LOCALONLY) ? "LOCAL_UNMAILBOX" :"UNMAILBOX";
struct dlist *kl;
int r;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s\n", cmd, mboxname);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s", cmd, mboxname);
kl = dlist_setatom(NULL, cmd, mboxname);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
r = sync_parse_response(cmd, sync_cs->backend->in, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT)
r = 0;
sync_uncache(sync_cs, mboxname);
return r;
}
int sync_set_sub(struct sync_client_state *sync_cs,
const char *userid, const char *mboxname, int add)
{
const char *cmd = add ? "SUB" : "UNSUB";
struct dlist *kl;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s %s\n", cmd, userid, mboxname);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s %s", cmd, userid, mboxname);
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "USERID", userid);
dlist_setatom(kl, "MBOXNAME", mboxname);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
return sync_parse_response(cmd, sync_cs->backend->in, NULL);
}
static int folder_setannotation(struct sync_client_state *sync_cs,
const char *mboxname, const char *entry,
const char *userid, const struct buf *value)
{
const char *cmd = "ANNOTATION";
struct dlist *kl;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s %s %s\n", cmd, mboxname, entry, userid);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s %s %s", cmd, mboxname, entry, userid);
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "MBOXNAME", mboxname);
dlist_setatom(kl, "ENTRY", entry);
dlist_setatom(kl, "USERID", userid);
dlist_setmap(kl, "VALUE", value->s, value->len);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
return sync_parse_response(cmd, sync_cs->backend->in, NULL);
}
static int folder_unannotation(struct sync_client_state *sync_cs,
const char *mboxname, const char *entry,
const char *userid)
{
const char *cmd = "UNANNOTATION";
struct dlist *kl;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s %s %s\n", cmd, mboxname, entry, userid);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s %s %s", cmd, mboxname, entry, userid);
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "MBOXNAME", mboxname);
dlist_setatom(kl, "ENTRY", entry);
dlist_setatom(kl, "USERID", userid);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
return sync_parse_response(cmd, sync_cs->backend->in, NULL);
}
/* ====================================================================== */
static int sieve_upload(struct sync_client_state *sync_cs,
const char *userid, const char *filename,
unsigned long last_update)
{
const char *cmd = "SIEVE";
struct dlist *kl;
char *sieve;
uint32_t size;
sieve = sync_sieve_read(userid, filename, &size);
if (!sieve) return IMAP_IOERROR;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s %s\n", cmd, userid, filename);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s %s", cmd, userid, filename);
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "USERID", userid);
dlist_setatom(kl, "FILENAME", filename);
dlist_setdate(kl, "LAST_UPDATE", last_update);
dlist_setmap(kl, "CONTENT", sieve, size);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
free(sieve);
return sync_parse_response(cmd, sync_cs->backend->in, NULL);
}
static int sieve_delete(struct sync_client_state *sync_cs,
const char *userid, const char *filename)
{
const char *cmd = "UNSIEVE";
struct dlist *kl;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s %s\n", cmd, userid, filename);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s %s", cmd, userid, filename);
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "USERID", userid);
dlist_setatom(kl, "FILENAME", filename);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
return sync_parse_response(cmd, sync_cs->backend->in, NULL);
}
static int sieve_activate(struct sync_client_state *sync_cs,
const char *userid, const char *filename)
{
const char *cmd = "ACTIVATE_SIEVE";
struct dlist *kl;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s %s\n", cmd, userid, filename);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s %s", cmd, userid, filename);
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "USERID", userid);
dlist_setatom(kl, "FILENAME", filename);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
return sync_parse_response(cmd, sync_cs->backend->in, NULL);
}
static int sieve_deactivate(struct sync_client_state *sync_cs,
const char *userid)
{
const char *cmd = "UNACTIVATE_SIEVE";
struct dlist *kl;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s\n", cmd, userid);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s", cmd, userid);
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "USERID", userid);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
return sync_parse_response(cmd, sync_cs->backend->in, NULL);
}
/* ====================================================================== */
static int delete_quota(struct sync_client_state *sync_cs, const char *root)
{
const char *cmd = "UNQUOTA";
struct dlist *kl;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s\n", cmd, root);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s", cmd, root);
kl = dlist_setatom(NULL, cmd, root);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
return sync_parse_response(cmd, sync_cs->backend->in, NULL);
}
static int update_quota_work(struct sync_client_state *sync_cs,
struct quota *client,
struct sync_quota *server)
{
const char *cmd = "QUOTA";
struct dlist *kl;
int r;
r = quota_read(client, NULL, 0);
/* disappeared? Delete it*/
if (r == IMAP_QUOTAROOT_NONEXISTENT)
return delete_quota(sync_cs, client->root);
if (r) {
syslog(LOG_INFO, "Warning: failed to read quotaroot %s: %s",
client->root, error_message(r));
return r;
}
if (server) {
int changed = 0;
int res;
for (res = 0 ; res < QUOTA_NUMRESOURCES ; res++) {
if (client->limits[res] != server->limits[res])
changed++;
}
if (!changed)
return 0;
}
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("%s %s\n", cmd, client->root);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s", cmd, client->root);
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "ROOT", client->root);
sync_encode_quota_limits(kl, client->limits);
dlist_setnum64(kl, "MODSEQ", client->modseq);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
return sync_parse_response(cmd, sync_cs->backend->in, NULL);
}
static int copy_local(struct mailbox *mailbox, unsigned uid)
{
char *oldfname, *newfname;
struct index_record newrecord;
struct index_record oldrecord;
int r;
annotate_state_t *astate = NULL;
if (mailbox_find_index_record(mailbox, uid, &oldrecord)) {
/* not finding the record is an error! (should never happen) */
xsyslog(LOG_ERR, "IOERROR: couldn't find index record",
"uid=<%u>",
uid);
return IMAP_MAILBOX_NONEXISTENT;
}
/* create the new record as a clone of the old record */
newrecord = oldrecord;
newrecord.uid = mailbox->i.last_uid + 1;
/* copy the file in to place */
oldfname = xstrdup(mailbox_record_fname(mailbox, &oldrecord));
newfname = xstrdup(mailbox_record_fname(mailbox, &newrecord));
r = mailbox_copyfile(oldfname, newfname, 0);
free(oldfname);
free(newfname);
if (r) return r;
/* append the new record */
r = mailbox_append_index_record(mailbox, &newrecord);
if (r) return r;
/* ensure we have an astate connected to the destination
* mailbox, so that the annotation txn will be committed
* when we close the mailbox */
r = mailbox_get_annotate_state(mailbox, newrecord.uid, &astate);
if (r) return r;
/* Copy across any per-message annotations */
r = annotate_msg_copy(mailbox, oldrecord.uid,
mailbox, newrecord.uid,
NULL);
if (r) return r;
/* and expunge the old record */
oldrecord.internal_flags |= FLAG_INTERNAL_EXPUNGED;
r = mailbox_rewrite_index_record(mailbox, &oldrecord);
/* done - return */
return r;
}
static int fetch_file(struct sync_client_state *sync_cs,
struct mailbox *mailbox, unsigned uid,
const struct index_record *rp,
struct sync_msgid_list *part_list)
{
const char *cmd = "FETCH";
struct dlist *kin = NULL;
struct dlist *kl;
int r = 0;
struct sync_msgid *msgid;
struct message_guid *guid = NULL;
size_t size = 0;
const char *fname = NULL;
msgid = sync_msgid_lookup(part_list, &rp->guid);
/* already reserved? great */
if (msgid && msgid->fname) {
syslog(LOG_NOTICE, "trying to get already uploaded %u: %s", uid, message_guid_encode(&rp->guid));
return 0;
}
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "MBOXNAME", mailbox->name);
dlist_setatom(kl, "PARTITION", mailbox->part);
dlist_setatom(kl, "UNIQUEID", mailbox->uniqueid);
dlist_setguid(kl, "GUID", &rp->guid);
dlist_setnum32(kl, "UID", uid);
sync_send_lookup(kl, sync_cs->backend->out);
dlist_free(&kl);
r = sync_parse_response(cmd, sync_cs->backend->in, &kin);
if (r) {
xsyslog(LOG_ERR, "IOERROR: parse response failed",
"error=<%s>",
error_message(r));
return r;
}
if (!dlist_tofile(kin->head, NULL, &guid, (unsigned long *) &size, &fname)) {
r = IMAP_MAILBOX_NONEXISTENT;
xsyslog(LOG_ERR, "IOERROR: dlist_tofile failed",
"error=<%s>",
error_message(r));
goto done;
}
/* well, we can copy it back or we can re-reserve... */
if (message_guid_equal(guid, &rp->guid) && (size == rp->size)) {
msgid = sync_msgid_insert(part_list, &rp->guid);
msgid->need_upload = 1;
msgid->size = size;
if (!msgid->fname) msgid->fname = xstrdup(fname);
}
else {
r = IMAP_MAILBOX_NONEXISTENT;
xsyslog(LOG_ERR, "IOERROR: GUID MISMATCH",
"error=<%s>",
error_message(r));
r = IMAP_IOERROR;
}
done:
dlist_free(&kin);
return r;
}
static int copy_remote(struct mailbox *mailbox, uint32_t uid,
struct dlist *kr, struct sync_msgid_list *part_list)
{
struct index_record record;
struct dlist *ki;
int r;
struct sync_annot_list *annots = NULL;
for (ki = kr->head; ki; ki = ki->next) {
r = parse_upload(ki, mailbox, &record, &annots);
if (r) {
xsyslog(LOG_ERR, "IOERROR: parse_upload failed",
"uid=<%u>",
uid);
return r;
}
if (record.uid == uid) {
/* choose the destination UID */
record.uid = mailbox->i.last_uid + 1;
/* append the file */
r = sync_append_copyfile(mailbox, &record, annots, part_list);
sync_annot_list_free(&annots);
return r;
}
sync_annot_list_free(&annots);
}
/* not finding the record is an error! (should never happen) */
xsyslog(LOG_ERR, "IOERROR: couldn't find index record",
"uid=<%u>",
uid);
return IMAP_MAILBOX_NONEXISTENT;
}
static int copyback_one_record(struct sync_client_state *sync_cs,
struct mailbox *mailbox,
struct index_record *rp,
const struct sync_annot_list *annots,
struct dlist *kaction,
struct sync_msgid_list *part_list)
{
int r;
/* don't want to copy back expunged records! */
if (rp->internal_flags & FLAG_INTERNAL_EXPUNGED)
return 0;
/* if the UID is lower than master's last_uid,
* we'll need to renumber */
if (rp->uid <= mailbox->i.last_uid) {
/* Ok, now we need to check if it's just really stale
* (has been cleaned out locally) or an error.
* In the error case we copy back, stale
* we remove from the replica */
if (rp->modseq < mailbox->i.deletedmodseq) {
if (kaction)
dlist_setnum32(kaction, "EXPUNGE", rp->uid);
}
else {
r = fetch_file(sync_cs, mailbox, rp->uid, rp, part_list);
if (r) return r;
if (kaction)
dlist_setnum32(kaction, "COPYBACK", rp->uid);
}
}
/* otherwise we can pull it in with the same UID,
* which saves causing renumbering on the replica
* end, so is preferable */
else {
/* grab the file */
r = fetch_file(sync_cs, mailbox, rp->uid, rp, part_list);
if (r) return r;
/* make sure we're actually making changes now */
if (!kaction) return 0;
/* append the file */
r = sync_append_copyfile(mailbox, rp, annots, part_list);
if (r) return r;
}
return 0;
}
static int renumber_one_record(const struct index_record *mp,
struct dlist *kaction)
{
/* don't want to renumber expunged records */
if (mp->internal_flags & FLAG_INTERNAL_EXPUNGED)
return 0;
if (kaction)
dlist_setnum32(kaction, "RENUMBER", mp->uid);
return 0;
}
static const char *make_flags(struct mailbox *mailbox, struct index_record *record)
{
static char buf[4096];
const char *sep = "";
int flag;
if (record->system_flags & FLAG_DELETED) {
snprintf(buf, 4096, "%s\\Deleted", sep);
sep = " ";
}
if (record->system_flags & FLAG_ANSWERED) {
snprintf(buf, 4096, "%s\\Answered", sep);
sep = " ";
}
if (record->system_flags & FLAG_FLAGGED) {
snprintf(buf, 4096, "%s\\Flagged", sep);
sep = " ";
}
if (record->system_flags & FLAG_DRAFT) {
snprintf(buf, 4096, "%s\\Draft", sep);
sep = " ";
}
if (record->internal_flags & FLAG_INTERNAL_EXPUNGED) {
snprintf(buf, 4096, "%s\\Expunged", sep);
sep = " ";
}
if (record->system_flags & FLAG_SEEN) {
snprintf(buf, 4096, "%s\\Seen", sep);
sep = " ";
}
/* print user flags in mailbox order */
for (flag = 0; flag < MAX_USER_FLAGS; flag++) {
if (!mailbox->flagname[flag])
continue;
if (!(record->user_flags[flag/32] & (1<<(flag&31))))
continue;
snprintf(buf, 4096, "%s%s", sep, mailbox->flagname[flag]);
sep = " ";
}
return buf;
}
static void log_record(const char *name, struct mailbox *mailbox,
struct index_record *record)
{
syslog(LOG_NOTICE, "SYNCNOTICE: %s uid:%u modseq:" MODSEQ_FMT " "
"last_updated:" TIME_T_FMT " internaldate:" TIME_T_FMT " flags:(%s) cid:" CONV_FMT,
name, record->uid, record->modseq,
record->last_updated, record->internaldate,
make_flags(mailbox, record), record->cid);
}
static void log_mismatch(const char *reason, struct mailbox *mailbox,
struct index_record *mp,
struct index_record *rp)
{
syslog(LOG_NOTICE, "SYNCNOTICE: record mismatch with replica: %s %s",
mailbox->name, reason);
log_record("master", mailbox, mp);
log_record("replica", mailbox, rp);
}
static int compare_one_record(struct sync_client_state *sync_cs,
struct mailbox *mailbox,
struct index_record *mp,
struct index_record *rp,
const struct sync_annot_list *mannots,
const struct sync_annot_list *rannots,
struct dlist *kaction,
struct sync_msgid_list *part_list)
{
int i;
int r;
/* if both ends are expunged, then we do no more processing. This
* allows a split brain cleanup to not break things forever. It
* does mean that an expunged message might not replicate in that
* case, but the only way to fix this is add ANOTHER special flag
* for BROKEN and only ignore GUID mismatches in that case, after
* moving the message up. I guess we could force UNLINK immediately
* too... hmm. Not today. */
if ((mp->internal_flags & FLAG_INTERNAL_EXPUNGED) &&
(rp->internal_flags & FLAG_INTERNAL_EXPUNGED))
return 0;
/* first of all, check that GUID matches. If not, we have had a split
* brain, so the messages both need to be fixed up to their new UIDs.
* After this function succeeds, both the local and remote copies of this
* current UID will be actually EXPUNGED, so the earlier return applies. */
if (!message_guid_equal(&mp->guid, &rp->guid)) {
char *mguid = xstrdup(message_guid_encode(&mp->guid));
char *rguid = xstrdup(message_guid_encode(&rp->guid));
syslog(LOG_ERR, "SYNCERROR: guid mismatch %s %u (%s %s)",
mailbox->name, mp->uid, rguid, mguid);
free(rguid);
free(mguid);
/* we will need to renumber both ends to get in sync */
/* ORDERING - always lower GUID first */
if (message_guid_cmp(&mp->guid, &rp->guid) > 0) {
r = copyback_one_record(sync_cs, mailbox, rp, rannots, kaction, part_list);
if (!r) r = renumber_one_record(mp, kaction);
}
else {
r = renumber_one_record(mp, kaction);
if (!r) r = copyback_one_record(sync_cs, mailbox, rp, rannots, kaction, part_list);
}
return r;
}
/* are there any differences? */
if (mp->modseq != rp->modseq)
goto diff;
if (mp->last_updated != rp->last_updated)
goto diff;
if (mp->internaldate != rp->internaldate)
goto diff;
if (mp->system_flags != rp->system_flags)
goto diff;
if ((mp->internal_flags & FLAG_INTERNAL_EXPUNGED) !=
(rp->internal_flags & FLAG_INTERNAL_EXPUNGED))
goto diff;
if (mp->cid != rp->cid)
goto diff;
if (mp->basecid != rp->basecid)
goto diff;
if (mp->savedate != rp->savedate)
goto diff;
if (mp->createdmodseq != rp->createdmodseq)
goto diff;
if (diff_annotations(mannots, rannots))
goto diff;
for (i = 0; i < MAX_USER_FLAGS/32; i++) {
if (mp->user_flags[i] != rp->user_flags[i])
goto diff;
}
/* no changes found, whoopee */
return 0;
diff:
/* if differences we'll have to rewrite to bump the modseq
* so that regular replication will cause an update */
/* interesting case - expunged locally */
if (mp->internal_flags & FLAG_INTERNAL_EXPUNGED) {
/* if expunged, fall through - the rewrite will lift
* the modseq to force the change to stick */
}
else if (rp->internal_flags & FLAG_INTERNAL_EXPUNGED) {
/* mark expunged - rewrite will cause both sides to agree
* again */
mp->internal_flags |= FLAG_INTERNAL_EXPUNGED;
}
/* otherwise, is the replica "newer"? Better grab those flags */
else {
if (rp->modseq > mp->modseq &&
rp->last_updated >= mp->last_updated) {
log_mismatch("more recent on replica", mailbox, mp, rp);
/* then copy all the flag data over from the replica */
mp->system_flags = rp->system_flags;
mp->internal_flags &= ~FLAG_INTERNAL_EXPUNGED;
mp->internal_flags |= rp->internal_flags & FLAG_INTERNAL_EXPUNGED;
mp->cid = rp->cid;
for (i = 0; i < MAX_USER_FLAGS/32; i++)
mp->user_flags[i] = rp->user_flags[i];
}
}
/* are we making changes yet? */
if (!kaction) return 0;
int hadsnoozed = 0;
/* even expunged messages get annotations synced */
r = apply_annotations(mailbox, mp, mannots, rannots, 0, &hadsnoozed);
if (r) return r;
if (hadsnoozed) mp->internal_flags |= FLAG_INTERNAL_SNOOZED;
else mp->internal_flags &= ~FLAG_INTERNAL_SNOOZED;
/* this will bump the modseq and force a resync either way :) */
return mailbox_rewrite_index_record(mailbox, mp);
}
static int mailbox_update_loop(struct sync_client_state *sync_cs,
struct mailbox *mailbox,
struct dlist *ki,
uint32_t last_uid,
modseq_t highestmodseq,
struct dlist *kaction,
struct sync_msgid_list *part_list)
{
struct index_record rrecord;
struct sync_annot_list *mannots = NULL;
struct sync_annot_list *rannots = NULL;
int r;
struct mailbox_iter *iter = mailbox_iter_init(mailbox, 0, 0);
const message_t *msg = mailbox_iter_step(iter);
const struct index_record *mrecord = msg ? msg_record(msg) : NULL;
/* while there are more records on either master OR replica,
* work out what to do with them */
while (ki || msg) {
sync_annot_list_free(&mannots);
sync_annot_list_free(&rannots);
/* most common case - both a master AND a replica record exist */
if (ki && mrecord) {
r = read_annotations(mailbox, mrecord, &mannots, 0, 0);
if (r) goto out;
r = parse_upload(ki, mailbox, &rrecord, &rannots);
if (r) goto out;
/* same UID - compare the records */
if (rrecord.uid == mrecord->uid) {
mailbox_read_basecid(mailbox, mrecord);
r = compare_one_record(sync_cs, mailbox,
(struct index_record *)mrecord, &rrecord,
mannots, rannots,
kaction, part_list);
if (r) goto out;
/* increment both */
msg = mailbox_iter_step(iter);
mrecord = msg ? msg_record(msg) : NULL;
ki = ki->next;
}
else if (rrecord.uid > mrecord->uid) {
/* record only exists on the master */
if (!(mrecord->internal_flags & FLAG_INTERNAL_EXPUNGED)) {
syslog(LOG_ERR, "SYNCNOTICE: only exists on master %s %u (%s)",
mailbox->name, mrecord->uid,
message_guid_encode(&mrecord->guid));
r = renumber_one_record(mrecord, kaction);
if (r) goto out;
}
/* only increment master */
msg = mailbox_iter_step(iter);
mrecord = msg ? msg_record(msg) : NULL;
}
else {
/* record only exists on the replica */
if (!(rrecord.internal_flags & FLAG_INTERNAL_EXPUNGED)) {
if (kaction)
syslog(LOG_ERR, "SYNCNOTICE: only exists on replica %s %u (%s)",
mailbox->name, rrecord.uid,
message_guid_encode(&rrecord.guid));
r = copyback_one_record(sync_cs, mailbox, &rrecord, rannots, kaction, part_list);
if (r) goto out;
}
/* only increment replica */
ki = ki->next;
}
}
/* no more replica records, but still master records */
else if (mrecord) {
/* if the replica has seen this UID, we need to renumber.
* Otherwise it will replicate fine as-is */
if (mrecord->uid <= last_uid) {
r = renumber_one_record(mrecord, kaction);
if (r) goto out;
}
else if (mrecord->modseq <= highestmodseq) {
if (kaction) {
/* bump our modseq so we sync */
syslog(LOG_NOTICE, "SYNCNOTICE: bumping modseq %s %u",
mailbox->name, mrecord->uid);
r = mailbox_rewrite_index_record(mailbox, (struct index_record *)mrecord);
if (r) goto out;
}
}
msg = mailbox_iter_step(iter);
mrecord = msg ? msg_record(msg) : NULL;
}
/* record only exists on the replica */
else {
r = parse_upload(ki, mailbox, &rrecord, &rannots);
if (r) goto out;
if (kaction)
syslog(LOG_NOTICE, "SYNCNOTICE: only on replica %s %u",
mailbox->name, rrecord.uid);
/* going to need this one */
r = copyback_one_record(sync_cs, mailbox, &rrecord, rannots, kaction, part_list);
if (r) goto out;
ki = ki->next;
}
}
r = 0;
out:
mailbox_iter_done(&iter);
sync_annot_list_free(&mannots);
sync_annot_list_free(&rannots);
return r;
}
static int mailbox_full_update(struct sync_client_state *sync_cs,
struct sync_folder *local,
struct sync_reserve_list *reserve_list,
unsigned flags)
{
const char *cmd = "FULLMAILBOX";
struct mailbox *mailbox = NULL;
int r;
struct dlist *kin = NULL;
struct dlist *kr = NULL;
struct dlist *ka = NULL;
struct dlist *kuids = NULL;
struct dlist *kl = NULL;
struct dlist *kaction = NULL;
struct dlist *kexpunge = NULL;
modseq_t highestmodseq;
modseq_t foldermodseq = 0;
uint32_t uidvalidity;
uint32_t last_uid;
struct sync_annot_list *mannots = NULL;
struct sync_annot_list *rannots = NULL;
int remote_modseq_was_higher = 0;
modseq_t xconvmodseq = 0;
struct sync_msgid_list *part_list;
annotate_state_t *astate = NULL;
if (flags & SYNC_FLAG_VERBOSE)
printf("%s %s\n", cmd, local->name);
if (flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s", cmd, local->name);
kl = dlist_setatom(NULL, cmd, local->name);
sync_send_lookup(kl, sync_cs->backend->out);
dlist_free(&kl);
r = sync_parse_response(cmd, sync_cs->backend->in, &kin);
if (r) return r;
// we know the remote state, so cache it
r = sync_cache(sync_cs, local->name, kin);
if (r) return r;
kl = kin->head;
if (!kl) {
r = IMAP_MAILBOX_NONEXISTENT;
goto done;
}
/* XXX - handle the header. I want to do some ordering on timestamps
* in particular here - if there's more recent data on the replica then
* it should be copied back. This depends on having a nice way to
* parse the mailbox structure back in to a struct index_header rather
* than the by hand stuff though, because that sucks. NOTE - this
* doesn't really matter too much, because we'll blat the replica's
* values anyway! */
if (!dlist_getnum64(kl, "HIGHESTMODSEQ", &highestmodseq)) {
r = IMAP_PROTOCOL_BAD_PARAMETERS;
goto done;
}
if (!dlist_getnum32(kl, "UIDVALIDITY", &uidvalidity)) {
r = IMAP_PROTOCOL_BAD_PARAMETERS;
goto done;
}
if (!dlist_getnum32(kl, "LAST_UID", &last_uid)) {
r = IMAP_PROTOCOL_BAD_PARAMETERS;
goto done;
}
if (!dlist_getlist(kl, "RECORD", &kr)) {
r = IMAP_PROTOCOL_BAD_PARAMETERS;
goto done;
}
/* optional */
dlist_getnum64(kl, "XCONVMODSEQ", &xconvmodseq);
dlist_getnum64(kl, "FOLDERMODSEQ", &foldermodseq);
/* we'll be updating it! */
if (local->mailbox) {
mailbox = local->mailbox;
}
else {
r = mailbox_open_iwl(local->name, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
}
if (r) goto done;
part_list = sync_reserve_partlist(reserve_list, mailbox->part);
/* if local UIDVALIDITY is lower, copy from remote, otherwise
* remote will copy ours when we sync */
if (mailbox->i.uidvalidity < uidvalidity) {
syslog(LOG_NOTICE, "SYNCNOTICE: uidvalidity higher on replica %s"
", updating %u => %u",
mailbox->name, mailbox->i.uidvalidity, uidvalidity);
mailbox_index_dirty(mailbox);
mailbox->i.uidvalidity = mboxname_setuidvalidity(mailbox->name, uidvalidity);
}
if (mailbox->i.highestmodseq < highestmodseq) {
/* highestmodseq on replica is dirty - we must copy and then dirty
* so we go one higher! */
syslog(LOG_NOTICE, "SYNCNOTICE: highestmodseq higher on replica %s"
", updating " MODSEQ_FMT " => " MODSEQ_FMT,
mailbox->name, mailbox->i.highestmodseq, highestmodseq+1);
mailbox->modseq_dirty = 0;
mailbox->i.highestmodseq = highestmodseq;
mailbox_modseq_dirty(mailbox);
remote_modseq_was_higher = 1;
}
/* hold the annotate state open */
r = mailbox_get_annotate_state(mailbox, ANNOTATE_ANY_UID, &astate);
if (r) goto done;
annotate_state_begin(astate);
r = mailbox_update_loop(sync_cs, mailbox, kr->head, last_uid,
highestmodseq, NULL, part_list);
if (r) {
syslog(LOG_ERR, "SYNCNOTICE: failed to prepare update for %s: %s",
mailbox->name, error_message(r));
goto done;
}
/* OK - now we're committed to make changes! */
/* this is safe because "larger than" logic is embedded
* inside update_xconvmodseq */
if (mailbox_has_conversations(mailbox)) {
r = mailbox_update_xconvmodseq(mailbox, xconvmodseq, /* force */0);
if (r) goto done;
}
if (foldermodseq) {
// by writing the same ACL with the updated foldermodseq, this will bounce it
// if needed
r = mboxlist_sync_setacls(mailbox->name, mailbox->acl, foldermodseq);
if (r) goto done;
}
kaction = dlist_newlist(NULL, "ACTION");
r = mailbox_update_loop(sync_cs, mailbox, kr->head, last_uid,
highestmodseq, kaction, part_list);
if (r) goto cleanup;
/* if replica still has a higher last_uid, bump our local
* number to match so future records don't clash */
if (mailbox->i.last_uid < last_uid) {
mailbox_index_dirty(mailbox);
mailbox->i.last_uid = last_uid;
}
/* ugly variable reuse */
dlist_getlist(kl, "ANNOTATIONS", &ka);
if (ka) decode_annotations(ka, &rannots, mailbox, NULL);
r = read_annotations(mailbox, NULL, &mannots, 0, 0);
if (r) goto cleanup;
r = apply_annotations(mailbox, NULL, mannots, rannots,
!remote_modseq_was_higher, NULL);
if (r) goto cleanup;
/* blatant reuse 'r' us */
kexpunge = dlist_newkvlist(NULL, "EXPUNGE");
dlist_setatom(kexpunge, "MBOXNAME", mailbox->name);
dlist_setatom(kexpunge, "UNIQUEID", mailbox->uniqueid); /* just for safety */
kuids = dlist_newlist(kexpunge, "UID");
for (ka = kaction->head; ka; ka = ka->next) {
if (!strcmp(ka->name, "EXPUNGE")) {
dlist_setnum32(kuids, "UID", dlist_num(ka));
}
else if (!strcmp(ka->name, "COPYBACK")) {
r = copy_remote(mailbox, dlist_num(ka), kr, part_list);
if (r) goto cleanup;
dlist_setnum32(kuids, "UID", dlist_num(ka));
}
else if (!strcmp(ka->name, "RENUMBER")) {
r = copy_local(mailbox, dlist_num(ka));
if (r) goto cleanup;
}
}
/* we still need to do the EXPUNGEs */
cleanup:
sync_annot_list_free(&mannots);
sync_annot_list_free(&rannots);
/* close the mailbox before sending any expunges
* to avoid deadlocks */
if (!local->mailbox) mailbox_close(&mailbox);
/* only send expunge if we have some UIDs to expunge */
if (kuids && kuids->head) {
int r2;
sync_send_apply(kexpunge, sync_cs->backend->out);
r2 = sync_parse_response("EXPUNGE", sync_cs->backend->in, NULL);
if (r2) {
syslog(LOG_ERR, "SYNCERROR: failed to expunge in cleanup %s",
local->name);
}
}
done:
if (r && mailbox)
annotate_state_abort(&mailbox->annot_state);
if (mailbox && !local->mailbox) mailbox_close(&mailbox);
dlist_free(&kin);
dlist_free(&kaction);
dlist_free(&kexpunge);
/* kuids points into the tree rooted at kexpunge
* so we don't need to free it explicitly here */
return r;
}
static int is_unchanged(struct mailbox *mailbox, struct sync_folder *remote)
{
/* look for any mismatches */
unsigned options = mailbox->i.options & MAILBOX_OPTIONS_MASK;
modseq_t xconvmodseq = 0;
if (!remote) return 0;
if (remote->mbtype != mailbox->mbtype) return 0;
if (remote->last_uid != mailbox->i.last_uid) return 0;
if (remote->highestmodseq != mailbox->i.highestmodseq) return 0;
if (remote->uidvalidity != mailbox->i.uidvalidity) return 0;
if (remote->recentuid != mailbox->i.recentuid) return 0;
if (remote->recenttime != mailbox->i.recenttime) return 0;
if (remote->pop3_last_login != mailbox->i.pop3_last_login) return 0;
if (remote->pop3_show_after != mailbox->i.pop3_show_after) return 0;
if (remote->options != options) return 0;
if (remote->foldermodseq && remote->foldermodseq != mailbox->foldermodseq) return 0;
if (strcmp(remote->acl, mailbox->acl)) return 0;
if (config_getswitch(IMAPOPT_REVERSEACLS)) {
modseq_t raclmodseq = mboxname_readraclmodseq(mailbox->name);
// don't bail if either are zero, that could be version skew
if (raclmodseq && remote->raclmodseq && remote->raclmodseq != raclmodseq) return 0;
}
if (mailbox_has_conversations(mailbox)) {
int r = mailbox_get_xconvmodseq(mailbox, &xconvmodseq);
if (r) return 0;
if (remote->xconvmodseq != xconvmodseq) return 0;
}
/* compare annotations */
{
struct sync_annot_list *mannots = NULL;
int r = read_annotations(mailbox, NULL, &mannots, 0, 0);
if (r) return 0;
if (diff_annotations(mannots, remote->annots)) {
sync_annot_list_free(&mannots);
return 0;
}
sync_annot_list_free(&mannots);
}
/* if we got here then we should force check the CRCs */
if (!mailbox_crceq(remote->synccrcs, mailbox_synccrcs(mailbox, /*force*/0)))
if (!mailbox_crceq(remote->synccrcs, mailbox_synccrcs(mailbox, /*force*/1)))
return 0;
/* otherwise it's unchanged! */
return 1;
}
/* XXX kind of nasty having this here, but i think it probably
* shouldn't be in .h with the rest of them */
#define SYNC_FLAG_ISREPEAT (1<<15)
#define SYNC_FLAG_FULLANNOTS (1<<16)
static int update_mailbox_once(struct sync_client_state *sync_cs,
struct sync_folder *local,
struct sync_folder *remote,
const char *topart,
struct sync_reserve_list *reserve_list,
unsigned flags)
{
struct sync_msgid_list *part_list;
struct mailbox *mailbox = NULL;
int r = 0;
const char *cmd =
(flags & SYNC_FLAG_LOCALONLY) ? "LOCAL_MAILBOX" : "MAILBOX";
struct dlist *kl = dlist_newkvlist(NULL, cmd);
struct dlist *kupload = dlist_newlist(NULL, "MESSAGE");
annotate_state_t *astate = NULL;
struct sync_folder_list *myremotes = NULL;
if (flags & SYNC_FLAG_ISREPEAT) {
// we have to fetch the sync_folder again!
myremotes = sync_folder_list_create();
struct dlist *mbkl = dlist_newlist(NULL, "MAILBOXES");
dlist_setatom(mbkl, "MBOXNAME", local->name);
if (flags & SYNC_FLAG_VERBOSE)
printf("MAILBOXES %s\n", local->name);
sync_send_lookup(mbkl, sync_cs->backend->out);
dlist_free(&mbkl);
r = sync_response_parse(sync_cs, "MAILBOXES", myremotes,
NULL, NULL, NULL, NULL);
if (r) goto done;
remote = myremotes->head;
}
if (local->mailbox) {
mailbox = local->mailbox;
}
else {
r = mailbox_open_iwl(local->name, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
}
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* been deleted in the meanwhile... it will get picked up by the
* delete call later */
r = 0;
goto done;
}
else if (r)
goto done;
if (!topart) topart = mailbox->part;
/* hold the annotate state open */
r = mailbox_get_annotate_state(mailbox, ANNOTATE_ANY_UID, &astate);
if (r) goto done;
/* and force it to hold a transaction while it does stuff */
annotate_state_begin(astate);
/* definitely bad if these don't match! */
if (strcmp(mailbox->uniqueid, local->uniqueid) ||
strcmp(mailbox->part, local->part)) {
r = IMAP_MAILBOX_MOVED;
goto done;
}
/* check that replication stands a chance of succeeding */
if (remote && !(flags & SYNC_FLAG_ISREPEAT)) {
if (mailbox->i.deletedmodseq > remote->highestmodseq) {
syslog(LOG_NOTICE, "inefficient replication ("
MODSEQ_FMT " > " MODSEQ_FMT ") %s",
mailbox->i.deletedmodseq, remote->highestmodseq,
local->name);
r = IMAP_AGAIN;
goto done;
}
}
/* if local UIDVALIDITY is lower, copy from remote, otherwise
* remote will copy ours when we sync */
if (remote && mailbox->i.uidvalidity < remote->uidvalidity) {
syslog(LOG_NOTICE, "SYNCNOTICE: uidvalidity higher on replica %s"
", updating %u => %u",
mailbox->name, mailbox->i.uidvalidity, remote->uidvalidity);
mailbox_index_dirty(mailbox);
mailbox->i.uidvalidity = mboxname_setuidvalidity(mailbox->name, remote->uidvalidity);
}
/* make sure CRC is updated if we're retrying */
if (flags & SYNC_FLAG_ISREPEAT) {
r = mailbox_index_recalc(mailbox);
if (r) goto done;
}
/* bump the raclmodseq if it's higher on the replica */
if (remote && remote->raclmodseq) {
mboxname_setraclmodseq(mailbox->name, remote->raclmodseq);
}
/* bump the foldermodseq if it's higher on the replica */
if (remote && remote->foldermodseq > mailbox->foldermodseq) {
mboxlist_sync_setacls(mailbox->name, mailbox->acl, remote->foldermodseq);
mailbox->foldermodseq = remote->foldermodseq;
}
/* nothing changed - nothing to send */
if (is_unchanged(mailbox, remote))
goto done;
if (!topart) topart = mailbox->part;
part_list = sync_reserve_partlist(reserve_list, topart);
r = sync_prepare_dlists(mailbox, local, remote, topart, part_list, kl,
kupload, 1, /*XXX flags & SYNC_FLAG_FULLANNOTS*/1, !(flags & SYNC_FLAG_ISREPEAT));
if (r) goto done;
/* keep the mailbox locked for shorter time! Unlock the index now
* but don't close it, because we need to guarantee that message
* files don't get deleted until we're finished with them... */
if (!local->mailbox) mailbox_unlock_index(mailbox, NULL);
if (flags & SYNC_FLAG_VERBOSE)
printf("%s %s\n", cmd, local->name);
if (flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "%s %s", cmd, local->name);
/* upload in small(ish) blocks to avoid timeouts */
while (kupload->head) {
struct dlist *kul1 = dlist_splice(kupload, 1024);
sync_send_apply(kul1, sync_cs->backend->out);
r = sync_parse_response("MESSAGE", sync_cs->backend->in, NULL);
dlist_free(&kul1);
if (r) goto done; /* abort earlier */
}
/* close before sending the apply - all data is already read */
if (!local->mailbox) mailbox_close(&mailbox);
/* update the mailbox */
sync_send_apply(kl, sync_cs->backend->out);
r = sync_parse_response("MAILBOX", sync_cs->backend->in, NULL);
// if we succeeded, cache!
if (!r) r = sync_cache(sync_cs, local->name, kl);
done:
if (mailbox && !local->mailbox) mailbox_close(&mailbox);
// any error, nuke our remote cache.
if (r) sync_uncache(sync_cs, local->name);
sync_folder_list_free(&myremotes);
dlist_free(&kupload);
dlist_free(&kl);
return r;
}
int sync_do_update_mailbox(struct sync_client_state *sync_cs,
struct sync_folder *local,
struct sync_folder *remote,
const char *topart,
struct sync_reserve_list *reserve_list)
{
mbentry_t *mbentry = NULL;
// it should exist! Guess we lost a race, force it to retry
int r = mboxlist_lookup_allow_all(local->name, &mbentry, NULL);
if (r) return r;
if (mbentry->mbtype & MBTYPE_INTERMEDIATE) {
struct dlist *kl = dlist_newkvlist(NULL, "MAILBOX");
dlist_setatom(kl, "UNIQUEID", mbentry->uniqueid);
dlist_setatom(kl, "MBOXNAME", mbentry->name);
dlist_setatom(kl, "MBOXTYPE",
mboxlist_mbtype_to_string(mbentry->mbtype));
dlist_setnum64(kl, "HIGHESTMODSEQ", mbentry->foldermodseq);
dlist_setnum64(kl, "CREATEDMODSEQ", mbentry->createdmodseq);
dlist_setnum64(kl, "FOLDERMODSEQ", mbentry->foldermodseq);
sync_send_apply(kl, sync_cs->backend->out);
r = sync_parse_response("MAILBOX", sync_cs->backend->in, NULL);
// on error, clear cache - otherwise cache this state
if (r) sync_uncache(sync_cs, mbentry->name);
else r = sync_cache(sync_cs, mbentry->name, kl);
dlist_free(&kl);
mboxlist_entry_free(&mbentry);
return 0;
}
mboxlist_entry_free(&mbentry);
int flags = sync_cs->flags;
r = update_mailbox_once(sync_cs, local, remote, topart, reserve_list, flags);
flags |= SYNC_FLAG_ISREPEAT;
if (r == IMAP_SYNC_CHECKSUM) {
syslog(LOG_NOTICE, "SYNC_NOTICE: CRC failure on sync %s, recalculating counts and trying again", local->name);
r = update_mailbox_once(sync_cs, local, remote, topart, reserve_list, flags);
}
/* never retry - other end should always sync cleanly */
if (flags & SYNC_FLAG_NO_COPYBACK) return r;
if (r == IMAP_AGAIN) {
local->ispartial = 0; /* don't batch the re-update, means sync to 2.4 will still work after fullsync */
r = mailbox_full_update(sync_cs, local, reserve_list, flags);
if (!r) r = update_mailbox_once(sync_cs, local, remote, topart,
reserve_list, flags);
}
else if (r == IMAP_SYNC_CHECKSUM) {
syslog(LOG_ERR, "CRC failure on sync for %s, trying full update",
local->name);
r = mailbox_full_update(sync_cs, local, reserve_list, flags);
if (!r) r = update_mailbox_once(sync_cs, local, remote, topart,
reserve_list, flags|SYNC_FLAG_FULLANNOTS);
}
return r;
}
/* ====================================================================== */
static int update_seen_work(struct sync_client_state *sync_cs,
const char *user, const char *uniqueid,
struct seendata *sd)
{
const char *cmd = "SEEN";
struct dlist *kl;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("SEEN %s %s\n", user, uniqueid);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "SEEN %s %s", user, uniqueid);
/* Update seen list */
kl = dlist_newkvlist(NULL, cmd);
dlist_setatom(kl, "USERID", user);
dlist_setatom(kl, "UNIQUEID", uniqueid);
dlist_setdate(kl, "LASTREAD", sd->lastread);
dlist_setnum32(kl, "LASTUID", sd->lastuid);
dlist_setdate(kl, "LASTCHANGE", sd->lastchange);
dlist_setatom(kl, "SEENUIDS", sd->seenuids);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
return sync_parse_response(cmd, sync_cs->backend->in, NULL);
}
int sync_do_seen(struct sync_client_state *sync_cs,
const char *userid, char *uniqueid)
{
int r = 0;
struct seen *seendb = NULL;
struct seendata sd = SEENDATA_INITIALIZER;
/* ignore read failures */
r = seen_open(userid, SEEN_SILENT, &seendb);
if (r) return 0;
// XXX: we should pipe the channel through to here
struct mboxlock *synclock = sync_lock(sync_cs, userid);
if (!synclock) {
r = IMAP_MAILBOX_LOCKED;
goto done;
}
r = seen_read(seendb, uniqueid, &sd);
if (!r) r = update_seen_work(sync_cs, userid, uniqueid, &sd);
done:
seen_close(&seendb);
seen_freedata(&sd);
mboxname_release(&synclock);
return r;
}
/* ====================================================================== */
int sync_do_quota(struct sync_client_state *sync_cs,
const char *root)
{
int r = 0;
char *userid = mboxname_to_userid(root);
struct mboxlock *synclock = sync_lock(sync_cs, userid);
if (!synclock) {
r = IMAP_MAILBOX_LOCKED;
goto done;
}
struct quota q;
quota_init(&q, root);
r = update_quota_work(sync_cs, &q, NULL);
quota_free(&q);
done:
mboxname_release(&synclock);
free(userid);
return r;
}
static int do_annotation_cb(const char *mailbox __attribute__((unused)),
uint32_t uid __attribute__((unused)),
const char *entry, const char *userid,
const struct buf *value,
const struct annotate_metadata *mdata,
void *rock)
{
struct sync_annot_list *l = (struct sync_annot_list *) rock;
sync_annot_list_add(l, entry, userid, value, mdata->modseq);
return 0;
}
static int parse_annotation(struct dlist *kin,
struct sync_annot_list *replica_annot)
{
struct dlist *kl;
const char *entry;
const char *userid = "";
const char *valmap = NULL;
size_t vallen = 0;
struct buf value = BUF_INITIALIZER;
modseq_t modseq = 0;
for (kl = kin->head; kl; kl = kl->next) {
if (!dlist_getatom(kl, "ENTRY", &entry))
return IMAP_PROTOCOL_BAD_PARAMETERS;
if (!dlist_getmap(kl, "VALUE", &valmap, &vallen))
return IMAP_PROTOCOL_BAD_PARAMETERS;
dlist_getatom(kl, "USERID", &userid); /* optional */
dlist_getnum64(kl, "MODSEQ", &modseq); /* optional */
buf_init_ro(&value, valmap, vallen);
sync_annot_list_add(replica_annot, entry, userid, &value, modseq);
buf_free(&value);
}
return 0;
}
static int do_getannotation(struct sync_client_state *sync_cs, const char *mboxname,
struct sync_annot_list *replica_annot)
{
const char *cmd = "ANNOTATION";
struct dlist *kl;
struct dlist *kin = NULL;
int r;
/* Update seen list */
kl = dlist_setatom(NULL, cmd, mboxname);
sync_send_lookup(kl, sync_cs->backend->out);
dlist_free(&kl);
r = sync_parse_response(cmd, sync_cs->backend->in, &kin);
if (r) return r;
r = parse_annotation(kin, replica_annot);
dlist_free(&kin);
return r;
}
int sync_do_annotation(struct sync_client_state *sync_cs, const char *mboxname)
{
int r;
struct sync_annot_list *replica_annot = sync_annot_list_create();
struct sync_annot_list *master_annot = sync_annot_list_create();
struct sync_annot *ma, *ra;
int n;
char *userid = mboxname_to_userid(mboxname);
// XXX: we should pipe the channel through to here
struct mboxlock *synclock = sync_lock(sync_cs, userid);
if (!synclock) {
r = IMAP_MAILBOX_LOCKED;
goto bail;
}
r = do_getannotation(sync_cs, mboxname, replica_annot);
if (r) goto bail;
r = annotatemore_findall(mboxname, 0, "*", /*modseq*/0, &do_annotation_cb,
master_annot, /*flags*/0);
if (r) {
xsyslog(LOG_ERR, "IOERROR: fetching annotations failed",
"mboxname=<%s>", mboxname);
r = IMAP_IOERROR;
goto bail;
}
/* both lists are sorted, so we work our way through the lists
top-to-bottom and determine what we need to do based on order */
ma = master_annot->head;
ra = replica_annot->head;
while (ma || ra) {
if (!ra) n = -1; /* add all master annotations */
else if (!ma) n = 1; /* remove all replica annotations */
else if ((n = strcmp(ma->entry, ra->entry)) == 0)
n = strcmp(ma->userid, ra->userid);
if (n > 0) {
/* remove replica annotation */
r = folder_unannotation(sync_cs, mboxname, ra->entry, ra->userid);
if (r) goto bail;
ra = ra->next;
continue;
}
if (n == 0) {
/* already have the annotation, but is the value different? */
if (!buf_cmp(&ra->value, &ma->value)) {
ra = ra->next;
ma = ma->next;
continue;
}
ra = ra->next;
}
/* add the current client annotation */
r = folder_setannotation(sync_cs, mboxname, ma->entry, ma->userid, &ma->value);
if (r) goto bail;
ma = ma->next;
}
bail:
sync_annot_list_free(&master_annot);
sync_annot_list_free(&replica_annot);
mboxname_release(&synclock);
free(userid);
return r;
}
/* ====================================================================== */
static int do_folders(struct sync_client_state *sync_cs,
struct sync_name_list *mboxname_list, const char *topart,
struct sync_folder_list *replica_folders,
int flags)
{
int r = 0;
struct sync_folder_list *master_folders;
struct sync_rename_list *rename_folders;
struct sync_reserve_list *reserve_list;
struct sync_folder *mfolder, *rfolder;
const char *part;
uint32_t batchsize = 0;
if (flags & SYNC_FLAG_BATCH) {
batchsize = config_getint(IMAPOPT_SYNC_BATCHSIZE);
}
master_folders = sync_folder_list_create();
rename_folders = sync_rename_list_create();
reserve_list = sync_reserve_list_create(SYNC_MSGID_LIST_HASH_SIZE);
r = reserve_messages(sync_cs, mboxname_list, topart, master_folders,
replica_folders, reserve_list, batchsize);
if (r) {
syslog(LOG_ERR, "reserve messages: failed: %s", error_message(r));
goto bail;
}
/* Tag folders on server which still exist on the client. Anything
* on the server which remains untagged can be deleted immediately */
for (mfolder = master_folders->head; mfolder; mfolder = mfolder->next) {
if (mfolder->mark) continue;
rfolder = sync_folder_lookup(replica_folders, mfolder->uniqueid);
if (!rfolder) continue;
if (rfolder->mark) continue;
rfolder->mark = 1;
/* does it need a rename? partition change is a rename too */
part = topart ? topart : mfolder->part;
if (strcmp(mfolder->name, rfolder->name) || (rfolder->part && strcmpsafe(part, rfolder->part))) {
sync_rename_list_add(rename_folders, mfolder->uniqueid, rfolder->name,
mfolder->name, part, mfolder->uidvalidity);
}
}
/* XXX - sync_log_channel_user on any issue here rather than trying to solve,
* and remove all entries related to that user from both lists */
/* Delete folders on server which no longer exist on client */
if (flags & SYNC_FLAG_DELETE_REMOTE) {
for (rfolder = replica_folders->head; rfolder; rfolder = rfolder->next) {
if (rfolder->mark) continue;
mbentry_t *tombstone = NULL;
r = mboxlist_lookup_allow_all(rfolder->name, &tombstone, NULL);
if (r == 0 && (tombstone->mbtype & MBTYPE_DELETED) == MBTYPE_DELETED) {
r = sync_do_folder_delete(sync_cs, rfolder->name);
if (r) {
syslog(LOG_ERR, "sync_do_folder_delete(): failed: %s '%s'",
rfolder->name, error_message(r));
goto bail;
}
}
else {
syslog(LOG_ERR, "%s: no tombstone for deleted mailbox %s (%s)",
__func__, rfolder->name, error_message(r));
/* XXX copy the missing local mailbox back from the replica? */
}
}
}
/* Need to rename folders in an order which avoids dependancy conflicts
* following isn't wildly efficient, but rename_folders will typically be
* short and contain few dependancies. Algorithm is to simply pick a
* rename operation which has no dependancy and repeat until done */
while (rename_folders->done < rename_folders->count) {
int rename_success = 0;
struct sync_rename *item, *item2 = NULL;
for (item = rename_folders->head; item; item = item->next) {
if (item->done) continue;
/* don't skip rename to different partition */
if (strcmp(item->oldname, item->newname)) {
item2 = sync_rename_lookup(rename_folders, item->newname);
if (item2 && !item2->done) continue;
}
/* Found unprocessed item which should rename cleanly */
r = folder_rename(sync_cs, item->oldname, item->newname, item->part,
item->uidvalidity);
if (r) {
syslog(LOG_ERR, "do_folders(): failed to rename: %s -> %s ",
item->oldname, item->newname);
goto bail;
}
rename_folders->done++;
item->done = 1;
rename_success = 1;
}
if (!rename_success) {
/* Scanned entire list without a match */
const char *name = "unknown";
if (item2) name = item2->oldname;
syslog(LOG_ERR,
"do_folders(): failed to order folders correctly at %s", name);
r = IMAP_AGAIN;
goto bail;
}
}
/* if we renamed anything, we want to resync the mailbox list before doing the
* mailbox contents */
if (rename_folders->count) {
syslog(LOG_DEBUG,
"do_folders(): did some renames, so retrying");
r = IMAP_AGAIN;
goto bail;
}
for (mfolder = master_folders->head; mfolder; mfolder = mfolder->next) {
if (mfolder->mark) continue;
/* NOTE: rfolder->name may now be wrong, but we're guaranteed that
* it was successfully renamed above, so just use mfolder->name for
* all commands */
rfolder = sync_folder_lookup(replica_folders, mfolder->uniqueid);
r = sync_do_update_mailbox(sync_cs, mfolder, rfolder, topart, reserve_list);
if (r) {
syslog(LOG_ERR, "do_folders(): update failed: %s '%s'",
mfolder->name, error_message(r));
goto bail;
}
if (sync_cs->channel && mfolder->ispartial) {
sync_log_channel_mailbox(sync_cs->channel, mfolder->name);
}
}
bail:
sync_folder_list_free(&master_folders);
sync_rename_list_free(&rename_folders);
sync_reserve_list_free(&reserve_list);
return r;
}
int sync_do_mailboxes(struct sync_client_state *sync_cs,
struct sync_name_list *mboxname_list,
const char *topart, int flags)
{
struct sync_name *mbox;
struct sync_folder_list *replica_folders = sync_folder_list_create();
struct buf buf = BUF_INITIALIZER;
int r;
strarray_t userids = STRARRAY_INITIALIZER;
ptrarray_t locks = PTRARRAY_INITIALIZER;
// what a pain, we need to lock all the users in order, so..
for (mbox = mboxname_list->head; mbox; mbox = mbox->next) {
char *userid = mboxname_to_userid(mbox->name);
strarray_add(&userids, userid ? userid : "");
free(userid);
}
strarray_sort(&userids, cmpstringp_raw);
int i;
for (i = 0; i < strarray_size(&userids); i++) {
const char *userid = strarray_nth(&userids, i);
struct mboxlock *lock = sync_lock(sync_cs, userid);
if (!lock) {
r = IMAP_MAILBOX_LOCKED;
goto done;
}
ptrarray_append(&locks, lock);
}
int tries = 0;
redo:
tries++;
if (tries > 3) {
syslog(LOG_ERR, "failed to settle renames after 3 tries!");
r = IMAP_SYNC_CHANGED;
goto done;
}
struct dlist *kl = NULL;
struct dlist *cachel = NULL;
for (mbox = mboxname_list->head; mbox; mbox = mbox->next) {
struct dlist *cl = NULL;
// check if it's in the cache, then we don't need to look it up
if (!sync_readcache(sync_cs, mbox->name, &cl) && cl) {
if (!cachel) cachel = dlist_newlist(NULL, "MAILBOXES");
dlist_stitch(cachel, cl);
if ((flags & SYNC_FLAG_VERBOSE) || (flags & SYNC_FLAG_LOGGING))
buf_printf(&buf, " (%s)", mbox->name);
}
// if it's not in the cache, then we need to ask for it
else {
if (!kl) kl = dlist_newlist(NULL, "MAILBOXES");
dlist_setatom(kl, "MBOXNAME", mbox->name);
if ((flags & SYNC_FLAG_VERBOSE) || (flags & SYNC_FLAG_LOGGING))
buf_printf(&buf, " %s", mbox->name);
}
}
if (flags & SYNC_FLAG_VERBOSE)
printf("MAILBOXES%s\n", buf_cstring(&buf));
if (flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "MAILBOXES%s", buf_cstring(&buf));
buf_free(&buf);
if (kl) {
sync_send_lookup(kl, sync_cs->backend->out);
dlist_free(&kl);
r = sync_response_parse(sync_cs, "MAILBOXES", replica_folders,
NULL, NULL, NULL, NULL);
if (r) goto done;
}
if (cachel) {
r = sync_kl_parse(cachel, replica_folders, NULL, NULL, NULL, NULL);
dlist_free(&cachel);
if (r) goto done;
}
/* we don't want to delete remote folders which weren't found locally,
* because we may be racing with a rename, and we don't want to lose
* the remote files. A real delete will always have inserted a
* UNMAILBOX anyway */
flags &= ~SYNC_FLAG_DELETE_REMOTE;
r = do_folders(sync_cs, mboxname_list, topart, replica_folders, flags);
if (r == IMAP_AGAIN) {
sync_folder_list_free(&replica_folders);
replica_folders = sync_folder_list_create();
goto redo;
}
done:
sync_folder_list_free(&replica_folders);
strarray_fini(&userids);
for (i = 0; i < ptrarray_size(&locks); i++) {
struct mboxlock *lock = ptrarray_nth(&locks, i);
mboxname_release(&lock);
}
ptrarray_fini(&locks);
return r;
}
/* ====================================================================== */
struct mboxinfo {
struct sync_name_list *mboxlist;
struct sync_name_list *quotalist;
};
static int do_mailbox_info(const mbentry_t *mbentry, void *rock)
{
struct mailbox *mailbox = NULL;
struct mboxinfo *info = (struct mboxinfo *)rock;
int r = 0;
/* XXX - check for deleted? */
if (mbentry->mbtype & MBTYPE_INTERMEDIATE) {
sync_name_list_add(info->mboxlist, mbentry->name);
return 0;
}
r = mailbox_open_irl(mbentry->name, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
/* doesn't exist? Probably not finished creating or removing yet */
if (r == IMAP_MAILBOX_NONEXISTENT) {
r = 0;
goto done;
}
if (r == IMAP_MAILBOX_RESERVED) {
r = 0;
goto done;
}
if (r) goto done;
if (info->quotalist && mailbox->quotaroot) {
sync_name_list_add(info->quotalist, mailbox->quotaroot);
}
sync_name_list_add(info->mboxlist, mbentry->name);
done:
mailbox_close(&mailbox);
return r;
}
int sync_do_user_quota(struct sync_client_state *sync_cs,
struct sync_name_list *master_quotaroots,
struct sync_quota_list *replica_quota)
{
int r;
struct sync_name *mitem;
struct sync_quota *rquota;
struct quota q;
/* set any new or changed quotas */
for (mitem = master_quotaroots->head; mitem; mitem = mitem->next) {
rquota = sync_quota_lookup(replica_quota, mitem->name);
if (rquota)
rquota->done = 1;
quota_init(&q, mitem->name);
r = update_quota_work(sync_cs, &q, rquota);
quota_free(&q);
if (r) return r;
}
/* delete any quotas no longer on the master */
for (rquota = replica_quota->head; rquota; rquota = rquota->next) {
if (rquota->done) continue;
r = delete_quota(sync_cs, rquota->root);
if (r) return r;
}
return 0;
}
static int do_user_main(struct sync_client_state *sync_cs,
const char *userid, const char *topart,
struct sync_folder_list *replica_folders,
struct sync_quota_list *replica_quota)
{
int r = 0;
struct mboxinfo info;
info.mboxlist = sync_name_list_create();
info.quotalist = sync_name_list_create();
r = mboxlist_usermboxtree(userid, NULL, do_mailbox_info, &info, MBOXTREE_DELETED);
/* we know all the folders present on the master, so it's safe to delete
* anything not mentioned here on the replica - at least until we get
* real tombstones */
int flags = sync_cs->flags;
flags |= SYNC_FLAG_DELETE_REMOTE;
if (!r) r = do_folders(sync_cs, info.mboxlist, topart, replica_folders, flags);
if (!r) r = sync_do_user_quota(sync_cs, info.quotalist, replica_quota);
sync_name_list_free(&info.mboxlist);
sync_name_list_free(&info.quotalist);
if (r && r != IMAP_AGAIN) {
xsyslog(LOG_ERR, "IOERROR: user replication failed",
"error=<%s> userid=<%s> channel=<%s> servername=<%s>",
error_message(r), userid,
sync_cs->channel, sync_cs->servername);
}
return r;
}
int sync_do_user_sub(struct sync_client_state *sync_cs, const char *userid,
struct sync_name_list *replica_subs)
{
struct sync_name *rsubs;
int r = 0;
int i;
/* Includes subsidiary nodes automatically */
strarray_t *msubs = mboxlist_sublist(userid);
if (!msubs) {
xsyslog(LOG_ERR, "IOERROR: fetching subscriptions failed",
"userid=<%s>", userid);
r = IMAP_IOERROR;
goto bail;
}
/* add any folders that need adding, and mark any which
* still exist */
for (i = 0; i < msubs->count; i++) {
const char *name = strarray_nth(msubs, i);
rsubs = sync_name_lookup(replica_subs, name);
if (rsubs) {
rsubs->mark = 1;
continue;
}
r = sync_set_sub(sync_cs, userid, name, 1);
if (r) goto bail;
}
/* remove any no-longer-subscribed folders */
for (rsubs = replica_subs->head; rsubs; rsubs = rsubs->next) {
if (rsubs->mark)
continue;
r = sync_set_sub(sync_cs, userid, rsubs->name, 0);
if (r) goto bail;
}
bail:
strarray_free(msubs);
return r;
}
static int get_seen(const char *uniqueid, struct seendata *sd, void *rock)
{
struct sync_seen_list *list = (struct sync_seen_list *)rock;
sync_seen_list_add(list, uniqueid, sd->lastread, sd->lastuid,
sd->lastchange, sd->seenuids);
return 0;
}
int sync_do_user_seen(struct sync_client_state *sync_cs, const char *userid,
struct sync_seen_list *replica_seen)
{
int r;
struct sync_seen *mseen, *rseen;
struct seen *seendb = NULL;
struct sync_seen_list *list;
/* silently ignore errors */
r = seen_open(userid, SEEN_SILENT, &seendb);
if (r) return 0;
list = sync_seen_list_create();
seen_foreach(seendb, get_seen, list);
seen_close(&seendb);
for (mseen = list->head; mseen; mseen = mseen->next) {
rseen = sync_seen_list_lookup(replica_seen, mseen->uniqueid);
if (rseen) {
rseen->mark = 1;
if (seen_compare(&rseen->sd, &mseen->sd))
continue; /* nothing changed */
}
r = update_seen_work(sync_cs, userid, mseen->uniqueid, &mseen->sd);
}
/* XXX - delete seen on the replica for records that don't exist? */
sync_seen_list_free(&list);
return 0;
}
int sync_do_user_sieve(struct sync_client_state *sync_cs, const char *userid,
struct sync_sieve_list *replica_sieve)
{
int r = 0;
struct sync_sieve_list *master_sieve;
struct sync_sieve *mitem, *ritem;
int master_active = 0;
int replica_active = 0;
char *ext;
master_sieve = sync_sieve_list_generate(userid);
if (!master_sieve) {
syslog(LOG_ERR, "Unable to list sieve scripts for %s", userid);
return IMAP_IOERROR;
}
/* Upload missing and out of date or mismatching scripts */
for (mitem = master_sieve->head; mitem; mitem = mitem->next) {
ritem = sync_sieve_lookup(replica_sieve, mitem->name);
if (ritem) {
ritem->mark = 1;
/* compare the GUID if known */
if (!message_guid_isnull(&ritem->guid)) {
if (message_guid_equal(&ritem->guid, &mitem->guid))
continue;
/* XXX: copyback support */
}
/* fallback to date comparison */
else if (ritem->last_update >= mitem->last_update)
continue; /* changed */
}
/* Don't upload compiled bytecode */
ext = strrchr(mitem->name, '.');
if (ext && !strcmp(ext, ".bc"))
continue;
r = sieve_upload(sync_cs, userid, mitem->name, mitem->last_update);
if (r) goto bail;
}
/* Delete scripts which no longer exist on the master */
replica_active = 0;
for (ritem = replica_sieve->head; ritem; ritem = ritem->next) {
if (ritem->mark) {
if (ritem->active)
replica_active = 1;
} else {
r = sieve_delete(sync_cs, userid, ritem->name);
if (r) goto bail;
}
}
/* Change active script if necessary */
master_active = 0;
for (mitem = master_sieve->head; mitem; mitem = mitem->next) {
if (!mitem->active)
continue;
master_active = 1;
ritem = sync_sieve_lookup(replica_sieve, mitem->name);
if (ritem && ritem->active)
break;
r = sieve_activate(sync_cs, userid, mitem->name);
if (r) goto bail;
replica_active = 1;
break;
}
if (!master_active && replica_active)
r = sieve_deactivate(sync_cs, userid);
bail:
sync_sieve_list_free(&master_sieve);
return(r);
}
int sync_do_user(struct sync_client_state *sync_cs,
const char *userid, const char *topart)
{
int r = 0;
struct sync_folder_list *replica_folders = sync_folder_list_create();
struct sync_name_list *replica_subs = sync_name_list_create();
struct sync_sieve_list *replica_sieve = sync_sieve_list_create();
struct sync_seen_list *replica_seen = sync_seen_list_create();
struct sync_quota_list *replica_quota = sync_quota_list_create();
struct dlist *kl = NULL;
struct mailbox *mailbox = NULL;
struct mboxlock *userlock = sync_lock(sync_cs, userid);
if (!userlock) {
r = IMAP_MAILBOX_LOCKED;
goto done;
}
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("USER %s\n", userid);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "USER %s", userid);
int tries = 0;
redo:
tries++;
if (tries > 3) {
syslog(LOG_ERR, "failed to sync user %s after 3 tries", userid);
r = IMAP_SYNC_CHANGED;
goto done;
}
kl = dlist_setatom(NULL, "USER", userid);
sync_send_lookup(kl, sync_cs->backend->out);
dlist_free(&kl);
r = sync_response_parse(sync_cs, "USER", replica_folders, replica_subs,
replica_sieve, replica_seen, replica_quota);
/* can happen! */
if (r == IMAP_MAILBOX_NONEXISTENT) r = 0;
if (r) goto done;
/* check that the inbox exists locally to be allowed to sync this user at all */
char *inbox = mboxname_user_mbox(userid, NULL);
r = mailbox_open_irl(inbox, &mailbox);
if (!r) r = sync_mailbox_version_check(&mailbox);
free(inbox);
if (r == IMAP_MAILBOX_NONEXISTENT) {
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("Does not exist locally %s\n", userid);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "Does not exist locally %s", userid);
// just skip this user. XXX - tombstone for user -> sync_reset?
r = 0;
goto done;
}
if (r) goto done;
/* we don't hold locks while sending commands */
mailbox_close(&mailbox);
r = do_user_main(sync_cs, userid, topart, replica_folders, replica_quota);
if (r == IMAP_AGAIN) {
// we've done a rename - have to try again!
sync_folder_list_free(&replica_folders);
sync_name_list_free(&replica_subs);
sync_sieve_list_free(&replica_sieve);
sync_seen_list_free(&replica_seen);
sync_quota_list_free(&replica_quota);
replica_folders = sync_folder_list_create();
replica_subs = sync_name_list_create();
replica_sieve = sync_sieve_list_create();
replica_seen = sync_seen_list_create();
replica_quota = sync_quota_list_create();
goto redo;
}
if (r) goto done;
r = sync_do_user_sub(sync_cs, userid, replica_subs);
if (r) goto done;
r = sync_do_user_sieve(sync_cs, userid, replica_sieve);
if (r) goto done;
r = sync_do_user_seen(sync_cs, userid, replica_seen);
done:
sync_folder_list_free(&replica_folders);
sync_name_list_free(&replica_subs);
sync_sieve_list_free(&replica_sieve);
sync_seen_list_free(&replica_seen);
sync_quota_list_free(&replica_quota);
mboxname_release(&userlock);
return r;
}
/* ====================================================================== */
int sync_do_meta(struct sync_client_state *sync_cs, const char *userid)
{
struct sync_name_list *replica_subs = sync_name_list_create();
struct sync_sieve_list *replica_sieve = sync_sieve_list_create();
struct sync_seen_list *replica_seen = sync_seen_list_create();
struct dlist *kl = NULL;
int r = 0;
if (sync_cs->flags & SYNC_FLAG_VERBOSE)
printf("META %s\n", userid);
if (sync_cs->flags & SYNC_FLAG_LOGGING)
syslog(LOG_INFO, "META %s", userid);
kl = dlist_setatom(NULL, "META", userid);
sync_send_lookup(kl, sync_cs->backend->out);
dlist_free(&kl);
r = sync_response_parse(sync_cs, "META", NULL,
replica_subs, replica_sieve, replica_seen, NULL);
if (!r) r = sync_do_user_seen(sync_cs, userid, replica_seen);
if (!r) r = sync_do_user_sub(sync_cs, userid, replica_subs);
if (!r) r = sync_do_user_sieve(sync_cs, userid, replica_sieve);
sync_seen_list_free(&replica_seen);
sync_name_list_free(&replica_subs);
sync_sieve_list_free(&replica_sieve);
return r;
}
/* ====================================================================== */
EXPORTED const char *sync_apply(struct dlist *kin, struct sync_reserve_list *reserve_list, struct sync_state *state)
{
int r = IMAP_PROTOCOL_ERROR;
ucase(kin->name);
if (!strcmp(kin->name, "MESSAGE"))
r = sync_apply_message(kin, reserve_list, state);
else if (!strcmp(kin->name, "EXPUNGE"))
r = sync_apply_expunge(kin, state);
/* dump protocol */
else if (!strcmp(kin->name, "ACTIVATE_SIEVE"))
r = sync_apply_activate_sieve(kin, state);
else if (!strcmp(kin->name, "ANNOTATION"))
r = sync_apply_annotation(kin, state);
else if (!strcmp(kin->name, "MAILBOX"))
r = sync_apply_mailbox(kin, reserve_list, state);
else if (!strcmp(kin->name, "LOCAL_MAILBOX")) {
state->local_only = 1;
r = sync_apply_mailbox(kin, reserve_list, state);
}
else if (!strcmp(kin->name, "QUOTA"))
r = sync_apply_quota(kin, state);
else if (!strcmp(kin->name, "SEEN"))
r = sync_apply_seen(kin, state);
else if (!strcmp(kin->name, "RENAME"))
r = sync_apply_rename(kin, state);
else if (!strcmp(kin->name, "LOCAL_RENAME")) {
state->local_only = 1;
r = sync_apply_rename(kin, state);
}
else if (!strcmp(kin->name, "RESERVE"))
r = sync_apply_reserve(kin, reserve_list, state);
else if (!strcmp(kin->name, "SIEVE"))
r = sync_apply_sieve(kin, state);
else if (!strcmp(kin->name, "SUB"))
r = sync_apply_changesub(kin, state);
/* "un"dump protocol ;) */
else if (!strcmp(kin->name, "UNACTIVATE_SIEVE"))
r = sync_apply_unactivate_sieve(kin, state);
else if (!strcmp(kin->name, "UNANNOTATION"))
r = sync_apply_unannotation(kin, state);
else if (!strcmp(kin->name, "UNMAILBOX"))
r = sync_apply_unmailbox(kin, state);
else if (!strcmp(kin->name, "LOCAL_UNMAILBOX")) {
state->local_only = 1;
r = sync_apply_unmailbox(kin, state);
}
else if (!strcmp(kin->name, "UNQUOTA"))
r = sync_apply_unquota(kin, state);
else if (!strcmp(kin->name, "UNSIEVE"))
r = sync_apply_unsieve(kin, state);
else if (!strcmp(kin->name, "UNSUB"))
r = sync_apply_changesub(kin, state);
/* user is a special case that's not paired, there's no "upload user"
* as such - we just call the individual commands with their items */
else if (!strcmp(kin->name, "UNUSER"))
r = sync_apply_unuser(kin, state);
else if (!strcmp(kin->name, "LOCAL_UNUSER")) {
state->local_only = 1;
r = sync_apply_unuser(kin, state);
}
else {
syslog(LOG_ERR, "SYNCERROR: unknown command %s", kin->name);
r = IMAP_PROTOCOL_ERROR;
}
return sync_response(r);
}
EXPORTED const char *sync_get(struct dlist *kin, struct sync_state *state)
{
int r = IMAP_PROTOCOL_ERROR;
ucase(kin->name);
if (!strcmp(kin->name, "ANNOTATION"))
r = sync_get_annotation(kin, state);
else if (!strcmp(kin->name, "FETCH"))
r = sync_get_message(kin, state);
else if (!strcmp(kin->name, "FETCH_SIEVE"))
r = sync_get_sieve(kin, state);
else if (!strcmp(kin->name, "FULLMAILBOX"))
r = sync_get_fullmailbox(kin, state);
else if (!strcmp(kin->name, "MAILBOXES"))
r = sync_get_mailboxes(kin, state);
else if (!strcmp(kin->name, "UNIQUEIDS"))
r = sync_get_uniqueids(kin, state);
else if (!strcmp(kin->name, "META"))
r = sync_get_meta(kin, state);
else if (!strcmp(kin->name, "QUOTA"))
r = sync_get_quota(kin, state);
else if (!strcmp(kin->name, "USER"))
r = sync_get_user(kin, state);
else
r = IMAP_PROTOCOL_ERROR;
return sync_response(r);
}
EXPORTED const char *sync_restore(struct dlist *kin,
struct sync_reserve_list *reserve_list,
struct sync_state *state)
{
int r = IMAP_PROTOCOL_ERROR;
ucase(kin->name);
if (!strcmp(kin->name, "MAILBOX"))
r = sync_restore_mailbox(kin, reserve_list, state);
else if (!strcmp(kin->name, "LOCAL_MAILBOX")) {
state->local_only = 1;
r = sync_restore_mailbox(kin, reserve_list, state);
}
else {
syslog(LOG_ERR, "SYNCERROR: unknown command %s", kin->name);
r = IMAP_PROTOCOL_ERROR;
}
return sync_response(r);
}
/* ====================================================================== */
static int do_unuser(struct sync_client_state *sync_cs, const char *userid)
{
const char *cmd = "UNUSER";
struct mailbox *mailbox = NULL;
struct dlist *kl;
int r;
/* nothing to do if there's no userid */
if (!userid || !userid[0]) {
syslog(LOG_WARNING, "ignoring attempt to %s() without userid", __func__);
return 0;
}
/* check local mailbox first */
char *inbox = mboxname_user_mbox(userid, NULL);
r = mailbox_open_irl(inbox, &mailbox);
/* only remove from server if there's no local mailbox */
if (r == IMAP_MAILBOX_NONEXISTENT) {
kl = dlist_setatom(NULL, cmd, userid);
sync_send_apply(kl, sync_cs->backend->out);
dlist_free(&kl);
r = sync_parse_response(cmd, sync_cs->backend->in, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) r = 0;
}
mailbox_close(&mailbox);
free(inbox);
return r;
}
/* ====================================================================== */
static int user_sub(struct sync_client_state *sync_cs,
const char *userid, const char *mboxname)
{
int r;
r = mboxlist_checksub(mboxname, userid);
switch (r) {
case CYRUSDB_OK:
return sync_set_sub(sync_cs, userid, mboxname, 1);
case CYRUSDB_NOTFOUND:
return sync_set_sub(sync_cs, userid, mboxname, 0);
default:
return r;
}
}
/* ====================================================================== */
static int do_unmailbox(struct sync_client_state *sync_cs, const char *mboxname)
{
struct mailbox *mailbox = NULL;
int r;
r = mailbox_open_irl(mboxname, &mailbox);
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* make sure there's an explicit local tombstone */
mbentry_t *tombstone = NULL;
r = mboxlist_lookup_allow_all(mboxname, &tombstone, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) {
// otherwise we don't change anything on the replica
syslog(LOG_NOTICE, "SYNCNOTICE: attempt to UNMAILBOX without a tombstone %s", mboxname);
r = 0;
goto skip;
}
if (r) {
syslog(LOG_ERR, "%s: mboxlist_lookup() failed: %s '%s'",
__func__, mboxname, error_message(r));
}
else if ((tombstone->mbtype & MBTYPE_DELETED) == 0) {
syslog(LOG_ERR, "attempt to UNMAILBOX non-tombstone: \"%s\"",
mboxname);
}
else {
r = sync_do_folder_delete(sync_cs, mboxname);
if (r) {
syslog(LOG_ERR, "%s: sync_do_folder_delete(): failed: %s '%s'",
__func__, mboxname, error_message(r));
}
}
skip:
mboxlist_entry_free(&tombstone);
}
mailbox_close(&mailbox);
return r;
}
/* ====================================================================== */
static void remove_meta(char *user, struct sync_action_list *list)
{
struct sync_action *action;
for (action = list->head ; action ; action = action->next) {
if (!strcmp(user, action->user)) {
action->active = 0;
}
}
}
/* ====================================================================== */
#define report_verbose(...) syslog(LOG_INFO, __VA_ARGS__)
#define report_verbose_error(...) syslog(LOG_ERR, __VA_ARGS__)
static int do_mailboxes(struct sync_client_state *sync_cs,
struct sync_name_list *mboxname_list,
struct sync_action_list *user_list,
int flags)
{
struct sync_name *mbox;
int r = 0;
if (mboxname_list->count) {
r = sync_do_mailboxes(sync_cs, mboxname_list, NULL, flags);
if (sync_cs->channel && r == IMAP_MAILBOX_LOCKED) {
for (mbox = mboxname_list->head; mbox; mbox = mbox->next) {
if (mbox->mark) continue;
sync_log_channel_mailbox(sync_cs->channel, mbox->name);
report_verbose(" Deferred: MAILBOX %s\n", mbox->name);
}
r = 0;
}
else if (r) {
/* promote failed personal mailboxes to USER */
int nonuser = 0;
for (mbox = mboxname_list->head; mbox; mbox = mbox->next) {
/* done OK? Good :) */
if (mbox->mark)
continue;
char *userid = mboxname_to_userid(mbox->name);
if (userid) {
mbox->mark = 1;
sync_action_list_add(user_list, NULL, userid);
report_verbose(" Promoting: MAILBOX %s -> USER %s\n",
mbox->name, userid);
free(userid);
}
else
nonuser = 1; /* there was a non-user mailbox */
}
if (!nonuser) r = 0;
}
}
return r;
}
int sync_do_restart(struct sync_client_state *sync_cs)
{
sync_send_restart(sync_cs->backend->out);
return sync_parse_response("RESTART", sync_cs->backend->in, NULL);
}
struct split_user_mailboxes_rock {
struct sync_client_state *sync_cs;
struct sync_action_list *user_list;
int r;
};
static void split_user_mailboxes(const char *key __attribute__((unused)),
void *data,
void *rock)
{
struct split_user_mailboxes_rock *smrock =
(struct split_user_mailboxes_rock *) rock;
struct sync_action_list *mailbox_list = (struct sync_action_list *) data;
struct sync_name_list *mboxname_list = sync_name_list_create();;
struct sync_action *action;
for (action = mailbox_list->head; action; action = action->next) {
if (!action->active)
continue;
sync_name_list_add(mboxname_list, action->name);
}
if (mboxname_list->count) {
syslog(LOG_DEBUG, "sync_mailboxes: doing %lu",
mboxname_list->count);
smrock->r = do_mailboxes(smrock->sync_cs, mboxname_list,
smrock->user_list, smrock->sync_cs->flags);
if (!smrock->r) smrock->r = sync_do_restart(smrock->sync_cs);
}
sync_name_list_free(&mboxname_list);
}
/* need this lil wrapper for free_hash_table callback */
static void sync_action_list_free_wrapper(void *p)
{
struct sync_action_list *l = (struct sync_action_list *) p;
sync_action_list_free(&l);
}
int sync_do_reader(struct sync_client_state *sync_cs, sync_log_reader_t *slr)
{
struct sync_action_list *user_list = sync_action_list_create();
struct sync_action_list *unuser_list = sync_action_list_create();
struct sync_action_list *meta_list = sync_action_list_create();
struct sync_action_list *unmailbox_list = sync_action_list_create();
struct sync_action_list *quota_list = sync_action_list_create();
struct sync_action_list *annot_list = sync_action_list_create();
struct sync_action_list *seen_list = sync_action_list_create();
struct sync_action_list *sub_list = sync_action_list_create();
hash_table user_mailboxes = HASH_TABLE_INITIALIZER;
const char *args[3];
struct sync_action *action;
int r = 0;
construct_hash_table(&user_mailboxes, 1024 /* XXX */, 0);
while (sync_log_reader_getitem(slr, args) != EOF) {
if (!strcmp(args[0], "USER"))
sync_action_list_add(user_list, NULL, args[1]);
else if (!strcmp(args[0], "UNUSER"))
sync_action_list_add(unuser_list, NULL, args[1]);
else if (!strcmp(args[0], "META"))
sync_action_list_add(meta_list, NULL, args[1]);
else if (!strcmp(args[0], "SIEVE"))
sync_action_list_add(meta_list, NULL, args[1]);
else if ((!strcmp(args[0], "APPEND")) /* just a mailbox event */
|| (!strcmp(args[0], "MAILBOX"))
|| (!strcmp(args[0], "DOUBLEMAILBOX"))) {
char *freeme = NULL;
const char *userid;
struct sync_action_list *mailbox_list;
userid = freeme = mboxname_to_userid(args[1]);
if (!userid) userid = ""; /* treat non-user mboxes as a single cohort */
mailbox_list = hash_lookup(userid, &user_mailboxes);
if (!mailbox_list) {
mailbox_list = sync_action_list_create();
hash_insert(userid, mailbox_list, &user_mailboxes);
}
sync_action_list_add(mailbox_list, args[1], NULL);
if (args[2]) {
/* if there's a second MAILBOX recorded (i.e. a copy or move), add
* it to the same user's mailbox_list (even if it's a diff user),
* so that the order doesn't get lost.
*/
sync_action_list_add(mailbox_list, args[2], NULL);
}
free(freeme);
}
else if (!strcmp(args[0], "RENAME")) {
char *freeme1 = NULL, *freeme2 = NULL;
const char *userid1, *userid2;
struct sync_action_list *mailbox_list;
userid1 = freeme1 = mboxname_to_userid(args[1]);
if (!userid1) userid1 = "";
userid2 = freeme2 = mboxname_to_userid(args[2]);
if (!userid2) userid2 = "";
/* add both mboxnames to the list for the first one's user */
mailbox_list = hash_lookup(userid1, &user_mailboxes);
if (!mailbox_list) {
mailbox_list = sync_action_list_create();
hash_insert(userid1, mailbox_list, &user_mailboxes);
}
sync_action_list_add(mailbox_list, args[1], NULL);
sync_action_list_add(mailbox_list, args[2], NULL);
/* if the second mboxname's user is different, add both names there too */
if (strcmp(userid1, userid2) != 0) {
mailbox_list = hash_lookup(userid2, &user_mailboxes);
if (!mailbox_list) {
mailbox_list = sync_action_list_create();
hash_insert(userid2, mailbox_list, &user_mailboxes);
}
sync_action_list_add(mailbox_list, args[1], NULL);
sync_action_list_add(mailbox_list, args[2], NULL);
}
free(freeme1);
free(freeme2);
}
else if (!strcmp(args[0], "UNMAILBOX"))
sync_action_list_add(unmailbox_list, args[1], NULL);
else if (!strcmp(args[0], "QUOTA"))
sync_action_list_add(quota_list, args[1], NULL);
else if (!strcmp(args[0], "ANNOTATION"))
sync_action_list_add(annot_list, args[1], NULL);
else if (!strcmp(args[0], "SEEN"))
sync_action_list_add(seen_list, args[2], args[1]);
else if (!strcmp(args[0], "SUB"))
sync_action_list_add(sub_list, args[2], args[1]);
else if (!strcmp(args[0], "UNSUB"))
sync_action_list_add(sub_list, args[2], args[1]);
else
syslog(LOG_ERR, "Unknown action type: %s", args[0]);
}
/* Optimise out redundant clauses */
for (action = user_list->head; action; action = action->next) {
/* remove per-user items */
remove_meta(action->user, meta_list);
remove_meta(action->user, seen_list);
remove_meta(action->user, sub_list);
}
/* duplicate removal for unuser - we also strip all the user events */
for (action = unuser_list->head; action; action = action->next) {
/* remove per-user items */
remove_meta(action->user, meta_list);
remove_meta(action->user, seen_list);
remove_meta(action->user, sub_list);
/* unuser trumps user */
remove_meta(action->user, user_list);
}
for (action = meta_list->head; action; action = action->next) {
/* META action overrides any user SEEN or SUB/UNSUB action
for same user */
remove_meta(action->user, seen_list);
remove_meta(action->user, sub_list);
}
/* And then run tasks. */
if (hash_numrecords(&user_mailboxes)) {
struct split_user_mailboxes_rock smrock;
smrock.sync_cs = sync_cs;
smrock.user_list = user_list;
smrock.r = 0;
/* process user_mailboxes in sets of ~1000, splitting only on
* user boundaries */
hash_enumerate(&user_mailboxes, split_user_mailboxes, &smrock);
r = smrock.r;
if (r) goto cleanup;
}
for (action = quota_list->head; action; action = action->next) {
if (!action->active)
continue;
r = sync_do_quota(sync_cs, action->name);
if (sync_cs->channel && r == IMAP_MAILBOX_LOCKED) {
sync_log_channel_quota(sync_cs->channel, action->name);
report_verbose(" Deferred: QUOTA %s\n", action->name);
}
else if (r) {
sync_action_list_add(user_list, action->name, NULL);
report_verbose(" Promoting: QUOTA %s -> USER %s\n",
action->name, action->name);
}
}
for (action = annot_list->head; action; action = action->next) {
if (!action->active)
continue;
/* NOTE: ANNOTATION "" is a special case - it's a server
* annotation, hence the check for a character at the
* start of the name */
r = sync_do_annotation(sync_cs, action->name);
if (!*action->name) continue;
if (sync_cs->channel && r == IMAP_MAILBOX_LOCKED) {
sync_log_channel_annotation(sync_cs->channel, action->name);
report_verbose(" Deferred: ANNOTATION %s\n", action->name);
}
else if (r) {
sync_action_list_add(user_list, action->name, NULL);
report_verbose(" Promoting: ANNOTATION %s -> USER %s\n",
action->name, action->name);
}
}
for (action = seen_list->head; action; action = action->next) {
if (!action->active)
continue;
r = sync_do_seen(sync_cs, action->user, action->name);
if (sync_cs->channel && r == IMAP_MAILBOX_LOCKED) {
sync_log_channel_seen(sync_cs->channel, action->user, action->name);
report_verbose(" Deferred: SEEN %s %s\n",
action->user, action->name);
}
else if (r) {
char *userid = mboxname_to_userid(action->name);
if (userid && mboxname_isusermailbox(action->name, 1) && !strcmp(userid, action->user)) {
sync_action_list_add(user_list, NULL, action->user);
report_verbose(" Promoting: SEEN %s %s -> USER %s\n",
action->user, action->name, action->user);
} else {
sync_action_list_add(meta_list, NULL, action->user);
report_verbose(" Promoting: SEEN %s %s -> META %s\n",
action->user, action->name, action->user);
}
free(userid);
}
}
for (action = sub_list->head; action; action = action->next) {
if (!action->active)
continue;
r = user_sub(sync_cs, action->user, action->name);
if (sync_cs->channel && r == IMAP_MAILBOX_LOCKED) {
sync_log_channel_subscribe(sync_cs->channel, action->user, action->name);
report_verbose(" Deferred: SUB %s %s\n",
action->user, action->name);
}
else if (r) {
sync_action_list_add(meta_list, NULL, action->user);
report_verbose(" Promoting: SUB %s %s -> META %s\n",
action->user, action->name, action->user);
}
}
/* XXX - is unmailbox used much anyway - we need to see if it's logged for a rename,
* e.g.
* RENAME A B:
* MAILBOX A
* MAILBOX B
* UNMAILBOX A
*
* suggestion: PROMOTE ALL UNMAILBOX on user accounts to USER foo
*/
for (action = unmailbox_list->head; action; action = action->next) {
if (!action->active)
continue;
r = do_unmailbox(sync_cs, action->name);
if (sync_cs->channel && r == IMAP_MAILBOX_LOCKED) {
sync_log_channel_unmailbox(sync_cs->channel, action->name);
report_verbose(" Deferred: UNMAILBOX %s\n", action->name);
}
else if (r) goto cleanup;
}
for (action = meta_list->head; action; action = action->next) {
if (!action->active)
continue;
r = sync_do_meta(sync_cs, action->user);
if (sync_cs->channel && r == IMAP_MAILBOX_LOCKED) {
sync_log_channel_sieve(sync_cs->channel, action->user);
report_verbose(" Deferred: META %s\n", action->user);
}
else if (r == IMAP_INVALID_USER) {
goto cleanup;
}
else if (r) {
sync_action_list_add(user_list, NULL, action->user);
report_verbose(" Promoting: META %s -> USER %s\n",
action->user, action->user);
}
}
for (action = user_list->head; action; action = action->next) {
if (!action->active)
continue;
r = sync_do_user(sync_cs, action->user, NULL);
if (sync_cs->channel && r == IMAP_MAILBOX_LOCKED) {
sync_log_channel_user(sync_cs->channel, action->user);
report_verbose(" Deferred: USER %s\n", action->user);
}
else if (r) goto cleanup;
r = sync_do_restart(sync_cs);
if (r) goto cleanup;
}
for (action = unuser_list->head; action; action = action->next) {
if (!action->active)
continue;
r = do_unuser(sync_cs, action->user);
if (sync_cs->channel && r == IMAP_MAILBOX_LOCKED) {
sync_log_channel_unuser(sync_cs->channel, action->user);
report_verbose(" Deferred: UNUSER %s\n", action->user);
}
else if (r) goto cleanup;
}
cleanup:
if (r) {
report_verbose_error("Error in do_sync(): bailing out! %s", error_message(r));
}
sync_action_list_free(&user_list);
sync_action_list_free(&unuser_list);
sync_action_list_free(&meta_list);
sync_action_list_free(&unmailbox_list);
sync_action_list_free(&quota_list);
sync_action_list_free(&annot_list);
sync_action_list_free(&seen_list);
sync_action_list_free(&sub_list);
free_hash_table(&user_mailboxes, sync_action_list_free_wrapper);
return r;
}
EXPORTED int sync_connect(struct sync_client_state *sync_cs)
{
sasl_callback_t *cb;
int timeout;
const char *port, *auth_status = NULL;
int try_imap;
struct backend *backend = sync_cs->backend;
int verbose = (sync_cs->flags & SYNC_FLAG_VERBOSE);
sync_cs->backend = NULL;
buf_free(&sync_cs->tagbuf);
cb = mysasl_callbacks(NULL,
sync_get_config(sync_cs->channel, "sync_authname"),
sync_get_config(sync_cs->channel, "sync_realm"),
sync_get_config(sync_cs->channel, "sync_password"));
/* get the right port */
port = sync_get_config(sync_cs->channel, "sync_port");
if (port) {
imap_csync_protocol.service = port;
csync_protocol.service = port;
}
try_imap = sync_get_switchconfig(sync_cs->channel, "sync_try_imap");
if (try_imap) {
backend = backend_connect(backend, sync_cs->servername,
&imap_csync_protocol, "", cb, &auth_status,
(verbose > 1 ? fileno(stderr) : -1));
if (backend) {
if (backend->capability & CAPA_REPLICATION) {
/* attach our IMAP tag buffer to our protstreams as userdata */
backend->in->userdata = backend->out->userdata = &sync_cs->tagbuf;
goto connected;
}
else {
backend_disconnect(backend);
backend = NULL;
}
}
}
backend = backend_connect(backend, sync_cs->servername,
&csync_protocol, "", cb, NULL,
(verbose > 1 ? fileno(stderr) : -1));
// auth_status means there was an error
if (!backend) return IMAP_AGAIN;
connected:
free_callbacks(cb);
cb = NULL;
if (sync_cs->servername[0] != '/' && backend->sock >= 0) {
tcp_disable_nagle(backend->sock);
tcp_enable_keepalive(backend->sock);
}
#ifdef HAVE_ZLIB
/* Does the backend support compression? */
if (CAPA(backend, CAPA_COMPRESS)) {
prot_printf(backend->out, "%s\r\n",
backend->prot->u.std.compress_cmd.cmd);
prot_flush(backend->out);
if (sync_parse_response("COMPRESS", backend->in, NULL)) {
syslog(LOG_NOTICE, "Failed to enable compression, continuing uncompressed");
}
else {
prot_setcompress(backend->in);
prot_setcompress(backend->out);
}
}
#endif
/* Set inactivity timer */
timeout = config_getduration(IMAPOPT_SYNC_TIMEOUT, 's');
if (timeout < 3) timeout = 3;
prot_settimeout(backend->in, timeout);
/* Force use of LITERAL+ so we don't need two way communications */
prot_setisclient(backend->in, 1);
prot_setisclient(backend->out, 1);
sync_cs->backend = backend;
return 0;
}
EXPORTED void sync_disconnect(struct sync_client_state *sync_cs)
{
if (!sync_cs->backend) return;
if (sync_cs->backend->timeout)
prot_removewaitevent(sync_cs->clientin, sync_cs->backend->timeout);
sync_cs->clientin = NULL;
sync_cs->backend->timeout = NULL;
backend_disconnect(sync_cs->backend);
// backend may have put stuff here, free it so we don't leak memory
buf_free(&sync_cs->tagbuf);
// drop any cache database too
if (sync_cs->cachedb) {
cyrusdb_close(sync_cs->cachedb);
sync_cs->cachedb = NULL;
}
}
static struct prot_waitevent *
sync_rightnow_timeout(struct protstream *s __attribute__((unused)),
struct prot_waitevent *ev __attribute__((unused)),
void *rock __attribute__((unused)))
{
syslog(LOG_DEBUG, "sync_rightnow_timeout()");
/* too long since we last used the syncer - disconnect */
sync_disconnect(&rightnow_sync_cs);
free(rightnow_sync_cs.backend);
rightnow_sync_cs.backend = NULL;
return NULL;
}
EXPORTED int sync_checkpoint(struct protstream *clientin)
{
struct buf *buf = sync_log_rightnow_buf();
if (!buf) return 0;
time_t when = time(NULL) + 30;
if (rightnow_sync_cs.backend) {
if (rightnow_sync_cs.backend->timeout->mark) {
rightnow_sync_cs.backend->timeout->mark = when;
}
}
else {
const char *conf = config_getstring(IMAPOPT_SYNC_RIGHTNOW_CHANNEL);
if (conf && strcmp(conf, "\"\""))
rightnow_sync_cs.channel = conf;
rightnow_sync_cs.servername = sync_get_config(rightnow_sync_cs.channel, "sync_host");
rightnow_sync_cs.flags = SYNC_FLAG_LOGGING;
syslog(LOG_DEBUG, "sync_rightnow_connect(%s)", rightnow_sync_cs.servername);
sync_connect(&rightnow_sync_cs);
if (!rightnow_sync_cs.backend) {
syslog(LOG_ERR, "SYNCERROR sync_rightnow: failed to connect to server: %s",
rightnow_sync_cs.servername);
// dammit, but the show must go on
buf_reset(buf);
return 0;
}
rightnow_sync_cs.clientin = clientin;
rightnow_sync_cs.backend->timeout
= prot_addwaitevent(clientin, when, sync_rightnow_timeout, NULL);
}
sync_log_reader_t *slr = sync_log_reader_create_with_content(buf_cstring(buf));
int r = sync_log_reader_begin(slr);
if (!r) r = sync_do_reader(&rightnow_sync_cs, slr);
if (r) {
syslog(LOG_ERR, "SYNCERROR sync_rightnow: error syncing to: %s (%s)",
rightnow_sync_cs.servername, error_message(r));
}
sync_log_reader_end(slr);
sync_log_reader_free(slr);
// mark these items consumed!
buf_reset(buf);
return 0;
}
diff --git a/lib/imapoptions b/lib/imapoptions
index 6890b12e8..28973d18a 100644
--- a/lib/imapoptions
+++ b/lib/imapoptions
@@ -1,3060 +1,3066 @@
# things inside of C comments get copied to the manpage
# things starting with # are ignored
/* .\" -*- nroff -*-
.TH IMAPD.CONF 5 "Project Cyrus" CMU
.\"
.\" 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.
.SH NAME
imapd.conf \- IMAP configuration file
.SH DESCRIPTION
\fB/etc/imapd.conf\fR
is the configuration file for the Cyrus IMAP server. It defines
local parameters for IMAP.
.PP
Each line of the \fB/etc/imapd.conf\fR file has the form
.IP
\fIoption\fR: \fIvalue\fR
.PP
where \fIoption\fR is the name of the configuration option being set
and \fIvalue\fR is the value that the configuration option is being
set to.
.PP
Although there is no limit to the length of a line, a ``\\''
(backslash) character may be used as the last character on a line to
force it to continue on the next one. No additional whitespace is
inserted before or after the ``\\''. Note that a line that is split
using ``\\'' character(s) is still considered a single line.
For example
.IP
\fIoption\fR:\\
.br
.in +1
\fIvalue\fR1 \fIvalue\fR2 \\
.br
.in +1
\fIvalue\fR3
.PP
is equivalent to
.IP
\fIoption\fR: \fIvalue\fR1 \fIvalue\fR2 \fIvalue\fR3
.PP
Blank lines and lines beginning with ``#'' are ignored.
.PP
For boolean and enumerated options, the values ``yes'', ``on'', ``t'',
``true'' and ``1'' turn the option on, the values ``no'', ``off'',
``f'', ``false'' and ``0'' turn the option off.
.PP
Duration options take the form of a number followed by a unit, for example
\fB32m\fR (32 minutes). Units are \fBd\fR (days), \fBh\fR (hours), \fBm\fR
(minutes) and \fBs\fR (seconds). Multiple units can be combined and will
be summed together, for example \fB1h30m\fR is equivalent to \fB90m\fR. If
no unit is specified, an option-specific backward-compatible default unit
is assumed (documented on an option-by-option basis). These are simple time
units: 1d=24h, 1h=60m, 1m=60s (daylight savings, timezones, leap adjustments,
etc are not considered).
.SH FIELD DESCRIPTIONS
.PP
The sections below detail options that can be placed in the
\fB/etc/imapd.conf\fR file, and show each option's default value.
Some options have no default value, these are listed with
``<no default>''. Some options default to the empty string, these
are listed with ``<none>''.
*/
# OPTIONS
{ "addressbookprefix", "#addressbooks", STRING, "2.5.0" }
/* The prefix for the addressbook mailboxes hierarchies. The hierarchy
delimiter will be automatically appended. The public addressbook
hierarchy will be at the toplevel of the shared namespace. A
user's personal addressbook hierarchy will be a child of their Inbox. */
{ "admins", "", STRING, "2.3.17" }
/* The list of userids with administrative rights. Separate each userid
with a space. Sites using Kerberos authentication may use
separate "admin" instances.
.PP
Note that accounts used by users should not be administrators.
Administrative accounts should not receive mail. That is, if user
"jbRo" is a user reading mail, he should not also be in the admins line.
Some problems may occur otherwise, most notably the ability of
administrators to create top-level mailboxes visible to users,
but not writable by users. */
{ "afspts_localrealms", NULL, STRING, "2.3.17" }
/* The list of realms which are to be treated as local, and thus stripped
during identifier canonicalization (for the AFSPTS ptloader module).
This is different from loginrealms in that it occurs later in the
authorization process (as the user id is canonified for PTS lookup) */
{ "afspts_mycell", NULL, STRING, "2.3.17" }
/* Cell to use for AFS PTS lookups. Defaults to the local cell. */
{ "allowallsubscribe", 0, SWITCH, "2.3.17" }
/* Allow subscription to nonexistent mailboxes. This option is
typically used on backend servers in a Murder so that users can
subscribe to mailboxes that don't reside on their "home" server.
This option can also be used as a workaround for IMAP clients which
don't play well with nonexistent or unselectable mailboxes (e.g.,
Microsoft Outlook). */
{ "allowanonymouslogin", 0, SWITCH, "2.3.17" }
/* Permit logins by the user "anonymous" using any password. Also
allows use of the SASL ANONYMOUS mechanism. */
{ "allowapop", 1, SWITCH, "2.3.17" }
/* Allow use of the POP3 APOP authentication command.
.PP
Note that this command requires that SASL is compiled with APOP
support, that the plaintext passwords are available in a SASL auxprop
backend (e.g., sasldb), and that the system can provide enough entropy
(e.g., from /dev/urandom) to create a challenge in the banner. */
{ "allowdeleted", 0, SWITCH, "3.1.8" }
/* Allow access to deleted and expunged data via vendor.cmu-* access */
{ "allownewnews", 0, SWITCH, "2.3.17" }
/* Allow use of the NNTP NEWNEWS command.
.PP
Note that this is a very expensive command and should only be
enabled when absolutely necessary. */
{ "allowplaintext", 0, SWITCH, "3.0.0" }
/* If enabled, allows the use of cleartext passwords on the wire.
.PP
By default, the use of cleartext passwords requires a TLS/SSL
encryption layer to be negotiated prior to any cleartext
authentication mechanisms being advertised or allowed. To require a
TLS/SSL encryption layer to be negotiated prior to ANY
authentication, see the \fItls_required\fR option. */
{ "allowsetacl", 1, SWITCH, "3.1.8" }
/* Defaults to enabled. If disabled, disallows the use of the SETACL
command at all via IMAP. */
{ "allowusermoves", 0, SWITCH, "2.3.17" }
/* Allow moving user accounts (with associated meta-data) via RENAME
or XFER.
.PP
Note that measures should be taken to make sure that the user being
moved is not logged in, and cannot login during the move. Failure
to do so may result in the user's meta-data (seen state,
subscriptions, etc) being corrupted or out of date. */
{ "altnamespace", 1, SWITCH, "3.0.0" }
/* Use the alternate IMAP namespace, where personal folders reside at the
same level in the hierarchy as INBOX.
.PP
This option ONLY applies where interaction takes place with the
client/user. Currently this is limited to the IMAP protocol (imapd)
and Sieve scripts (lmtpd). This option does NOT apply to admin tools
such as cyradm (admins ONLY), reconstruct, quota, etc., NOR does it
affect LMTP delivery of messages directly to mailboxes via
plus-addressing. The default changed in 3.0 from off to on. */
{ "altprefix", "Alt Folders", STRING, "3.0.0" }
/* Alternative INBOX spellings that can't be accessed in altnamespace
otherwise go under here */
{ "annotation_db", "twoskip", STRINGLIST("skiplist", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for mailbox annotations. */
{ "annotation_db_path", NULL, STRING, "2.5.0" }
/* The absolute path to the annotations db file. If not specified,
will be configdirectory/annotations.db */
{ "anyoneuseracl", 1, SWITCH, "2.3.17" }
/* Should non-admin users be allowed to set ACLs for the 'anyone'
user on their mailboxes? In a large organization this can cause
support problems, but it's enabled by default. */
{ "annotation_allow_undefined", 0, SWITCH, "2.5.0" }
/* Allow clients to store values for entries which are not
defined either by Cyrus or in the annotations_definitions
file. */
{ "annotation_definitions", NULL, STRING, "2.5.0" }
/* File containing external (third-party) annotation definitions.
.PP
Each line of the file specifies the properties of an annotation and
has the following form:
.IP
\fIname\fR, \fIscope\fR, \fIattrib-type\fR, \fIproxy-type\fR,
\fIattrib-names\fR, \fIacl\fR
.\"
.IP \fIname\fR 5
is the hierarchical name as in RFC 5257 or RFC 5464 (in the latter case,
without the leading \fB/shared\fR or \fB/private\fR). For example,
/vendor/acme/blurdybloop.
.\"
.IP \fIscope\fR 5
specifies whether the annotation is for the \fBserver\fR, a
\fBmailbox\fR, or a \fBmessage\fR.
.\"
.IP \fIattrib-type\fR 5
.RS 5
specifies the attribute data type, which is used only to check the
string value passed by clients when setting annotations. The
\fIattrib-type\fR is one of:
.\"
.IP \fBstring\fR 5
any value is accepted.
.\"
.IP \fBcontent-type\fR 5
this obsolete data type, which was useful for early drafts of the standard,
is accepted but silently translated to \fBstring\fR.
.\"
.IP \fBboolean\fR 5
only the strings "true" or "false" are accepted. Checking is
case-insensitive but the value is forced to lowercase.
.\"
.IP \fBint\fR 5
integers are accepted.
.\"
.IP \fBuint\fR 5
non-negative integers are accepted.
.\"
.RE
.\"
.IP \fIproxy-type\fR 5
specifies whether this attribute is for the \fBbackend\fR or
\fBproxy\fR servers or both (\fBproxy_and_backend\fR)
.\"
.IP \fIattrib-names\fR 5
is the space-separated list of available attributes for the
annotation. Possible attribute names are \fBvalue.shared\fR,
\fBvalue.priv\fR, and \fBvalue\fR (which permits both \fBvalue.priv\fR
and \fBvalue.shared\fR). The attribute names \fBsize\fR,
\fBsize.shared\fR, and \fBsize.priv\fR are accepted but ignored; these
attributes are automatically provided by the server if the corresponding
\fBvalue\fR attribute is specified. Some obsolete attributes, which were
defined early drafts of the standard, are accepted and ignored with a
warning.
.\"
.IP \fIextra-permissions\fR 5
is the extra ACL permission bits required for setting this annotation, in
standard IMAP ACL permission bit string format. Note that this is
in addition to the permission bits specified in RFC 5257 and RFC 5464,
so leaving this field empty is harmless. Note also that there is no way
to specify that an annotation can only be set by an admin user; in
particular the \fBa\fP permission bit does not achieve this.
.PP
Blank lines and lines beginning with ``#'' are ignored.
*/
{ "annotation_callout", NULL, STRING, "2.5.0" }
/* The pathname of a callout to be used to automatically add annotations
or flags to a message when it is appended to a mailbox. The path can
be either an executable (including a script), or a UNIX domain
socket. */
{ "annotation_callout_disable_append", 0, SWITCH, "3.1.2" }
/* Disables annotations on append with xrunannotator */
{ "annotation_enable_legacy_commands", 0, SWITCH, "3.1.6" }
/* Whether to enable the legacy GETANNOTATION/SETANNOTATION commands.
These commands are deprecated and will be removed in the future,
but might be useful in the meantime for supporting old clients that
do not implement the RFC 5464 IMAP METADATA extension. */
{ "aps_topic", NULL, STRING, "3.0.0" }
/* Topic for Apple Push Service registration. */
{ "aps_topic_caldav", NULL, STRING, "3.0.0" }
/* Topic for Apple Push Service registration for CalDAV. */
{ "aps_topic_carddav", NULL, STRING, "3.0.0" }
/* Topic for Apple Push Service registration for CardDAV. */
{ "archive_enabled", 0, SWITCH, "3.0.0" }
/* Is archiving enabled for this server. You also need to have an
archivepartition for the mailbox. Archiving allows older email
to be stored on slower, cheaper disks - even within the same
mailbox, as distinct from partitions. */
{ "archive_days", NULL, DURATION, "3.1.8", "3.1.8", "archive_after" }
/* Deprecated in favour of \fIarchive_after\fR. */
{ "archive_after", "7d", DURATION, "3.1.8" }
/* The duration after which to move messages to the archive partition
if archiving is enabled.
.PP
For backward compatibility, if no unit is specified, days is
assumed. */
*/
{ "archive_maxsize", 1024, INT, "3.0.0" }
/* The size in kilobytes of the largest message that won't be archived
immediately. Default is 1Mb */
{ "archive_keepflagged", 0, SWITCH, "3.0.0" }
/* If set, messages with the \\Flagged system flag won't be archived,
provided they are smaller than \fBarchive_maxsize\fR. */
# Commented out - there's no such thing as "archivepartition-name",
# but we need this for the man page
# { "archivepartition-name", NULL, STRING, "3.0.0" }
/* The pathname of the archive partition \fIname\fR, corresponding to
spool partition \fBpartition-name\fR. For any mailbox residing in
a directory on \fBpartition-name\fR, the archived messages will be
stored in a corresponding directory on \fBarchivepartition-name\fR.
Note that not every \fBpartition-name\fR option is strictly required
to have a corresponding \fBarchivepartition-name\fR option, but that
without one there's no benefit to enabling archiving. */
{ "auditlog", 0, SWITCH, "2.4.0" }
/* Should cyrus output log entries for every action taken on a message
file or mailboxes list entry? It's noisy so disabled by default, but
can be very useful for tracking down what happened if things look strange */
{ "auth_mech", "unix", STRINGLIST("unix", "pts", "krb", "krb5"), "2.3.17" }
/* The authorization mechanism to use. */
{ "autocreateinboxfolders", NULL, STRING, "2.5.0", "2.5.0", "autocreate_inbox_folders" }
/* Deprecated in favor of \fIautocreate_inbox_folders\fR. */
{ "autocreatequota", 0, INT, "2.5.0", "2.5.0", "autocreate_quota" }
/* Deprecated in favor of \fIautocreate_quota\fR. */
{ "autocreatequotamsg", -1, INT, "2.5.0", "2.5.0", "autocreate_quota_messages" }
/* Deprecated in favor of \fIautocreate_quota_messages\fR. */
{ "autosievefolders", NULL, STRING, "2.5.0", "2.5.0", "autocreate_sieve_folders" }
/* Deprecated in favor of \fIautocreate_sieve_folders\fR. */
{ "generate_compiled_sieve_script", 0, SWITCH, "2.5.0", "2.5.0", "autocreate_sieve_script_compile" }
/* Deprecated in favor of \fIautocreate_sieve_script_compile\fR. */
{ "autocreate_sieve_compiled_script", NULL, STRING, "2.5.0", "2.5.0", "autocreate_sieve_script_compiled" }
/* Deprecated in favor of \fIautocreate_sieve_script_compiled\fR. */
{ "autosubscribeinboxfolders", NULL, STRING, "2.5.0", "2.5.0", "autocreate_subscribe_folders" }
/* Deprecated in favor of \fIautocreate_subscribe_folders\fR. */
{ "autosubscribesharedfolders", NULL, STRING, "2.5.0", "2.5.0", "autocreate_subscribe_sharedfolders" }
/* Deprecated in favor of \fIautocreate_subscribe_sharedfolders\fR. */
{ "autosubscribe_all_sharedfolders", 0, SWITCH, "2.5.0", "2.5.0", "autocreate_subscribe_sharedfolders_all" }
/* Deprecated in favor of \fIautocreate_subscribe_sharedfolders_all\fR. */
# Commented out - there's no single setting "autocreate_acl",
# but we need this for the man page
# { "autocreate_acl", NULL, STRING, "3.2.0" }
/* If folders are to be created by \fIautocreate_inbox_folders\fR, this
setting can be used to apply additional ACLs to the autocreated
folders. The syntax is "autocreate_acl folder identifier rights",
where \fIfolder\fR must match one of the \fIautocreate_inbox_folders\fR
folders, \fIidentifier\fR must be a valid cyrus identifier, and
\fIrights\fR must be a valid cyrus rights string. Multiple
identifier|rights pairs can be assigned to a single folder by providing
this setting multiple times.
.PP
For example, "autocreate_acl Plus anyone p" would allow lmtp delivery
to a folder named "Plus".
*/
{ "autocreate_inbox_folders", NULL, STRING, "2.5.0" }
/* If a user does not have an INBOX already, and the INBOX is to be
created, create the list of folders in this setting as well.
\fIautocreate_inbox_folders\fR is a list of INBOX's subfolders
separated by a "|", that are automatically created by the server
under the following two scenarios. Leading and trailing whitespace is
stripped, so "Junk | Trash" results in two folders: "Junk" and
"Trash". See also the \fIxlist-flag\fR option, for setting
special-use flags on autocreated folders.
.PP
INBOX folders are created under both the following conditions:
.\"
.IP 1.
The user logins via the IMAP or the POP3 protocol.
\fIautocreate_quota\fR option must have a value of zero or greater.
.\"
.IP 2.
A message arrives for the user through the \fIlmtpd(8)\fR.
\fIautocreate_post\fR option must be enabled.
.PP
*/
{ "autocreate_post", 0, SWITCH, "2.5.0" }
/* If enabled, when \fIlmtpd(8)\fR receives an incoming mail for an
INBOX that does not exist, then the INBOX is automatically created
by \fIlmtpd(8)\fR and delivery of the message continues. */
{ "autocreate_quota", -1, INT, "2.5.0" }
/* If set to a value of zero or higher, users have their INBOX folders
created upon a successful login event or upon \fIlmtpd(8)\fR
message delivery if \fIautocreate_post\fR is enabled, provided their
INBOX did not yet already exist.
.PP
The user's quota is set to the value if it is greater than zero,
otherwise the user has unlimited quota.
.PP
Note that quota is specified in kilobytes. */
{ "autocreate_quota_messages", -1, INT, "3.0.0" }
/* If set to a value of zero or higher, users who have their INBOX
folders created upon a successful login event (see
\fIautocreate_quota\fR), or upon \fIlmtpd(8)\fR message delivery if
\fIautocreate_post\fR is enabled, receive the message quota
configured in this option.
.PP
The default of -1 disables assigning message quota.
.PP
For consistency with \fIautocreate_quota\fR, a value of zero is treated
as unlimited message quota, rather than a message quota of zero. */
{ "autocreate_sieve_folders", NULL, STRING, "2.5.0" }
/* A "|" separated list of subfolders of INBOX that will be
automatically created, if requested by a sieve filter, through the
"fileinto" action. The default is to create no folders
automatically.
.PP
Leading and trailing whitespace is stripped from each folder, so a
setting of "Junk | Trash" will create two folders: "Junk" and
"Trash". */
{ "autocreate_sieve_script", NULL, STRING, "2.5.0" }
/* The full path of a file that contains a sieve script. This script
automatically becomes a user's initial default sieve filter script.
.PP
When this option is not defined, no default sieve filter is created.
The file must be readable by the Cyrus daemon. */
{ "autocreate_sieve_script_compile", 0, SWITCH, "2.5.0" }
/* If set to yes and no compiled sieve script file exists, the sieve script which is
compiled on the fly will be saved in the file name that autocreate_sieve_compiledscript
option points to. In order a compiled script to be generated, autocreate_sieve_script and
autocreate_sieve_compiledscript must have valid values */
{ "autocreate_sieve_script_compiled", NULL, STRING, "2.5.0" }
/* The full path of a file that contains a compiled in bytecode sieve script. This script
automatically becomes a user's initial default sieve filter script. If this option is
not specified, or the filename doesn't exist then the script defined by
autocreate_sieve_script is compiled on the fly and installed as the user's default
sieve script */
{ "autocreate_subscribe_folders", NULL, STRING, "2.5.0" }
/* A list of folder names, separated by "|", that the users get automatically subscribed to,
when their INBOX is created. These folder names must have been included in the
autocreateinboxfolders option of the imapd.conf. */
{ "autocreate_subscribe_sharedfolders", NULL, STRING, "2.5.0" }
/* A list of shared folders (bulletin boards), separated by "|", that the users get
automatically subscribed to, after their INBOX is created. The shared folder must
have been created and the user must have the required permissions to get subscribed
to it. Otherwise, subscribing to the shared folder fails. */
{ "autocreate_subscribe_sharedfolders_all", 0, SWITCH, "2.5.0" }
/* If set to yes, the user is automatically subscribed to all shared folders, one has permission
to subscribe to. */
{ "autocreate_users", "anyone", STRING, "2.5.0" }
/* A space separated list of users and/or groups that are allowed their INBOX to be
automatically created. */
{ "autoexpunge", 0, SWITCH, "3.1.7" }
/* If set to yes, then all \Deleted messages will be automatically expunged whenever
an index is closed, whether CLOSE, UNSELECT, SELECT or on disconnect */
# Commented out - there's no such thing as "backuppartition-name",
# but we need this for the man page
# { "backuppartition-name", NULL, STRING, "3.0.0" }
/* The pathname of the backup partition \fIname\fR. At least one backup
partition pathname MUST be specified if backups are in use. Note that
there is no relationship between spool partitions and backup partitions. */
{ "backup_compact_minsize", 0, INT, "3.0.0" }
/* The minimum size in kilobytes of chunks in each backup. The compact tool
will try to combine adjacent chunks that are smaller than this.
.PP
Setting this value to zero or negative disables combining of chunks. */
{ "backup_compact_maxsize", 0, INT, "3.0.0" }
/* The maximum size in kilobytes of chunks in each backup. The compact tool
will try to split chunks larger than this into smaller chunks.
.PP
Setting this value to zero or negative disables splitting of chunks. */
{ "backup_compact_work_threshold", 1, INT, "3.0.0" }
/* The number of chunks that must obviously need compaction before the compact
tool will go ahead with the compaction. If set to less than one, the value
is treated as being one. */
{ "backup_staging_path", NULL, STRING, "3.0.0" }
/* The absolute path of the backup staging area. If not specified,
will be temp_path/backup */
{ "backup_retention_days", NULL, DURATION, "3.1.8", "3.1.8", "backup_retention" }
/* Deprecated in favor of \fIbackup_retention\fR. */
{ "backup_retention", "7d", DURATION, "3.1.0" }
/* How long to keep content in backup after it has been deleted
from the source. If set to a negative value or zero, deleted content
will be kept indefinitely.
.PP
For backward compatibility, if no unit is specified, days is
assumed. */
{ "backup_db", "twoskip", STRINGLIST("skiplist", "sql", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the backup locations database. */
{ "backup_db_path", NULL, STRING, "3.0.0" }
/* The absolute path to the backup db file. If not specified,
will be configdirectory/backups.db */
{ "backup_keep_previous", 0, SWITCH, "3.0.0" }
/* Whether the \fBctl_backups compact\fR and \fBctl_backups reindex\fR
commands should preserve the original file. The original file will
be named with a timestamped suffix. This is mostly useful for
debugging.
.PP
Note that with this enabled, compacting a backup will actually
increase the disk used by it (because there will now be an extra
copy: the original version, and the compacted version). */
{ "boundary_limit", 1000, INT, "2.5.0" }
/* messages are parsed recursively and a deep enough MIME structure
can cause a stack overflow. Do not parse deeper than this many
layers of MIME structure. The default of 1000 is much higher
than any sane message should have. */
{ "caldav_allowattach", 1, SWITCH, "3.0.0" }
/* Enable managed attachments support on the CalDAV server. */
{ "caldav_allowcalendaradmin", 0, SWITCH, "3.1.2" }
/* Enable per-user calendar administration web UI on the CalDAV server. */
{ "caldav_allowscheduling", "on", ENUM("off", "on", "apple"), "2.5.0" }
/* Enable calendar scheduling operations. If set to "apple", the
server will emulate Apple CalendarServer behavior as closely as
possible. */
{ "caldav_create_attach", 1, SWITCH, "3.0.0" }
/* Create the 'Attachments' collection if it doesn't already exist */
{ "caldav_create_default", 1, SWITCH, "3.0.0" }
/* Create the 'Default' calendar if it doesn't already exist */
{ "caldav_create_sched", 1, SWITCH, "3.0.0" }
/* Create the 'Inbox' and 'Outbox' calendars if they don't already exist */
{ "caldav_historical_age", "7d", DURATION, "3.1.8" }
/* How long after an occurrence of event or task has concluded
that it is considered 'historical'. Changes to historical
occurrences of events or tasks WILL NOT have invite or reply
messages sent for them. A negative value means that events
and tasks are NEVER considered historical.
.PP
For backward compatibility, if no unit is specified, days is
assumed. */
*/
{ "caldav_maxdatetime", "20380119T031407Z", STRING, "2.5.0" }
/* The latest date and time accepted by the server (ISO format). This
value is also used for expanding non-terminating recurrence rules.
.PP
Note that increasing this value will require the DAV databases for
calendars to be reconstructed with the \fBdav_reconstruct\fR
utility in order to see its effect on serer-side time-based
queries. */
{ "caldav_mindatetime", "19011213T204552Z", STRING, "2.5.0" }
/* The earliest date and time accepted by the server (ISO format). */
{ "caldav_realm", NULL, STRING, "2.5.0" }
/* The realm to present for HTTP authentication of CalDAV resources.
If not set (the default), the value of the "servername" option will
be used.*/
{ "calendarprefix", "#calendars", STRING, "2.5.0" }
/* The prefix for the calendar mailboxes hierarchies. The hierarchy
delimiter will be automatically appended. The public calendar
hierarchy will be at the toplevel of the shared namespace. A
user's personal calendar hierarchy will be a child of their Inbox. */
{ "calendar_default_displayname", "personal", STRING, "3.3.0" }
/* The displayname to be used when creating a user's 'Default' calendar. */
{ "calendar_user_address_set", NULL, STRING, "2.5.0" }
/* Space-separated list of domains corresponding to calendar user
addresses for which the server is responsible. If not set (the
default), the value of the "servername" option will be used. */
{ "calendar_component_set", "VEVENT VTODO VJOURNAL VFREEBUSY VAVAILABILITY VPOLL", BITFIELD("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY", "VPOLL"), "3.1.7" }
/* Space-separated list of iCalendar component types that calendar
object resources may contain in a calendar collection.
This restriction is only set at calendar creation time and only
if the CalDAV client hasn't specified a restriction in the creation
request. */
{ "carddav_allowaddmember", 0, SWITCH, "3.1.3" }
/* Enable support for POST add-member on the CardDAV server. */
{ "carddav_allowaddressbookadmin", 0, SWITCH, "3.1.2" }
/* Enable per-user addressbook administration web UI on the CardDAV server. */
{ "carddav_realm", NULL, STRING, "2.5.0" }
/* The realm to present for HTTP authentication of CardDAV resources.
If not set (the default), the value of the "servername" option will
be used.*/
{ "carddav_repair_vcard", 0, SWITCH, "3.0.0", "3.3.1" }
/* If enabled, VCARDs with invalid content are attempted to be repaired
during creation. */
{ "chatty", 0, SWITCH, "2.5.0" }
/* If yes, syslog tags and commands for every IMAP command, mailboxes
for every lmtp connection, every POP3 command, etc */
{ "client_bind", 0, SWITCH, "3.0.0" }
/* If enabled, a specific IP will be bound when performing a client
connection. \fBclient_bind_name\fR is used if it is set, otherwise
\fBservername\fR is used. This is useful on multi-homed servers where
Cyrus should not use other services' interfaces.
.PP
If not enabled (the default), no bind will be performed. Client
connections will use an IP chosen by the operating system. */
{ "client_bind_name", NULL, STRING, "3.0.0" }
/* IPv4, IPv6 address or hostname to bind for client connections when
\fBclient_bind\fR is enabled. If not set (the default),
\fRservername\fR will be used. */
{ "client_timeout", "10s", DURATION, "3.1.8" }
/* Time to wait before returning a timeout failure when performing a
client connection (e.g. in a murder environment).
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "commandmintimer", NULL, STRING, "2.4.0" }
/* Time in seconds. Any imap command that takes longer than this
time is logged. */
{ "configdirectory", NULL, STRING, "2.3.17" }
/* The pathname of the IMAP configuration directory. This field is
required. */
{ "createonpost", 0, SWITCH, "2.5.0", "2.5.0", "autocreate_post" }
/* Deprecated in favor of \fIautocreate_post\fR. */
{ "conversations", 0, SWITCH, "3.0.0" }
/* Enable the XCONVERSATIONS extensions. Extract conversation
tracking information from incoming messages and track them
in per-user databases. */
{ "conversations_counted_flags", NULL, STRING, "3.0.0" }
/* space-separated list of flags for which per-conversation counts
will be kept. Note that you need to reconstruct the conversations
database with ctl_conversationsdb if you change this option on a
running server, or the counts will be wrong. */
{ "conversations_db", "skiplist", STRINGLIST("skiplist", "sql", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the per-user conversations database. */
{ "conversations_expire_days", NULL, DURATION, "3.1.8", "3.1.8", "conversations_expire_after" }
/* Deprecated in favor of \fIconversations_expire_after\fR. */
{ "conversations_expire_after", "90d", DURATION, "3.1.8" }
/* How long the conversations database keeps the message tracking
information needed for receiving new messages in existing
conversations.
.PP
For backward compatibility, if no unit is specified, days is
assumed. */
{ "conversations_keep_existing", 1, SWITCH, "3.3.0" }
/* during conversations cleanup, don't clean up if there are still existing emails
with one of the mentioned CIDs */
{ "conversations_max_thread", 100, INT, "3.1.1" }
/* maximum size for a single thread. Threads will split if they have this many
messages in them and another message arrives */
{ "conversations_max_guidrecords", 5000, INT, "3.3.0" }
/* maximum records with the same guid. This is just a sanity check to stop the same
email being added and removed over and over, so the default is 5000 */
{ "conversations_max_guidexists", 100, INT, "3.3.0" }
/* maximum records with the same guid. This maps to "labels", so with the default
of 100, you can only have 100 labels on an email in JMAP */
{ "conversations_max_guidinfolder", 10, INT, "3.3.0" }
/* maximum records with the same guid in the same folder. You can't do this via JMAP,
but could via IMAP. The default of 10 should be heaps normally! */
{ "crossdomains", 0, SWITCH, "3.0.0" }
/* Enable cross domain sharing. This works best with alt namespace and
unix hierarchy separators on, so you get Other Users/foo@example.com/... */
{ "crossdomains_onlyother", 0, SWITCH, "3.0.0" }
/* only show the domain for users in other domains than your own (for
backwards compatibility if you're already sharing */
{ "cyrus_group", NULL, STRING, "3.1.7" }
/* The name of the group Cyrus services will run as. If not configured, the
primary group of cyrus_user will be used. Can be further overridden by
setting the $CYRUS_GROUP environment variable. */
{ "cyrus_user", NULL, STRING, "3.0.0" }
/* The username to use as the 'cyrus' user. If not configured, the compile
time default will be used. Can be further overridden by setting the
$CYRUS_USER environment variable. */
{ "davdriveprefix", "#drive", STRING, "3.0.0" }
/* The prefix for the DAV storage mailboxes hierarchies. The hierarchy
delimiter will be automatically appended. The public storage
hierarchy will be at the toplevel of the shared namespace. A
user's personal storage hierarchy will be a child of their Inbox. */
{ "davnotificationsprefix", "#notifications", STRING, "3.0.0" }
/* The prefix for the DAV notifications hierarchy. The hierarchy
delimiter will be automatically appended. The public notifications
hierarchy will be at the toplevel of the shared namespace. A
user's personal notifications hierarchy will be a child of their Inbox. */
{ "dav_realm", NULL, STRING, "2.5.0" }
/* The realm to present for HTTP authentication of generic DAV
resources (principals). If not set (the default), the value of the
"servername" option will be used.*/
{ "dav_lock_timeout", "20s", DURATION, "3.1.8" }
/* The maximum time to wait for a write lock on the per-user DAV database
before timeout. For HTTP requests, the HTTP status code 503 is returned
if the lock can not be obtained within this time.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
+{ "debug", 0, SWITCH, "2.5.0" }
+/* If enabled, allow syslog() to pass LOG_DEBUG messages. */
+
{ "debug_command", NULL, STRING, "2.3.17" }
/* Debug command to be used by processes started with -D option. The string
is a C format string that gets 3 options: the first is the name of the
executable (as specified in the cmd parameter in cyrus.conf). The second
is the pid (integer) and the third is the service ID.
Example: /usr/local/bin/gdb /usr/cyrus/bin/%s %d */
+{ "debug_writefail_guid", NULL, STRING, "UNRELEASED" }
+/* If set, any arriving message with this guid will fail as if the underlying
+ disk write had failed, pretending to be a disk full condition. This is
+ mainly useful for regression testing certain edge case handling.
+ Currently only implemented for replication uploads. */
+
{ "defaultacl", "anyone lrs", STRING, "2.3.17" }
/* The Access Control List (ACL) placed on a newly-created (non-user)
mailbox that does not have a parent mailbox. */
{ "defaultdomain", "internal", STRING, "3.0.0" }
/* The default domain for virtual domain support */
{ "defaultpartition", NULL, STRING, "2.3.17" }
/* The partition name used by default for new mailboxes. If not
specified, the partition with the most free space will be used for
new mailboxes.
.PP
Note that the partition specified by this option must also be
specified as \fIpartition-name\fR, where you substitute 'name'
for the alphanumeric string you set \fIdefaultpartition\fR to. */
{ "defaultsearchtier", "", STRING, "3.0.0" }
/* Name of the default tier that messages will be indexed to. Search
indexes can be organized in tiers to allow index storage in different
directories and physical media. See the man page of squatter for
details. The default search tier also requires the definition
of an according \fIsearchtierpartition-name\fR entry.
.PP
This option MUST be specified for xapian search. */
{ "defaultserver", NULL, STRING, "2.3.17" }
/* The backend server name used by default for new mailboxes. If not
specified, the server with the most free space will be used for new
mailboxes. */
{ "deletedprefix", "DELETED", STRING, "2.3.17" }
/* With \fBdelete_mode\fR set to \fIdelayed\fR, the
\fBdeletedprefix\fR setting defines the prefix for the hierarchy of
deleted mailboxes.
.PP
The hierarchy delimiter will be automatically appended.
*/
{ "delete_mode", "delayed", ENUM("immediate", "delayed"), "2.5.0" }
/* The manner in which mailboxes are deleted. In the default
\fIdelayed\fR mode, mailboxes that are being deleted are renamed to
a special mailbox hierarchy under the \fBdeletedprefix\fR, to be
removed later by \fBcyr_expire(8)\fR.
.PP
In \fIimmediate\fR mode, the mailbox is removed from the filesystem
immediately.
*/
{ "delete_unsubscribe", 0, SWITCH, "3.0.0" }
/* Whether to also unsubscribe from mailboxes when they are deleted.
Note that this behaviour contravenes RFC 3501 section 6.3.9, but
may be useful for avoiding user/client software confusion.
The default is 'no'. */
{ "deleteright", "c", STRING, "2.3.17" }
/* Deprecated - only used for backwards compatibility with existing
installations. Lists the old RFC 2086 right which was used to
grant the user the ability to delete a mailbox. If a user has this
right, they will automatically be given the new 'x' right. */
{ "disable_user_namespace", 0, SWITCH, "2.5.0" }
/* Preclude list command on user namespace. If set to 'yes', the
LIST response will never include any other user's mailbox. Admin
users will always see all mailboxes. The default is 'no' */
{ "disable_shared_namespace", 0, SWITCH, "2.5.0" }
/* Preclude list command on shared namespace. If set to 'yes', the
LIST response will never include any non-user mailboxes. Admin
users will always see all mailboxes. The default is 'no' */
{ "disconnect_on_vanished_mailbox", 0, SWITCH, "2.3.17" }
/* If enabled, IMAP/POP3/NNTP clients will be disconnected by the
server if the currently selected mailbox is (re)moved by another
session. Otherwise, the missing mailbox is treated as empty while
in use by the client.*/
{ "ischedule_dkim_domain", NULL, STRING, "2.5.0" }
/* The domain to be reported as doing iSchedule DKIM signing. */
{ "ischedule_dkim_key_file", NULL, STRING, "2.5.0" }
/* File containing the private key for iSchedule DKIM signing. */
{ "ischedule_dkim_required", 1, SWITCH, "3.1.4" }
/* A DKIM signature is required on received iSchedule requests. */
{ "ischedule_dkim_selector", NULL, STRING, "2.5.0" }
/* Name of the selector subdividing the domain namespace. This
specifies the actual key used for iSchedule DKIM signing within the
domain. */
{ "duplicate_db", "twoskip", STRINGLIST("skiplist", "sql", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the duplicate delivery suppression
and sieve. */
{ "duplicate_db_path", NULL, STRING, "2.5.0" }
/* The absolute path to the duplicate db file. If not specified,
will be configdirectory/deliver.db */
{ "duplicatesuppression", 1, SWITCH, "2.3.17" }
/* If enabled, lmtpd will suppress delivery of a message to a mailbox if
a message with the same message-id (or resent-message-id) is recorded
as having already been delivered to the mailbox. Records the mailbox
and message-id/resent-message-id of all successful deliveries. */
{ "event_content_inclusion_mode", "standard", ENUM("standard", "message", "header", "body", "headerbody"), "2.5.0" }
/* The mode in which message content may be included with MessageAppend and
MessageNew. "standard" mode is the default behavior in which message is
included up to a size with the notification. In "message" mode, the message
is included and may be truncated to a size. In "header" mode, it includes
headers truncated to a size. In "body" mode, it includes body truncated
to a size. In "headerbody" mode, it includes full headers and body truncated
to a size */
{ "event_content_size", 0, INT, "2.5.0" }
/* Truncate the message content that may be included with MessageAppend and
MessageNew. Set 0 to include the entire message itself */
{ "event_exclude_flags", NULL, STRING, "2.5.0" }
/* Don't send event notification for given IMAP flag(s) */
{ "event_exclude_specialuse", "\\Junk", STRING, "2.5.0" }
/* Don't send event notification for folder with given special-use attributes.
Set ALL for any folder */
{ "event_extra_params", "timestamp", BITFIELD("bodyStructure", "clientAddress", "diskUsed", "flagNames", "messageContent", "messageSize", "messages", "modseq", "service", "timestamp", "uidnext", "vnd.cmu.midset", "vnd.cmu.unseenMessages", "vnd.cmu.envelope", "vnd.cmu.sessionId", "vnd.cmu.mailboxACL", "vnd.cmu.mbtype", "vnd.cmu.davFilename", "vnd.cmu.davUid", "vnd.fastmail.clientId", "vnd.fastmail.sessionId", "vnd.fastmail.convExists", "vnd.fastmail.convUnseen", "vnd.fastmail.cid", "vnd.fastmail.counters", "vnd.cmu.emailid", "vnd.cmu.threadid"), "3.1.6" }
/* Space-separated list of extra parameters to add to any appropriated event. */
{ "event_groups", "message mailbox", BITFIELD("message", "quota", "flags", "access", "mailbox", "subscription", "calendar", "applepushservice" ), "3.0.0" }
/* Space-separated list of groups of related events to turn on notification */
{ "event_notifier", NULL, STRING, "2.5.0" }
/* Notifyd(8) method to use for "EVENT" notifications which are based on
the RFC 5423. If not set, "EVENT" notifications are disabled. */
{ "expunge_mode", "delayed", ENUM("immediate", "semidelayed", "delayed"), "3.1.1" }
/* The mode in which messages (and their corresponding cache entries)
are expunged. "semidelayed" mode is the old behavior in which the
message files are purged at the time of the EXPUNGE, but index
and cache records are retained to facilitate QRESYNC.
In "delayed" mode, which is the default since Cyrus 2.5.0,
the message files are also retained, allowing unexpunge to
rescue them. In "immediate" mode, both the message files and the
index records are removed as soon as possible. In all cases,
nothing will be finally purged until all other processes have
closed the mailbox to ensure they never see data disappear under
them. In "semidelayed" or "delayed" mode, a later run of "cyr_expire"
will clean out the retained records (and possibly message files).
This reduces the amount of I/O that takes place at the time of
EXPUNGE and should result in greater responsiveness for the client,
especially when expunging a large number of messages. */
{ "failedloginpause", "3s", DURATION, "3.1.8" }
/* Time to pause after a failed login.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "flushseenstate", 1, SWITCH, "2.5.0", "2.5.0" }
/* Deprecated. No longer used */
{ "foolstupidclients", 0, SWITCH, "2.3.17" }
/* If enabled, only list the personal namespace when a LIST "*" is performed
(it changes the request to a LIST "INBOX*"). */
{ "force_sasl_client_mech", NULL, STRING, "2.3.17" }
/* Force preference of a given SASL mechanism for client side operations
(e.g., murder environments). This is separate from (and overridden by)
the ability to use the <host shortname>_mechs option to set preferred
mechanisms for a specific host */
{ "fulldirhash", 0, SWITCH, "2.3.17" }
/* If enabled, uses an improved directory hashing scheme which hashes
on the entire username instead of using just the first letter as
the hash. This changes hash algorithm used for quota and user
directories and if \fIhashimapspool\fR is enabled, the entire mail
spool.
.PP
Note that this option CANNOT be changed on a live system. The
server must be quiesced and then the directories moved with the
\fBrehash\fR utility. */
{ "hashimapspool", 0, SWITCH, "2.3.17" }
/* If enabled, the partitions will also be hashed, in addition to the
hashing done on configuration directories. This is recommended if
one partition has a very bushy mailbox tree. */
-{ "debug", 0, SWITCH, "2.5.0" }
-/* If enabled, allow syslog() to pass LOG_DEBUG messages. */
-
# Commented out - there's no such thing as "hostname_mechs", but we need
# this for the man page
# { "hostname_mechs", NULL, STRING, "2.3.17" }
/* Force a particular list of SASL mechanisms to be used when authenticating
to the backend server hostname (where hostname is the short hostname of
the server in question). If it is not specified it will query the server
for available mechanisms and pick one to use. - Cyrus Murder */
# Commented out - there's no such thing as "hostname_password", but we need
# this for the man page
# { "hostname_password", NULL, STRING, "2.3.17" }
/* The password to use for authentication to the backend server hostname
(where hostname is the short hostname of the server) - Cyrus Murder */
{ "httpallowcompress", 1, SWITCH, "2.5.0" }
/* If enabled, the server will compress response payloads if the client
indicates that it can accept them. Note that the compressed data
will appear in telemetry logs, leaving only the response headers as
human-readable.*/
{ "httpallowcors", NULL, STRING, "2.5.0" }
/* A wildmat pattern specifying a list of origin URIs ( scheme "://"
host [ ":" port ] ) that are allowed to make Cross-Origin Resource
Sharing (CORS) requests on the server. By default, CORS requests
are disabled.
.PP
Note that the scheme and host should both be lowercase, the port
should be omitted if using the default for the scheme (80 for http,
443 for https), and there should be no trailing '/' (e.g.:
"http://www.example.com:8080", "https://example.org"). */
{ "httpallowtrace", 0, SWITCH, "2.5.0" }
/* Allow use of the TRACE method.
.PP
Note that sensitive data might be disclosed by the response. */
{ "httpallowedurls", NULL, STRING, "2.5.0" }
/* Space-separated list of relative URLs (paths) rooted at
"httpdocroot" (see below) to be served by httpd. If set, this
option will limit served static content to only those paths specified
(returning "404 Not Found" to any other client requested URLs).
Otherwise, httpd will serve any content found in "httpdocroot".
.PP
Note that any path specified by "rss_feedlist_template" is an
exception to this rule.*/
{ "httpcontentmd5", 0, SWITCH, "2.5.0" }
/* If enabled, HTTP responses will include a Content-MD5 header for
the purpose of providing an end-to-end message integrity check
(MIC) of the payload body. Note that enabling this option will
use additional CPU to generate the MD5 digest, which may be ignored
by clients anyways. */
{ "httpdocroot", NULL, STRING, "2.5.0" }
/* If set, http will serve the static content (html/text/jpeg/gif
files, etc) rooted at this directory. Otherwise, httpd will not
serve any static content. */
{ "httpkeepalive", "20s", DURATION, "3.1.8" }
/* Set the length of the HTTP server's keepalive heartbeat. The
default is 20 seconds. The minimum value is 0, which will disable
the keepalive heartbeat. When enabled, if a request takes longer
than \fIhttpkeepalive\fR to process, the server will send the client
provisional responses every \fIhttpkeepalive\fR until the final
response can be sent.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "httplogheaders", NULL, STRING, "3.3.1" }
/* Space-separated list of HTTP header fields that will be included
in the requests logged by httpd(8). */
{ "httpmodules", "", BITFIELD("admin", "caldav", "carddav", "cgi", "domainkey", "freebusy", "ischedule", "jmap", "prometheus", "rss", "tzdist", "webdav"), "3.1.7" }
/* Space-separated list of HTTP modules that will be enabled in
httpd(8). This option has no effect on modules that are disabled
at compile time due to missing dependencies (e.g. libical).
.PP
Note that "domainkey" depends on "ischedule" being enabled, and
that both "freebusy" and "ischedule" depend on "caldav" being
enabled. */
{ "httpprettytelemetry", 0, SWITCH, "2.5.0" }
/* If enabled, HTTP response payloads including server-generated
markup languages (HTML, XML) will utilize line breaks and
indentation to promote better human-readability in telemetry logs.
Note that enabling this option will increase the amount of data
sent across the wire. */
{ "httptimeout", "5m", DURATION, "3.1.8" }
/* Set the length of the HTTP server's inactivity autologout timer.
The default is 5 minutes. The minimum value is 0, which will
disable persistent connections.
.PP
For backwards compatibility, if no unit is specified, minutes
is assumed. */
{ "idlesocket", "{configdirectory}/socket/idle", STRING, "2.3.17" }
/* Unix domain socket that idled listens on. */
{ "ignorereference", 0, SWITCH, "2.3.17" }
/* For backwards compatibility with Cyrus 1.5.10 and earlier -- ignore
the reference argument in LIST or LSUB commands. */
{ "imapidlepoll", "60s", DURATION, "3.1.8" }
/* The interval for polling for mailbox changes and ALERTs while running
the IDLE command. This option is used when idled is not enabled or
cannot be contacted. The minimum value is 1 second. A value of 0
will disable IDLE.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "imapidresponse", 1, SWITCH, "2.3.17" }
/* If enabled, the server responds to an ID command with a parameter
list containing: version, vendor, support-url, os, os-version,
command, arguments, environment. Otherwise the server returns NIL. */
{ "imapmagicplus", 0, SWITCH, "2.3.17" }
/* Only list a restricted set of mailboxes via IMAP by using
userid+namespace syntax as the authentication/authorization id.
Using userid+ (with an empty namespace) will list only subscribed
mailboxes. */
{ "imipnotifier", NULL, STRING, "3.0.0" }
/* Notifyd(8) method to use for "IMIP" notifications which are based on
the RFC 6047. If not set, "IMIP" notifications are disabled. */
{ "implicit_owner_rights", "lkxan", STRING, "3.1.2" }
/* The implicit Access Control List (ACL) for the owner of a mailbox. */
# Commented out - there's no such thing as "@include", but we need
# this for the man page
# { "@include", NULL, STRING, "2.3.17" }
/* Directive which includes the specified file as part of the
configuration. If the path to the file is not absolute, CYRUS_PATH
is prepended. */
{ "improved_mboxlist_sort", 0, SWITCH, "2.3.17" }
/* If enabled, a special comparator will be used which will correctly
sort mailbox names that contain characters such as ' ' and '-'.
.PP
Note that this option SHOULD NOT be changed on a live system. The
mailboxes database should be dumped (ctl_mboxlist) before the
option is changed, removed, and then undumped after changing the
option. When not using flat files for the subscriptions databases
the same has to be done (cyr_dbtool) for each subscription database
See improved_mboxlist_sort.html.*/
{ "jmap_emailsearch_db_path", NULL, STRING, "3.1.6" }
/* The absolute path to the JMAP email search cache file. If not
specified, JMAP Email/query and Email/queryChanges will not
cache email search results. */
{ "jmap_preview_annot", NULL, STRING, "3.1.1" }
/* The name of the per-message annotation, if any, to store message
previews. */
{ "jmap_imagesize_annot", NULL, STRING, "3.1.1" }
/* The name of the per-message annotation, if any, that stores a
JSON object, mapping message part numbers of MIME image types
to an array of their image dimensions. The array must have at
least two entries, where the first entry denotes the width
and the second entry the height of the image. Any additional
values are ignored.
For example, if message part 1.2 contains an image of width 300
and height 200, then the value of this annotation would be:
{ "1.2" : [ 300, 200 ] }
*/
{ "jmap_inlinedcids_annot", NULL, STRING, "3.1.1" }
/* The name of the per-message annotation, if any, that stores a
JSON object, mapping RFC 2392 Content-IDs referenced in HTML bodies
to the respective HTML body part number.
For example, if message part 1.2 contains HTML and references an
inlined image at "cid:foo", then the value of this annotation
would be:
{ "<foo>" : "1.2" }
Note that the Content-ID key must be URL-unescaped and enclosed in
angular brackets, as defined in RFC 2392. */
{ "jmap_preview_length", 64, INT, "3.1.1" }
/* The maximum byte length of dynamically generated message previews. Previews
stored in jmap_preview_annot take precedence. */
{ "jmap_max_size_upload", 1048576, INT, "3.1.6" }
/* The maximum size (in kilobytes) that the JMAP API accepts
for blob uploads. Returned as the maxSizeUpload property
value of the JMAP \"urn:ietf:params:jmap:core\" capabilities object.
Default is 1Gb. */
{ "jmap_max_size_blob_set", 4096, INT, "3.3.0" }
/* The maximum size (in kilobytes) that the JMAP API accepts
for Blob/set. Returned as the maxSizeBlobSet property
value of the JMAP \"https://cyrusimap.org/ns/jmap/blob\" capabilities object.
Default is 4Mb. */
{ "jmap_max_concurrent_upload", 5, INT, "3.1.6" }
/* The value to return for the maxConcurrentUpload property of
the JMAP \"urn:ietf:params:jmap:core\" capabilities object. The Cyrus JMAP
implementation does not enforce this rate-limit. */
{ "jmap_max_size_request", 10240, INT, "3.1.6" }
/* The maximum size (in kilobytes) that the JMAP API accepts
for requests at the API endpoint. Returned as the
maxSizeRequest property value of the JMAP \"urn:ietf:params:jmap:core\"
capabilities object. Default is 10Mb. */
{ "jmap_max_concurrent_requests", 5, INT, "3.1.6" }
/* The value to return for the maxConcurrentRequests property of
the JMAP \"urn:ietf:params:jmap:core\" capabilities object. The Cyrus JMAP
implementation does not enforce this rate-limit. */
{ "jmap_max_calls_in_request", 50, INT, "3.1.6" }
/* The maximum number of calls per JMAP request object.
Returned as the maxCallsInRequest property value of the
JMAP \"urn:ietf:params:jmap:core\" capabilities object. */
{ "jmap_max_delayed_send", "512d", DURATION, "3.1.8" }
/* The value to return for the maxDelayedSend property of
the JMAP \"urn:ietf:params:jmap:emailsubmission\" capabilities object.
The Cyrus JMAP implementation does not enforce this limit.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "jmap_max_objects_in_get", 4096, INT, "3.1.6" }
/* The maximum number of ids that a JMAP client may request in
a single \"/get\" type method call. The actual number
of returned objects in the response may exceed this number
if the JMAP object type supports unbounded \"/get\" calls.
Returned as the maxObjectsInGet property value of the
JMAP \"urn:ietf:params:jmap:core\" capabilities object. */
{ "jmap_max_objects_in_set", 4096, INT, "3.1.6" }
/* The maximum number of objects a JMAP client may send to create,
update or destroy in a single /set type method call.
Returned as the maxObjectsInSet property value of the
JMAP \"urn:ietf:params:jmap:core\" capabilities object. */
{ "jmap_mail_max_size_attachments_per_email", 10240, INT, "3.1.6" }
/* The value (in kilobytes) to return for the maxSizeAttachmentsPerEmail
property of the JMAP \"urn:ietf:params:jmap:mail\" capabilities object. The Cyrus
JMAP implementation does not enforce this size limit. Default is 10 Mb.*/
{ "jmap_nonstandard_extensions", 0, SWITCH, "3.1.9" }
/* If enabled, support non-standard JMAP extensions. If not enabled,
only IETF standard JMAP functionality is supported. */
{ "jmap_set_has_attachment", 1, SWITCH, "3.1.5" }
/* If enabled, the $hasAttachment flag is determined and set for new messages
created with the JMAP Email/set or Email/import methods. This option should
typically be enabled, but installations using Cyrus-external message
annatotors to determine the $hasAttachment flag might want to disable it. */
{ "jmap_vacation", 1, SWITCH, "3.1.8" }
/* If enabled, support the JMAP vacation extension */
{ "jmapuploadfolder", "#jmap", STRING, "3.1.1" }
/* the name of the folder for JMAP uploads (#jmap) */
{ "jmapsubmission_deleteonsend", 1, SWITCH, "3.1.8" }
/* If enabled (the default) then delete the EmailSubmission as soon as the email
* has been sent */
{ "jmapsubmissionfolder", "#jmapsubmission", STRING, "3.1.8" }
/* the name of the folder for JMAP Submissions (#jmapsubmission) */
{ "jmappushsubscriptionfolder", "#jmappushsubscription", STRING, "3.1.8" }
/* the name of the folder for JMAP Push Subscriptions (#jmappushsubscription) */
{ "jmapnotificationfolder", "#jmapnotification", STRING, "3.3.0" }
/* the name of the folder for JMAP notifications (#jmapnotification) */
{ "iolog", 0, SWITCH, "2.5.0" }
/* Should cyrus output I/O log entries */
{ "ldap_authz", NULL, STRING, "2.3.17" }
/* SASL authorization ID for the LDAP server */
{ "ldap_base", "", STRING, "2.3.17" }
/* Contains the LDAP base dn for the LDAP ptloader module */
{ "ldap_bind_dn", NULL, STRING, "2.3.17" }
/* Bind DN for the connection to the LDAP server (simple bind).
Do not use for anonymous simple binds */
{ "ldap_deref", "never", STRINGLIST("search", "find", "always", "never"), "2.3.17" }
/* Specify how aliases dereferencing is handled during search. */
{ "ldap_domain_base_dn", "", STRING, "2.5.0" }
/* Base DN to search for domain name spaces. */
{ "ldap_domain_filter", "(&(objectclass=domainrelatedobject)(associateddomain=%s))", STRING, "2.5.0" }
/* Filter to use searching for domains */
{ "ldap_domain_name_attribute", "associateddomain", STRING, "2.5.0" }
/* The attribute name for domains. */
{ "ldap_domain_scope", "sub", STRINGLIST("sub", "one", "base"), "2.5.0" }
/* Search scope */
{ "ldap_domain_result_attribute", "inetdomainbasedn", STRING, "2.5.0" }
/* Result attribute */
{ "ldap_filter", "(uid=%u)", STRING, "2.3.17" }
/* Specify a filter that searches user identifiers. The following tokens can be
used in the filter string:
%% = %
%u = user
%U = user portion of %u (%U = test when %u = test@domain.tld)
%d = domain portion of %u if available (%d = domain.tld when %u =
test@domain.tld), otherwise same as %R
%R = domain portion of %u starting with @ (%R = @domain.tld
when %u = test@domain.tld)
%D = user dn. (use when ldap_member_method: filter)
%1-9 = domain tokens (%1 = tld, %2 = domain when %d = domain.tld)
ldap_filter is not used when ldap_sasl is enabled. */
{ "ldap_group_base", "", STRING, "2.3.17" }
/* LDAP base dn for ldap_group_filter. */
{ "ldap_group_filter", "(cn=%u)", STRING, "2.3.17" }
/* Specify a filter that searches for group identifiers.
See ldap_filter for more options. */
{ "ldap_group_scope", "sub", STRINGLIST("sub", "one", "base"), "2.3.17" }
/* Specify search scope for ldap_group_filter. */
{ "ldap_id", NULL, STRING, "2.3.17" }
/* SASL authentication ID for the LDAP server */
{ "ldap_mech", NULL, STRING, "2.3.17" }
/* SASL mechanism for LDAP authentication */
{ "ldap_user_attribute", NULL, STRING, "2.5.0" }
/* Specify LDAP attribute to use as canonical user id */
{ "ldap_member_attribute", NULL, STRING, "2.3.17" }
/* See ldap_member_method. */
{ "ldap_member_base", "", STRING, "2.3.17" }
/* LDAP base dn for ldap_member_filter. */
{ "ldap_member_filter", "(member=%D)", STRING, "2.3.17" }
/* Specify a filter for "ldap_member_method: filter".
See ldap_filter for more options. */
{ "ldap_member_method", "attribute", STRINGLIST("attribute", "filter"), "2.3.17" }
/* Specify a group method. The "attribute" method retrieves groups from
a multi-valued attribute specified in ldap_member_attribute.
The "filter" method uses a filter, specified by ldap_member_filter, to find
groups; ldap_member_attribute is a single-value attribute group name. */
{ "ldap_member_scope", "sub", STRINGLIST("sub", "one", "base"), "2.3.17" }
/* Specify search scope for ldap_member_filter. */
{ "ldap_password", NULL, STRING, "2.3.17" }
/* Password for the connection to the LDAP server (SASL and simple bind).
Do not use for anonymous simple binds */
{ "ldap_realm", NULL, STRING, "2.3.17" }
/* SASL realm for LDAP authentication */
{ "ldap_referrals", 0, SWITCH, "2.3.17" }
/* Specify whether or not the client should follow referrals. */
{ "ldap_restart", 1, SWITCH, "2.3.17" }
/* Specify whether or not LDAP I/O operations are automatically restarted
if they abort prematurely. */
{ "ldap_sasl", 1, SWITCH, "2.3.17" }
/* Use SASL for LDAP binds in the LDAP PTS module. */
{ "ldap_sasl_authc", NULL, STRING, "2.5.0", "2.5.0", "ldap_id" }
/* Deprecated. Use ldap_id */
{ "ldap_sasl_authz", NULL, STRING, "2.5.0", "2.5.0", "ldap_authz" }
/* Deprecated. Use ldap_authz */
{ "ldap_sasl_mech", NULL, STRING, "2.5.0", "2.5.0", "ldap_mech" }
/* Deprecated. Use ldap_mech */
{ "ldap_sasl_password", NULL, STRING, "2.5.0", "2.5.0", "ldap_password" }
/* Deprecated. User ldap_password */
{ "ldap_sasl_realm", NULL, STRING, "2.5.0", "2.5.0", "ldap_realm" }
/* Deprecated. Use ldap_realm */
{ "ldap_scope", "sub", STRINGLIST("sub", "one", "base"), "2.3.17" }
/* Specify search scope. */
{ "ldap_servers", "ldap://localhost/", STRING, "2.5.0", "2.5.0", "ldap_uri" }
/* Deprecated. Use ldap_uri */
{ "ldap_size_limit", 1, INT, "2.3.17" }
/* Specify a number of entries for a search request to return. */
{ "ldap_start_tls", 0, SWITCH, "2.3.17" }
/* Use transport layer security for ldap:// using STARTTLS. Do not use
ldaps:// in 'ldap_uri' with this option enabled. */
{ "ldap_time_limit", "5s", DURATION, "3.1.8" }
/* How long to wait for a search request to complete.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "ldap_timeout", "5s", DURATION, "3.1.8" }
/* How long a search can take before timing out.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "ldap_ca_dir", NULL, STRING, "2.5.0" }
/* Path to a directory with CA (Certificate Authority) certificates. */
{ "ldap_ca_file", NULL, STRING, "2.5.0" }
/* Path to a file containing CA (Certificate Authority) certificate(s). */
{ "ldap_ciphers", NULL, STRING, "2.5.0" }
/* List of SSL/TLS ciphers to allow. The format of the string is
described in ciphers(1). */
{ "ldap_client_cert", NULL, STRING, "2.5.0" }
/* File containing the client certificate. */
{ "ldap_client_key", NULL, STRING, "2.5.0" }
/* File containing the private client key. */
{ "ldap_verify_peer", 0, SWITCH, "2.5.0" }
/* Require and verify server certificate. If this option is yes,
you must specify ldap_ca_file or ldap_ca_dir. */
{ "ldap_tls_cacert_dir", NULL, STRING, "2.5.0", "2.5.0", "ldap_ca_dir" }
/* Deprecated in favor of \fIldap_ca_dir\fR. */
{ "ldap_tls_cacert_file", NULL, STRING, "2.5.0", "2.5.0", "ldap_ca_file" }
/* Deprecated in favor of \fIldap_ca_file\fR. */
{ "ldap_tls_cert", NULL, STRING, "2.5.0", "2.5.0", "ldap_client_cert" }
/* Deprecated in favor of \fIldap_client_cert\fR. */
{ "ldap_tls_key", NULL, STRING, "2.5.0", "2.5.0", "ldap_client_key" }
/* Deprecated in favor of \fIldap_client_key\fR. */
{ "ldap_tls_check_peer", 0, SWITCH, "2.5.0", "2.5.0", "ldap_verify_peer" }
/* Deprecated in favor of \fIldap_verify_peer\fR. */
{ "ldap_tls_ciphers", NULL, STRING, "2.5.0", "2.5.0", "ldap_ciphers" }
/* Deprecated in favor of \fIldap_ciphers\fR. */
{ "ldap_uri", NULL, STRING, "2.3.17" }
/* Contains a list of the URLs of all the LDAP servers when using the
LDAP PTS module. */
{ "ldap_version", 3, INT, "2.3.17" }
/* Specify the LDAP protocol version. If ldap_start_tls and/or
ldap_use_sasl are enabled, ldap_version will be automatically
set to 3. */
{ "literalminus", 0, SWITCH, "3.0.0" }
/* if enabled, CAPABILITIES will reply with LITERAL- rather than
LITERAL+ (RFC 7888). Doesn't actually size-restrict uploads though */
{ "lmtp_downcase_rcpt", 1, SWITCH, "2.5.0" }
/* If enabled, lmtpd will convert the recipient addresses to lowercase
(up to a '+' character, if present). */
{ "lmtp_exclude_specialuse", "\\Snoozed", STRING, "3.1.8" }
/* Don't allow delivery to folders with given special-use attributes.
.PP
Note that "snoozing" of emails can currently only be done via the
JMAP protocol, so delivery directly to the \Snoozed mailbox is
prohibited by default as it will not be moved back into INBOX
automatically. */
{ "lmtp_fuzzy_mailbox_match", 0, SWITCH, "2.3.17" }
/* If enabled, and the mailbox specified in the detail part of the
recipient (everything after the '+') does not exist, lmtpd will try
to find the closest match (ignoring case, ignoring whitespace,
falling back to parent) to the specified mailbox name. */
{ "lmtp_over_quota_perm_failure", 0, SWITCH, "2.3.17" }
/* If enabled, lmtpd returns a permanent failure code when a user's
mailbox is over quota. By default, the failure is temporary,
causing the MTA to queue the message and retry later. */
{ "lmtp_preparse", 0, SWITCH, "3.3.1" }
/* If enabled, lmtpd will map in the email and parse the xapian data
for jmapsearch. The advantage is that the parsing is done without
holding any locks. The disadvantage is that the parsing is done
even if it winds up not being needed. */
{ "lmtp_strict_quota", 0, SWITCH, "2.3.17" }
/* If enabled, lmtpd returns a failure code when the incoming message
will cause the user's mailbox to exceed its quota. By default, the
failure won't occur until the mailbox is already over quota. */
{ "lmtp_strict_rfc2821", 1, SWITCH, "2.5.0" }
/* By default, lmtpd will be strict (per RFC 2821) with regards to which
envelope addresses are allowed. If this option is set to false, 8bit
characters in the local-part of envelope addresses are changed to 'X'
instead. This is useful to avoid generating backscatter with
certain MTAs like Postfix or Exim which accept such messages. */
{ "lmtpsocket", "{configdirectory}/socket/lmtp", STRING, "2.3.17" }
/* Unix domain socket that lmtpd listens on, used by deliver(8). This should
match the path specified in cyrus.conf(5). */
{ "lmtptxn_timeout", "5m", DURATION, "3.1.8" }
/* Timeout used during a lmtp transaction to a remote backend (e.g. in a
murder environment). Can be used to prevent hung lmtpds on proxy hosts
when a backend server becomes unresponsive during a lmtp transaction.
The default is 5 minutes - change to zero for infinite.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "lock_debugtime", NULL, STRING, "3.1.4" }
/* A floating point number of seconds. If set, time how long we wait for
any lock, and syslog the filename and time if it's longer than this
value. The default of NULL means not to time locks. */
# xxx how does this tie into virtual domains?
{ "loginrealms", "", STRING, "2.3.17" }
/* The list of remote realms whose users may authenticate using cross-realm
authentication identifiers. Separate each realm name by a space. (A
cross-realm identity is considered any identity returned by SASL
with an "@" in it.). */
{ "loginuseacl", 0, SWITCH, "2.3.17" }
/* If enabled, any authentication identity which has \fBa\fR rights on a
user's INBOX may log in as that user. */
{ "logtimestamps", 0, SWITCH, "2.3.17" }
/* Include notations in the protocol telemetry logs indicating the number of
seconds since the last command or response. */
{ "mailbox_default_options", 0, INT, "2.3.17" }
/* Default "options" field for the mailbox on create. You'll want to know
what you're doing before setting this, but it can apply some default
annotations like duplicate suppression */
{ "mailbox_initial_flags", NULL, STRING, "2.5.0" }
/* space-separated list of permanent flags which will be pre-set in every
newly created mailbox. If you know you will require particular
flag names then this avoids a possible race condition against a client
that fills the entire 128 available slots. Default is NULL, which is
no flags. Example: $Label1 $Label2 $Label3 NotSpam Spam */
{ "mailbox_maxmessages_addressbook", 0, INT, "3.3.0" }
/* Limit the number of messages that may exist in a single mailbox of
"addressbook" type. Default (0) means no limit. This limit applies
after quotas are checked, so if you have both quota limits and this
set, then you will be denied if you are either over quota or over
this per-mailbox count. */
{ "mailbox_maxmessages_calendar", 0, INT, "3.3.0" }
/* Limit the number of messages that may exist in a single mailbox of
"calendar" type. Default (0) means no limit. This limit applies
after quotas are checked, so if you have both quota limits and this
set, then you will be denied if you are either over quota or over
this per-mailbox count. */
{ "mailbox_maxmessages_email", 0, INT, "3.3.0" }
/* Limit the number of messages that may exist in a single mailbox of
"email" (normal) type. Default (0) means no limit. This limit applies
after quotas are checked, so if you have both quota limits and this
set, then you will be denied if you are either over quota or over
this per-mailbox count. */
{ "mailnotifier", NULL, STRING, "2.3.17" }
/* Notifyd(8) method to use for "MAIL" notifications. If not set, "MAIL"
notifications are disabled. */
{ "master_bind_errors_fatal", 0, SWITCH, "3.3.0" }
/* If enabled, failure to bind a port during startup is treated as a fatal
error, causing master to shut down immediately. The default is to keep
running, with the affected service disabled until the next SIGHUP causes
it to retry.
.PP
Note that this only applies during startup. New services that fail to
come up in response to a reconfig+SIGHUP will just be logged and disabled
like the default behaviour, without causing master to exit. */
{ "maxheaderlines", 1000, INT, "2.3.17" }
/* Maximum number of lines of header that will be processed into cache
records. Default 1000. If set to zero, it is unlimited.
If a message hits the limit, an error will be logged and the rest of
the lines in the header will be skipped. This is to avoid malformed
messages causing giant cache records */
{ "maxlogins_per_host", 0, INT, "2.5.0" }
/* Maximum number of logged in sessions allowed per host,
zero means no limit */
{ "maxlogins_per_user", 0, INT, "2.5.0" }
/* Maximum number of logged in sessions allowed per user,
zero means no limit */
{ "maxmessagesize", 0, INT, "2.3.17" }
/* Maximum incoming LMTP message size. If non-zero, lmtpd will reject
messages larger than \fImaxmessagesize\fR bytes. If set to 0, this
will allow messages of any size (the default). */
{ "maxquoted", 131072, INT, "2.3.17" }
/* Maximum size of a single quoted string for the parser. Default 128k */
{ "maxword", 131072, INT, "2.3.17" }
/* Maximum size of a single word for the parser. Default 128k */
{ "mboxkey_db", "twoskip", STRINGLIST("skiplist", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for mailbox keys. */
{ "mboxlist_db", "twoskip", STRINGLIST("flat", "skiplist", "sql", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the mailbox list. */
{ "mboxlist_db_path", NULL, STRING, "2.5.0" }
/* The absolute path to the mailboxes db file. If not specified
will be configdirectory/mailboxes.db */
{ "mboxname_lockpath", NULL, STRING, "2.4.0" }
/* Path to mailbox name lock files (default $conf/lock) */
{ "metapartition_files", "", BITFIELD("header", "index", "cache", "expunge", "squat", "annotations", "lock", "dav", "archivecache"), "3.0.0" }
/* Space-separated list of metadata files to be stored on a
\fImetapartition\fR rather than in the mailbox directory on a spool
partition. */
# Commented out - there's no such thing as "metapartition-name",
# but we need this for the man page
# { "metapartition-name", NULL, STRING, "2.3.17" }
/* The pathname of the metadata partition \fIname\fR, corresponding to
spool partition \fBpartition-name\fR. For any mailbox residing in
a directory on \fBpartition-name\fR, the metadata files listed in
\fImetapartition_files\fR will be stored in a corresponding directory on
\fBmetapartition-name\fR. Note that not every
\fBpartition-name\fR option is required to have a corresponding
\fBmetapartition-name\fR option, so that you can selectively choose
which spool partitions will have separate metadata partitions. */
{ "mupdate_authname", NULL, STRING, "2.3.17" }
/* The SASL username (Authentication Name) to use when authenticating to the
mupdate server (if needed). */
{ "mupdate_config", "standard", ENUM("standard", "unified", "replicated"), "2.3.17" }
/* The configuration of the mupdate servers in the Cyrus Murder.
The "standard" config is one in which there are discreet frontend
(proxy) and backend servers. The "unified" config is one in which
a server can be both a frontend and backend. The "replicated"
config is one in which multiple backend servers all share the same
mailspool, but each have their own "replicated" copy of
mailboxes.db. */
{ "munge8bit", 1, SWITCH, "2.3.17" }
/* If enabled, lmtpd munges messages with 8-bit characters in the
headers. The 8-bit characters are changed to `X'. If
\fBreject8bit\fR is enabled, setting \fBmunge8bit\fR has no effect.
(A proper solution to non-ASCII characters in headers is offered by
RFC 2047 and its predecessors.) */
# xxx badly worded
{ "mupdate_connections_max", 128, INT, "2.3.17" }
/* The max number of connections that a mupdate process will allow, this
is related to the number of file descriptors in the mupdate process.
Beyond this number connections will be immediately issued a BYE response. */
{ "mupdate_password", NULL, STRING, "2.3.17" }
/* The SASL password (if needed) to use when authenticating to the
mupdate server. */
{ "mupdate_port", 3905, INT, "2.3.17" }
/* The port of the mupdate server for the Cyrus Murder */
{ "mupdate_realm", NULL, STRING, "2.3.17" }
/* The SASL realm (if needed) to use when authenticating to the mupdate
server. */
{ "mupdate_retry_delay", 20, INT, "2.3.17" }
/* The base time to wait between connection retries to the mupdate server. */
{ "mupdate_server", NULL, STRING, "2.3.17" }
/* The mupdate server for the Cyrus Murder */
{ "mupdate_username", "", STRING, "2.3.17" }
/* The SASL username (Authorization Name) to use when authenticating to
the mupdate server */
{ "mupdate_workers_max", 50, INT, "2.3.17" }
/* The maximum number of mupdate worker threads (overall) */
{ "mupdate_workers_maxspare", 10, INT, "2.3.17" }
/* The maximum number of idle mupdate worker threads */
{ "mupdate_workers_minspare", 2, INT, "2.3.17" }
/* The minimum number of idle mupdate worker threads */
{ "mupdate_workers_start", 5, INT, "2.3.17" }
/* The number of mupdate worker threads to start */
{ "netscapeurl", NULL, STRING, "2.3.17" }
/* If enabled at compile time, this specifies a URL to reply when
Netscape asks the server where the mail administration HTTP server
is. Administrators should set this to a local resource. */
{ "newsaddheaders", "to", BITFIELD("to", "replyto"), "2.5.0" }
/* Space-separated list of headers to be added to incoming usenet
articles. Added \fITo:\fR headers will contain email
delivery addresses corresponding to each newsgroup in the
\fINewsgroups:\fR header. Added \fIReply-To:\fR headers will
contain email delivery addresses corresponding to each newsgroup in
the \fIFollowup-To:\fR or \fINewsgroups:\fR header. If the
specified header(s) already exist in an article, the email
delivery addresses will be appended to the original header body(s).
.br
.sp
This option applies if and only if the \fBnewspostuser\fR option is
set. */
{ "newsgroups", "*", STRING, "2.4.0" }
/* A wildmat pattern specifying which mailbox hierarchies should be
treated as newsgroups. Only mailboxes matching the wildmat will
accept and/or serve articles via NNTP. If not set, a default
wildmat of "*" (ALL shared mailboxes) will be used. If the
\fInewsprefix\fR option is also set, the default wildmat will be
translated to "<newsprefix>.*" */
{ "newsmaster", "news", STRING, "2.3.17" }
/* Userid that is used for checking access controls when executing
Usenet control messages. For instance, to allow articles to be
automatically deleted by cancel messages, give the "news" user
the 'd' right on the desired mailboxes. To allow newsgroups to be
automatically created, deleted and renamed by the corresponding
control messages, give the "news" user the 'c' right on the desired
mailbox hierarchies. */
{ "newspeer", NULL, STRING, "2.3.17" }
/* A list of whitespace-separated news server specifications to which
articles should be fed. Each server specification is a string of
the form [user[:pass]@]host[:port][/wildmat] where 'host' is the fully
qualified hostname of the server, 'port' is the port on which the
server is listening, 'user' and 'pass' are the authentication
credentials and 'wildmat' is a pattern that specifies which groups
should be fed. If no 'port' is specified, port 119 is used. If
no 'wildmat' is specified, all groups are fed. If 'user' is specified
(even if empty), then the NNTP POST command will be used to feed
the article to the server, otherwise the IHAVE command will be
used.
.br
.sp
A '@' may be used in place of '!' in the wildmat to prevent feeding
articles cross-posted to the given group, otherwise cross-posted
articles are fed if any part of the wildmat matches. For example,
the string "peer.example.com:*,!control.*,@local.*" would feed all
groups except control messages and local groups to
peer.example.com. In the case of cross-posting to local groups,
these articles would not be fed. */
{ "newspostuser", NULL, STRING, "2.5.0" }
/* Userid used to deliver usenet articles to newsgroup folders
(usually via lmtp2nntp). For example, if set to "post", email sent
to "post+comp.mail.imap" would be delivered to the "comp.mail.imap"
folder.
.br
.sp
When set, the Cyrus NNTP server will add the header(s) specified in
the \fBnewsaddheaders\fR option to each incoming usenet article.
The added header(s) will contain email delivery addresses
corresponding to each relevant newsgroup. If not set, no headers
are added to usenet articles. */
{ "newsprefix", NULL, STRING, "2.3.17" }
/* Prefix to be prepended to newsgroup names to make the corresponding
IMAP mailbox names. */
{ "newsrc_db_path", NULL, STRING, "2.5.0" }
/* The absolute path to the newsrc db file. If not specified,
will be configdirectory/fetchnews.db */
{ "nntptimeout", "3m", DURATION, "3.1.8" }
/* Set the length of the NNTP server's inactivity autologout timer.
The minimum value is 3 minutes, also the default.
.PP
For backward compatibility, if no unit is specified, minutes is
assumed. */
{ "notesmailbox", NULL, STRING, "3.0.0" }
/* The top level mailbox in each user's account which is used to store
* Apple-style Notes. Default is blank (disabled) */
{ "notifysocket", "{configdirectory}/socket/notify", STRING, "2.3.17" }
/* Unix domain socket that the mail notification daemon listens on. */
{ "notify_external", NULL, STRING, "2.4.0" }
/* Path to the external program that notifyd(8) will call to send mail
notifications.
.PP
The external program will be called with the following
command line options:
.TP
.BI \-c " class"
.\"
.TP
.BI \-p " priority"
.\"
.TP
.BI \-u " user"
.\"
.TP
.BI \-m " mailbox"
.PP
And the notification message will be available on \fIstdin\fR.
*/
# Commented out - there's no such thing as "partition-name", but we need
# this for the man page
# { "partition-name", NULL, STRING, "2.3.17" }
/* The pathname of the partition \fIname\fR. At least one partition
pathname MUST be specified. If the \fBdefaultpartition\fR option is
used, then its pathname MUST be specified. For example, if the
value of the \fBdefaultpartion\fR option is \fBpart1\fR, then the
\fBpartition-part1\fR field is required. */
{ "partition_select_mode", "freespace-most", STRINGLIST("random", "freespace-most", "freespace-percent-most", "freespace-percent-weighted", "freespace-percent-weighted-delta"), "2.5.0" }
/* Partition selection mode.
.PP
.IP \fIrandom\fR 5
(pseudo-)random selection
.\"
.IP \fIfreespace-most\fR 5
partition with the most free space (KiB)
.\"
.IP \fIfreespace-percent-most\fR 5
partition with the most free space (%)
.\"
.IP \fIfreespace-percent-weighted\fR 5
each partition is weighted according to its free space (%); the more free space
the partition has, the more chances it has to be selected
.\"
.IP \fIfreespace-percent-weighted-delta\fR 5
each partition is weighted according to its difference of free space (%)
compared to the most used partition; the more the partition is lagging behind
the most used partition, the more chances it has to be selected
.PP
Note that actually even the most used partition has a few chances to be
selected, and those chances increase when other partitions get closer
*/
{ "partition_select_exclude", NULL, STRING, "2.5.0" }
/* List of partitions to exclude from selection mode. */
{ "partition_select_usage_reinit", 0, INT, "2.5.0" }
/* For a given session, number of \fBoperations\fR (e.g. partition selection)
for which partitions usage data are cached. */
{ "partition_select_soft_usage_limit", 0, INT, "2.5.0" }
/* Limit of partition usage (%): if a partition is over that limit, it is
automatically excluded from selection mode.
.PP
If all partitions are over that limit, this feature is not used anymore.
*/
{ "plaintextloginpause", NULL, DURATION, "3.1.8" }
/* Time to pause after a successful plaintext login. For systems that
support strong authentication, this permits users to perceive a cost
of using plaintext passwords. (This does not affect the use of PLAIN
in SASL authentications.)
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "plaintextloginalert", NULL, STRING, "2.3.17" }
/* Message to send to client after a successful plaintext login. */
{ "popexpiretime", "-1", DURATION, "3.1.8" }
/* The duration advertised as being the minimum a message may be
left on the POP server before it is deleted (via the CAPA command,
defined in the POP3 Extension Mechanism, which some clients may
support). This duration has a granularity of whole days, with partial
days truncated (so e.g. "45m" is effectively "0d"). "NEVER", the
default, may be specified with a negative number.
.PP
The Cyrus POP3 server never deletes mail, no matter what the value of
this parameter is. However, if a site implements a less liberal policy,
it needs to change this parameter accordingly.
.PP
For backward compatibility, if no unit is specified, days is
assumed. */
{ "popminpoll", NULL, DURATION, "3.1.8" }
/* Set the minimum amount of time the server forces users to wait
between successive POP logins.
.PP
For backward compatibility, if no unit is specified, minutes is
assumed. */
{ "popsubfolders", 0, SWITCH, "2.3.17" }
/* Allow access to subfolders of INBOX via POP3 by using
userid+subfolder syntax as the authentication/authorization id. */
{ "poppollpadding", 1, INT, "2.3.17" }
/* Create a softer minimum poll restriction. Allows \fIpoppollpadding\fR
connections before the minpoll restriction is triggered. Additionally,
one padding entry is recovered every \fIpopminpoll\fR minutes.
This allows for the occasional polling rate faster than popminpoll,
(i.e., for clients that require a send/receive to send mail) but still
enforces the rate long-term. Default is 1 (disabled).
.br
.sp
The easiest way to think of it is a queue of past connections, with one
slot being filled for every connection, and one slot being cleared
every \fIpopminpoll\fR minutes. When the queue is full, the user
will not be able to check mail again until a slot is cleared. If the
user waits a sufficient amount of time, they will get back many or all
of the slots. */
{ "poptimeout", "10m", DURATION, "3.1.8" }
/* Set the length of the POP server's inactivity autologout timer.
The minimum value is 10 minutes, the default.
.PP
For backward compatibility, if no unit is specified, minutes is
assumed. */
{ "popuseacl", 0, SWITCH, "2.3.17" }
/* Enforce IMAP ACLs in the pop server. Due to the nature of the POP3
protocol, the only rights which are used by the pop server are 'r',
't', and 's' for the owner of the mailbox. The 'r' right allows the
user to open the mailbox and list/retrieve messages. The 't' right
allows the user to delete messages. The 's' right allows messages
retrieved by the user to have the \\Seen flag set (only if
\fIpopuseimapflags\fR is also enabled). */
{ "popuseimapflags", 0, SWITCH, "2.3.17" }
/* If enabled, the pop server will set and obey IMAP flags. Messages
having the \\Deleted flag are ignored as if they do not exist.
Messages that are retrieved by the client will have the \\Seen flag
set. All messages will have the \\Recent flag unset. */
{ "postmaster", "postmaster", STRING, "2.3.17" }
/* Username that is used as the 'From' address in rejection MDNs produced
by sieve. */
{ "postspec", NULL, STRING, "2.3.17" }
{ "postuser", "", STRING, "2.3.17" }
/* Userid used to deliver messages to shared folders. For example, if
set to "bb", email sent to "bb+shared.blah" would be delivered to
the "shared.blah" folder. By default, an email address of
"+shared.blah" would be used. */
{ "proc_path", NULL, STRING, "2.5.0" }
/* Path to proc directory. Default is NULL - must be an absolute path
if specified. If not specified, the path $configdirectory/proc/ will be
used. */
{ "prometheus_enabled", 0, SWITCH, "3.1.2" }
/* Whether tracking of service metrics for Prometheus is enabled. */
{ "prometheus_need_auth", "admin", STRINGLIST("none", "user", "admin"), "3.1.2" }
/* Authentication level required to fetch Prometheus metrics. */
{ "prometheus_update_freq", "10s", DURATION, "3.1.8" }
/* Frequency in at which promstatsd should re-collate its statistics
report. The minimum value is 1 second, the default is 10 seconds.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
*/
{ "prometheus_stats_dir", NULL, STRING, "3.1.2" }
/* Directory to use for gathering prometheus statistics. If specified,
must be an absolute path. If not specified, the default path
$configdirectory/stats/ will be used. It may be advantageous to locate this
directory on ephemeral storage. */
{ "proxy_authname", "proxy", STRING, "2.3.17" }
/* The authentication name to use when authenticating to a backend server
in the Cyrus Murder. */
{ "proxy_compress", 0, SWITCH, "2.3.17" }
/* Try to enable protocol-specific compression when performing a client
connection to a backend server in the Cyrus Murder.
.PP
Note that this should only be necessary over slow network
connections. Also note that currently only IMAP and MUPDATE support
compression. */
{ "proxy_password", NULL, STRING, "2.3.17" }
/* The default password to use when authenticating to a backend server
in the Cyrus Murder. May be overridden on a host-specific basis using
the hostname_password option. */
{ "proxy_realm", NULL, STRING, "2.3.17" }
/* The authentication realm to use when authenticating to a backend server
in the Cyrus Murder */
{ "proxyd_allow_status_referral", 0, SWITCH, "2.3.17" }
/* Set to true to allow proxyd to issue referrals to clients that support it
when answering the STATUS command. This is disabled by default since
some clients issue many STATUS commands in a row, and do not cache the
connections that these referrals would cause, thus resulting in a higher
authentication load on the respective backend server. */
{ "proxyd_disable_mailbox_referrals", 0, SWITCH, "2.3.17" }
/* Set to true to disable the use of mailbox-referrals on the
proxy servers. */
{ "proxyservers", NULL, STRING, "2.3.17" }
/* A list of users and groups that are allowed to proxy for other
users, separated by spaces. Any user listed in this will be
allowed to login for any other user: use with caution.
In a standard murder this option should ONLY be set on backends.
DO NOT SET on frontends or things won't work properly. */
{ "pts_module", "afskrb", STRINGLIST("afskrb", "ldap"), "2.3.17" }
/* The PTS module to use. */
{ "ptloader_sock", NULL, STRING, "2.3.17" }
/* Unix domain socket that ptloader listens on.
(defaults to configdirectory/ptclient/ptsock) */
{ "ptscache_db", "twoskip", STRINGLIST("skiplist", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the pts cache. */
{ "ptscache_db_path", NULL, STRING, "2.5.0" }
/* The absolute path to the ptscache db file. If not specified,
will be configdirectory/ptscache.db */
{ "ptscache_timeout", "3h", DURATION, "3.1.8" }
/* The timeout for the PTS cache database when using the auth_krb_pts
authorization method (default: 3 hours).
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "ptskrb5_convert524", 1, SWITCH, "2.3.16" }
/* When using the AFSKRB ptloader module with Kerberos 5 canonicalization,
do the final 524 conversion to get a n AFS style name (using '.' instead
of '/', and using short names */
{ "ptskrb5_strip_default_realm", 1, SWITCH, "2.3.17" }
/* When using the AFSKRB ptloader module with Kerberos 5 canonicalization,
strip the default realm from the userid (this does not affect the stripping
of realms specified by the afspts_localrealms option) */
{ "qosmarking", "cs0", ENUM("cs0", "cs1", "cs2", "cs3", "cs4", "cs5", "cs6", "cs7", "af11", "af12", "af13", "af21", "af22", "af23", "af31", "af32", "af33", "af41", "af42", "af43", "ef"), "2.5.0" }
/* This specifies the Class Selector or Differentiated Services Code Point
designation on IP headers (in the ToS field). */
{ "quota_db", "quotalegacy", STRINGLIST("flat", "skiplist", "sql", "quotalegacy", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for quotas. */
{ "quota_db_path", NULL, STRING, "2.5.0" }
/* The absolute path for the quota database (if you choose a single-file
quota DB type - or the base path if you choose quotalegacy). If
not specified will be configdirectory/quotas.db or configdirectory/quota/ */
{ "quota_use_conversations", 0, SWITCH, "3.3.0" }
/* If conversations it enabled and quotaroot is a user folder, use the
conversations quota counts, which count multiple copies of exactly the
same message (by GUID) as only one */
{ "quotawarn", 90, INT, "2.3.17" }
/* The percent of quota utilization over which the server generates
warnings. */
{ "quotawarnkb", 0, INT, "2.3.17" }
/* The maximum amount of free space (in kB) at which to give a quota
warning (if this value is 0, or if the quota is smaller than this
amount, then warnings are always given). */
{ "quotawarnmsg", 0, INT, "2.5.0" }
/* The maximum amount of messages at which to give a quota warning
(if this value is 0, or if the quota is smaller than this
amount, then warnings are always given). */
{ "readonly", 0, SWITCH, "3.3.0" }
/* If enabled, all IMAP, POP and JMAP connections are read-only,
* no writes allowed */
{ "reject8bit", 0, SWITCH, "2.3.17" }
/* If enabled, lmtpd rejects messages with 8-bit characters in the
headers. */
{ "restore_authname", NULL, STRING, "3.0.0" }
/* The authentication used by the restore tool when authenticating
to an IMAP/sync server. */
{ "restore_password", NULL, STRING, "3.0.0" }
/* The password used by the restore tool when authenticating to an
IMAP/sync server. */
{ "restore_realm", NULL, STRING, "3.0.0" }
/* The authentication realm used by the restore tool when
authenticating to an IMAP/sync server. */
{ "reverseacls", 0, SWITCH, "3.0.0" }
/* At startup time, ctl_cyrusdb -r will check this value and it
will either add or remove reverse ACL pointers from mailboxes.db */
{ "reverseuniqueids", 1, SWITCH, "3.3.0" }
/* At startup time, ctl_cyrusdb -r will check this value and it
will either add or remove reverse UNIQUEID pointers from mailboxes.db */
{ "rfc2046_strict", 0, SWITCH, "2.3.17" }
/* If enabled, imapd will be strict (per RFC 2046) when matching MIME
boundary strings. This means that boundaries containing other
boundaries as substrings will be treated as identical. Since
enabling this option will break some messages created by Eudora 5.1
(and earlier), it is recommended that it be left disabled unless
there is good reason to do otherwise. */
{ "rfc2047_utf8", 0, SWITCH, "3.0.0" }
/* If enabled, imapd will parse any non-encoded character sequence in
MIME header values as UTF8. This is useful for installations that
either advertise the UTF8SMTP (RFC 5335) extension or receive mails
with improperly escaped UTF-8 byte sequences. It is recommended that
this option is left disabled unless there is good reason to do
otherwise. */
{ "rfc3028_strict", 1, SWITCH, "2.3.17" }
/* If enabled, Sieve will be strict (per RFC 3028) with regards to
which headers are allowed to be used in address and envelope tests.
This means that only those headers which are defined to contain addresses
will be allowed in address tests and only "to" and "from" will be
allowed in envelope tests. When disabled, ANY grammatically correct header
will be allowed. */
{ "rss_feedlist_template", NULL, STRING, "2.5.0" }
/* File containing HTML that will be used as a template for displaying
the list of available RSS feeds. A single instance of the variable
%RSS_FEEDLIST% should appear in the file, which will be replaced by
a nested unordered list of feeds. The toplevel unordered list will
be tagged with an id of "feed" (<ul id='feed'>) which can be used
by stylesheet(s) in your template. The dynamically created list of
feeds based on the HTML template will be accessible at the "/rss"
URL on the server. */
{ "rss_feeds", "*", STRING, "2.5.0" }
/* A wildmat pattern specifying which mailbox hierarchies should be
treated as RSS feeds. Only mailboxes matching the wildmat will
have their messages available via RSS. If not set, a default
wildmat of "*" (ALL mailboxes) will be used. */
{ "rss_maxage", NULL, DURATION, "3.1.8" }
/* Maximum age of items to display in an RSS channel. If non-zero,
httpd will only display items received within this time period.
If set to 0, all available items will be displayed (the default).
.PP
For backward compatibility, if no unit is specified, days is
assumed. */
{ "rss_maxitems", 0, INT, "2.5.0" }
/* Maximum number of items to display in an RSS channel. If non-zero,
httpd will display no more than the \fIrss_maxitems\fR most recent
items. If set to 0, all available items will be displayed (the
default). */
{ "rss_maxsynopsis", 0, INT, "2.5.0" }
/* Maximum RSS item synopsis length. If non-zero, httpd will display
no more than the first \fIrss_maxsynopsis\fR characters of an
item's synopsis. If set to 0, the entire synopsis will be
displayed (the default). */
{ "rss_realm", NULL, STRING, "2.5.0" }
/* The realm to present for HTTP authentication of RSS feeds. If not
set (the default), the value of the "servername" option will be
used.*/
# Commented out - used by libsasl
# { "sasl_auto_transition", 0, SWITCH, "2.3.17" }
/* If enabled, the SASL library will automatically create authentication
secrets when given a plaintext password. See the SASL documentation. */
{ "sasl_maximum_layer", 256, INT, "2.3.17" }
/* Maximum SSF (security strength factor) that the server will allow a
client to negotiate. */
{ "sasl_minimum_layer", 0, INT, "2.3.17" }
/* The minimum SSF that the server will allow a client to negotiate.
A value of 1 requires integrity protection; any higher value
requires some amount of encryption. */
# Commented out - used by libsasl
# { "sasl_option", 0, STRING, "2.3.17" }
/* Any SASL option can be set by preceding it with \fBsasl_\fR. This
file overrides the SASL configuration file. */
# Commented out - used by libsasl
# { "sasl_pwcheck_method", NULL, STRING, "2.3.17" }
/* The mechanism used by the server to verify plaintext passwords.
Possible values include "auxprop", "saslauthd", and "pwcheck". */
{ "search_batchsize", 20, INT, "3.0.0" }
/* The number of messages to be indexed in one batch (default 20).
Note that long batches may delay user commands or mail delivery. */
{ "search_attachment_extractor_url", NULL, STRING, "3.3.1" }
/* A HTTP or HTTPS URL to extract search text from rich text attachments
and other media during search indexing. The server at this URL must
implement the following protocol:
1. For each attachment of an email, Cyrus sends a GET request to the
URL <extractor-url>/<cyrus-id>, where <extractor-url> is the
configured URL and <cyrus-id> is a Cyrus-chosen path segment that
uniquely identifies this attachment.
2. If the extractor already has a cached plain text extract of the
attachment identified by <cyrus-id> then it may return HTTP status
code 200 (OK) and the plain text extract with a Content-Type
"text/plain" header. Otherwise it must return HTTP status 404 (Not Found).
3. If Cyrus receives the HTTP status code 404 (Not Found), then it sends
a PUT request to the same URL as previously. The PUT request body
contains the decoded, binary body of the attachment. The Content-Type
request header has the same value as declared in the MIME part
headers, including any type parameters.
4. The extractor must return the plain text extract with either HTTP status
200 (OK) or 201 (Created) and a Content-Type "text/plain" header.
If no text can be extracted, then the extractor may return any return code
in the range 4xx, or 200 and an empty response body.
Any other HTTP status code is treated as an error. For performance
reasons, the Cyrus indexer attempts to keep-alive the TCP connection
to the extractor.
Xapian only.
*/
{ "search_index_language", 0, SWITCH, "3.3.1" }
/*
If enabled, then messages bodies are stemmed by detected language
in addition to the default English stemmer.
Xapian only.
*/
{ "search_index_parts", 0, SWITCH, "3.2.1", "3.2.1" }
/*
Deprecated. No longer used.
*/
{ "search_index_skip_domains", NULL, STRING, "3.3.1" }
/*
A space separated list of domains - if set, any users in the listed domains
will be skipped when indexing.
*/
{ "search_index_skip_users", NULL, STRING, "3.3.1" }
/*
A space separated list of usernames - if set, any users in the list
will be skipped when indexing.
*/
{ "search_query_language", 0, SWITCH, "3.3.0", "3.3.0" }
/*
Deprecated. No longer used.
*/
{ "search_normalisation_max", 1000, INT, "3.0.0" }
/* A resource bound for the combinatorial explosion of search expression
tree complexity caused by normalising expressions with many OR nodes.
These can use more CPU time to optimise than they save IO time in scanning
folders. */
{ "search_engine", "none", ENUM("none", "squat", "xapian"), "3.1.2" }
/* The indexing engine used to speed up searching. */
{ "search_fuzzy_always", 0, SWITCH, "3.1.5" }
/* Whether to enable RFC 6203 FUZZY search for all IMAP SEARCH. If turned
on, search attributes will be searched using FUZZY search by default.
If turned off, clients have to explicitly use the FUZZY search key to
enable fuzzy search for regular SEARCH commands. */
{ "search_index_headers", 1, SWITCH, "3.0.0" }
/* Whether to index headers other than From, To, Cc, Bcc, and Subject.
Experiment shows that some headers such as Received and DKIM-Signature
can contribute up to 2/3rds of the index size but almost nothing to
the utility of searching. Note that if header indexing is disabled,
headers can still be searched, the searches will just be slower.
*/
{ "search_indexed_db", "twoskip", STRINGLIST("flat", "skiplist", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the search latest indexed uid state. Xapian only. */
{ "search_maxtime", NULL, STRING, "3.0.0" }
/* The maximum number of seconds to run a search for before aborting. Default
of no value means search "forever" until other timeouts. */
{ "search_queryscan", 5000, INT, "3.1.7" }
/* The minimum number of records require to do a direct scan of all G keys
* rather than indexed lookups. A value of 0 means always do indexed lookups.
*/
{ "search_skipdiacrit", 1, SWITCH, "2.5.0" }
/* When searching, should diacriticals be stripped from the search
terms. The default is "true", a search for "hav" will match
"Håvard". This is not RFC 5051 compliant, but it backwards
compatible, and may be preferred by some sites. */
{ "search_skiphtml", 0, SWITCH, "3.0.0" }
/* If enabled, HTML parts of messages are skipped, i.e. not indexed and
not searchable. Otherwise, they're indexed. */
{ "search_whitespace", "merge", ENUM("skip", "merge", "keep"), "2.5.0" }
/* When searching, how whitespace should be handled. Options are:
"skip" (default in 2.3 and earlier series) - where a search for
"equi" would match "the quick brown fox". "merge" - the default,
where "he qu" would match "the quick brownfox", and "keep",
where whitespace must match exactly. The default of "merge" is
recommended for most cases - it's a good compromise which
keeps words separate. */
{ "search_snippet_length", 255, INT, "3.0.0" }
/* The maximum byte length of a snippet generated by the XSNIPPETS
command. Only supported by the Xapian search backend, which
attempts to always fill search_snippet_length bytes in the
generated snippet. */
{ "search_stopword_path", NULL, STRING, "3.1.7" }
/* The absolute base path to the search stopword lists. If not specified,
no stopwords will be taken into account during search indexing. Currently,
the only supported and default stop word file is english.txt. */
# Commented out - there's no such thing as "searchpartition-name",
# but we need this for the man page
# { "searchpartition-name", NULL, STRING, "3.1.4" }
/* The pathname where to store the xapian search indexes of \fIsearchtier\fR
for mailboxes of partition \fIname\fR. This must be configured for the
\fIdefaultsearchtier\fR and any additional search tier (see squatter for
details).
.PP
For example: if \fIdefaultpartition\fR is defined as part1 and
\fIdefaultsearchtier\fR as tier1 then the configuration must contain
an entry \fItier1searchpartition-part1\fR that defines the path where to
store this tier1's search index for the part1 partition.
.PP
This option MUST be specified for xapian search. */
{ "seenstate_db", "twoskip", STRINGLIST("flat", "skiplist", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the seen state. */
{ "sendmail", "/usr/lib/sendmail", STRING, "2.3.17" }
/* The pathname of the sendmail executable. Sieve invokes sendmail
for sending rejections, redirects and vacation responses. */
{ "sendmail_auth_id", "CYRUS_SENDMAIL_AUTH_ID", STRING, "3.1.3" }
/* The name of an environment variable to set when invoking sendmail.
The value of this environment variable will contain the user id
of the currently authenticated user. If no user is authenticated
the environment variable is not set. */
{ "serverlist", NULL, STRING, "2.3.17" }
/* Whitespace separated list of backend server names. Used for
finding server with the most available free space for proxying
CREATE. */
{ "serverlist_select_mode", "freespace-most", STRINGLIST("random", "freespace-most", "freespace-percent-most", "freespace-percent-weighted", "freespace-percent-weighted-delta"), "2.5.0" }
/* Server selection mode.
.PP
.IP \fIrandom\fR 5
(pseudo-)random selection
.\"
.IP \fIfreespace-most\fR 5
backend with the most (total) free space (KiB)
.\"
.IP \fIfreespace-percent-most\fR 5
backend whose partition has the most free space (%)
.\"
.IP \fIfreespace-percent-weighted\fR 5
same as for partition selection, comparing the free space (%) of the least used
partition of each backend
.\"
.IP \fIfreespace-percent-weighted-delta\fR 5
same as for partition selection, comparing the free space (%) of the least used
partition of each backend.
.PP
*/
{ "serverlist_select_usage_reinit", 0, INT, "2.5.0" }
/* For a given session, number of \fBoperations\fR (e.g. backend selection)
for which backend usage data are cached. */
{ "serverlist_select_soft_usage_limit", 0, INT, "2.5.0" }
/* Limit of backend usage (%): if a backend is over that limit, it is
automatically excluded from selection mode.
.PP
If all backends are over that limit, this feature is not used anymore.
*/
{ "servername", NULL, STRING, "2.3.17" }
/* This is the hostname visible in the greeting messages of the POP,
IMAP and LMTP daemons. If it is unset, then the result returned
from gethostname(2) is used. This is also the value used by murder
clusters to identify the host name. It should be resolvable by
DNS to the correct host, and unique within an active cluster. If
you are using low level replication (e.g. drbd) then it should be
the same on each copy and the DNS name should also be moved to
the new master on failover. */
{ "serverinfo", "on", ENUM("off", "min", "on"), "2.3.17" }
/* The server information to display in the greeting and capability
responses. Information is displayed as follows:
.IP
"off" = no server information in the greeting or capabilities
.br
"min" = \fIservername\fR in the greeting; no server information in the capabilities
.br
"on" = \fIservername\fR and product version in the greeting;
product version in the capabilities
.PP
*/
{ "sharedprefix", "Shared Folders", STRING, "2.3.17" }
/* If using the alternate IMAP namespace, the prefix for the shared
namespace. The hierarchy delimiter will be automatically appended.
*/
{ "sieve_allowreferrals", 1, SWITCH, "2.3.17" }
/* If enabled, timsieved will issue referrals to clients when the
user's scripts reside on a remote server (in a Murder).
Otherwise, timsieved will proxy traffic to the remote server. */
{ "sieve_duplicate_max_expiration", "90d", DURATION, "3.1.8" }
/* Maximum expiration time for duplicate message tracking records.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
*/
{ "sieve_extensions", "fileinto reject vacation vacation-seconds notify include envelope environment body relational regex subaddress copy date index imap4flags mailbox mboxmetadata servermetadata variables editheader extlists duplicate ihave fcc special-use redirect-dsn redirect-deliverby mailboxid vnd.cyrus.log vnd.cyrus.jmapquery snooze", BITFIELD("fileinto", "reject", "vacation", "vacation-seconds", "notify", "include", "envelope", "environment", "body", "relational", "regex", "subaddress", "copy", "date", "index", "imap4flags=imapflags", "mailbox", "mboxmetadata", "servermetadata", "variables", "editheader", "extlists", "duplicate", "ihave", "fcc", "special-use", "redirect-dsn", "redirect-deliverby", "mailboxid", "vnd.cyrus.log=x-cyrus-log", "vnd.cyrus.jmapquery=x-cyrus-jmapquery", "snooze=vnd.cyrus.snooze=x-cyrus-snooze"), "3.3.1" }
/* Space-separated list of Sieve extensions allowed to be used in
sieve scripts, enforced at submission by timsieved(8). Any
previously installed script will be unaffected by this option and
will continue to execute regardless of the extensions used. This
option has no effect on options that are disabled at compile time
(e.g., "regex"). */
{ "sieve_maxscriptsize", 32, INT, "2.3.17" }
/* Maximum size (in kilobytes) any sieve script can be, enforced at
submission by timsieved(8). */
{ "sieve_maxscripts", 5, INT, "2.3.17" }
/* Maximum number of sieve scripts any user may have, enforced at
submission by timsieved(8). */
{ "sieve_utf8fileinto", 0, SWITCH, "2.3.17" }
/* If enabled, the sieve engine expects folder names for the
\fIfileinto\fR action in scripts to use UTF8 encoding. Otherwise,
modified UTF7 encoding should be used. */
{ "sieve_sasl_send_unsolicited_capability", 0, SWITCH, "2.3.17" }
/* If enabled, timsieved will emit a capability response after a successful
SASL authentication, per draft-martin-managesieve-12.txt . */
{ "sieve_use_lmtp_reject", 1, SWITCH, "3.1.1" }
/* Enabled by default. If reject can be done via LMTP, then return a 550
rather than generating the bounce message in Cyrus. */
{ "sieve_vacation_min_response", "3d", DURATION, "3.1.8" }
/* Minimum time interval between consecutive vacation responses, per
draft-ietf-vacation-seconds.txt. The default is 3 days.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "sieve_vacation_max_response", "90d", DURATION, "3.1.8" }
/* Maximum time interval between consecutive vacation responses, per
draft-ietf-vacation-seconds.txt. The default is 90 days. The
minimum is 7 days.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "sievedir", "/usr/sieve", STRING, "2.3.17" }
/* If sieveusehomedir is false, this directory is searched for Sieve
scripts. */
{ "sievenotifier", NULL, STRING, "2.3.17" }
/* Notifyd(8) method to use for "SIEVE" notifications. If not set, "SIEVE"
notifications are disabled.
.PP
This method is only used when no method is specified in the script. */
{ "sieveusehomedir", 0, SWITCH, "2.3.17" }
/* If enabled, lmtpd will look for Sieve scripts in user's home
directories: ~user/.sieve. */
{ "anysievefolder", 0, SWITCH, "2.5.0" }
/* It must be "yes" in order to permit the autocreation of any INBOX subfolder
requested by a sieve filter, through the "fileinto" action. (default = no) */
{ "singleinstancestore", 1, SWITCH, "2.3.17" }
/* If enabled, imapd, lmtpd and nntpd attempt to only write one copy
of a message per partition and create hard links, resulting in a
potentially large disk savings. */
{ "skiplist_always_checkpoint", 1, SWITCH, "2.3.17" }
/* If enabled, this option forces the skiplist cyrusdb backend to
always checkpoint when doing a recovery. This causes slightly
more IO, but on the other hand leads to more efficient databases,
and the entire file is already "hot". */
{ "skiplist_unsafe", 0, SWITCH, "2.3.17" }
/* If enabled, this option forces the skiplist cyrusdb backend to
not sync writes to the disk. Enabling this option is NOT RECOMMENDED. */
{ "smtp_backend", "sendmail", STRINGLIST("host", "sendmail"), "3.1.4" }
/* The SMTP backend to use for sending email.
The \"host\" backend sends message submissions via
a TCP socket to the SMTP host defined in the config
option smtp_host.
The \"sendmail\" backend forks the Cyrus process into
the executable defined in the config option sendmail.
The executable must accept \"-bs\" as command line
argument, read from stdin and must implement the minimum
SMTP protocol as defined in section 4.5.1 of RFC 5321.
If the SMTP EHLO command reports AUTH (RFC 4954) as a
supported extension, then the MAIL FROM command includes
the AUTH parameter, with its value set to the name of any
authenticated user which triggered the email. The AUTH
parameter is omitted if the user is unknown to the calling
process.
If the directory
\fIconfigdirectory\fR/log/smtpclient.\ \fIsmtp_backend\fR
exists, then telemetry logs for outgoing SMTP sessions will
be created in this directory.
*/
{ "smtp_host", "localhost:587", STRING, "3.1.4" }
/* The SMTP host to use for sending mail (also see the
smtp_backend option). The value of this option must
the name or IP address of a TCP host, followed optionally
by a colon and the port or service to use. The default
port is 587. TLS may be activated by appending \"/tls\"
to the value. Authentication is enabled if smtp_auth_authname
is set. Authentication can be explicitly disabled by appending
\"/noauth\" to the host address. */
{ "smtp_auth_authname", NULL, STRING, "3.1.4" }
/* The authentication name to use when authenticating to the SMTP
server defined in smtp_host. */
{ "smtp_auth_password", NULL, STRING, "3.1.4" }
/* The password to use when authenticating to the SMTP server defined
in smtp_host. */
{ "smtp_auth_realm", NULL, STRING, "3.1.4" }
/* The authentication SASL realm to use when authenticating to a SMTP
server. */
{ "soft_noauth", 1, SWITCH, "2.3.17" }
/* If enabled, lmtpd returns temporary failures if the client does not
successfully authenticate. Otherwise lmtpd returns permanent failures
(causing the mail to bounce immediately). */
{ "sortcache_db", "twoskip", STRINGLIST("skiplist", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for caching sort results (currently only
used for xconvmultisort) */
{ "specialuse_extra", NULL, STRING, "2.5.0" }
/* Whitespace separated list of extra special-use attributes
that can be set on a mailbox. RFC 6154 currently lists
what special-use attributes can be set. This allows
extending that list in the future or adding your own
if needed. */
{ "specialuse_nochildren", NULL, STRING, "3.3.2" }
/* Whitespace separated list of special-use attributes that may not contain
child folders. If set, mailboxes with any of these attributes may not
have child folders created, and these attributes cannot be added to
mailboxes that already have children.. */
{ "specialuse_protect", "\\Archive \\Drafts \\Important \\Junk \\Sent \\Trash", STRING, "3.1.7" }
/* Whitespace separated list of special-use attributes
to protect the mailboxes for. If set, don't allow
mailboxes with these special use attributes to be deleted
or renamed to have a different parent. Default is the built-in list*/
{ "specialusealways", 1, SWITCH, "3.1.1" }
/* If enabled, this option causes LIST and LSUB output to always include
the XLIST "special-use" flags */
{ "sql_database", NULL, STRING, "2.3.17" }
/* Name of the database which contains the cyrusdb table(s). */
{ "sql_engine", NULL, STRINGLIST("mysql", "pgsql", "sqlite"), "2.3.17" }
/* Name of the SQL engine to use. */
{ "sql_hostnames", "", STRING, "2.3.17" }
/* Comma separated list of SQL servers (in host[:port] format). */
{ "sql_passwd", NULL, STRING, "2.3.17" }
/* Password to use for authentication to the SQL server. */
{ "sql_user", NULL, STRING, "2.3.17" }
/* Username to use for authentication to the SQL server. */
{ "sql_usessl", 0, SWITCH, "2.3.17" }
/* If enabled, a secure connection will be made to the SQL server. */
{ "srs_alwaysrewrite", 0, SWITCH, "2.5.0" }
/* If true, perform SRS rewriting for ALL forwarding, even when not required. */
{ "srs_domain", NULL, STRING, "3.1.2" }
/* The domain to use in rewritten addresses. This must point only to machines
which know the encoding secret used by this system. When present, SRS is
enabled. */
{ "srs_hashlength", 0, INT, "3.1.2" }
/* The hash length to generate in a rewritten address. */
{ "srs_secrets", NULL, STRING, "3.1.2" }
/* A list of secrets with which to generate addresses. */
{ "srs_separator", NULL, STRING, "3.1.2" }
/* The separator to appear immediately after SRS[01] in rewritten addresses. */
{ "srvtab", "", STRING, "2.3.17" }
/* The pathname of \fIsrvtab\fR file containing the server's private
key. This option is passed to the SASL library and overrides its
default setting. */
{ "submitservers", NULL, STRING, "2.3.17" }
/* A list of users and groups that are allowed to resolve "urlauth=submit+"
IMAP URLs, separated by spaces. Any user listed in this will be
allowed to fetch the contents of any valid "urlauth=submit+" IMAP URL:
use with caution. */
{ "subscription_db", "flat", STRINGLIST("flat", "skiplist", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the subscriptions list. */
{ "suppress_capabilities", NULL, STRING, "3.0.0" }
/* Suppress the named capabilities from any capability response. Use the
exact case as it appears in the response, e.g.
"suppress_capabilities: ESEARCH QRESYNC WITHIN XLIST LIST-EXTENDED"
if you have a murder with 2.3.x backends and don't want clients being
confused by new capabilities that some backends don't support. */
{ "statuscache", 0, SWITCH, "2.3.17" }
/* Enable/disable the imap status cache. */
{ "statuscache_db", "twoskip", STRINGLIST("skiplist", "sql", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the imap status cache. */
{ "statuscache_db_path", NULL, STRING, "2.5.0" }
/* The absolute path to the statuscache db file. If not specified,
will be configdirectory/statuscache.db */
{ "sync_authname", NULL, STRING, "2.5.0" }
/* The authentication name to use when authenticating to a sync server.
Prefix with a channel name to only apply for that channel */
{ "sync_batchsize", 8192, INT, "3.0.0" }
/* the number of messages to upload in a single mailbox replication.
Default is 8192. If there are more than this many messages appended
to the mailbox, generate a synthetic partial state and send that. */
{ "sync_cache_db", "twoskip", STRINGLIST("skiplist", "sql", "twoskip", "zeroskip"), "3.3.1" }
/* The cyrusdb backend to use for the replication cache. */
{ "sync_cache_db_path", NULL, STRING, "3.3.1" }
/* The path for the replication cache. Prefix with a
channel name to apply for that channel. NOTE, it's
quite important to have a different one per backend! */
{ "sync_host", NULL, STRING, "2.5.0" }
/* Name of the host (replica running sync_server(8)) to which
replication actions will be sent by sync_client(8).
Prefix with a channel name to only apply for that channel */
{ "sync_log", 0, SWITCH, "2.3.17" }
/* Enable replication action logging by lmtpd(8), imapd(8), pop3d(8),
and nntpd(8). The log {configdirectory}/sync/log is used by
sync_client(8) for "rolling" replication. */
{ "sync_log_chain", 0, SWITCH, "2.4.0" }
/* Enable replication action logging by sync_server as well, allowing
chaining of replicas. Use this on 'B' for A => B => C replication layout */
{ "sync_log_channels", NULL, STRING, "2.5.0" }
/* If specified, log all events to multiple log files in directories
specified by each "channel". Each channel can then be processed
separately, such as by multiple sync_client(8)s in a mesh replication
scheme, or by squatter(8) for rolling search index updates.
.PP
You can use "" (the two-character string U+22 U+22) to mean the
default sync channel. */
{ "sync_log_unsuppressable_channels", "squatter", STRING, "2.5.0" }
/* If specified, the named channels are exempt from the effect of setting
sync_log_chain:off, i.e. they are always logged to by the sync_server
process. This is only really useful to allow rolling search indexing
on a replica. */
{ "sync_password", NULL, STRING, "2.5.0" }
/* The default password to use when authenticating to a sync server.
Prefix with a channel name to only apply for that channel */
{ "sync_port", NULL, STRING, "3.0.0" }
/* Name of the service (or port number) of the replication service on
replica host. Prefix with a channel name to only apply for that
channel. If not specified, and if sync_try_imap is set to "yes"
(the default), then the replication client will first try "imap"
(port 143) to check if imapd supports replication. otherwise it
will default to "csync" (usually port 2005). */
{ "sync_realm", NULL, STRING, "2.5.0" }
/* The authentication realm to use when authenticating to a sync server.
Prefix with a channel name to only apply for that channel */
{ "sync_repeat_interval", "1s", DURATION, "3.1.8" }
/* Minimum interval between replication runs in rolling replication
mode. If a replication run takes longer than this time, we repeat
immediately. Prefix with a channel name to only apply for that
channel.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "sync_rightnow_channel", NULL, STRING, "3.3.1" }
/* if set, run sync_client to this channel immediately. As with channels,
set this value to '""' to sync the default channel! */
{ "sync_shutdown_file", NULL, STRING, "2.5.0" }
/* Simple latch used to tell sync_client(8) that it should shut down at the
next opportunity. Safer than sending signals to running processes.
Prefix with a channel name to only apply for that channel */
{ "sync_timeout", "30m", DURATION, "3.1.8" }
/* How long to wait for a response before returning a timeout failure
when talking to a replication peer (client or server). The minimum
duration is 3 seconds, the default is 30 minutes.
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "sync_try_imap", 1, SWITCH, "3.0.0" }
/* Whether sync_client should try to perform an IMAP connection
before falling back to csync. If this is set to "no",
sync_client will only use csync. Prefix with a channel name to
apply only for that channel */
{ "syslog_prefix", NULL, STRING, "3.1.8" }
/* String to be prepended to the process name in syslog entries. Can
be further overridden by setting the $CYRUS_SYSLOG_PREFIX environment
variable.
.PP
Using the $CYRUS_SYSLOG_PREFIX environment variable has the additional
advantage that it can be set before the \fBimapd.conf\fR is read, so
errors while reading the config file can be syslogged with the correct
prefix. */
{ "syslog_facility", NULL, STRING, "2.5.0" }
/* Configure a syslog facility. The default is whatever is compiled
in. Allowed values are: DAEMON, MAIL, NEWS, USER, and LOCAL0
through to LOCAL7 */
{ "tcp_keepalive", 0, SWITCH, "2.4.0" }
/* Enable keepalive on TCP connections */
{ "tcp_keepalive_cnt", 0, INT, "2.4.0" }
/* Number of TCP keepalive probes to send before declaring the
connection dead (0 == system default) */
{ "tcp_keepalive_idle", "0", DURATION, "3.1.8" }
/* How long a connection must be idle before keepalive probes are sent
(0 == system default).
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "tcp_keepalive_intvl", "0", DURATION, "3.1.8" }
/* Time between keepalive probes (0 == system default).
.PP
For backward compatibility, if no unit is specified, seconds is
assumed. */
{ "temp_path", "/tmp", STRING, "3.3.0" }
/* The pathname to store temporary files in. It is recommended to
use an in-memory filesystem such as tmpfs for this path. */
{ "telemetry_bysessionid", 0, SWITCH, "3.0.0" }
/* If true, log by sessionid instead of PID for telemetry */
{ "timeout", "32m", DURATION, "3.1.8" }
/* The length of the IMAP server's inactivity autologout timer.
The minimum value is 30 minutes. The default is 32 minutes,
to allow a bit of leeway for clients that try to NOOP every
30 minutes.
.PP
For backward compatibility, if no unit is specified, minutes
is assumed. */
{ "imapidletimeout", NULL, DURATION, "3.1.8" }
/* Timeout for idling clients (RFC 2177). If not set (the default),
the value of "timeout" will be used instead.
.PP
For backward compatibility, if no unit is specified, minutes
is assumed. */
{ "tls_ca_file", NULL, STRING, "2.5.0", "2.5.0", "tls_client_ca_file" }
/* Deprecated in favor of \fItls_client_ca_file\fR. */
{ "tls_ca_path", NULL, STRING, "2.5.0", "2.5.0", "tls_client_ca_dir" }
/* Deprecated in favor of \fItls_client_ca_dir\fR. */
{ "tlscache_db", "twoskip", STRINGLIST("skiplist", "sql", "twoskip", "zeroskip"), "2.5.0", "2.5.0", "tls_sessions_db" }
/* Deprecated in favor of \fItls_sessions_db\fR. */
{ "tlscache_db_path", NULL, STRING, "2.5.0", "2.5.0", "tls_sessions_db_path" }
/* Deprecated in favor of \fItls_sessions_db_path\fR. */
{ "tls_cert_file", NULL, STRING, "2.5.0", "2.5.0", "tls_server_cert" }
/* Deprecated in favor of \fItls_server_cert\fR. */
{ "tls_cipher_list", "DEFAULT", STRING, "2.5.0", "2.5.0", "tls_ciphers" }
/* Deprecated in favor of \fItls_ciphers\fR. */
{ "tls_ciphers", "DEFAULT", STRING, "2.5.0" }
/* The list of SSL/TLS ciphers to allow. The format of the string
(and definition of "DEFAULT") is described in \fBciphers(1)\fR.
.PP
See also Mozilla's server-side TLS recommendations:
.PP
https://wiki.mozilla.org/Security/Server_Side_TLS */
{ "tls_crl_file", NULL, STRING, "3.1.2" }
/* Path to a file containing the Certificate Revocation List */
{ "tls_client_ca_dir", NULL, STRING, "2.5.0" }
/* Path to a directory containing the CA certificates used to verify
client SSL certificates used for authentication. */
{ "tls_client_ca_file", NULL, STRING, "2.5.0" }
/* Path to a file containing the CA certificate(s) used to verify
client SSL certificates used for authentication. */
{ "tls_client_cert", NULL, STRING, "2.5.0" }
/* File containing the certificate presented to a server for authentication
during STARTTLS. A value of "disabled" will disable this server's use
of certificate-based authentication. */
{ "tls_client_certs", "optional", ENUM("off", "optional", "require"), "2.5.0" }
/* Disable ("off"), allow ("optional", default) or require ("require") the
use of SSL certificates by clients to authenticate themselves. */
{ "tls_client_key", NULL, STRING, "2.5.0" }
/* File containing the private key belonging to the tls_client_cert
certificate. A value of "disabled" will disable this server's use
of certificate-based authentication. */
{ "tls_eccurve", "prime256v1", STRING, "2.5.0" }
/* The elliptic curve used for ECDHE. Default is NIST Suite B prime256.
See 'openssl ecparam -list_curves' for possible values. */
{ "tls_key_file", NULL, STRING, "2.5.0", "2.5.0", "tls_server_key" }
/* Deprecated in favor of \fItls_server_key\fR. */
{ "tls_required", 0, SWITCH, "3.0.0" }
/* If enabled, require a TLS/SSL encryption layer to be negotiated
prior to ANY authentication mechanisms being advertised or allowed. */
{ "tls_prefer_server_ciphers", 0, SWITCH, "2.5.0" }
/* Prefer the ciphers on the server side instead of client side. */
{ "tls_server_ca_dir", NULL, STRING, "2.5.0" }
/* Path to a directory with CA certificates used to verify certificates
offered by the server, when cyrus acts as client. This directory must
have filenames with the hashed value of the certificates (see
openssl(1)). */
{ "tls_server_ca_file", NULL, STRING, "2.5.0" }
/* Path to a file containing CA certificates used to verify certificates
offered by the server, when cyrus acts as client. */
{ "tls_server_cert", NULL, STRING, "3.1.8" }
/* File containing the certificate, including the full chain, presented to clients.
Two certificates can be set, e.g RSA and EC, if the filenames are separated with
comma without spaces. */
{ "tls_server_dhparam", NULL, STRING, "3.1.4" }
/* File containing the DH parameters belonging to the certificate in
tls_server_cert. */
{ "tls_server_key", NULL, STRING, "3.1.8" }
/* File containing the private key belonging to the certificate in
tls_server_cert. If not set, tls_server_cert must contain both private and
public key. Two files with keys can be set, if two certifates are used, in
which case the files must be separated with comma without spaces */
{ "tls_sessions_db", "twoskip", STRINGLIST("skiplist", "sql", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the TLS cache. */
{ "tls_sessions_db_path", NULL, STRING, "2.5.0" }
/* The absolute path to the TLS sessions db file. If not specified,
will be configdirectory/tls_sessions.db */
{ "tls_session_timeout", "24h", DURATION, "3.1.8" }
/* The length of time that a TLS session will be cached for later
reuse. The maximum value is 24 hours, also the default. A
value of 0 will disable session caching.
.PP
For backward compatibility, if no unit is specified, minutes is
assumed. */
{ "tls_versions", "tls1_0 tls1_1 tls1_2 tls1_3", STRING, "3.1.8" }
/* A list of SSL/TLS versions to not disable. Cyrus IMAP SSL/TLS starts
with all protocols, and subtracts protocols not in this list. Newer
versions of SSL/TLS will need to be added here to allow them to get
disabled. */
{ "uidl_format", "cyrus", ENUM("uidonly", "cyrus", "dovecot", "courier"), "3.0.0" }
/* Choose the format for UIDLs in pop3. Possible values are "uidonly",
"cyrus", "dovecot" and "courier". "uidonly" forces the old default
of UID, "cyrus" is UIDVALIDITY.UID. Dovecot is 8 digits of leading
hex (lower case) each UID UIDVALIDITY. Courier is UIDVALIDITY-UID. */
{ "umask", "077", STRING, "2.3.17" }
/* The umask value used by various Cyrus IMAP programs. */
{ "userdeny_db", "flat", STRINGLIST("flat", "skiplist", "sql", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for the user access list. */
{ "userdeny_db_path", NULL, STRING, "2.5.0" }
/* The absolute path to the userdeny db file. If not specified,
will be configdirectory/user_deny.db */
{ "username_tolower", 1, SWITCH, "2.3.17" }
/* Convert usernames to all lowercase before login/authentication. This
is useful with authentication backends which ignore case during
username lookups (such as LDAP). */
{ "userprefix", "Other Users", STRING, "2.3.17" }
/* If using the alternate IMAP namespace, the prefix for the other users
namespace. The hierarchy delimiter will be automatically appended. */
# xxx badly worded
{ "unix_group_enable", 1, SWITCH, "2.3.17" }
/* Should we look up groups when using auth_unix (disable this if you are
not using groups in ACLs for your IMAP server, and you are using auth_unix
with a backend (such as LDAP) that can make getgrent() calls very
slow) */
{ "unixhierarchysep", 1, SWITCH, "3.0.0" }
/* Use the UNIX separator character '/' for delimiting levels of
mailbox hierarchy. Turn off to use the netnews separator
character '.'. Note that with the newnews separator, no dots may
occur in mailbox names. The default switched in 3.0 from off to on. */
{ "virtdomains", "off", ENUM("off", "userid", "on"), "3.1.8" }
/* Configure virtual domain support.
.PP
.IP off
Cyrus does not know or care about domains. Only the local part of email
addresses is ever considered. This is not recommended for any deployment,
but is currently the default.
.\"
.IP userid
The user's domain is determined by splitting a fully qualified userid at the
last '@' or '%' symbol. If the userid is unqualified, the \fIdefaultdomain\fR
will be used. This is the recommended configuration for all deployments.
If you wish to provide calendaring services you must use this configuration.
.\"
.IP on
Fully qualified userids are respected, as per "userid". Unqualified userids
will have their domain determined by doing a reverse lookup on the IP address
of the incoming network interface, or if no record is found, the
\fIdefaultdomain\fR will be used.
.PP
*/
{ "virusscan_notification_subject", "Automatically deleted mail", STRING, "3.1.8" }
/* The text used in the subject of email notifications created by
\fBcyr_virusscan(8)\fR when deleting infected mail. */
{ "virusscan_notification_template", NULL, STRING, "3.1.8" }
/* The absolute path to a file containing a template to use to describe
infected messages that have been deleted by \fBcyr_virusscan(8)\fR.
See \fBcyr_virusscan(8)\fR for specification of the format of this file.
If not specified, the builtin default template will be used. */
{ "xbackup_enabled", 0, SWITCH, "3.0.0" }
/* Enable support for the XBACKUP command in imapd. If enabled, admin
users can use this command to provoke a replication of specified users
to the named backup channel. */
# Commented out - there's no such thing as "xlist-flag", but we need
# this for the man page
# { "xlist-flag", NULL, STRING, "3.0.0" }
/* Set the special-use flag \fIflag\fR on the specified folder when it
is autocreated (see the \fIautocreate_inbox_folders\fR option). For
example, if \fBxlist-junk: Spam\fR is set, and the folder \fBSpam\fR
is autocreated, the special-use flag \fB\\Junk\fR will be set on it.
.PP
(This option is so named for backward compatibility with old config
files.)
*/
{ "lmtp_catchall_mailbox", NULL, STRING, "2.5.0" }
/* Mail sent to mailboxes which do not exist, will be delivered to
this user. NOTE: This must be an existing local user name with an
INBOX, NOT an email address! */
{ "zoneinfo_db", "twoskip", STRINGLIST("flat", "skiplist", "twoskip", "zeroskip"), "3.1.6" }
/* The cyrusdb backend to use for zoneinfo. This database is used by the
"tzdist" \fIhttpmodules\fR, and is managed by \fBctl_zoneinfo(8)\fR.*/
{ "zoneinfo_db_path", NULL, STRING, "2.5.0" }
/* The absolute path to the zoneinfo db file. If not specified,
will be configdirectory/zoneinfo.db */
{ "zoneinfo_dir", NULL, STRING, "3.2.0" }
/* The absolute path to the zoneinfo directory, containing timezone
definitions as generated by the vzic tool. If not specified, whatever
definitions libical finds will be used.
.PP
If you are providing a Time Zone Data Distribution Service (i.e. you have
"tzdist" listed in \fIhttpmodules\fR), then this configuration option MUST
be specified. */
{ "object_storage_enabled", 0, SWITCH, "3.0.0" }
/* Is Object storage enabled for this server. You also need to have
archiving enabled and archivepartition for the mailbox.
Only email files will be stored on object Storage archive partition will be
used to store any other files */
{ "object_storage_dummy_spool", NULL, STRING, "3.0.0" }
/* Dummy object storage spool; this is for test only.
Spool where user directory (container) will be created to store all emails
in a flat structure */
{ "openio_namespace", NULL, STRING, "3.0.0" }
/* The OpenIO namespace used to store archived email messages. A namespace
identifies the physical platform cyrus must contact. This directive is used
by the OpenIO's SDK to locate its platform entry point. */
{ "openio_account", NULL, STRING, "3.0.0" }
/* The OpenIO account used to account for stored emails. Accounts are unique
in their namespace. They provides virtual partitions, with quotas and QoS
features. */
{ "openio_rawx_timeout", "30s", DURATION, "3.1.8" }
/* The OpenIO timeout to query to the RAWX services (default 30 sec). */
{ "openio_proxy_timeout", "5s", DURATION, "3.1.8" }
/* The OpenIO timeout to query to the PROXY services (default 5 sec). */
{ "openio_autocreate", 0, SWITCH, "3.0.0" }
/* Allow the OpenIO SDK to autocreate containers. Mainly destined to be turned
on development environments. In production, the container should have been
provisioned with the mailboxes. */
{ "openio_verbosity", NULL, STRING, "3.0.0" }
/* Sets the logging verbosity of the OpenIO's internal behavior. Admissible
values are: "warning", "notice", "info", "debug", "trace", "quiet".
The default verbosity is "warning". Set to "notice" for a few lines on a
per-client basis. Set to "info" for a few lines on a per-request basis. Set
to "debug" Set to "trace" to activate the underlying libcurl debug
output. Enabling a verbosity higher to equal than "debug" requires
the cyrus to be set in debug mode. The special "quiet" value disables all
kinds of logging at the GLib level. */
{ "caringo_hostname", NULL, STRING, "3.0.0" }
/* The Caringo hostname used to store archived email messages. A hostname
identifies the physical platform cyrus must contact. This directive is used
by the Caringo's SDK (CastorSDK: Caringo Simple Content Storage Protocol (SCSP)
on HTTP 1.1 using a RESTful architecture */
{ "caringo_port", 80, INT, "3.0.0" }
/* The port of the caringo server (caringo_hostname); default is 80. */
{ "fastmailsharing", 0, SWITCH, "3.0.0" }
/* If enabled, use FastMail style sharing (oldschool full server paths) */
/*
.SH SEE ALSO
.PP
\fBimapd(8)\fR, \fBpop3d(8)\fR, \fBnntpd(8)\fR, \fBlmtpd(8)\fR,
\fBhttpd(8)\fR, \fBtimsieved(8)\fR, \fBidled(8)\fR, \fBnotifyd(8)\fR,
\fBdeliver(8)\fR, \fBmaster(8)\fR, \fBciphers(1)\fR
*/

File Metadata

Mime Type
text/x-diff
Expires
Fri, Apr 24, 1:41 PM (1 d, 12 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18880897
Default Alt Text
(918 KB)

Event Timeline