diff --git a/lib/mu-msg-json.c b/lib/mu-msg-json.c new file mode 100644 index 00000000..dceb6a4a --- /dev/null +++ b/lib/mu-msg-json.c @@ -0,0 +1,529 @@ +/* +** Copyright (C) 2018 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include +#include + +#include + +#include "mu-msg.h" +#include "mu-msg-iter.h" +#include "mu-msg-part.h" +#include "mu-maildir.h" + +static void +add_list_member (JsonBuilder *bob, const char* elm, const GSList *lst) +{ + const GSList *cur; + + if (!lst) + return; /* empty list, don't include */ + + bob = json_builder_set_member_name (bob, elm); + bob = json_builder_begin_array (bob); + + for (cur = lst; cur; cur = g_slist_next(cur)) + bob = json_builder_add_string_value (bob, (const char*)cur->data); + + bob = json_builder_end_array (bob); +} + +static void +add_string_member (JsonBuilder *bob, const char* elm, const char *str) +{ + if (!str) + return; /* don't include */ + + bob = json_builder_set_member_name (bob, elm); + bob = json_builder_add_string_value (bob, str); +} + +static void +add_int_member (JsonBuilder *bob, const char* elm, gint64 num) +{ + bob = json_builder_set_member_name (bob, elm); + bob = json_builder_add_int_value (bob, num); +} + +static void +add_bool_member (JsonBuilder *bob, const char* elm, gboolean b) +{ + bob = json_builder_set_member_name (bob, elm); + bob = json_builder_add_boolean_value (bob, b); +} + + +static void +consume_array_member (JsonBuilder *bob, const char* elm, JsonArray *arr) +{ + JsonNode *node; + + if (!arr) + return; /* nothing to do */ + + node = json_node_new (JSON_NODE_ARRAY); + json_node_init_array (node, arr); + json_array_unref (arr); + + bob = json_builder_set_member_name (bob, elm); + bob = json_builder_add_value (bob, node); /* consumes */ +} + + + +typedef struct { + JsonArray *from, *to, *cc, *bcc, *reply_to; +} ContactData; + +static void +add_contact (JsonArray **arr, MuMsgContact *c) +{ + JsonObject *cell; + + if (!*arr) + *arr = json_array_new (); + + cell = json_object_new (); + if (c->name) + json_object_set_string_member (cell, "name", c->name); + if (c->address) + json_object_set_string_member (cell, "email", c->address); + + json_array_add_object_element (*arr, cell); /* consumes */ +} + +static gboolean +each_contact (MuMsgContact *c, ContactData *cdata) +{ + switch (mu_msg_contact_type (c)) { + + case MU_MSG_CONTACT_TYPE_FROM: + add_contact(&cdata->from, c); + break; + case MU_MSG_CONTACT_TYPE_TO: + add_contact(&cdata->to ,c); + break; + case MU_MSG_CONTACT_TYPE_CC: + add_contact(&cdata->cc, c); + break; + case MU_MSG_CONTACT_TYPE_BCC: + add_contact(&cdata->bcc, c); + break; + case MU_MSG_CONTACT_TYPE_REPLY_TO: + add_contact(&cdata->reply_to, c); + break; + default: g_return_val_if_reached (FALSE); + } + + return TRUE; +} + + +static void +maybe_append_list_post_as_reply_to (JsonBuilder *bob, MuMsg *msg) +{ + /* some mailing lists do not set the reply-to; see pull #1278. So for + * those cases, check the List-Post address and use that instead */ + + GMatchInfo *minfo; + GRegex *rx; + const char* list_post; + + list_post = mu_msg_get_header (msg, "List-Post"); + if (!list_post) + return; + + rx = g_regex_new ("^(?", G_REGEX_CASELESS, 0, NULL); + g_return_if_fail(rx); + + if (g_regex_match (rx, list_post, 0, &minfo)) { + char *addr; + addr = g_match_info_fetch (minfo, 2); + + bob = json_builder_set_member_name (bob, "reply-to"); + bob = json_builder_begin_array(bob); + bob = json_builder_begin_object(bob); + add_string_member(bob, "email", addr); + g_free (addr); + + bob = json_builder_end_object(bob); + bob = json_builder_end_array(bob); + } + + g_match_info_free (minfo); + g_regex_unref (rx); +} + + +static void +add_contacts (JsonBuilder *bob, MuMsg *msg) +{ + ContactData cdata; + memset (&cdata, 0, sizeof(cdata)); + + mu_msg_contact_foreach (msg, + (MuMsgContactForeachFunc)each_contact, + &cdata); + + consume_array_member (bob, "to" , cdata.to); + consume_array_member (bob, "from" , cdata.from); + consume_array_member (bob, "cc" , cdata.cc); + consume_array_member (bob, "bcc" , cdata.bcc); + consume_array_member (bob, "reply-to", cdata.reply_to); + + if (!cdata.reply_to) + maybe_append_list_post_as_reply_to (bob, msg); +} + +struct _FlagData { + JsonBuilder *bob; + MuFlags msgflags; +}; +typedef struct _FlagData FlagData; + +static void +each_flag (MuFlags flag, FlagData *fdata) +{ + if (!(flag & fdata->msgflags)) + return; + + json_builder_add_string_value (fdata->bob, + mu_flag_name(flag)); +} + +static void +add_flags (JsonBuilder *bob, MuMsg *msg) +{ + FlagData fdata; + + fdata.msgflags = mu_msg_get_flags (msg); + fdata.bob = bob; + + bob = json_builder_set_member_name (bob, "flags"); + + bob = json_builder_begin_array (bob); + mu_flags_foreach ((MuFlagsForeachFunc)each_flag, &fdata); + bob = json_builder_end_array (bob); + +} + +static char* +get_temp_file (MuMsg *msg, MuMsgOptions opts, unsigned index) +{ + char *path; + GError *err; + + err = NULL; + path = mu_msg_part_get_cache_path (msg, opts, index, &err); + if (!path) + goto errexit; + + if (!mu_msg_part_save (msg, opts, path, index, &err)) + goto errexit; + + return path; + +errexit: + g_warning ("failed to save mime part: %s", + err->message ? err->message : "something went wrong"); + g_clear_error (&err); + g_free (path); + return NULL; +} + + +static gchar* +get_temp_file_maybe (MuMsg *msg, MuMsgPart *part, MuMsgOptions opts) +{ + char *tmp, *tmpfile; + + opts |= MU_MSG_OPTION_USE_EXISTING; + + if (!(opts & MU_MSG_OPTION_EXTRACT_IMAGES) || + g_ascii_strcasecmp (part->type, "image") != 0) + return NULL; + + return get_temp_file (msg, opts, part->index); +} + + +struct _PartInfo { + JsonBuilder *bob; + MuMsgOptions opts; +}; +typedef struct _PartInfo PartInfo; + + +static void +add_part_crypto (JsonBuilder *bob, MuMsgPart *mpart, PartInfo *pinfo) +{ + char *signers, *s; + const char *verdict; + MuMsgPartSigStatusReport *report; + + + add_string_member (bob, "decryption", + pinfo->opts & MU_MSG_PART_TYPE_DECRYPTED ? "ok" : + pinfo->opts & MU_MSG_PART_TYPE_ENCRYPTED ? "failed" : + NULL); + + report = mpart->sig_status_report; + if (!report) + return; + + switch (report->verdict) { + case MU_MSG_PART_SIG_STATUS_GOOD: verdict = "verified"; break; + case MU_MSG_PART_SIG_STATUS_BAD: verdict = "bad"; break; + case MU_MSG_PART_SIG_STATUS_ERROR: verdict = "unverified"; break; + default: verdict = NULL; + } + + add_string_member (bob, "signature", verdict); + add_string_member (bob, "signers", report->signers); +} + +static void +add_part_type (JsonBuilder *bob, MuMsgPartType ptype) +{ + unsigned u; + struct PartTypes { + MuMsgPartType ptype; + const char* name; + } ptypes[] = { + { MU_MSG_PART_TYPE_LEAF, "leaf" }, + { MU_MSG_PART_TYPE_MESSAGE, "message" }, + { MU_MSG_PART_TYPE_INLINE, "inline" }, + { MU_MSG_PART_TYPE_ATTACHMENT, "attachment" }, + { MU_MSG_PART_TYPE_SIGNED, "signed" }, + { MU_MSG_PART_TYPE_ENCRYPTED, "encrypted" } + }; + + bob = json_builder_set_member_name (bob, "type"); + bob = json_builder_begin_array(bob); + + for (u = 0; u!= G_N_ELEMENTS(ptypes); ++u) + if (ptype & ptypes[u].ptype) + json_builder_add_string_value (bob, ptypes[u].name); + + bob = json_builder_end_array(bob); +} + + +static void +each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo) +{ + char *name, *tmpfile; + const char *cid; + + pinfo->bob = json_builder_begin_object(pinfo->bob); + + name = mu_msg_part_get_filename (part, TRUE); + tmpfile = get_temp_file_maybe (msg, part, pinfo->opts); + cid = mu_msg_part_get_content_id(part); + + add_int_member (pinfo->bob, "index", part->index); + add_string_member (pinfo->bob, "name", name); + + if (part->type && part->subtype) { + char *mime_type = + g_strdup_printf ("%s/%s", part->type, part->subtype); + add_string_member (pinfo->bob, "mime-type", mime_type); + g_free(mime_type); + } + + add_string_member (pinfo->bob, "temp", tmpfile); + add_part_type (pinfo->bob, part->part_type); + + if (mu_msg_part_maybe_attachment (part)) + add_bool_member (pinfo->bob, "attachment", TRUE); + + add_string_member (pinfo->bob, "cid", mu_msg_part_get_content_id(part)); + add_int_member (pinfo->bob, "size", part->size); + + add_part_crypto (pinfo->bob, part, pinfo); + + g_free (name); + g_free (tmpfile); + + pinfo->bob = json_builder_end_object(pinfo->bob); +} + + +static void +add_parts (JsonBuilder *bob, MuMsg *msg, MuMsgOptions opts) +{ + PartInfo pinfo; + + pinfo.opts = opts; + bob = json_builder_set_member_name (bob, "parts"); + bob = json_builder_begin_array (bob); + + mu_msg_part_foreach (msg, opts, (MuMsgPartForeachFunc)each_part, &pinfo); + + bob = json_builder_end_array (bob); +} + +static void +add_thread_info (JsonBuilder *bob, const MuMsgIterThreadInfo *ti) +{ + bob = json_builder_set_member_name (bob, "thread"); + bob = json_builder_begin_object(bob); + + add_string_member (bob, "path", ti->threadpath); + add_int_member (bob, "level", ti->level); + + bob = json_builder_set_member_name (bob, "flags"); + bob = json_builder_begin_array (bob); + + if (ti->prop & MU_MSG_ITER_THREAD_PROP_FIRST_CHILD) + bob = json_builder_add_string_value (bob, "first-child"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_LAST_CHILD) + bob = json_builder_add_string_value (bob, "last-child"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT) + bob = json_builder_add_string_value (bob, "empty-parent"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_DUP) + bob = json_builder_add_string_value (bob, "duplicate"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_HAS_CHILD) + bob = json_builder_add_string_value (bob, "has-child"); + + bob = json_builder_end_array (bob); + bob = json_builder_end_object(bob); +} + +static void +add_body_txt_params (JsonBuilder *bob, MuMsg *msg, MuMsgOptions opts) +{ + const GSList *params; + + params = mu_msg_get_body_text_content_type_parameters (msg, opts); + if (!params) + return; + + bob = json_builder_set_member_name (bob, "body-txt-params"); + bob = json_builder_begin_array (bob); + + while (params) { + const char *key, *val; + + key = (const char *)params->data; + params = g_slist_next(params); + if (!params) + break; + val = (const char *)params->data; + + if (key && val) { + bob = json_builder_begin_object(bob); + add_string_member(bob, key, val); + bob = json_builder_end_object(bob); + } + + params = g_slist_next(params); + } + + bob = json_builder_end_array(bob); +} + +static void /* ie., parts that require opening the message file */ +add_file_parts (JsonBuilder *bob, MuMsg *msg, MuMsgOptions opts) +{ + const char *str; + GError *err; + + err = NULL; + + if (!mu_msg_load_msg_file (msg, &err)) { + g_warning ("failed to load message file: %s", + err ? err->message : "some error occured"); + g_clear_error (&err); + return; + } + + add_parts (bob, msg, opts); + add_contacts (bob, msg); + + /* add the user-agent / x-mailer */ + str = mu_msg_get_header (msg, "User-Agent"); + if (!str) + str = mu_msg_get_header (msg, "X-Mailer"); + add_string_member (bob, "user-agent", str); + add_body_txt_params (bob, msg, opts); + add_string_member (bob, "body-txt", mu_msg_get_body_text(msg, opts)); + add_string_member (bob, "body-html", mu_msg_get_body_html(msg, opts)); +} + +struct _JsonNode* +mu_msg_to_json (MuMsg *msg, unsigned docid, const MuMsgIterThreadInfo *ti, + MuMsgOptions opts) +{ + JsonNode *node; + JsonBuilder *bob; + + time_t t; + size_t s; + + g_return_val_if_fail (msg, NULL); + g_return_val_if_fail (!((opts & MU_MSG_OPTION_HEADERS_ONLY) && + (opts & MU_MSG_OPTION_EXTRACT_IMAGES)),NULL); + bob = json_builder_new (); + bob = json_builder_begin_object (bob); + + if (ti) + add_thread_info (bob, ti); + + add_string_member (bob, "subject", mu_msg_get_subject (msg)); + + /* in the no-headers-only case (see below) we get a more complete list + * of contacts, so no need to get them here if that's the case */ + if (opts & MU_MSG_OPTION_HEADERS_ONLY) + add_contacts (bob, msg); + + t = mu_msg_get_date (msg); + if (t != (time_t)-1) + add_int_member (bob, "date", t); + + s = mu_msg_get_size (msg); + if (s != (size_t)-1) + add_int_member (bob, "size", s); + + add_string_member (bob, "message-id", mu_msg_get_msgid (msg)); + add_string_member (bob, "mailing-list", mu_msg_get_mailing_list (msg)); + add_string_member (bob, "path", mu_msg_get_path (msg)); + add_string_member (bob, "maildir", mu_msg_get_maildir (msg)); + add_string_member (bob, "priority", mu_msg_prio_name(mu_msg_get_prio(msg))); + + add_flags (bob, msg); + + add_list_member (bob, "tags", mu_msg_get_tags(msg)); + add_list_member (bob, "references", mu_msg_get_references (msg)); + add_string_member (bob, "in-reply-to", + mu_msg_get_header (msg, "In-Reply-To")); + + /* headers are retrieved from the database, views from the + * message file file attr things can only be gotten from the + * file (ie., mu view), not from the database (mu find). */ + if (!(opts & MU_MSG_OPTION_HEADERS_ONLY)) + add_file_parts (bob, msg, opts); + + bob = json_builder_end_object (bob); + node = json_builder_get_root (bob); + + g_clear_object (&bob); + + return node; +} diff --git a/lib/mu-msg.h b/lib/mu-msg.h index 508cfa88..e63324e0 100644 --- a/lib/mu-msg.h +++ b/lib/mu-msg.h @@ -425,7 +425,6 @@ const GSList* mu_msg_get_tags (MuMsg *self); int mu_msg_cmp (MuMsg *m1, MuMsg *m2, MuMsgFieldId mfid); - /** * check whether there there's a readable file behind this message * @@ -465,6 +464,30 @@ char* mu_msg_to_sexp (MuMsg *msg, unsigned docid, MuMsgOptions ops) G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; +#ifdef HAVE_JSON_GLIB + +struct _JsonNode; /* forward declaration */ + +/** + * convert the msg to json + * + * @param msg a valid message + * @param docid the docid for this message, or 0 + * @param ti thread info for the current message, or NULL + * @param opts, bitwise OR'ed; + * - MU_MSG_OPTION_HEADERS_ONLY: only include message fields which can be + * obtained from the database (this is much faster if the MuMsg is + * database-backed, so no file needs to be opened) + * - MU_MSG_OPTION_EXTRACT_IMAGES: extract image attachments as temporary + * files and include links to those in the sexp + * + * @return a string with the sexp (free with g_free) or NULL in case of error + */ +struct _JsonNode* mu_msg_to_json (MuMsg *msg, unsigned docid, + const struct _MuMsgIterThreadInfo *ti, + MuMsgOptions ops) G_GNUC_WARN_UNUSED_RESULT; +#endif /*HAVE_JSON_GLIB*/ + /** * move a message to another maildir; note that this does _not_ update * the database @@ -488,7 +511,6 @@ gboolean mu_msg_move_to_maildir (MuMsg *msg, const char *maildir, gboolean new_name, GError **err); - /** * Tickle a message -- ie., rename a message to some new semi-random name,while * maintaining the maildir and flags. This can be useful when dealing with @@ -505,7 +527,6 @@ gboolean mu_msg_move_to_maildir (MuMsg *msg, const char *maildir, gboolean mu_msg_tickle (MuMsg *msg, GError **err); - enum _MuMsgContactType { /* Reply-To:? */ MU_MSG_CONTACT_TYPE_TO = 0, MU_MSG_CONTACT_TYPE_FROM,