Page MenuHomePhorge

http_jmap.c
No OneTemporary

Authored By
Unknown
Size
41 KB
Referenced Files
None
Subscribers
None

http_jmap.c

/* http_jmap.c -- Routines for handling JMAP requests in httpd
*
* Copyright (c) 1994-2018 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <config.h>
#include <errno.h>
#include "acl.h"
#include "append.h"
#include "httpd.h"
#include "http_jmap.h"
#include "http_proxy.h"
#include "http_ws.h"
#include "mboxname.h"
#include "proxy.h"
#include "times.h"
#include "syslog.h"
#include "xstrlcpy.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include "imap/imap_err.h"
#include "imap/jmap_err.h"
#define JMAP_ROOT "/jmap"
#define JMAP_BASE_URL JMAP_ROOT "/"
#define JMAP_UPLOAD_COL "upload/"
#define JMAP_UPLOAD_TPL "{accountId}/"
#define JMAP_DOWNLOAD_COL "download/"
#define JMAP_DOWNLOAD_TPL "{accountId}/{blobId}/{name}?accept={type}"
struct namespace jmap_namespace;
static time_t compile_time;
/* Namespace callbacks */
static void jmap_init(struct buf *serverinfo);
static int jmap_need_auth(struct transaction_t *txn);
static int jmap_auth(const char *userid);
static void jmap_shutdown(void);
/* HTTP method handlers */
static int jmap_get(struct transaction_t *txn, void *params);
static int jmap_post(struct transaction_t *txn, void *params);
/* JMAP Requests */
static int jmap_settings(struct transaction_t *txn);
static int jmap_download(struct transaction_t *txn);
static int jmap_upload(struct transaction_t *txn);
/* JMAP Core API Methods */
static int jmap_core_echo(jmap_req_t *req);
static int jmap_blob_copy(jmap_req_t *req);
static int jmap_quota_get(jmap_req_t *req);
/* WebSocket handler */
#define JMAP_WS_PROTOCOL "jmap"
static int jmap_ws(struct buf *inbuf, struct buf *outbuf,
struct buf *logbuf, void **rock);
static struct connect_params ws_params = {
JMAP_BASE_URL, JMAP_WS_PROTOCOL, &jmap_ws
};
/* Namespace for JMAP */
struct namespace_t namespace_jmap = {
URL_NS_JMAP, 0, "jmap", JMAP_ROOT, "/.well-known/jmap",
jmap_need_auth, /*authschemes*/0,
/*mbtype*/0,
(ALLOW_READ | ALLOW_POST),
&jmap_init, &jmap_auth, NULL, &jmap_shutdown, NULL, /*bearer*/NULL,
{
{ NULL, NULL }, /* ACL */
{ NULL, NULL }, /* BIND */
{ &meth_connect, &ws_params }, /* CONNECT */
{ NULL, NULL }, /* COPY */
{ NULL, NULL }, /* DELETE */
{ &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 */
}
};
/*
* Namespace callbacks
*/
static jmap_settings_t my_jmap_settings = {
HASH_TABLE_INITIALIZER, STRARRAY_INITIALIZER, NULL, { 0 }
};
jmap_method_t jmap_core_methods[] = {
{ "Core/echo", &jmap_core_echo, JMAP_SHARED_CSTATE },
{ "Blob/copy", &jmap_blob_copy, 0/*flags*/ },
{ "Quota/get", &jmap_quota_get, JMAP_SHARED_CSTATE },
{ NULL, NULL, 0/*flags*/ }
};
static void jmap_core_init()
{
#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(my_jmap_settings.limits[MAX_SIZE_UPLOAD],
IMAPOPT_JMAP_MAX_SIZE_UPLOAD);
my_jmap_settings.limits[MAX_SIZE_UPLOAD] *= 1024;
_read_opt(my_jmap_settings.limits[MAX_CONCURRENT_UPLOAD],
IMAPOPT_JMAP_MAX_CONCURRENT_UPLOAD);
_read_opt(my_jmap_settings.limits[MAX_SIZE_REQUEST],
IMAPOPT_JMAP_MAX_SIZE_REQUEST);
my_jmap_settings.limits[MAX_SIZE_REQUEST] *= 1024;
_read_opt(my_jmap_settings.limits[MAX_CONCURRENT_REQUESTS],
IMAPOPT_JMAP_MAX_CONCURRENT_REQUESTS);
_read_opt(my_jmap_settings.limits[MAX_CALLS_IN_REQUEST],
IMAPOPT_JMAP_MAX_CALLS_IN_REQUEST);
_read_opt(my_jmap_settings.limits[MAX_OBJECTS_IN_GET],
IMAPOPT_JMAP_MAX_OBJECTS_IN_GET);
_read_opt(my_jmap_settings.limits[MAX_OBJECTS_IN_SET],
IMAPOPT_JMAP_MAX_OBJECTS_IN_SET);
#undef _read_opt
strarray_push(&my_jmap_settings.can_use, JMAP_URN_CORE);
strarray_push(&my_jmap_settings.can_use, JMAP_QUOTA_EXTENSION);
construct_hash_table(&my_jmap_settings.methods, 128, 0);
jmap_method_t *mp;
for (mp = jmap_core_methods; mp->name; mp++) {
hash_insert(mp->name, mp, &my_jmap_settings.methods);
}
}
static void jmap_core_capabilities()
{
my_jmap_settings.capabilities =
json_pack("{s:{s:i s:i s:i s:i s:i s:i s:i s:o}}",
JMAP_URN_CORE,
"maxSizeUpload",
my_jmap_settings.limits[MAX_SIZE_UPLOAD],
"maxConcurrentUpload",
my_jmap_settings.limits[MAX_CONCURRENT_UPLOAD],
"maxSizeRequest",
my_jmap_settings.limits[MAX_SIZE_REQUEST],
"maxConcurrentRequests",
my_jmap_settings.limits[MAX_CONCURRENT_REQUESTS],
"maxCallsInRequest",
my_jmap_settings.limits[MAX_CALLS_IN_REQUEST],
"maxObjectsInGet",
my_jmap_settings.limits[MAX_OBJECTS_IN_GET],
"maxObjectsInSet",
my_jmap_settings.limits[MAX_OBJECTS_IN_SET],
"collationAlgorithms", json_array()
);
if (ws_enabled()) {
json_object_set_new(my_jmap_settings.capabilities, JMAP_URN_WEBSOCKET,
json_pack("{s:s}", "wsUrl", JMAP_BASE_URL));
}
json_object_set_new(my_jmap_settings.capabilities,
XML_NS_CYRUS "performance", json_object());
}
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();
jmap_core_init();
jmap_mail_init(&my_jmap_settings);
jmap_contact_init(&my_jmap_settings);
jmap_calendar_init(&my_jmap_settings);
}
static int jmap_auth(const char *userid __attribute__((unused)))
{
/* Set namespace */
mboxname_init_namespace(&jmap_namespace,
httpd_userisadmin || httpd_userisproxyadmin);
return 0;
}
static int jmap_need_auth(struct transaction_t *txn __attribute__((unused)))
{
/* All endpoints require authentication */
return HTTP_UNAUTHORIZED;
}
static void jmap_shutdown(void)
{
free_hash_table(&my_jmap_settings.methods, NULL);
strarray_fini(&my_jmap_settings.can_use);
if (my_jmap_settings.capabilities)
json_decref(my_jmap_settings.capabilities);
}
/*
* HTTP method handlers
*/
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;
}
/* Perform a GET/HEAD request */
static int jmap_get(struct transaction_t *txn,
void *params __attribute__((unused)))
{
int r = jmap_parse_path(txn);
if (!(txn->req_tgt.allow & ALLOW_READ)) {
return HTTP_NOT_FOUND;
}
else if (r) return r;
if (txn->req_tgt.flags == JMAP_ENDPOINT_API) {
/* Upgrade to WebSockets over HTTP/1.1 on API endpoint, if requested */
if (txn->flags.upgrade & UPGRADE_WS) {
return ws_start_channel(txn, JMAP_WS_PROTOCOL, &jmap_ws);
}
return jmap_settings(txn);
}
return jmap_download(txn);
}
static int json_response(int code, struct transaction_t *txn, json_t *root)
{
size_t flags = JSON_PRESERVE_ORDER;
char *buf;
/* Dump JSON object into a text buffer */
flags |= (config_httpprettytelemetry ? JSON_INDENT(2) : JSON_COMPACT);
buf = json_dumps(root, flags);
json_decref(root);
if (!buf) {
txn->error.desc = "Error dumping JSON object";
return HTTP_SERVER_ERROR;
}
/* Output the JSON object */
switch (code) {
case HTTP_OK:
case HTTP_CREATED:
txn->resp_body.type = "application/json; charset=utf-8";
break;
default:
txn->resp_body.type = "application/problem+json; charset=utf-8";
break;
}
write_body(code, txn, buf, strlen(buf));
free(buf);
return 0;
}
/* Perform a POST request */
static int jmap_post(struct transaction_t *txn,
void *params __attribute__((unused)))
{
int ret;
json_t *res = NULL;
ret = jmap_parse_path(txn);
if (ret) return ret;
if (!(txn->req_tgt.allow & ALLOW_POST)) {
return HTTP_NOT_ALLOWED;
}
/* Handle uploads */
if (txn->req_tgt.flags == JMAP_ENDPOINT_UPLOAD) {
return jmap_upload(txn);
}
/* Regular JMAP API request */
ret = jmap_api(txn, &res, &my_jmap_settings);
if (!ret) {
/* Output the JSON object */
ret = json_response(HTTP_OK, txn, res);
}
syslog(LOG_DEBUG, ">>>> jmap_post: Exit\n");
return ret;
}
/*
* JMAP Requests
*/
static char *parse_accept_header(const char **hdr)
{
char *val = NULL;
struct accept *accept = parse_accept(hdr);
if (accept) {
char *type = NULL;
char *subtype = NULL;
struct param *params = NULL;
message_parse_type(accept->token, &type, &subtype, &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;
}
/* Handle a GET on the download endpoint */
static int jmap_download(struct transaction_t *txn)
{
const char *userid = txn->req_tgt.resource;
const char *slash = strchr(userid, '/');
if (!slash) {
/* XXX - error, needs AccountId */
return HTTP_NOT_FOUND;
}
#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, 1/*shared*/, &cstate);
if (r) {
txn->error.desc = error_message(r);
res = (r == IMAP_MAILBOX_BADNAME) ? HTTP_NOT_FOUND : HTTP_SERVER_ERROR;
free(accountid);
return res;
}
/* now we're allocating memory, so don't return from here! */
char *blobid = NULL;
char *ctype = NULL;
/* Initialize request context */
struct jmap_req req;
jmap_initreq(&req);
req.userid = httpd_userid;
req.accountid = accountid;
req.cstate = cstate;
req.authstate = httpd_authstate;
req.txn = txn;
/* Initialize ACL mailbox cache for findblob */
hash_table mboxrights = HASH_TABLE_INITIALIZER;
construct_hash_table(&mboxrights, 64, 0);
req.mboxrights = &mboxrights;
blobid = xstrndup(blobbase, bloblen);
struct 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 = jmap_findblob(&req, NULL/*accountid*/, blobid,
&mbox, &mr, &body, &part, &msg_buf);
if (r) {
res = HTTP_NOT_FOUND; // XXX errors?
txn->error.desc = "failed to find blob by id";
goto done;
}
if (!buf_base(&msg_buf)) {
/* 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;
}
}
struct strlist *param;
if ((param = hash_lookup("accept", &txn->req_qparams))) {
accept_mime = xstrdup(param->s);
}
const char **hdr;
if (!accept_mime && (hdr = spool_getheader(txn->req_hdrs, "Accept"))) {
accept_mime = parse_accept_header(hdr);
}
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);
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;
else {
if (r) {
syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
mbentry->name, error_message(r));
}
goto done;
}
}
else if (r) goto done;
if (mailbox) {
/* Open mailbox for writing */
r = mailbox_open_iwl(mbentry->name, mailbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mbentry->name, error_message(r));
}
}
done:
mboxlist_entry_free(&mbentry);
return r;
}
/* Helper function to determine domain of data */
enum {
DOMAIN_7BIT = 0,
DOMAIN_8BIT,
DOMAIN_BINARY
};
static int data_domain(const char *p, size_t n)
{
int r = DOMAIN_7BIT;
while (n--) {
if (!*p) return DOMAIN_BINARY;
if (*p & 0x80) r = DOMAIN_8BIT;
p++;
}
return r;
}
/* Handle a POST on the upload endpoint */
static int jmap_upload(struct transaction_t *txn)
{
strarray_t flags = STRARRAY_INITIALIZER;
struct body *body = NULL;
int ret = HTTP_CREATED;
hdrcache_t hdrcache = txn->req_hdrs;
struct stagemsg *stage = NULL;
FILE *f = NULL;
const char **hdr;
time_t now = time(NULL);
struct appendstate as;
struct mailbox *mailbox = NULL;
int r = 0;
/* Read body */
txn->req_body.flags |= BODY_DECODE;
r = http_read_req_body(txn);
if (r) {
txn->flags.conn = CONN_CLOSE;
return r;
}
const char *data = buf_base(&txn->req_body.payload);
size_t datalen = buf_len(&txn->req_body.payload);
if (datalen > (size_t) my_jmap_settings.limits[MAX_SIZE_UPLOAD]) {
txn->error.desc = "JSON upload byte size exceeds maxSizeUpload";
return HTTP_PAYLOAD_TOO_LARGE;
}
/* Resource must be {accountId}/ with no trailing path */
char *accountid = xstrdup(txn->req_tgt.resource);
char *slash = strchr(accountid, '/');
if (!slash || *(slash + 1) != '\0') {
ret = HTTP_NOT_FOUND;
goto done;
}
*slash = '\0';
r = 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 blob_id[42];
jmap_set_blobid(&body->content_guid, blob_id);
/* Create response object */
json_t *resp = json_pack("{s:s}", "accountId", accountid);
json_object_set_new(resp, "blobId", json_string(blob_id));
json_object_set_new(resp, "size", json_integer(datalen));
json_object_set_new(resp, "expires", json_string(datestr));
/* 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;
}
struct findaccounts_data {
json_t *accounts;
struct buf userid;
int rw;
int has_mail;
int has_contacts;
int has_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_URN_MAIL));
json_array_append_new(has_data_for, json_string(JMAP_URN_SUBMISSION));
}
if (ctx->has_contacts)
json_array_append_new(has_data_for, json_string(JMAP_URN_CONTACTS));
if (ctx->has_calendars)
json_array_append_new(has_data_for, json_string(JMAP_URN_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,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_URN_MAIL,
JMAP_URN_SUBMISSION,
JMAP_URN_CONTACTS,
JMAP_URN_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);
char *inboxname = mboxname_user_mbox(userid, NULL);
struct buf state = BUF_INITIALIZER;
buf_printf(&state, MODSEQ_FMT, mboxname_readraclmodseq(inboxname));
free(inboxname);
json_t *jsettings = json_pack("{s:s s:o s:O s:s s:s s:s s:s}",
"username", userid,
"accounts", accounts,
"capabilities", my_jmap_settings.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,
"state", buf_cstring(&state));
buf_free(&state);
return jsettings;
}
/* Handle a GET on the settings endpoint */
static int jmap_settings(struct transaction_t *txn)
{
assert(httpd_userid);
if (!my_jmap_settings.capabilities) {
jmap_core_capabilities();
jmap_mail_capabilities(&my_jmap_settings);
jmap_contact_capabilities(&my_jmap_settings);
jmap_calendar_capabilities(&my_jmap_settings);
}
/* 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);
}
/*
* JMAP Core API Methods
*/
/* Core/echo method */
static int jmap_core_echo(jmap_req_t *req)
{
json_array_append_new(req->response,
json_pack("[s,O,s]", "Core/echo", req->args, req->tag));
return 0;
}
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;
struct buf msg_buf = BUF_INITIALIZER;
FILE *to_fp = NULL;
struct stagemsg *stage = NULL;
int r = jmap_findblob(req, from_accountid, blobid,
&mbox, &mr, &body, &part, &msg_buf);
if (r) return r;
if (!part)
part = body;
if (!buf_base(&msg_buf)) {
/* Map the message into memory */
r = msgrecord_get_body(mr, &msg_buf);
if (r) {
syslog(LOG_ERR, "jmap_copyblob(%s): msgrecord_get_body: %s",
blobid, error_message(r));
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. */
fwrite(buf_base(&msg_buf) + part->header_offset,
part->header_size + part->content_size, 1, to_fp);
if (ferror(to_fp)) {
syslog(LOG_ERR, "jmap_copyblob(%s): tofp=%s: %s",
blobid, append_stagefname(stage), strerror(errno));
r = IMAP_IOERROR;
goto done;
}
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 (to_fp) fclose(to_fp);
buf_free(&msg_buf);
message_free_body(body);
free(body);
msgrecord_unref(&mr);
jmap_closembox(req, &mbox);
return r;
}
/* Blob/copy method */
static int jmap_blob_copy(jmap_req_t *req)
{
struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
struct jmap_copy copy;
json_t *val, *err = NULL;
size_t i = 0;
int r = 0;
struct mailbox *to_mbox = NULL;
/* Parse request */
jmap_copy_parse(req->args, &parser, req, NULL, &copy, &err);
if (err) {
jmap_error(req, err);
goto cleanup;
}
/* Check if we can upload to toAccountId */
r = create_upload_collection(req->accountid, &to_mbox);
if (r == IMAP_PERMISSION_DENIED) {
json_array_foreach(copy.create, i, val) {
json_object_set(copy.not_created, json_string_value(val),
json_pack("{s:s}", "type", "toAccountNotFound"));
}
goto done;
} else if (r) {
syslog(LOG_ERR, "jmap_blob_copy: create_upload_collection(%s): %s",
req->accountid, error_message(r));
goto cleanup;
}
/* Copy blobs one by one. XXX should we batch copy here? */
json_array_foreach(copy.create, i, val) {
const char *blobid = json_string_value(val);
r = jmap_copyblob(req, blobid, copy.from_account_id, to_mbox);
if (r == IMAP_NOTFOUND || r == IMAP_PERMISSION_DENIED) {
json_object_set_new(copy.not_created, blobid,
json_pack("{s:s}", "type", "blobNotFound"));
}
else if (r) goto cleanup;
else json_object_set_new(copy.created, blobid, json_string(blobid));
}
done:
/* Build response */
jmap_ok(req, jmap_copy_reply(&copy));
r = 0;
cleanup:
jmap_parser_fini(&parser);
jmap_copy_fini(&copy);
mailbox_close(&to_mbox);
return r;
}
/* Quota/get method */
static const jmap_property_t quota_props[] = {
{ "id", JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE },
{ "used", JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE },
{ "total", JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE },
{ NULL, 0 }
};
static int jmap_quota_get(jmap_req_t *req)
{
struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
struct jmap_get get;
json_t *err = NULL;
char *inboxname = mboxname_user_mbox(req->accountid, NULL);
/* Parse request */
jmap_get_parse(req->args, &parser, req, quota_props, NULL, NULL,
&get, /*allow_null_ids*/1, &err);
if (err) {
jmap_error(req, err);
goto done;
}
int want_mail_quota = !get.ids || json_is_null(get.ids);
size_t i;
json_t *jval;
json_array_foreach(get.ids, i, jval) {
if (strcmp("mail", json_string_value(jval))) {
json_array_append(get.not_found, jval);
}
else want_mail_quota = 1;
}
if (want_mail_quota) {
struct quota quota;
quota_init(&quota, inboxname);
int r = quota_read(&quota, NULL, 0);
if (!r) {
quota_t total = quota.limits[QUOTA_STORAGE] * quota_units[QUOTA_STORAGE];
quota_t used = quota.useds[QUOTA_STORAGE];
json_t *jquota = json_object();
json_object_set_new(jquota, "id", json_string("mail"));
json_object_set_new(jquota, "used", json_integer(used));
json_object_set_new(jquota, "total", json_integer(total));
json_array_append_new(get.list, jquota);
}
else {
syslog(LOG_ERR, "jmap_quota_get: can't read quota for %s: %s",
inboxname, error_message(r));
json_array_append_new(get.not_found, json_string("mail"));
}
quota_free(&quota);
}
modseq_t quotamodseq = mboxname_readquotamodseq(inboxname);
struct buf buf = BUF_INITIALIZER;
buf_printf(&buf, MODSEQ_FMT, quotamodseq);
get.state = buf_release(&buf);
jmap_ok(req, jmap_get_reply(&get));
done:
jmap_parser_fini(&parser);
jmap_get_fini(&get);
free(inboxname);
return 0;
}
/*
* WebSockets data callback ('jmap' sub-protocol): Process JMAP API request.
*
* Can be tested with:
* https://github.com/websockets/wscat
* https://chrome.google.com/webstore/detail/web-socket-client/lifhekgaodigcpmnakfhaaaboididbdn
*
* WebSockets over HTTP/2 currently only available in:
* https://www.google.com/chrome/browser/canary.html
*/
static int jmap_ws(struct buf *inbuf, struct buf *outbuf,
struct buf *logbuf, void **rock)
{
struct transaction_t **txnp = (struct transaction_t **) rock;
struct transaction_t *txn = *txnp;
json_t *res = NULL;
int ret;
if (!txn) {
/* Create a transaction rock to use for API requests */
txn = *txnp = xzmalloc(sizeof(struct transaction_t));
txn->req_body.flags = BODY_DONE;
/* Create header cache */
txn->req_hdrs = spool_new_hdrcache();
if (!txn->req_hdrs) {
free(txn);
return HTTP_SERVER_ERROR;
}
/* Set Content-Type of request payload */
spool_cache_header(xstrdup("Content-Type"),
xstrdup("application/json"), txn->req_hdrs);
}
else if (!inbuf) {
/* Free transaction rock */
transaction_free(txn);
free(txn);
return 0;
}
/* Set request payload */
buf_init_ro(&txn->req_body.payload, buf_base(inbuf), buf_len(inbuf));
/* Process the API request */
ret = jmap_api(txn, &res, &my_jmap_settings);
/* Free request payload */
buf_free(&txn->req_body.payload);
if (logbuf) {
/* Log JMAP methods */
const char **hdr = spool_getheader(txn->req_hdrs, ":jmap");
if (hdr) buf_printf(logbuf, "; jmap=%s", hdr[0]);
}
if (!ret) {
/* Return the JSON object */
size_t flags = JSON_PRESERVE_ORDER;
char *buf;
/* Dump JSON object into a text buffer */
flags |= (config_httpprettytelemetry ? JSON_INDENT(2) : JSON_COMPACT);
buf = json_dumps(res, flags);
json_decref(res);
buf_initm(outbuf, buf, strlen(buf));
}
return ret;
}

File Metadata

Mime Type
text/x-c
Expires
Mon, Apr 6, 12:49 AM (5 d, 5 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831757
Default Alt Text
http_jmap.c (41 KB)

Event Timeline