/* ** Copyright (C) 2022 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 "mu-message.hh" #include "mu-maildir.hh" #include #include #include #include #include #include #include #include #include "gmime/gmime-message.h" using namespace Mu; /* note, we do the gmime initialization here rather than in mu-runtime, because this way * we don't need mu-runtime for simple cases -- such as our unit tests. Also note that we * need gmime init even for the doc backend, as we use the address parsing functions also * there. */ static bool gmime_maybe_init(void) { static std::atomic_bool gmime_initialized = false; if (gmime_initialized) return true; static std::mutex lock; g_debug("initializing gmime %u.%u.%u", gmime_major_version, gmime_minor_version, gmime_micro_version); g_mime_init(); std::atexit([] { g_debug("shutting down gmime"); g_mime_shutdown(); gmime_initialized = false; }); return true; } static GMimeMessage* make_mime_message(const std::string& path, GError** err) { GMimeStream *stream{g_mime_stream_file_open(path.c_str(), "r", err)}; if (!stream) return {}; GMimeParser *parser{g_mime_parser_new_with_stream(stream)}; g_object_unref(stream); if (!parser) { g_set_error(err,MU_ERROR_DOMAIN, MU_ERROR_GMIME, "cannot create mime parser for %s", path.c_str()); return {}; } GMimeMessage *mime_msg{g_mime_parser_construct_message(parser, NULL)}; g_object_unref(parser); if (!mime_msg) { g_set_error(err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, "message seems invalid, ignoring (%s)", path.c_str()); return {}; } return mime_msg; } static void fill_document(Document& doc, GMimeMessage *mime_msg); Message::Message(const std::string& path, const std::string& mdir) { gmime_maybe_init(); /* * sanity checks. */ if (!g_path_is_absolute(path.c_str())) throw Error(Error::Code::File, "path '%s' is not absolute", path.c_str()); if (::access(path.c_str(), R_OK) != 0) throw Error(Error::Code::File, "file @ '%s' is not readable", path.c_str()); struct stat statbuf{}; if (::stat(path.c_str(), &statbuf) < 0) throw Error(Error::Code::File, "cannot stat %s: %s", path.c_str(), g_strerror(errno)); if (!S_ISREG(statbuf.st_mode)) throw Error(Error::Code::File, "not a regular file: %s", path); /* * let's get the mime message */ GError *err{}; mime_msg_ = make_mime_message(path, &err); if (!mime_msg_) throw Error(Error::Code::File, &err, "invalid message"); doc_.add(Field::Id::Path, Mu::from_gchars(g_canonicalize_filename(path.c_str(), NULL))); doc_.add(Field::Id::Maildir, mdir); doc_.add(Field::Id::Size, static_cast(statbuf.st_size)); // rest of the fields //fill_fields(doc_, mime_msg_); } Message::~Message() { g_clear_object(&mime_msg_); } Message& Message::operator=(const Message& rhs) { if (this != &rhs) { doc_ = rhs.doc_; g_clear_object(&mime_msg_); if (rhs.mime_msg_) mime_msg_ = g_object_ref(rhs.mime_msg_); } return *this; } Message& Message::operator=(Message&& rhs) { if (this != &rhs) { doc_ = std::move(rhs.doc_); rhs.doc_ = {}; g_clear_object(&mime_msg_); mime_msg_ = rhs.mime_msg_; rhs.mime_msg_ = {}; } return *this; } static Priority parse_prio_str(const char* priostr) { int i; struct { const char* _str; Priority _prio; } str_prio[] = {{"high", Priority::High}, {"1", Priority::High}, {"2", Priority::High}, {"normal", Priority::Normal}, {"3", Priority::Normal}, {"low", Priority::Low}, {"list", Priority::Low}, {"bulk", Priority::Low}, {"4", Priority::Low}, {"5", Priority::Low}}; for (i = 0; i != G_N_ELEMENTS(str_prio); ++i) if (g_ascii_strcasecmp(priostr, str_prio[i]._str) == 0) return str_prio[i]._prio; /* e.g., last-fm uses 'fm-user'... as precedence */ return Priority::Normal; } static Priority get_priority(GMimeMessage *mime_msg) { auto obj{GMIME_OBJECT(mime_msg)}; auto priostr = g_mime_object_get_header(obj, "Precedence"); if (!priostr) priostr = g_mime_object_get_header(obj, "X-Priority"); if (!priostr) priostr = g_mime_object_get_header(obj, "Importance"); if (!priostr) return Priority::Normal; else return parse_prio_str(priostr); } static gboolean looks_like_attachment(GMimeObject* part) { GMimeContentDisposition* disp; GMimeContentType* ctype; const char* dispstr; guint u; const struct { const char* type; const char* sub_type; } att_types[] = {{"image", "*"}, {"audio", "*"}, {"application", "*"}, {"application", "x-patch"}}; disp = g_mime_object_get_content_disposition(part); if (!GMIME_IS_CONTENT_DISPOSITION(disp)) return FALSE; dispstr = g_mime_content_disposition_get_disposition(disp); if (g_ascii_strcasecmp(dispstr, "attachment") == 0) return TRUE; /* we also consider patches, images, audio, and non-pgp-signature * application attachments to be attachments... */ ctype = g_mime_object_get_content_type(part); if (g_mime_content_type_is_type(ctype, "*", "pgp-signature")) return FALSE; /* don't consider as a signature */ if (g_mime_content_type_is_type(ctype, "text", "*")) { if (g_mime_content_type_is_type(ctype, "*", "plain") || g_mime_content_type_is_type(ctype, "*", "html")) return FALSE; else return TRUE; } for (u = 0; u != G_N_ELEMENTS(att_types); ++u) if (g_mime_content_type_is_type(ctype, att_types[u].type, att_types[u].sub_type)) return TRUE; return FALSE; } static void msg_cflags_cb(GMimeObject* parent, GMimeObject* part, Flags* flags) { if (GMIME_IS_MULTIPART_SIGNED(part)) *flags |= Flags::Signed; /* FIXME: An encrypted part might be signed at the same time. * In that case the signed flag is lost. */ if (GMIME_IS_MULTIPART_ENCRYPTED(part)) *flags |= Flags::Encrypted; /* smime */ if (GMIME_IS_APPLICATION_PKCS7_MIME(part)) { GMimeApplicationPkcs7Mime *pkcs7; pkcs7 = GMIME_APPLICATION_PKCS7_MIME(part); if (pkcs7) { switch(pkcs7->smime_type) { case GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA: *flags |= Flags::Encrypted; break; case GMIME_SECURE_MIME_TYPE_SIGNED_DATA: *flags |= Flags::Signed; break; default: break; } } } if (any_of(*flags & Flags::HasAttachment)) return; if (!GMIME_IS_PART(part)) return; if (looks_like_attachment(part)) *flags |= Flags::HasAttachment; } static Flags get_content_flags(GMimeMessage *mime_msg) { Flags flags{Flags::None}; /* toplevel */ msg_cflags_cb(NULL, GMIME_OBJECT(mime_msg), &flags); /* parts */ // mu_mime_message_foreach(mime_msg, // FALSE, /* never decrypt for this */ // (GMimeObjectForeachFunc)msg_cflags_cb, // &flags); // char *ml{get_mailing_list(self)}; // if (ml) { // flags |= Flags::MailingList; // g_free(ml); // } return flags; } static Flags get_flags(GMimeMessage *mime_msg, const std::string& path) { auto flags{mu_maildir_flags_from_path(path) .value_or(Flags::None)}; flags |= get_content_flags(mime_msg); /* pseudo-flag --> unread means either NEW or NOT SEEN, just * for searching convenience */ if (any_of(flags & Flags::New) || none_of(flags & Flags::Seen)) flags |= Flags::Unread; return flags; } static void fill_document(Document& doc, GMimeMessage *mime_msg) { //const auto contacts{mu_msg_get_contacts(msg)}; const auto path{doc.string_value(Field::Id::Path)}; // auto add_str=[&](Document& doc, Field::Id field_id, const char *str) { // if (str) // doc.add(field_id, std::string(str)); // }; field_for_each([&](auto&& field) { if (!field.is_indexable_term() && !field.is_normal_term() && !field.is_value()) return; // else if (field.is_contact()) // doc.add(field.id, contacts); else if (field.id == Field::Id::Priority) doc.add(get_priority(mime_msg)); // else if (field.id == Field::Id::Flags) // doc.add(get_flags(mime_ else if (field.id == Field::Id::ThreadId) { // refs contains a list of parent messages, with the // oldest one first until the last one, which is the // direct parent of the current message. of course, it // may be empty. // // NOTE: there may be cases where the list is truncated; // we happily ignore that case. // const auto refs{mu_msg_get_references(msg)}; // const auto thread_id{refs ? (const char*)refs->data : mu_msg_get_msgid(msg)}; // doc.add(Field::Id::ThreadId, std::string(thread_id)); } // else if (field.id == Field::Id::BodyText) // add_str(doc, field.id, mu_msg_get_body_text(msg, MU_MSG_OPTION_NONE)); // else if (field.id == Field::Id::BodyHtml) // add_str(doc, field.id, mu_msg_get_body_html(msg, MU_MSG_OPTION_NONE)); // else if (field.id == Field::Id::EmbeddedText || field.id == Field::Id::File) { // /* handle with MIME */ // } else if (field.id == Field::Id::Mime) // mu_msg_part_foreach(msg, MU_MSG_OPTION_RECURSE_RFC822, // (MuMsgPartForeachFunc)each_part, &doc); // else if (field.is_numerical()) // doc.add(field.id, mu_msg_get_field_numeric(msg, field.id)); // else if (field.is_string()) // add_str(doc, field.id, mu_msg_get_field_string(msg, field.id)); // else if (field.is_string_list()) { // std::vector vec; // auto vals{mu_msg_get_field_string_list(msg, field.id)}; // while (vals) { // vec.emplace_back ((const char*)vals->data); // vals = g_slist_next((GList*)vals); // } // doc.add(field.id, vec); else { g_warning("unhandled field %*s", STR_V(field.name)); } }); //contacts_cache_.add(std::move(contacts)); } std::string Message::header(const std::string& header_field) const { if (!mime_msg_) return {}; const char *hdr = g_mime_object_get_header(GMIME_OBJECT(mime_msg_), header_field.c_str()); if (!hdr) return {}; if (!g_utf8_validate(hdr, -1, {})) { char *hdr_u{g_strdup(hdr)}; for (auto c = hdr_u; c && *c; ++c) { if ((!isprint(*c) && !isspace (*c)) || !isascii(*c)) *c = '.'; } return from_gchars(std::move(hdr_u)); } return hdr; }