Page MenuHomePhorge

http_jmap.c
No OneTemporary

Authored By
Unknown
Size
69 KB
Referenced Files
None
Subscribers
None

http_jmap.c

/* http_jmap.c -- Routines for handling JMAP requests in httpd
*
* Copyright (c) 1994-2014 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <config.h>
#include <sasl/sasl.h>
#include <sasl/saslutil.h>
#ifdef HAVE_SSL
#include <openssl/hmac.h>
#include <openssl/rand.h>
#endif /* HAVE_SSL */
#include <errno.h>
#include "append.h"
#include "cyrusdb.h"
#include "hash.h"
#include "httpd.h"
#include "http_dav.h"
#include "http_proxy.h"
#include "mboxname.h"
#include "msgrecord.h"
#include "proxy.h"
#include "times.h"
#include "syslog.h"
#include "xstrlcpy.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include "imap/imap_err.h"
#include "imap/jmap_err.h"
#include "http_jmap.h"
#define JMAP_ROOT "/jmap"
#define JMAP_BASE_URL JMAP_ROOT "/"
#define JMAP_UPLOAD_COL "upload/"
#define JMAP_UPLOAD_TPL "{accountId}/"
#define JMAP_DOWNLOAD_COL "download/"
#define JMAP_DOWNLOAD_TPL "{accountId}/{blobId}/{name}"
struct namespace jmap_namespace;
static time_t compile_time;
static json_t *jmap_capabilities = NULL;
/* HTTP method handlers */
static int jmap_get(struct transaction_t *txn, void *params);
static int jmap_post(struct transaction_t *txn, void *params);
/* Namespace callbacks */
static void jmap_init(struct buf *serverinfo);
static int jmap_need_auth(struct transaction_t *txn);
static int jmap_auth(const char *userid);
static int jmap_settings(struct transaction_t *txn);
static int jmap_initreq(jmap_req_t *req);
static void jmap_finireq(jmap_req_t *req);
static int jmap_blob_copy(jmap_req_t *req);
static int myrights(struct auth_state *authstate,
const mbentry_t *mbentry,
hash_table *mboxrights);
static int myrights_byname(struct auth_state *authstate,
const char *mboxname,
hash_table *mboxrights);
/* Namespace for JMAP */
struct namespace_t namespace_jmap = {
URL_NS_JMAP, 0, JMAP_ROOT, "/.well-known/jmap",
jmap_need_auth, /*authschemes*/0,
/*mbtype*/0,
(ALLOW_READ | ALLOW_POST),
&jmap_init, &jmap_auth, NULL, NULL, NULL, /*bearer*/NULL,
{
{ NULL, NULL }, /* ACL */
{ NULL, NULL }, /* BIND */
{ NULL, NULL }, /* COPY */
{ NULL, NULL }, /* DELETE */
{ &jmap_get, NULL }, /* GET */
{ &jmap_get, NULL }, /* HEAD */
{ NULL, NULL }, /* LOCK */
{ NULL, NULL }, /* MKCALENDAR */
{ NULL, NULL }, /* MKCOL */
{ NULL, NULL }, /* MOVE */
{ &meth_options, NULL }, /* OPTIONS */
{ NULL, NULL }, /* PATCH */
{ &jmap_post, NULL }, /* POST */
{ NULL, NULL }, /* PROPFIND */
{ NULL, NULL }, /* PROPPATCH */
{ NULL, NULL }, /* PUT */
{ NULL, NULL }, /* REPORT */
{ &meth_trace, NULL }, /* TRACE */
{ NULL, NULL }, /* UNBIND */
{ NULL, NULL } /* UNLOCK */
}
};
static int json_response(int code, struct transaction_t *txn, json_t *root)
{
size_t flags = JSON_PRESERVE_ORDER;
char *buf;
/* Dump JSON object into a text buffer */
flags |= (config_httpprettytelemetry ? JSON_INDENT(2) : JSON_COMPACT);
buf = json_dumps(root, flags);
json_decref(root);
if (!buf) {
txn->error.desc = "Error dumping JSON object";
return HTTP_SERVER_ERROR;
}
/* Output the JSON object */
switch (code) {
case HTTP_OK:
case HTTP_CREATED:
txn->resp_body.type = "application/json; charset=utf-8";
break;
default:
txn->resp_body.type = "application/problem+json; charset=utf-8";
break;
}
write_body(code, txn, buf, strlen(buf));
free(buf);
return 0;
}
static int json_error_response(struct transaction_t *txn, long code)
{
long http_code = HTTP_BAD_REQUEST;
const char *type, *title, *limit = NULL;
json_t *root;
/* Error string is encoded as type NUL title [ NUL limit ] */
type = error_message(code);
title = type + strlen(type) + 1;
switch (code) {
case JMAP_NOT_JSON:
case JMAP_NOT_REQUEST:
case JMAP_UNKNOWN_CAPABILITY:
break;
case JMAP_LIMIT_SIZE:
http_code = HTTP_PAYLOAD_TOO_LARGE;
GCC_FALLTHROUGH
case JMAP_LIMIT_CALLS:
case JMAP_LIMIT_OBJS_GET:
case JMAP_LIMIT_OBJS_SET:
limit = title + strlen(title) + 1;
break;
default:
/* Actually an HTTP code, not a JMAP error code */
return code;
}
root = json_pack("{s:s s:s s:i}", "type", type, "title", title,
"status", atoi(error_message(http_code)));
if (!root) {
txn->error.desc = "Unable to create JSON response";
return HTTP_SERVER_ERROR;
}
if (limit) {
json_object_set_new(root, "limit", json_string(limit));
}
if (txn->error.desc) {
json_object_set_new(root, "detail", json_string(txn->error.desc));
}
return json_response(http_code, txn, root);
}
enum {
JMAP_ENDPOINT_API,
JMAP_ENDPOINT_UPLOAD,
JMAP_ENDPOINT_DOWNLOAD
};
static int jmap_parse_path(struct transaction_t *txn)
{
struct request_target_t *tgt = &txn->req_tgt;
size_t len;
char *p;
if (*tgt->path) return 0; /* Already parsed */
/* Make a working copy of target path */
strlcpy(tgt->path, txn->req_uri->path, sizeof(tgt->path));
p = tgt->path;
/* Sanity check namespace */
len = strlen(namespace_jmap.prefix);
if (strlen(p) < len ||
strncmp(namespace_jmap.prefix, p, len) ||
(tgt->path[len] && tgt->path[len] != '/')) {
txn->error.desc = "Namespace mismatch request target path";
return HTTP_FORBIDDEN;
}
/* Skip namespace */
p += len;
if (!*p) {
/* Canonicalize URL */
txn->location = JMAP_BASE_URL;
return HTTP_MOVED;
}
/* Check for path after prefix */
if (*++p) {
/* Get "collection" */
tgt->collection = p;
if (!strncmp(tgt->collection, JMAP_UPLOAD_COL,
strlen(JMAP_UPLOAD_COL))) {
tgt->flags = JMAP_ENDPOINT_UPLOAD;
tgt->allow = ALLOW_POST;
/* Get "resource" which must be the accountId */
tgt->resource = tgt->collection + strlen(JMAP_UPLOAD_COL);
}
else if (!strncmp(tgt->collection,
JMAP_DOWNLOAD_COL, strlen(JMAP_DOWNLOAD_COL))) {
tgt->flags = JMAP_ENDPOINT_DOWNLOAD;
tgt->allow = ALLOW_READ;
/* Get "resource" */
tgt->resource = tgt->collection + strlen(JMAP_DOWNLOAD_COL);
}
else {
return HTTP_NOT_ALLOWED;
}
}
else {
tgt->flags = JMAP_ENDPOINT_API;
tgt->allow = ALLOW_POST|ALLOW_READ;
}
return 0;
}
static hash_table jmap_methods = HASH_TABLE_INITIALIZER;
static jmap_method_t *find_methodproc(const char *name)
{
return hash_lookup(name, &jmap_methods);
}
struct mymblist_rock {
mboxlist_cb *proc;
void *rock;
struct auth_state *authstate;
hash_table *mboxrights;
int all;
};
static int mymblist_cb(const mbentry_t *mbentry, void *rock)
{
struct mymblist_rock *myrock = rock;
if (!myrock->all) {
if (mbentry->mbtype & MBTYPE_DELETED)
return 0;
int rights = myrights(myrock->authstate, mbentry, myrock->mboxrights);
if (!(rights & ACL_LOOKUP))
return 0;
}
return myrock->proc(mbentry, myrock->rock);
}
static int mymblist(const char *userid,
const char *accountid,
struct auth_state *authstate,
hash_table *mboxrights,
mboxlist_cb *proc,
void *rock,
int all)
{
int flags = all ? (MBOXTREE_TOMBSTONES|MBOXTREE_DELETED) : 0;
/* skip ACL checks if account owner */
if (!strcmp(userid, accountid))
return mboxlist_usermboxtree(userid, proc, rock, flags);
/* Open the INBOX first */
struct mymblist_rock myrock = { proc, rock, authstate, mboxrights, all };
return mboxlist_usermboxtree(accountid, mymblist_cb, &myrock, flags);
}
EXPORTED int jmap_mboxlist(jmap_req_t *req, mboxlist_cb *proc, void *rock)
{
return mymblist(req->userid, req->accountid, req->authstate,
req->mboxrights, proc, rock, 0/*all*/);
}
static long jmap_max_size_upload = 0;
static long jmap_max_concurrent_upload = 0;
static long jmap_max_size_request = 0;
static long jmap_max_concurrent_requests = 0;
static long jmap_max_calls_in_request = 0;
static long jmap_max_objects_in_get = 0;
static long jmap_max_objects_in_set = 0;
static void jmap_init(struct buf *serverinfo __attribute__((unused)))
{
namespace_jmap.enabled =
config_httpmodules & IMAP_ENUM_HTTPMODULES_JMAP;
if (!namespace_jmap.enabled) return;
compile_time = calc_compile_time(__TIME__, __DATE__);
initialize_JMAP_error_table();
#define _read_opt(val, optkey) \
val = config_getint(optkey); \
if (val <= 0) { \
syslog(LOG_ERR, "jmap: invalid property value: %s", \
imapopts[optkey].optname); \
val = 0; \
}
_read_opt(jmap_max_size_upload, IMAPOPT_JMAP_MAX_SIZE_UPLOAD);
jmap_max_size_upload *= 1024;
_read_opt(jmap_max_concurrent_upload, IMAPOPT_JMAP_MAX_CONCURRENT_UPLOAD);
_read_opt(jmap_max_size_request, IMAPOPT_JMAP_MAX_SIZE_REQUEST);
jmap_max_size_request *= 1024;
_read_opt(jmap_max_concurrent_requests, IMAPOPT_JMAP_MAX_CONCURRENT_REQUESTS);
_read_opt(jmap_max_calls_in_request, IMAPOPT_JMAP_MAX_CALLS_IN_REQUEST);
_read_opt(jmap_max_objects_in_get, IMAPOPT_JMAP_MAX_OBJECTS_IN_GET);
_read_opt(jmap_max_objects_in_set, IMAPOPT_JMAP_MAX_OBJECTS_IN_SET);
#undef _read_opt
jmap_capabilities = json_pack("{s:{s:i s:i s:i s:i s:i s:i s:i s:o}}",
"ietf:jmap",
"maxSizeUpload", jmap_max_size_upload,
"maxConcurrentUpload", jmap_max_concurrent_upload,
"maxSizeRequest", jmap_max_size_request,
"maxConcurrentRequests", jmap_max_concurrent_requests,
"maxCallsInRequest",jmap_max_calls_in_request,
"maxObjectsInGet", jmap_max_objects_in_get,
"maxObjectsInSet", jmap_max_objects_in_set,
"collationAlgorithms", json_array()
);
construct_hash_table(&jmap_methods, 128, 0);
jmap_mail_init(&jmap_methods, jmap_capabilities);
jmap_contact_init(&jmap_methods, jmap_capabilities);
jmap_calendar_init(&jmap_methods, jmap_capabilities);
static jmap_method_t blobcopy = { "Blob/copy", &jmap_blob_copy };
hash_insert(blobcopy.name, &blobcopy, &jmap_methods);
}
static int jmap_auth(const char *userid __attribute__((unused)))
{
/* Set namespace */
mboxname_init_namespace(&jmap_namespace,
httpd_userisadmin || httpd_userisproxyadmin);
return 0;
}
/* Perform a GET/HEAD request */
static int jmap_get(struct transaction_t *txn,
void *params __attribute__((unused)))
{
int r = jmap_parse_path(txn);
if (r || !(txn->req_tgt.allow & ALLOW_READ)) {
return HTTP_NOT_FOUND;
}
if (txn->req_tgt.flags == JMAP_ENDPOINT_API) {
return jmap_settings(txn);
}
return jmap_download(txn);
}
static int is_accessible(const mbentry_t *mbentry,
void *rock __attribute__((unused)))
{
if ((mbentry->mbtype & MBTYPE_DELETED) ||
(mbentry->mbtype & MBTYPE_MOVING) ||
(mbentry->mbtype & MBTYPE_REMOTE) ||
(mbentry->mbtype & MBTYPE_RESERVE)) {
return 0;
}
return IMAP_OK_COMPLETED;
}
static json_t *extract_value(json_t *from, const char *path, ptrarray_t *refs);
static json_t *extract_array_value(json_t *val, const char *idx,
const char *path, ptrarray_t *pool)
{
if (!strcmp(idx, "*")) {
/* Build value from array traversal */
json_t *newval = json_pack("[]");
size_t i;
json_t *v;
json_array_foreach(val, i, v) {
json_t *x = extract_value(v, path, pool);
if (json_is_array(x)) {
/* JMAP spec: "If the result of applying the rest
* of the pointer tokens to a value was itself an
* array, its items should be included individually
* in the output rather than including the array
* itself." */
json_array_extend(newval, x);
} else if (x) {
json_array_append(newval, x);
} else {
json_decref(newval);
newval = NULL;
}
}
if (newval) {
ptrarray_add(pool, newval);
}
return newval;
}
/* Lookup array value by index */
const char *eot = NULL;
bit64 num;
if (parsenum(idx, &eot, 0, &num) || *eot) {
return NULL;
}
val = json_array_get(val, num);
if (!val) {
return NULL;
}
return extract_value(val, path, pool);
}
/* Extract the JSON value at position path from val.
*
* Return NULL, if the the value does not exist or if
* path is erroneous.
*/
static json_t *extract_value(json_t *val, const char *path, ptrarray_t *pool)
{
/* Return value for empty path */
if (*path == '\0') {
return val;
}
/* Be lenient: root path '/' is optional */
if (*path == '/') {
path++;
}
/* Walk over path segments */
while (val && *path) {
const char *top = NULL;
char *p = NULL;
/* Extract next path segment */
if (!(top = strchr(path, '/'))) {
top = strchr(path, '\0');
}
p = json_pointer_decode(path, top - path);
if (*p == '\0') {
return NULL;
}
/* Extract array value */
if (json_is_array(val)) {
val = extract_array_value(val, p, top, pool);
free(p);
return val;
}
/* Value MUST be an object now */
if (!json_is_object(val)) {
free(p);
return NULL;
}
/* Step down into object tree */
val = json_object_get(val, p);
free(p);
path = *top ? top + 1 : top;
}
return val;
}
static int process_resultrefs(json_t *args, json_t *resp)
{
json_t *ref;
const char *arg;
int ret = -1;
void *tmp;
json_object_foreach_safe(args, tmp, arg, ref) {
if (*arg != '#' || *(arg+1) == '\0') {
continue;
}
const char *of, *path, *name;
json_t *res = NULL;
/* Parse result reference object */
of = json_string_value(json_object_get(ref, "resultOf"));
if (!of || *of == '\0') {
goto fail;
}
path = json_string_value(json_object_get(ref, "path"));
if (!path || *path == '\0') {
goto fail;
}
name = json_string_value(json_object_get(ref, "name"));
if (!name || *name == '\0') {
goto fail;
}
/* Lookup referenced response */
json_t *v;
size_t i;
json_array_foreach(resp, i, v) {
const char *tag = json_string_value(json_array_get(v, 2));
if (!tag || strcmp(tag, of)) {
continue;
}
const char *mname = json_string_value(json_array_get(v, 0));
if (!mname || strcmp(name, mname)) {
goto fail;
}
res = v;
break;
}
if (!res) goto fail;
/* Extract the reference argument value. */
/* We maintain our own pool of newly created JSON objects, since
* tracking reference counts across newly created JSON arrays is
* a pain. Rule: If you incref an existing JSON value or create
* an entirely new one, put it into the pool for cleanup. */
ptrarray_t pool = PTRARRAY_INITIALIZER;
json_t *val = extract_value(json_array_get(res, 1), path, &pool);
if (!val) goto fail;
/* Replace both key and value of the reference entry */
json_object_set(args, arg + 1, val);
json_object_del(args, arg);
/* Clean up reference counts of pooled JSON objects */
json_t *ref;
while ((ref = ptrarray_pop(&pool))) {
json_decref(ref);
}
ptrarray_fini(&pool);
}
return 0;
fail:
return ret;
}
static int parse_json_body(struct transaction_t *txn, json_t **req)
{
const char **hdr;
json_error_t jerr;
int ret;
/* Check Content-Type */
if (!(hdr = spool_getheader(txn->req_hdrs, "Content-Type")) ||
!is_mediatype("application/json", hdr[0])) {
txn->error.desc = "This method requires a JSON request body";
return HTTP_BAD_MEDIATYPE;
}
/* Read body */
txn->req_body.flags |= BODY_DECODE;
ret = http_read_req_body(txn);
if (ret) {
txn->flags.conn = CONN_CLOSE;
return ret;
}
/* Parse the JSON request */
*req = json_loads(buf_cstring(&txn->req_body.payload), 0, &jerr);
if (!*req) {
buf_reset(&txn->buf);
buf_printf(&txn->buf,
"Unable to parse JSON request body: %s", jerr.text);
txn->error.desc = buf_cstring(&txn->buf);
return JMAP_NOT_JSON;
}
return 0;
}
static int validate_request(struct transaction_t *txn, json_t *req)
{
json_t *using = json_object_get(req, "using");
json_t *calls = json_object_get(req, "methodCalls");
if (!json_is_array(using) || !json_is_array(calls)) {
return JMAP_NOT_REQUEST;
}
/*
* XXX the following maximums are not enforced:
* maxConcurrentUpload
* maxConcurrentRequests
*/
if (buf_len(&txn->req_body.payload) > (size_t) jmap_max_size_request) {
return JMAP_LIMIT_SIZE;
}
size_t i;
json_t *val;
json_array_foreach(calls, i, val) {
if (json_array_size(val) != 3 ||
!json_is_string(json_array_get(val, 0)) ||
!json_is_object(json_array_get(val, 1)) ||
!json_is_string(json_array_get(val, 2))) {
return JMAP_NOT_REQUEST;
}
if (i >= (size_t) jmap_max_calls_in_request) {
return JMAP_LIMIT_CALLS;
}
const char *mname = json_string_value(json_array_get(val, 0));
mname = strchr(mname, '/');
if (!mname) continue;
if (!strcmp(mname, "get")) {
json_t *ids = json_object_get(json_array_get(val, 1), "ids");
if (json_array_size(ids) > (size_t) jmap_max_objects_in_get) {
return JMAP_LIMIT_OBJS_GET;
}
}
else if (!strcmp(mname, "set")) {
json_t *args = json_array_get(val, 1);
size_t size = json_object_size(json_object_get(args, "create"));
size += json_object_size(json_object_get(args, "update"));
size += json_array_size(json_object_get(args, "destroy"));
if (size > (size_t) jmap_max_objects_in_set) {
return JMAP_LIMIT_OBJS_SET;
}
}
}
json_array_foreach(using, i, val) {
const char *s = json_string_value(val);
if (!s) {
return JMAP_NOT_REQUEST;
}
if (!json_object_get(jmap_capabilities, s)) {
return JMAP_UNKNOWN_CAPABILITY;
}
}
return 0;
}
EXPORTED int jmap_is_valid_id(const char *id)
{
if (!id || *id == '\0') return 0;
const char *p;
for (p = id; *p; p++) {
if (('0' <= *p && *p <= '9'))
continue;
if (('a' <= *p && *p <= 'z') || ('A' <= *p && *p <= 'Z'))
continue;
if ((*p == '-') || (*p == '_'))
continue;
return 0;
}
return 1;
}
static void _make_created_ids(const char *creation_id, void *val, void *rock)
{
json_t *jcreatedIds = rock;
const char *id = val;
json_object_set_new(jcreatedIds, creation_id, json_string(id));
}
/* Perform a POST request */
static int jmap_post(struct transaction_t *txn,
void *params __attribute__((unused)))
{
json_t *jreq = NULL, *resp = NULL;
size_t i;
int ret;
char *inboxname = NULL;
hash_table *client_creation_ids = NULL;
hash_table *new_creation_ids = NULL;
hash_table accounts = HASH_TABLE_INITIALIZER;
hash_table mboxrights = HASH_TABLE_INITIALIZER;
strarray_t methods = STRARRAY_INITIALIZER;
ret = jmap_parse_path(txn);
if (ret) return ret;
if (!(txn->req_tgt.allow & ALLOW_POST)) {
return HTTP_NOT_ALLOWED;
}
/* Handle uploads */
if (txn->req_tgt.flags == JMAP_ENDPOINT_UPLOAD) {
return jmap_upload(txn);
}
/* Regular JMAP POST request */
ret = parse_json_body(txn, &jreq);
if (ret) return json_error_response(txn, ret);
/* Validate Request object */
if ((ret = validate_request(txn, jreq))) {
return json_error_response(txn, ret);
}
/* Start JSON response */
resp = json_array();
if (!resp) {
txn->error.desc = "Unable to create JSON response body";
ret = HTTP_SERVER_ERROR;
goto done;
}
/* Set up request-internal state */
construct_hash_table(&accounts, 8, 0);
construct_hash_table(&mboxrights, 64, 0);
/* Set up creation ids */
long max_creation_ids = (jmap_max_calls_in_request + 1) * jmap_max_objects_in_set;
new_creation_ids = xzmalloc(sizeof(hash_table));
construct_hash_table(new_creation_ids, max_creation_ids, 0);
/* Parse client-supplied creation ids */
json_t *jcreationIds = json_object_get(jreq, "creationIds");
if (json_is_object(jcreationIds)) {
client_creation_ids = xzmalloc(sizeof(hash_table));
construct_hash_table(client_creation_ids, json_object_size(jcreationIds)+1, 0);
const char *creation_id;
json_t *jval;
json_object_foreach(jcreationIds, creation_id, jval) {
if (!json_is_string(jval)) {
txn->error.desc = "Invalid creationIds argument";
ret = HTTP_BAD_REQUEST;
goto done;
}
const char *id = json_string_value(jval);
if (!jmap_is_valid_id(creation_id) || !jmap_is_valid_id(id)) {
txn->error.desc = "Invalid creationIds argument";
ret = HTTP_BAD_REQUEST;
goto done;
}
hash_insert(creation_id, xstrdup(id), client_creation_ids);
}
}
else if (jcreationIds && jcreationIds != json_null()) {
txn->error.desc = "Invalid creationIds argument";
ret = HTTP_BAD_REQUEST;
goto done;
}
/* Process each method call in the request */
json_t *mc;
json_array_foreach(json_object_get(jreq, "methodCalls"), i, mc) {
const jmap_method_t *mp;
const char *mname = json_string_value(json_array_get(mc, 0));
json_t *args = json_array_get(mc, 1), *arg;
const char *tag = json_string_value(json_array_get(mc, 2));
int r = 0;
strarray_append(&methods, mname);
/* Find the message processor */
if (!(mp = find_methodproc(mname))) {
json_array_append(resp, json_pack("[s {s:s} s]",
"error", "type", "unknownMethod", tag));
continue;
}
/* Determine account */
const char *accountid = httpd_userid;
arg = json_object_get(args, "accountId");
if (arg && arg != json_null()) {
if ((accountid = json_string_value(arg)) == NULL) {
json_t *err = json_pack("{s:s, s:[s]}",
"type", "invalidArguments", "arguments", "accountId");
json_array_append(resp, json_pack("[s,o,s]", "error", err, tag));
continue;
}
/* Check if any shared mailbox is accessible */
if (!hash_lookup(accountid, &accounts)) {
r = mymblist(httpd_userid, accountid, httpd_authstate,
&mboxrights, is_accessible, NULL, 0/*all*/);
if (r != IMAP_OK_COMPLETED) {
json_t *err = json_pack("{s:s}", "type", "accountNotFound");
json_array_append_new(resp,
json_pack("[s,o,s]", "error", err, tag));
continue;
}
hash_insert(accountid, (void*)1, &accounts);
}
}
free(inboxname);
inboxname = mboxname_user_mbox(accountid, NULL);
/* Pre-process result references */
if (process_resultrefs(args, resp)) {
json_array_append_new(resp, json_pack("[s,{s:s},s]",
"error", "type", "resultReference", tag));
continue;
}
struct conversations_state *cstate = NULL;
r = conversations_open_user(accountid, &cstate);
if (r) {
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
goto done;
}
struct jmap_req req;
memset(&req, 0, sizeof(struct jmap_req));
req.method = mname;
req.userid = httpd_userid;
req.accountid = accountid;
req.inboxname = inboxname;
req.cstate = cstate;
req.authstate = httpd_authstate;
req.args = args;
req.response = resp;
req.tag = tag;
req.client_creation_ids = client_creation_ids;
req.new_creation_ids = new_creation_ids;
req.txn = txn;
req.mboxrights = &mboxrights;
req.is_shared_account = strcmp(accountid, httpd_userid);
req.force_openmbox_rw = 0;
/* Initialize request context */
jmap_initreq(&req);
/* Read the current state data in */
r = mboxname_read_counters(inboxname, &req.counters);
if (r) goto done;
/* Call the message processor. */
r = mp->proc(&req);
/* Finalize request context */
jmap_finireq(&req);
if (r) {
conversations_abort(&req.cstate);
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
goto done;
}
conversations_commit(&req.cstate);
}
/* tell syslog which methods were called */
spool_cache_header(xstrdup(":jmap"),
strarray_join(&methods, ","), txn->req_hdrs);
/* Build responses */
json_t *res = json_pack("{s:O}", "methodResponses", resp);
if (client_creation_ids) {
json_t *jcreatedIds = json_object();
hash_enumerate(new_creation_ids, _make_created_ids, jcreatedIds);
json_object_set_new(res, "createdIds", jcreatedIds);
}
/* Output the JSON object */
ret = json_response(HTTP_OK, txn, res);
done:
free_hash_table(client_creation_ids, free);
free(client_creation_ids);
free_hash_table(new_creation_ids, free);
free(new_creation_ids);
free_hash_table(&accounts, NULL);
free_hash_table(&mboxrights, free);
free(inboxname);
json_decref(jreq);
json_decref(resp);
strarray_fini(&methods);
syslog(LOG_DEBUG, ">>>> jmap_post: Exit\n");
return ret;
}
const char *jmap_lookup_id(jmap_req_t *req, const char *creation_id)
{
if (req->client_creation_ids) {
const char *id = hash_lookup(creation_id, req->client_creation_ids);
if (id) return id;
}
if (!req->new_creation_ids)
return NULL;
return hash_lookup(creation_id, req->new_creation_ids);
}
void jmap_add_id(jmap_req_t *req, const char *creation_id, const char *id)
{
/* It's OK to overwrite existing ids, as per Foo/set:
* "A client SHOULD NOT reuse a creation id anywhere in the same API
* request. If a creation id is reused, the server MUST map the creation
* id to the most recently created item with that id."
*/
if (!req->new_creation_ids) {
req->new_creation_ids = xzmalloc(sizeof(hash_table));
construct_hash_table(req->new_creation_ids, 128, 0);
}
hash_insert(creation_id, xstrdup(id), req->new_creation_ids);
}
struct _mboxcache_rec {
struct mailbox *mbox;
int refcount;
int rw;
};
static int jmap_initreq(jmap_req_t *req)
{
req->mboxes = ptrarray_new();
return 0;
}
static void jmap_finireq(jmap_req_t *req)
{
int i;
for (i = 0; i < req->mboxes->count; i++) {
struct _mboxcache_rec *rec = ptrarray_nth(req->mboxes, i);
syslog(LOG_ERR, "jmap: force-closing mailbox %s (refcount=%d)",
rec->mbox->name, rec->refcount);
mailbox_close(&rec->mbox);
free(rec);
}
/* Fail after cleaning up open mailboxes */
assert(!req->mboxes->count);
ptrarray_free(req->mboxes);
req->mboxes = NULL;
}
EXPORTED int jmap_openmbox(jmap_req_t *req, const char *name,
struct mailbox **mboxp, int rw)
{
int i, r;
struct _mboxcache_rec *rec;
for (i = 0; i < req->mboxes->count; i++) {
rec = (struct _mboxcache_rec*) ptrarray_nth(req->mboxes, i);
if (!strcmp(name, rec->mbox->name)) {
if (rw && !rec->rw) {
/* Lock promotions are not supported */
syslog(LOG_ERR, "jmapmbox: failed to grab write-lock on cached read-only mailbox %s", name);
return IMAP_INTERNAL;
}
/* Found a cached mailbox. Increment refcount. */
rec->refcount++;
*mboxp = rec->mbox;
return 0;
}
}
/* Add mailbox to cache */
if (req->force_openmbox_rw)
rw = 1;
r = rw ? mailbox_open_iwl(name, mboxp) : mailbox_open_irl(name, mboxp);
if (r) {
syslog(LOG_ERR, "jmap_openmbox(%s): %s", name, error_message(r));
return r;
}
rec = xzmalloc(sizeof(struct _mboxcache_rec));
rec->mbox = *mboxp;
rec->refcount = 1;
rec->rw = rw;
ptrarray_add(req->mboxes, rec);
return 0;
}
EXPORTED int jmap_isopenmbox(jmap_req_t *req, const char *name)
{
int i;
struct _mboxcache_rec *rec;
for (i = 0; i < req->mboxes->count; i++) {
rec = (struct _mboxcache_rec*) ptrarray_nth(req->mboxes, i);
if (!strcmp(name, rec->mbox->name))
return 1;
}
return 0;
}
EXPORTED void jmap_closembox(jmap_req_t *req, struct mailbox **mboxp)
{
struct _mboxcache_rec *rec = NULL;
int i;
if (mboxp == NULL || *mboxp == NULL) return;
for (i = 0; i < req->mboxes->count; i++) {
rec = (struct _mboxcache_rec*) ptrarray_nth(req->mboxes, i);
if (rec->mbox == *mboxp) {
if (!(--rec->refcount)) {
ptrarray_remove(req->mboxes, i);
mailbox_close(&rec->mbox);
free(rec);
}
*mboxp = NULL;
return;
}
}
syslog(LOG_INFO, "jmap: ignoring non-cached mailbox %s", (*mboxp)->name);
}
EXPORTED char *jmap_blobid(const struct message_guid *guid)
{
char *blobid = xzmalloc(42);
blobid[0] = 'G';
memcpy(blobid+1, message_guid_encode(guid), 40);
return blobid;
}
struct findblob_data {
jmap_req_t *req;
const char *accountid;
int is_shared_account;
struct mailbox *mbox;
msgrecord_t *mr;
char *part_id;
};
static int findblob_cb(const conv_guidrec_t *rec, void *rock)
{
struct findblob_data *d = (struct findblob_data*) rock;
jmap_req_t *req = d->req;
int r = 0;
/* Ignore blobs that don't belong to the current accountId */
mbname_t *mbname = mbname_from_intname(rec->mboxname);
int is_accountid_mbox =
(mbname && !strcmp(mbname_userid(mbname), d->accountid));
mbname_free(&mbname);
if (!is_accountid_mbox)
return 0;
/* Check ACL */
if (d->is_shared_account) {
mbentry_t *mbentry = NULL;
r = mboxlist_lookup(rec->mboxname, &mbentry, NULL);
if (r) {
syslog(LOG_ERR, "jmap_findblob: no mbentry for %s", rec->mboxname);
return r;
}
int rights = jmap_myrights(req, mbentry);
mboxlist_entry_free(&mbentry);
if ((rights & (ACL_LOOKUP|ACL_READ)) != (ACL_LOOKUP|ACL_READ)) {
return 0;
}
}
r = jmap_openmbox(req, rec->mboxname, &d->mbox, 0);
if (r) return r;
r = msgrecord_find(d->mbox, rec->uid, &d->mr);
if (r) {
jmap_closembox(req, &d->mbox);
d->mr = NULL;
return r;
}
d->part_id = rec->part ? xstrdup(rec->part) : NULL;
return IMAP_OK_COMPLETED;
}
static int _findblob(jmap_req_t *req, const char *blobid,
const char *accountid,
struct mailbox **mbox, msgrecord_t **mr,
struct body **body, const struct body **part)
{
struct findblob_data data = {
req, /* req */
accountid, /* accountid */
strcmp(req->userid, accountid), /* is_shared_account */
NULL, /* mbox */
NULL, /* mr */
NULL /* part_id */
};
struct body *mybody = NULL;
const struct body *mypart = NULL;
int i, r;
if (blobid[0] != 'G')
return IMAP_NOTFOUND;
r = conversations_guid_foreach(req->cstate, blobid+1, findblob_cb, &data);
if (r != IMAP_OK_COMPLETED) {
if (!r) r = IMAP_NOTFOUND;
goto done;
}
r = msgrecord_get_bodystructure(data.mr, &mybody);
if (r) goto done;
/* Find part containing the data */
if (data.part_id) {
ptrarray_t parts = PTRARRAY_INITIALIZER;
struct message_guid content_guid;
message_guid_decode(&content_guid, blobid+1);
ptrarray_push(&parts, mybody);
while ((mypart = ptrarray_shift(&parts))) {
if (!message_guid_cmp(&content_guid, &mypart->content_guid)) {
break;
}
if (!mypart->subpart) continue;
ptrarray_push(&parts, mypart->subpart);
for (i = 1; i < mypart->numparts; i++)
ptrarray_push(&parts, mypart->subpart + i);
}
ptrarray_fini(&parts);
if (!mypart) {
r = IMAP_NOTFOUND;
goto done;
}
}
*mbox = data.mbox;
*mr = data.mr;
*part = mypart;
*body = mybody;
r = 0;
done:
if (r) {
if (data.mbox) jmap_closembox(req, &data.mbox);
if (mybody) message_free_body(mybody);
}
if (data.part_id) free(data.part_id);
return r;
}
EXPORTED int jmap_findblob(jmap_req_t *req, const char *blobid,
struct mailbox **mbox, msgrecord_t **mr,
struct body **body, const struct body **part)
{
return _findblob(req, blobid, req->accountid, mbox, mr, body, part);
}
static char *parse_accept_header(const char **hdr)
{
char *val = NULL;
struct accept *accept = parse_accept(hdr);
if (accept) {
char *type = NULL;
char *subtype = NULL;
struct param *params = NULL;
message_parse_type(accept->token, &type, &subtype, &params);
if (type && subtype && !strchr(type, '*') && !strchr(subtype, '*'))
val = xstrdup(accept->token);
free(type);
free(subtype);
param_free(&params);
struct accept *tmp;
for (tmp = accept; tmp && tmp->token; tmp++) {
free(tmp->token);
}
free(accept);
}
return val;
}
EXPORTED int jmap_download(struct transaction_t *txn)
{
const char *userid = txn->req_tgt.resource;
const char *slash = strchr(userid, '/');
if (!slash) {
/* XXX - error, needs AccountId */
return HTTP_NOT_FOUND;
}
#if 0
size_t userlen = slash - userid;
/* invalid user? */
if (!strncmp(userid, httpd_userid, userlen)) {
txn->error.desc = "failed to match userid";
return HTTP_BAD_REQUEST;
}
#endif
const char *blobbase = slash + 1;
slash = strchr(blobbase, '/');
if (!slash) {
/* XXX - error, needs blobid */
txn->error.desc = "failed to find blobid";
return HTTP_BAD_REQUEST;
}
size_t bloblen = slash - blobbase;
if (*blobbase != 'G') {
txn->error.desc = "invalid blobid (doesn't start with G)";
return HTTP_BAD_REQUEST;
}
if (bloblen != 41) {
/* incomplete or incorrect blobid */
txn->error.desc = "invalid blobid (not 41 chars)";
return HTTP_BAD_REQUEST;
}
const char *name = slash + 1;
char *accountid = xstrndup(userid, strchr(userid, '/') - userid);
int res = 0;
struct conversations_state *cstate = NULL;
int r = conversations_open_user(accountid, &cstate);
if (r) {
txn->error.desc = error_message(r);
res = (r == IMAP_MAILBOX_BADNAME) ? HTTP_NOT_FOUND : HTTP_SERVER_ERROR;
free(accountid);
return res;
}
/* now we're allocating memory, so don't return from here! */
char *inboxname = mboxname_user_mbox(httpd_userid, NULL);
char *blobid = NULL;
char *ctype = NULL;
struct jmap_req req;
req.userid = httpd_userid;
req.accountid = accountid;
req.inboxname = inboxname;
req.cstate = cstate;
req.authstate = httpd_authstate;
req.args = NULL;
req.response = NULL;
req.tag = NULL;
req.client_creation_ids = NULL;
req.new_creation_ids = NULL;
req.txn = txn;
req.is_shared_account = strcmp(req.accountid, req.userid);
req.force_openmbox_rw = 0;
/* Initialize ACL mailbox cache for findblob */
hash_table mboxrights = HASH_TABLE_INITIALIZER;
construct_hash_table(&mboxrights, 64, 0);
req.mboxrights = &mboxrights;
jmap_initreq(&req);
blobid = xstrndup(blobbase, bloblen);
struct mailbox *mbox = NULL;
msgrecord_t *mr = NULL;
struct body *body = NULL;
const struct body *part = NULL;
struct buf msg_buf = BUF_INITIALIZER;
char *decbuf = NULL;
strarray_t headers = STRARRAY_INITIALIZER;
char *accept_mime = NULL;
/* Find part containing blob */
r = _findblob(&req, blobid, accountid, &mbox, &mr, &body, &part);
if (r) {
res = HTTP_NOT_FOUND; // XXX errors?
txn->error.desc = "failed to find blob by id";
goto done;
}
/* Map the message into memory */
r = msgrecord_get_body(mr, &msg_buf);
if (r) {
res = HTTP_NOT_FOUND; // XXX errors?
txn->error.desc = "failed to map record";
goto done;
}
const char **hdr;
if ((hdr = spool_getheader(txn->req_hdrs, "Accept"))) {
accept_mime = parse_accept_header(hdr);
}
if (!accept_mime) accept_mime = xstrdup("application/octet-stream");
// default with no part is the whole message
const char *base = msg_buf.s;
size_t len = msg_buf.len;
txn->resp_body.type = accept_mime;
if (part) {
// map into just this part
base += part->content_offset;
len = part->content_size;
// binary decode if needed
int encoding = part->charset_enc & 0xff;
base = charset_decode_mimebody(base, len, encoding, &decbuf, &len);
}
txn->resp_body.len = len;
txn->resp_body.dispo.fname = name;
write_body(HTTP_OK, txn, base, len);
done:
free(accept_mime);
free_hash_table(&mboxrights, free);
free(accountid);
free(decbuf);
free(ctype);
strarray_fini(&headers);
if (mbox) jmap_closembox(&req, &mbox);
conversations_commit(&cstate);
if (body) {
message_free_body(body);
free(body);
}
if (mr) {
msgrecord_unref(&mr);
}
buf_free(&msg_buf);
free(blobid);
jmap_finireq(&req);
free(inboxname);
return res;
}
static int lookup_upload_collection(const char *accountid, mbentry_t **mbentry)
{
mbname_t *mbname;
const char *uploadname;
int r;
/* Create notification mailbox name from the parsed path */
mbname = mbname_from_userid(accountid);
mbname_push_boxes(mbname, config_getstring(IMAPOPT_JMAPUPLOADFOLDER));
/* XXX - hack to allow @domain parts for non-domain-split users */
if (httpd_extradomain) {
/* not allowed to be cross domain */
if (mbname_localpart(mbname) &&
strcmpsafe(mbname_domain(mbname), httpd_extradomain)) {
r = HTTP_NOT_FOUND;
goto done;
}
mbname_set_domain(mbname, NULL);
}
/* Locate the mailbox */
uploadname = mbname_intname(mbname);
r = http_mlookup(uploadname, mbentry, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* Find location of INBOX */
char *inboxname = mboxname_user_mbox(accountid, NULL);
int r1 = http_mlookup(inboxname, mbentry, NULL);
free(inboxname);
if (r1 == IMAP_MAILBOX_NONEXISTENT) {
r = IMAP_INVALID_USER;
goto done;
}
int rights = httpd_myrights(httpd_authstate, *mbentry);
if (!(rights & ACL_CREATE)) {
r = IMAP_PERMISSION_DENIED;
goto done;
}
if (*mbentry) free((*mbentry)->name);
else *mbentry = mboxlist_entry_create();
(*mbentry)->name = xstrdup(uploadname);
}
else if (!r) {
int rights = httpd_myrights(httpd_authstate, *mbentry);
if (!(rights & ACL_INSERT)) {
r = IMAP_PERMISSION_DENIED;
goto done;
}
}
done:
mbname_free(&mbname);
return r;
}
static int create_upload_collection(const char *accountid,
struct mailbox **mailbox)
{
/* notifications collection */
mbentry_t *mbentry = NULL;
int r = lookup_upload_collection(accountid, &mbentry);
if (r == IMAP_INVALID_USER) {
goto done;
}
else if (r == IMAP_PERMISSION_DENIED) {
goto done;
}
else if (r == IMAP_MAILBOX_NONEXISTENT) {
if (!mbentry) goto done;
else if (mbentry->server) {
proxy_findserver(mbentry->server, &http_protocol, httpd_userid,
&backend_cached, NULL, NULL, httpd_in);
goto done;
}
r = mboxlist_createmailbox(mbentry->name, MBTYPE_COLLECTION,
NULL, 1 /* admin */, accountid,
httpd_authstate, 0, 0, 0, 0, mailbox);
/* we lost the race, that's OK */
if (r == IMAP_MAILBOX_LOCKED) r = 0;
if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
mbentry->name, error_message(r));
}
else if (mailbox) {
/* Open mailbox for writing */
r = mailbox_open_iwl(mbentry->name, mailbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mbentry->name, error_message(r));
}
}
done:
mboxlist_entry_free(&mbentry);
return r;
}
/* Helper function to determine domain of data */
enum {
DOMAIN_7BIT = 0,
DOMAIN_8BIT,
DOMAIN_BINARY
};
static int data_domain(const char *p, size_t n)
{
int r = DOMAIN_7BIT;
while (n--) {
if (!*p) return DOMAIN_BINARY;
if (*p & 0x80) r = DOMAIN_8BIT;
p++;
}
return r;
}
EXPORTED int jmap_upload(struct transaction_t *txn)
{
strarray_t flags = STRARRAY_INITIALIZER;
struct body *body = NULL;
int ret = HTTP_CREATED;
hdrcache_t hdrcache = txn->req_hdrs;
struct stagemsg *stage = NULL;
FILE *f = NULL;
const char **hdr;
time_t now = time(NULL);
struct appendstate as;
struct mailbox *mailbox = NULL;
int r = 0;
/* Read body */
txn->req_body.flags |= BODY_DECODE;
r = http_read_req_body(txn);
if (r) {
txn->flags.conn = CONN_CLOSE;
return r;
}
const char *data = buf_base(&txn->req_body.payload);
size_t datalen = buf_len(&txn->req_body.payload);
if (datalen > (size_t) jmap_max_size_upload) {
txn->error.desc = "JSON upload byte size exceeds maxSizeUpload";
return HTTP_PAYLOAD_TOO_LARGE;
}
/* Resource must be {accountId}/ with no trailing path */
char *accountid = xstrdup(txn->req_tgt.resource);
char *slash = strchr(accountid, '/');
if (!slash || *(slash + 1) != '\0') {
ret = HTTP_NOT_FOUND;
goto done;
}
*slash = '\0';
r = create_upload_collection(accountid, &mailbox);
if (r) {
syslog(LOG_ERR, "jmap_upload: can't open upload collection for %s: %s",
error_message(r), accountid);
ret = HTTP_NOT_FOUND;
goto done;
}
/* Prepare to stage the message */
if (!(f = append_newstage(mailbox->name, now, 0, &stage))) {
syslog(LOG_ERR, "append_newstage(%s) failed", mailbox->name);
txn->error.desc = "append_newstage() failed";
ret = HTTP_SERVER_ERROR;
goto done;
}
/* Create RFC 5322 header for resource */
if ((hdr = spool_getheader(hdrcache, "User-Agent"))) {
fprintf(f, "User-Agent: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "From"))) {
fprintf(f, "From: %s\r\n", hdr[0]);
}
else {
char *mimehdr;
assert(!buf_len(&txn->buf));
if (strchr(httpd_userid, '@')) {
/* XXX This needs to be done via an LDAP/DB lookup */
buf_printf(&txn->buf, "<%s>", httpd_userid);
}
else {
buf_printf(&txn->buf, "<%s@%s>", httpd_userid, config_servername);
}
mimehdr = charset_encode_mimeheader(buf_cstring(&txn->buf),
buf_len(&txn->buf), 0);
fprintf(f, "From: %s\r\n", mimehdr);
free(mimehdr);
buf_reset(&txn->buf);
}
if ((hdr = spool_getheader(hdrcache, "Subject"))) {
fprintf(f, "Subject: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "Date"))) {
fprintf(f, "Date: %s\r\n", hdr[0]);
}
else {
char datestr[80];
time_to_rfc5322(now, datestr, sizeof(datestr));
fprintf(f, "Date: %s\r\n", datestr);
}
if ((hdr = spool_getheader(hdrcache, "Message-ID"))) {
fprintf(f, "Message-ID: %s\r\n", hdr[0]);
}
const char *type = "application/octet-stream";
if ((hdr = spool_getheader(hdrcache, "Content-Type"))) {
type = hdr[0];
}
fprintf(f, "Content-Type: %s\r\n", type);
int domain = data_domain(data, datalen);
switch (domain) {
case DOMAIN_BINARY:
fputs("Content-Transfer-Encoding: BINARY\r\n", f);
break;
case DOMAIN_8BIT:
fputs("Content-Transfer-Encoding: 8BIT\r\n", f);
break;
default:
break; // no CTE == 7bit
}
if ((hdr = spool_getheader(hdrcache, "Content-Disposition"))) {
fprintf(f, "Content-Disposition: %s\r\n", hdr[0]);
}
if ((hdr = spool_getheader(hdrcache, "Content-Description"))) {
fprintf(f, "Content-Description: %s\r\n", hdr[0]);
}
fprintf(f, "Content-Length: %u\r\n", (unsigned) datalen);
fputs("MIME-Version: 1.0\r\n\r\n", f);
/* Write the data to the file */
fwrite(data, datalen, 1, f);
fclose(f);
/* Prepare to append the message to the mailbox */
r = append_setup_mbox(&as, mailbox, httpd_userid, httpd_authstate,
0, /*quota*/NULL, 0, 0, /*event*/0);
if (r) {
syslog(LOG_ERR, "append_setup(%s) failed: %s",
mailbox->name, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_setup() failed";
goto done;
}
/* Append the message to the mailbox */
strarray_append(&flags, "\\Deleted");
strarray_append(&flags, "\\Expunged"); // custom flag to insta-expunge!
r = append_fromstage(&as, &body, stage, now, 0, &flags, 0, /*annots*/NULL);
if (r) {
append_abort(&as);
syslog(LOG_ERR, "append_fromstage(%s) failed: %s",
mailbox->name, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_fromstage() failed";
goto done;
}
r = append_commit(&as);
if (r) {
syslog(LOG_ERR, "append_commit(%s) failed: %s",
mailbox->name, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_commit() failed";
goto done;
}
char datestr[RFC3339_DATETIME_MAX];
time_to_rfc3339(now + 86400, datestr, RFC3339_DATETIME_MAX);
char *blobid = jmap_blobid(&body->content_guid);
/* Create response object */
json_t *resp = json_pack("{s:s}", "accountId", accountid);
json_object_set_new(resp, "blobId", json_string(blobid));
free(blobid);
json_object_set_new(resp, "size", json_integer(datalen));
json_object_set_new(resp, "expires", json_string(datestr));
/* Remove CFWS and encodings from type */
char *normalisedtype = charset_decode_mimeheader(type, CHARSET_SNIPPET);
json_object_set_new(resp, "type", json_string(normalisedtype));
free(normalisedtype);
/* Output the JSON object */
ret = json_response(HTTP_CREATED, txn, resp);
done:
free(accountid);
if (body) {
message_free_body(body);
free(body);
}
strarray_fini(&flags);
append_removestage(stage);
if (mailbox) {
if (r) mailbox_abort(mailbox);
else r = mailbox_commit(mailbox);
mailbox_close(&mailbox);
}
return ret;
}
static int JNOTNULL(json_t *item)
{
if (!item) return 0;
if (json_is_null(item)) return 0;
return 1;
}
static int jmap_copyblob(jmap_req_t *req,
const char *blobid,
const char *from_accountid,
struct mailbox *to_mbox)
{
struct mailbox *mbox = NULL;
msgrecord_t *mr = NULL;
struct body *body = NULL;
const struct body *part = NULL;
FILE *fp = NULL;
FILE *to_fp = NULL;
struct stagemsg *stage = NULL;
int r = _findblob(req, blobid, from_accountid, &mbox, &mr, &body, &part);
if (r) return r;
if (!part)
part = body;
/* Open source file */
const char *fname = NULL;
r = msgrecord_get_fname(mr, &fname);
if (r) {
syslog(LOG_ERR, "jmap_copyblob(%s): msgrecord_get_fname: %s",
blobid, error_message(r));
goto done;
}
fp = fopen(fname, "r");
if (!fp) {
syslog(LOG_ERR, "jmap_copyblob(%s): fopen(%s): %s",
blobid, fname, strerror(errno));
goto done;
}
/* Create staging file */
time_t internaldate = time(NULL);
if (!(to_fp = append_newstage(to_mbox->name, internaldate, 0, &stage))) {
syslog(LOG_ERR, "jmap_copyblob(%s): append_newstage(%s) failed",
blobid, mbox->name);
r = IMAP_INTERNAL;
goto done;
}
/* Copy blob. Keep the original MIME headers, we wouldn't really
* know which ones are safe to rewrite for arbitrary blobs. */
size_t nread = 0;
char cbuf[4096];
fseek(fp, part->header_offset, SEEK_SET);
while (nread < part->header_size + part->content_size) {
nread += fread(cbuf, 1, 4096, fp);
fwrite(cbuf, 1, nread, to_fp);
if (ferror(fp) || ferror(to_fp)) {
syslog(LOG_ERR, "jmap_copyblob(%s): fromfp=%s tofp=%s: %s",
blobid, fname, append_stagefname(stage), strerror(errno));
r = IMAP_IOERROR;
goto done;
}
}
fclose(fp);
fp = NULL;
fclose(to_fp);
to_fp = NULL;
/* Append blob to mailbox */
struct body *to_body = NULL;
struct appendstate as;
r = append_setup_mbox(&as, to_mbox, httpd_userid, httpd_authstate,
0, /*quota*/NULL, 0, 0, /*event*/0);
if (r) {
syslog(LOG_ERR, "jmap_copyblob(%s): append_setup_mbox: %s",
blobid, error_message(r));
goto done;
}
strarray_t flags = STRARRAY_INITIALIZER;
strarray_append(&flags, "\\Deleted");
strarray_append(&flags, "\\Expunged"); // custom flag to insta-expunge!
r = append_fromstage(&as, &to_body, stage, 0, internaldate, &flags, 0, NULL);
strarray_fini(&flags);
if (r) {
syslog(LOG_ERR, "jmap_copyblob(%s): append_fromstage: %s",
blobid, error_message(r));
append_abort(&as);
goto done;
}
message_free_body(to_body);
free(to_body);
r = append_commit(&as);
if (r) {
syslog(LOG_ERR, "jmap_copyblob(%s): append_commit: %s",
blobid, error_message(r));
goto done;
}
done:
if (stage) append_removestage(stage);
if (fp) fclose(fp);
if (to_fp) fclose(to_fp);
message_free_body(body);
free(body);
msgrecord_unref(&mr);
jmap_closembox(req, &mbox);
return r;
}
static int jmap_blob_copy(jmap_req_t *req)
{
json_t *args = req->args;
const char *from_accountid = NULL;
const char *to_accountid = NULL;
json_t *val, *blobids, *invalid = json_array();
size_t i = 0;
struct buf buf = BUF_INITIALIZER;
/* Parse request */
val = json_object_get(args, "fromAccountId");
if (JNOTNULL(val) && !json_is_string(val)) {
json_array_append_new(invalid, json_string("fromAccountId"));
}
from_accountid = json_string_value(val);
if (from_accountid == NULL) {
from_accountid = req->userid;
}
val = json_object_get(args, "toAccountId");
if (JNOTNULL(val) && !json_is_string(val)) {
json_array_append_new(invalid, json_string("toAccountId"));
}
to_accountid = json_string_value(val);
if (to_accountid == NULL) {
to_accountid = req->userid;
}
blobids = json_object_get(args, "blobIds");
if (!json_is_array(blobids)) {
json_array_append_new(invalid, json_string("blobIds"));
}
json_array_foreach(blobids, i, val) {
if (!json_is_string(val)) {
buf_printf(&buf, "blobIds[%zu]", i);
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
if (json_array_size(invalid)) {
json_t *err = json_pack("{s:s, s:o}",
"type", "invalidArguments", "arguments", invalid);
json_array_append_new(req->response, json_pack("[s,o,s]",
"error", err, req->tag));
return 0;
}
json_decref(invalid);
/* No return from here on */
struct mailbox *to_mbox = NULL;
json_t *not_copied = json_object();
json_t *copied = json_object();
/* Check if we can upload to toAccountId */
int r = create_upload_collection(to_accountid, &to_mbox);
if (r == IMAP_PERMISSION_DENIED) {
json_array_foreach(blobids, i, val) {
json_object_set(not_copied, json_string_value(val),
json_pack("{s:s}", "type", "toAccountNotFound"));
}
r = 0;
goto done;
} else if (r) {
syslog(LOG_ERR, "jmap_blob_copy: create_upload_collection(%s): %s",
to_accountid, error_message(r));
goto done;
}
/* Check if we can access any mailbox of fromAccountId */
r = mymblist(httpd_userid, from_accountid, httpd_authstate,
req->mboxrights, is_accessible, NULL, 0/*all*/);
if (r != IMAP_OK_COMPLETED) {
json_array_foreach(blobids, i, val) {
json_object_set(not_copied, json_string_value(val),
json_pack("{s:s}", "type", "fromAccountNotFound"));
}
r = 0;
goto done;
}
r = 0;
/* Copy blobs one by one. XXX should we batch copy here? */
json_array_foreach(blobids, i, val) {
const char *blobid = json_string_value(val);
r = jmap_copyblob(req, blobid, from_accountid, to_mbox);
if (r == IMAP_NOTFOUND) {
json_object_set_new(not_copied, blobid,
json_pack("{s:s}", "type", "blobNotFound"));
r = 0;
continue;
}
else if (r) goto done;
json_object_set_new(copied, blobid, json_string(blobid));
}
done:
if (!r) {
/* Build response */
if (!json_object_size(copied)) {
json_decref(copied);
copied = json_null();
}
if (!json_object_size(not_copied)) {
json_decref(not_copied);
not_copied = json_null();
}
json_t *res = json_pack("{s:O s:O s:o s:o}",
"fromAccountId", json_object_get(args, "fromAccountId"),
"toAccountId", json_object_get(args, "toAccountId"),
"copied", copied, "notCopied", not_copied);
json_array_append_new(req->response, json_pack("[s,o,s]",
"Blob/copy", res, req->tag));
}
mailbox_close(&to_mbox);
return r;
}
EXPORTED int jmap_cmpstate(jmap_req_t* req, json_t *state, int mbtype)
{
if (JNOTNULL(state)) {
const char *s = json_string_value(state);
if (!s) {
return -1;
}
modseq_t client_modseq = atomodseq_t(s);
modseq_t server_modseq = 0;
switch (mbtype) {
case MBTYPE_CALENDAR:
server_modseq = req->counters.caldavmodseq;
break;
case MBTYPE_ADDRESSBOOK:
server_modseq = req->counters.carddavmodseq;
break;
default:
server_modseq = req->counters.mailmodseq;
}
if (client_modseq < server_modseq)
return -1;
else if (client_modseq > server_modseq)
return 1;
else
return 0;
}
return 0;
}
EXPORTED modseq_t jmap_highestmodseq(jmap_req_t *req, int mbtype)
{
modseq_t modseq;
/* Determine current counter by mailbox type. */
switch (mbtype) {
case MBTYPE_CALENDAR:
modseq = req->counters.caldavmodseq;
break;
case MBTYPE_ADDRESSBOOK:
modseq = req->counters.carddavmodseq;
break;
case 0:
modseq = req->counters.mailmodseq;
break;
default:
modseq = req->counters.highestmodseq;
}
return modseq;
}
EXPORTED json_t* jmap_getstate(jmap_req_t *req, int mbtype, int refresh)
{
if (refresh)
assert (!mboxname_read_counters(req->inboxname, &req->counters));
struct buf buf = BUF_INITIALIZER;
json_t *state = NULL;
modseq_t modseq = jmap_highestmodseq(req, mbtype);
buf_printf(&buf, MODSEQ_FMT, modseq);
state = json_string(buf_cstring(&buf));
buf_free(&buf);
return state;
}
EXPORTED char *jmap_xhref(const char *mboxname, const char *resource)
{
/* XXX - look up root path from namespace? */
struct buf buf = BUF_INITIALIZER;
char *userid = mboxname_to_userid(mboxname);
const char *prefix = NULL;
if (mboxname_isaddressbookmailbox(mboxname, 0)) {
prefix = namespace_addressbook.prefix;
}
else if (mboxname_iscalendarmailbox(mboxname, 0)) {
prefix = namespace_calendar.prefix;
}
if (strchr(userid, '@') || !httpd_extradomain) {
buf_printf(&buf, "%s/%s/%s/%s", prefix, USER_COLLECTION_PREFIX,
userid, strrchr(mboxname, '.')+1);
}
else {
buf_printf(&buf, "%s/%s/%s@%s/%s", prefix, USER_COLLECTION_PREFIX,
userid, httpd_extradomain, strrchr(mboxname, '.')+1);
}
if (resource)
buf_printf(&buf, "/%s", resource);
free(userid);
return buf_release(&buf);
}
static int jmap_need_auth(struct transaction_t *txn __attribute__((unused)))
{
/* All endpoints require authentication */
return HTTP_UNAUTHORIZED;
}
struct findaccounts_data {
json_t *accounts;
struct buf userid;
int rw;
int has_mail;
int has_contacts;
int has_calendars;
};
#define JMAP_HAS_DATA_FOR_MAIL "urn:ietf:params:jmap:mail"
#define JMAP_HAS_DATA_FOR_CONTACTS "urn:ietf:params:jmap:contacts"
#define JMAP_HAS_DATA_FOR_CALENDARS "urn:ietf:params:jmap:calendars"
static void findaccounts_add(struct findaccounts_data *ctx)
{
if (!buf_len(&ctx->userid))
return;
const char *userid = buf_cstring(&ctx->userid);
json_t *has_data_for = json_array();
if (ctx->has_mail)
json_array_append_new(has_data_for, json_string(JMAP_HAS_DATA_FOR_MAIL));
if (ctx->has_contacts)
json_array_append_new(has_data_for, json_string(JMAP_HAS_DATA_FOR_CONTACTS));
if (ctx->has_calendars)
json_array_append_new(has_data_for, json_string(JMAP_HAS_DATA_FOR_CALENDARS));
json_t *account = json_object();
json_object_set_new(account, "name", json_string(userid));
json_object_set_new(account, "isPrimary", json_false());
json_object_set_new(account, "isReadOnly", json_boolean(!ctx->rw));
json_object_set_new(account, "hasDataFor", has_data_for);
json_object_set_new(ctx->accounts, userid, account);
}
static int findaccounts_cb(struct findall_data *data, void *rock)
{
if (!data || !data->mbentry)
return 0;
const mbentry_t *mbentry = data->mbentry;
mbname_t *mbname = mbname_from_intname(mbentry->name);
const char *userid = mbname_userid(mbname);
struct findaccounts_data *ctx = rock;
const strarray_t *boxes = mbname_boxes(mbname);
if (strcmp(buf_cstring(&ctx->userid), userid)) {
/* We haven't yet seen this account. Add any previous account and reset state */
findaccounts_add(ctx);
buf_setcstr(&ctx->userid, userid);
ctx->rw = 0;
ctx->has_mail = 0;
ctx->has_contacts = 0;
ctx->has_calendars = 0;
}
if (!ctx->rw) {
ctx->rw = httpd_myrights(httpd_authstate, data->mbentry) & ACL_READ_WRITE;
}
if (!ctx->has_mail) {
ctx->has_mail = mbentry->mbtype == MBTYPE_EMAIL;
}
if (!ctx->has_contacts) {
/* Only count children of user.foo.#addressbooks */
const char *prefix = config_getstring(IMAPOPT_ADDRESSBOOKPREFIX);
ctx->has_contacts =
strarray_size(boxes) > 1 && !strcmpsafe(prefix, strarray_nth(boxes, 0));
}
if (!ctx->has_calendars) {
/* Only count children of user.foo.#calendars */
const char *prefix = config_getstring(IMAPOPT_CALENDARPREFIX);
ctx->has_calendars =
strarray_size(boxes) > 1 && !strcmpsafe(prefix, strarray_nth(boxes, 0));
}
mbname_free(&mbname);
return 0;
}
static json_t *user_settings(const char *userid)
{
json_t *accounts = json_pack("{s:{s:s s:b s:b s:[s,s,s]}}",
userid, "name", userid,
"isPrimary", 1,
"isReadOnly", 0,
/* JMAP autoprovisions calendars and contacts,
* so these JMAP types always are available
* for the primary account */
"hasDataFor",
JMAP_HAS_DATA_FOR_MAIL,
JMAP_HAS_DATA_FOR_CONTACTS,
JMAP_HAS_DATA_FOR_CALENDARS);
/* Find all shared accounts */
strarray_t patterns = STRARRAY_INITIALIZER;
char *userpat = xstrdup("user.*");
userpat[4] = jmap_namespace.hier_sep;
strarray_append(&patterns, userpat);
struct findaccounts_data ctx = { accounts, BUF_INITIALIZER, 0, 0, 0, 0 };
int r = mboxlist_findallmulti(&jmap_namespace, &patterns, 0, userid,
httpd_authstate, findaccounts_cb, &ctx);
free(userpat);
strarray_fini(&patterns);
if (r) {
syslog(LOG_ERR, "Can't determine shared JMAP accounts for user %s: %s",
userid, error_message(r));
}
/* Finalise last seen account */
findaccounts_add(&ctx);
buf_free(&ctx.userid);
return json_pack("{s:s s:o s:O s:s s:s s:s}",
"username", userid,
"accounts", accounts,
"capabilities", jmap_capabilities,
"apiUrl", JMAP_BASE_URL,
"downloadUrl", JMAP_BASE_URL JMAP_DOWNLOAD_COL JMAP_DOWNLOAD_TPL,
/* FIXME eventSourceUrl */
"uploadUrl", JMAP_BASE_URL JMAP_UPLOAD_COL JMAP_UPLOAD_TPL);
}
/* Handle a GET on the settings endpoint */
static int jmap_settings(struct transaction_t *txn)
{
assert(httpd_userid);
/* Create the response object */
json_t *res = user_settings(httpd_userid);
if (!res) {
syslog(LOG_ERR, "JMAP auth: cannot determine user settings for %s",
httpd_userid);
return HTTP_SERVER_ERROR;
}
/* Write the JSON response */
return json_response(HTTP_OK, txn, res);
}
static int myrights(struct auth_state *authstate,
const mbentry_t *mbentry,
hash_table *mboxrights)
{
int *rightsptr = hash_lookup(mbentry->name, mboxrights);
if (!rightsptr) {
rightsptr = xmalloc(sizeof(int));
*rightsptr = httpd_myrights(authstate, mbentry);
hash_insert(mbentry->name, rightsptr, mboxrights);
}
return *rightsptr;
}
static int myrights_byname(struct auth_state *authstate,
const char *mboxname,
hash_table *mboxrights)
{
int *rightsptr = hash_lookup(mboxname, mboxrights);
if (!rightsptr) {
mbentry_t *mbentry = NULL;
if (mboxlist_lookup(mboxname, &mbentry, NULL)) {
return 0;
}
rightsptr = xmalloc(sizeof(int));
*rightsptr = httpd_myrights(authstate, mbentry);
mboxlist_entry_free(&mbentry);
hash_insert(mboxname, rightsptr, mboxrights);
}
return *rightsptr;
}
EXPORTED int jmap_myrights(jmap_req_t *req, const mbentry_t *mbentry)
{
int res = -1;
if (req->is_shared_account) {
if (mbentry->mbtype & MBTYPE_INTERMEDIATE) {
// if it's an intermediate mailbox, we get rights from the parent
mbentry_t *parententry = NULL;
if (mboxlist_findparent(mbentry->name, &parententry))
res = 0;
else
res = myrights(req->authstate, parententry, req->mboxrights);
mboxlist_entry_free(&parententry);
}
else
res = myrights(req->authstate, mbentry, req->mboxrights);
}
// intermediate mailboxes have limited rights, just see, create sub,
// rename and delete:
if (mbentry->mbtype & MBTYPE_INTERMEDIATE)
res &= ACL_LOOKUP | ACL_CREATE | ACL_DELETEMBOX;
return res;
}
EXPORTED int jmap_myrights_byname(jmap_req_t *req, const char *mboxname)
{
if (!req->is_shared_account) {
return -1;
}
return myrights_byname(req->authstate, mboxname, req->mboxrights);
}
EXPORTED void jmap_myrights_delete(jmap_req_t *req, const char *mboxname)
{
if (!req->is_shared_account) {
return;
}
int *rightsptr = hash_del(mboxname, req->mboxrights);
free(rightsptr);
}
EXPORTED json_t* jmap_patchobject_apply(json_t *val, json_t *patch)
{
const char *path;
json_t *newval, *dst;
dst = json_deep_copy(val);
json_object_foreach(patch, path, newval) {
/* Start traversal at root object */
json_t *it = dst;
const char *base = path, *top;
/* Find path in object tree */
while ((top = strchr(base, '/'))) {
char *name = json_pointer_decode(base, top-base);
it = json_object_get(it, name);
free(name);
base = top + 1;
}
if (!it) {
/* No such path in 'val' */
json_decref(dst);
return NULL;
}
/* Replace value at path */
char *name = json_pointer_decode(base, strlen(base));
json_object_set(it, name, newval);
free(name);
}
return dst;
}
static void jmap_patchobject_diff(json_t *patch, struct buf *buf, json_t *a, json_t *b)
{
const char *id;
json_t *o;
if (b == NULL || json_equal(a, b)) {
return;
}
if (!a || json_is_null(a) || json_typeof(b) != JSON_OBJECT) {
json_object_set(patch, buf_cstring(buf), b);
}
json_object_foreach(b, id, o) {
char *encid = json_pointer_encode(id);
size_t l = buf_len(buf);
if (!l) {
buf_setcstr(buf, encid);
} else {
buf_appendcstr(buf, "/");
buf_appendcstr(buf, encid);
}
jmap_patchobject_diff(patch, buf, json_object_get(a, id), o);
buf_truncate(buf, l);
free(encid);
}
}
EXPORTED json_t *jmap_patchobject_create(json_t *a, json_t *b)
{
json_t *patch = json_object();
struct buf buf = BUF_INITIALIZER;
jmap_patchobject_diff(patch, &buf, a, b);
buf_free(&buf);
return patch;
}

File Metadata

Mime Type
text/x-c
Expires
Fri, Apr 24, 10:16 AM (1 d, 3 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18896497
Default Alt Text
http_jmap.c (69 KB)

Event Timeline