diff --git a/lib/message/meson.build b/lib/message/meson.build index ff36e8f4..7f5c6d5d 100644 --- a/lib/message/meson.build +++ b/lib/message/meson.build @@ -20,6 +20,8 @@ lib_mu_message=static_library( [ 'mu-message.cc', 'mu-message.hh', + 'mu-message-part.cc', + 'mu-message-part.hh', 'mu-contact.hh', 'mu-contact.cc', 'mu-document.cc', @@ -29,7 +31,9 @@ lib_mu_message=static_library( 'mu-flags.hh', 'mu-flags.cc', 'mu-priority.hh', - 'mu-priority.cc' + 'mu-priority.cc', + 'mu-mime-object.cc', + 'mu-mime-object.hh' ], dependencies: [ glib_dep, @@ -39,12 +43,11 @@ lib_mu_message=static_library( lib_mu_utils_dep], install: false) -# some of the libme headers include xapian -xapian_incs = xapian_dep.get_pkgconfig_variable('includedir') lib_mu_message_dep = declare_dependency( link_with: lib_mu_message, + dependencies: [ xapian_dep, gmime_dep ], include_directories: - include_directories(['.', '..', xapian_incs])) + include_directories(['.', '..'])) # # tests @@ -77,6 +80,14 @@ test('test-flags', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-message', + executable('test-message', + 'mu-message.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + test('test-priority', executable('test-priority', 'mu-priority.cc', diff --git a/lib/message/mu-contact.cc b/lib/message/mu-contact.cc index ee6df9dc..1de64c50 100644 --- a/lib/message/mu-contact.cc +++ b/lib/message/mu-contact.cc @@ -36,7 +36,7 @@ Contact::display_name() const Mu::Contacts Mu::make_contacts(InternetAddressList* addr_lst, - Field::Id field_id, ::time_t message_date) + Field::Id field_id, int64_t message_date) { Contacts contacts; size_t num{}; @@ -70,7 +70,7 @@ Mu::make_contacts(InternetAddressList* addr_lst, Mu::Contacts Mu::make_contacts(const std::string& addrs, Field::Id field_id, - ::time_t message_date) + int64_t message_date) { auto addr_list = internet_address_list_parse(NULL, addrs.c_str()); if (!addr_list) { diff --git a/lib/message/mu-contact.hh b/lib/message/mu-contact.hh index a5e2d597..75c62108 100644 --- a/lib/message/mu-contact.hh +++ b/lib/message/mu-contact.hh @@ -29,6 +29,7 @@ #include #include +#include #include "mu-fields.hh" struct _InternetAddressList; @@ -54,7 +55,7 @@ struct Contact { * @param message_date_ data for the message for this contact */ Contact(const std::string& email_, const std::string& name_ = "", - std::optional field_id_ = {}, + Option field_id_ = {}, time_t message_date_ = 0) : email{email_}, name{name_}, field_id{field_id_}, message_date{message_date_}, personal{}, frequency{1}, tstamp{} @@ -101,7 +102,7 @@ struct Contact { /** * Get a hash-value for this contact, which gets lazily calculated. This - * is for use with container classes. This uses the _lowercase_ email + * * is for use with container classes. This uses the _lowercase_ email * address. * * @return the hash @@ -118,14 +119,14 @@ struct Contact { * data members */ - std::string email; /**< Email address for this contact.Not empty */ - std::string name; /**< Name for this contact; can be empty. */ - std::optional field_id; /**< Field Id of contact or nullopt */ - ::time_t message_date; /**< date of the message from which the - * contact originates */ - bool personal; /**< A personal message? */ - size_t frequency; /**< Frequency of this contact */ - int64_t tstamp; /**< Timestamp for this contact */ + std::string email; /**< Email address for this contact.Not empty */ + std::string name; /**< Name for this contact; can be empty. */ + Option field_id; /**< Field Id of contact or nullopt */ + int64_t message_date; /**< date of the message from which the + * contact originates (or 0) */ + bool personal; /**< A personal message? */ + size_t frequency; /**< Frequency of this contact */ + int64_t tstamp; /**< Timestamp for this contact (internal use) */ private: void cleanup_name() { // replace control characters by spaces. @@ -149,7 +150,7 @@ using Contacts = std::vector; */ Contacts make_contacts(/*const*/ struct _InternetAddressList* addr_lst, - Field::Id field_id, ::time_t message_date); + Field::Id field_id, int64_t message_date); /** * Create a sequence of Contact objects from an InternetAddressList @@ -162,7 +163,7 @@ make_contacts(/*const*/ struct _InternetAddressList* addr_lst, */ Contacts make_contacts(const std::string& addrs, - Field::Id field_id, ::time_t message_date); + Field::Id field_id, int64_t message_date); } // namespace Mu /** diff --git a/lib/message/mu-document.cc b/lib/message/mu-document.cc index 494eddfe..c8ab7e29 100644 --- a/lib/message/mu-document.cc +++ b/lib/message/mu-document.cc @@ -69,8 +69,10 @@ Document::add(Field::Id id, const std::string& val) void Document::add(Field::Id id, const std::vector& vals) { - const auto field{field_from_id(id)}; + if (vals.empty()) + return; + const auto field{field_from_id(id)}; if (field.is_value()) xdoc_.add_value(field.value_no(), Mu::join(vals, SepaChar1)); @@ -88,6 +90,9 @@ Document::string_vec_value(Field::Id field_id) const noexcept void Document::add(Field::Id id, const Contacts& contacts) { + if (contacts.empty()) + return; + const auto field{field_from_id(id)}; std::vector cvec; diff --git a/lib/message/mu-document.hh b/lib/message/mu-document.hh index 5ffbab49..aa9319bf 100644 --- a/lib/message/mu-document.hh +++ b/lib/message/mu-document.hh @@ -29,6 +29,7 @@ #include "mu-priority.hh" #include "mu-flags.hh" #include "mu-contact.hh" +#include namespace Mu { @@ -93,7 +94,6 @@ public: return *this; } - /* * updating a document with terms & values */ @@ -107,7 +107,7 @@ public: void add(Field::Id field_id, const std::string& val); /** - * Add a string-vec value to the document + * Add a string-vec value to the document, if non-empty * * @param field_id field id * @param val string-vec value @@ -116,13 +116,14 @@ public: /** - * Add message-contacts to the document + * Add message-contacts to the document, if non-empty * * @param field_id field id * @param contacts message contacts */ void add(Field::Id id, const Contacts& contacts); + /** * Add an integer value to the document * @@ -140,17 +141,27 @@ public: /** - * Add message flags to the document + * Add message flags to the document * * @param flags mesage flags. */ void add(Flags flags); + /** + * Generically adds an optional value, if set, to the document + * + * @param id the field 0d + * @param an optional value + */ + template void add(Field::Id id, const Option& val) { + if (val) + add(id, val.value()); + } + /* * Retrieving values */ - /** * Get a message-field as a string-value * diff --git a/lib/message/mu-fields.hh b/lib/message/mu-fields.hh index add13e97..a2fb5138 100644 --- a/lib/message/mu-fields.hh +++ b/lib/message/mu-fields.hh @@ -24,9 +24,9 @@ #include #include #include -#include #include #include +#include namespace Mu { @@ -320,7 +320,7 @@ static constexpr std::array Field::Type::String, "msgid", "Attachment MIME-type", - "mime:image/jpeg", + "msgid:abc@123", 'i', Field::Flag::GMime | Field::Flag::NormalTerm | @@ -502,11 +502,11 @@ void field_for_each(Func&& func) { * @return a message-field id, or nullopt if not found. */ template -std::optional field_find_if(Pred&& pred) { +Option field_find_if(Pred&& pred) { for (auto&& field: Fields) if (pred(field)) return field; - return std::nullopt; + return Nothing; } /** @@ -517,13 +517,13 @@ std::optional field_find_if(Pred&& pred) { * @return the message-field-id or nullopt. */ static inline -std::optional field_from_shortcut(char shortcut) { +Option field_from_shortcut(char shortcut) { return field_find_if([&](auto&& field){ return field.shortcut == shortcut; }); } static inline -std::optional field_from_name(const std::string& name) { +Option field_from_name(const std::string& name) { if (name.length() == 1) return field_from_shortcut(name[0]); else @@ -540,10 +540,10 @@ std::optional field_from_name(const std::string& name) { * @return Field::Id or nullopt */ static inline -std::optional field_from_number(size_t id) +Option field_from_number(size_t id) { if (id >= static_cast(Field::Id::_count_)) - return std::nullopt; + return Nothing; else return field_from_id(static_cast(id)); } diff --git a/lib/message/mu-message-part.cc b/lib/message/mu-message-part.cc new file mode 100644 index 00000000..1126db27 --- /dev/null +++ b/lib/message/mu-message-part.cc @@ -0,0 +1,85 @@ +/* +** 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-part.hh" +#include "mu-mime-object.hh" +#include "mu-utils.hh" + +using namespace Mu; + +MessagePart::MessagePart(const Mu::MimeObject& obj): + mime_obj{std::make_unique(obj)} +{} + +MessagePart::MessagePart(const MessagePart& other): + MessagePart(*other.mime_obj) +{} + +MessagePart::~MessagePart() = default; + + +Option +MessagePart::filename() const noexcept +{ + if (!mime_obj->is_part()) + return Nothing; + else + return MimePart(*mime_obj).filename(); +} + +Option +MessagePart::mime_type() const noexcept +{ + if (const auto ctype{mime_obj->content_type()}; ctype) + return ctype->media_type() + "/" + ctype->media_subtype(); + else + return Nothing; +} + +size_t +MessagePart::size() const noexcept +{ + if (!mime_obj->is_part()) + return 0; + else + return MimePart(*mime_obj).size(); +} + + +Option +MessagePart::to_string() const noexcept +{ + if (mime_obj->is_part()) + return MimePart(*mime_obj).to_string(); + else + return mime_obj->object_to_string(); +} + + + +Result +MessagePart::to_file(const std::string& path, bool overwrite) const noexcept +{ + if (!mime_obj->is_part()) + return Err(Error::Code::InvalidArgument, + "not a part"); + else + return MimePart(*mime_obj).to_file(path, overwrite); +} diff --git a/lib/message/mu-message-part.hh b/lib/message/mu-message-part.hh new file mode 100644 index 00000000..2a7d2456 --- /dev/null +++ b/lib/message/mu-message-part.hh @@ -0,0 +1,101 @@ +/* +** 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. +** +*/ + + +#ifndef MU_MESSAGE_PART_HH__ +#define MU_MESSAGE_PART_HH__ + +#include +#include +#include +#include + +namespace Mu { + +struct MimeObject; // forward declaration + +class MessagePart { +public: + /** + * Construct MessagePart from a MimeObject + * + * @param obj + */ + MessagePart(const MimeObject& obj); + + /** + * Copy CTOR + * + * @param other + */ + MessagePart(const MessagePart& other); + + /** + * DTOR + * + */ + ~MessagePart(); + + /** + * Filename for the mime-part + * + * @return the filename or Nothing if there is none + */ + Option filename() const noexcept; + + /** + * Mime-type for the mime-part (e.g. "text/plain") + * + * @return the mime-part or Nothing if there is none + */ + Option mime_type() const noexcept; + + /** + * Get the length of the (unencoded) MIME-part. + * + * @return the size + */ + size_t size() const noexcept; + + /** + * Write (decoded) mime-part contents to string + * + * @return a string or nothing if there is no contemt + */ + Option to_string() const noexcept; + + + /** + * Write (decoded) mime part to a file + * + * @param path path to file + * @param overwrite whether to possibly overwrite + * + * @return size of file or or an error. + */ + Result to_file(const std::string& path, bool overwrite) const noexcept; + + struct Private; +private: + std::unique_ptr mime_obj; +}; + +} // namespace Mu + +#endif /* MU_MESSAGE_PART_HH__ */ diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc index 6f475415..495995eb 100644 --- a/lib/message/mu-message.cc +++ b/lib/message/mu-message.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2022 Dirk-Jan C. Binnema + ** 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 @@ -19,8 +19,13 @@ #include "mu-message.hh" +#include "gmime/gmime-references.h" +#include "gmime/gmime-stream-mem.h" #include "mu-maildir.hh" +#include +#include +#include #include #include #include @@ -33,74 +38,37 @@ #include #include "gmime/gmime-message.h" +#include "mu-mime-object.hh" +#include "mu-msg-priv.hh" 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; +struct Message::Private { - if (gmime_initialized) - return true; + Document doc; + mutable Option mime_msg; - 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(); + Flags flags{}; + Option mailing_list; + std::vector references; + std::vector parts; /* - * sanity checks. + * we only need to index these, so we don't + * really need these copy if we re-arrange things + * a bit */ + Option body_txt; + Option body_html; + Option embedded; +}; + +static void fill_document(Message::Private& priv); + +Message::Message(const std::string& path, const std::string& mdir): + priv_{std::make_unique()} +{ if (!g_path_is_absolute(path.c_str())) throw Error(Error::Code::File, "path '%s' is not absolute", path.c_str()); @@ -115,311 +83,682 @@ Message::Message(const std::string& path, const std::string& mdir) 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"); + init_gmime(); + if (auto msg{MimeMessage::make_from_file(path)}; !msg) + throw msg.error(); + else + priv_->mime_msg = std::move(msg.value()); - 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)); + priv_->doc.add(Field::Id::Path, + Mu::from_gchars(g_canonicalize_filename(path.c_str(), NULL))); + priv_->doc.add(Field::Id::Maildir, mdir); + priv_->doc.add(Field::Id::Size, static_cast(statbuf.st_size)); // rest of the fields - //fill_fields(doc_, mime_msg_); + fill_document(*priv_); } -Message::~Message() +Message::Message(const std::string& text): + priv_{std::make_unique()} { - g_clear_object(&mime_msg_); -} + priv_->doc.add(Field::Id::Size, static_cast(text.size())); + priv_->doc.add(Field::Id::Path, ""); - -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; + init_gmime(); + if (auto msg{MimeMessage::make_from_string(text)}; !msg) + throw msg.error(); else - return parse_prio_str(priostr); + priv_->mime_msg = std::move(msg.value()); + + fill_document(*priv_); +} + +Message::Message(Message&& msg) = default; +Message::Message(Document& doc): + priv_{std::make_unique()} +{ + priv_->doc = doc; +} + +Message::~Message() = default; + +const Mu::Document& +Message::document() const +{ + return priv_->doc; +} + +bool +Message::load_mime_message(bool reload) const +{ + if (priv_->mime_msg && !reload) + return true; + + const auto path{document().string_value(Field::Id::Path)}; + if (auto mime_msg{MimeMessage::make_from_file(path)}; !mime_msg) { + g_warning("failed to load '%s': %s", + path.c_str(), mime_msg.error().what()); + return false; + } else { + priv_->mime_msg = std::move(mime_msg.value()); + return true; + } +} + +void +Message::unload_mime_message() const +{ + priv_->mime_msg = Nothing; +} +static Priority +get_priority(const MimeMessage& mime_msg) +{ + constexpr std::array, 10> + prio_alist = {{ + {"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} + }}; + + const auto opt_str = mime_msg.header("Precedence") + .disjunction(mime_msg.header("X-Priority")) + .disjunction(mime_msg.header("Importance")); + + if (!opt_str) + return Priority::Normal; + + const auto it = seq_find_if(prio_alist, [&](auto&& item) { + return g_ascii_strncasecmp(item.first.data(), opt_str->c_str(), + item.first.size()) == 0; }); + + return it == prio_alist.cend() ? Priority::Normal : it->second; } -static gboolean -looks_like_attachment(GMimeObject* part) +/* see: http://does-not-exist.org/mail-archives/mutt-dev/msg08249.html */ +static std::vector +get_tags(const MimeMessage& mime_msg) { - 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"}}; + constexpr std::array, 3> tag_headers = {{ + {"X-Label", ' '}, {"X-Keywords", ','}, {"Keywords", ' '} + }}; - disp = g_mime_object_get_content_disposition(part); + std::vector tags; + seq_for_each(tag_headers, [&](auto&& item) { + if (auto hdr{mime_msg.header(item.first)}; hdr) { + auto lst = split(*hdr, item.second); + tags.reserve(tags.size() + lst.size()); + tags.insert(tags.end(), lst.begin(), lst.end()); + } + }); - if (!GMIME_IS_CONTENT_DISPOSITION(disp)) - return FALSE; + return tags; +} - dispstr = g_mime_content_disposition_get_disposition(disp); +static Option +get_mailing_list(const MimeMessage& mime_msg) +{ + char *dechdr, *res; + const char *b, *e; - if (g_ascii_strcasecmp(dispstr, "attachment") == 0) - return TRUE; + const auto hdr{mime_msg.header("List-Id")}; + if (!hdr) + return {}; + + dechdr = g_mime_utils_header_decode_phrase(NULL, hdr->c_str()); + if (!dechdr) + return {}; + + e = NULL; + b = ::strchr(dechdr, '<'); + if (b) + e = strchr(b, '>'); + + if (b && e) + res = g_strndup(b + 1, e - b - 1); + else + res = g_strdup(dechdr); + + g_free(dechdr); + + return from_gchars(std::move(res)); +} + +static bool /* heuristic */ +looks_like_attachment(const MimePart& part, const Option& ctype) +{ + constexpr std::array, 4> att_types = {{ + {"image", "*"}, + {"audio", "*"}, + {"application", "*"}, + {"application", "x-patch"} + }}; + + if (part.is_attachment()) /* explicity set as attachment */ + return true; + else if (!ctype) + return false; /* we also consider patches, images, audio, and non-pgp-signature * application attachments to be attachments... */ - ctype = g_mime_object_get_content_type(part); + if (ctype->is_type("*", "pgp-signature")) + return false; /* don't consider as a signature */ - if (g_mime_content_type_is_type(ctype, "*", "pgp-signature")) - return FALSE; /* don't consider as a signature */ + if (ctype->is_type("text", "*") && + (ctype->is_type("*", "plain") || ctype->is_type("*", "html"))) + return false; /* not 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; + const auto it = seq_find_if(att_types, [&](auto&& item){ + return ctype->is_type(item.first, item.second); + }); + return it != att_types.cend(); /* if found, it's an attachment */ } static void -msg_cflags_cb(GMimeObject* parent, GMimeObject* part, Flags* flags) +accumulate_text(const MimePart& part, Message::Private& info, + const MimeContentType& ctype) { - if (GMIME_IS_MULTIPART_SIGNED(part)) - *flags |= Flags::Signed; + if (!ctype.is_type("text", "*")) + return; /* not a text type */ - /* 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; + auto append = [](Option& str, Option app) { + if (!str) + str = app; + else if (app) + str.value() += app.value(); + }; - /* 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; + if (part.is_attachment()) + append(info.embedded, part.to_string()); + else if (ctype.is_type("text", "plain")) + append(info.body_txt, part.to_string()); + else if (ctype.is_type("text", "html")) + append(info.body_html, part.to_string()); +} + +static void +process_part(const MimePart& part, Message::Private& info) +{ + const auto ctype{part.content_type()}; + if (!ctype) + return; + + if (looks_like_attachment(part, ctype)) + info.flags |= Flags::HasAttachment; + + // if there are text parts, gather. + accumulate_text(part, info, *ctype); + + //MimePart mypart(part); + info.parts.emplace_back(part); +} + +/** + * This message -- recursively walk through message, and initialize some + * other values that depend on another. + * + * @param mime_msg + * @param path + * @param info + */ +static void +process_message(const MimeMessage& mime_msg, const std::string& path, + Message::Private& info) +{ + info.flags = Flags::None; //mu_maildir_flags_from_path(path).value_or(Flags::None); + + /* pseudo-flag --> unread means either NEW or NOT SEEN, just + * for searching convenience */ + if (any_of(info.flags & Flags::New) || none_of(info.flags & Flags::Seen)) + info.flags |= Flags::Unread; + + // parts + mime_msg.for_each([&](auto&& parent, auto&& part) { + + if (part.is_part()) + process_part(part, info); + + if (part.is_multipart_signed()) + info.flags |= Flags::Signed; + else if (part.is_multipart_encrypted()) { + /* FIXME: An encrypted part might be signed at the same time. + * In that case the signed flag is lost. */ + info.flags |= Flags::Encrypted; + } else if (part.is_mime_application_pkcs7_mime()) { + MimeApplicationPkcs7Mime smime(part); + switch (smime.smime_type()) { + case Mu::MimeApplicationPkcs7Mime::SecureMimeType::SignedData: + info.flags |= Flags::Signed; break; - case GMIME_SECURE_MIME_TYPE_SIGNED_DATA: - *flags |= Flags::Signed; + case Mu::MimeApplicationPkcs7Mime::SecureMimeType::EnvelopedData: + info.flags |= Flags::Encrypted; 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)); + // get the mailing here, and use it do update flags, too. + info.mailing_list = get_mailing_list(mime_msg); + if (info.mailing_list) + info.flags |= Flags::MailingList; } - -std::string -Message::header(const std::string& header_field) const +static Mu::Result +calculate_sha256(const std::string& path) { - if (!mime_msg_) - return {}; + g_autoptr(GChecksum) checksum{g_checksum_new(G_CHECKSUM_SHA256)}; - const char *hdr = g_mime_object_get_header(GMIME_OBJECT(mime_msg_), - header_field.c_str()); - if (!hdr) - return {}; + FILE *file{::fopen(path.c_str(), "r")}; + if (!file) + return Err(Error{Error::Code::File, "failed to open %s: %s", + path.c_str(), ::strerror(errno)}); - 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)); + std::array buf{}; + while (true) { + const auto n = ::fread(buf.data(), 1, buf.size(), file); + if (n == 0) + break; + g_checksum_update(checksum, buf.data(), n); } - return hdr; + bool has_err = ::ferror(file) != 0; + ::fclose(file); + + if (has_err) + return Err(Error{Error::Code::File, "failed to read %s", path.c_str()}); + + return Ok(g_checksum_get_string(checksum)); } + + +static std::string +fake_message_id(const std::string path) +{ + constexpr auto mu_suffix{"@mu.id"}; + + if (path.empty()) + return format("12345@%s", mu_suffix); + else if (const auto sha256_res{calculate_sha256(path)}; !sha256_res) { + g_warning("failed to get sha-256: %s", sha256_res.error().what()); + // fallback... not a very good message-id, but should + // not happen in practice. + return format("%08x%s", g_str_hash(path.c_str()), mu_suffix); + } else + return format("%s%s", sha256_res.value().c_str(), mu_suffix); +} + + +static void +fill_document(Message::Private& priv) +{ + /* hunt & gather info from message tree */ + Document& doc{priv.doc}; + MimeMessage& mime_msg{priv.mime_msg.value()}; + + const auto path{doc.string_value(Field::Id::Path)}; + const auto refs{mime_msg.references()}; + const auto message_id{mime_msg.message_id().value_or(fake_message_id(path))}; + + process_message(mime_msg, path, priv); + + field_for_each([&](auto&& field) { + /* insist on expliclity handling each */ +#pragma GCC diagnostic push +#pragma GCC diagnostic error "-Wswitch" + using AddrType = MimeMessage::AddressType; + switch(field.id) { + case Field::Id::Bcc: + doc.add(field.id, mime_msg.addresses(AddrType::Bcc)); + break; + case Field::Id::BodyHtml: + doc.add(field.id, priv.body_html); + break; + case Field::Id::BodyText: + doc.add(field.id, priv.body_txt); + break; + case Field::Id::Cc: + doc.add(field.id, mime_msg.addresses(AddrType::Cc)); + break; + case Field::Id::Date: + doc.add(field.id, mime_msg.date()); + break; + case Field::Id::EmbeddedText: + doc.add(field.id, priv.embedded); + break; + case Field::Id::File: + for (auto&& part: priv.parts) + doc.add(field.id, part.filename()); + break; + case Field::Id::Flags: + doc.add(priv.flags); + break; + case Field::Id::From: + doc.add(field.id, mime_msg.addresses(AddrType::From)); + break; + case Field::Id::Maildir: /* already */ + break; + case Field::Id::MailingList: + doc.add(field.id, priv.mailing_list); + break; + case Field::Id::MessageId: + doc.add(field.id, message_id); + break; + case Field::Id::Mime: + for (auto&& part: priv.parts) + doc.add(field.id, part.mime_type()); + break; + case Field::Id::Path: /* already */ + break; + case Field::Id::Priority: + doc.add(get_priority(mime_msg)); + break; + case Field::Id::References: + doc.add(field.id, refs); + break; + case Field::Id::Size: /* already */ + break; + case Field::Id::Subject: + doc.add(field.id, mime_msg.subject()); + break; + case Field::Id::Tags: + doc.add(field.id, get_tags(mime_msg)); + break; + case Field::Id::ThreadId: + // either the oldest reference, or otherwise the message id + doc.add(field.id, refs.empty() ? message_id : refs.at(0)); + break; + case Field::Id::To: + doc.add(field.id, mime_msg.addresses(AddrType::To)); + break; + case Field::Id::Uid: + doc.add(field.id, path); // just a synonym for now. + break; + case Field::Id::_count_: + break; + } +#pragma GCC diagnostic pop + + }); +} + +Option +Message::header(const std::string& header_field) const +{ + if (!load_mime_message()) + return Nothing; + + return priv_->mime_msg->header(header_field); +} + +Option +Message::body_text() const +{ + if (!load_mime_message()) + return {}; + + return priv_->body_txt; +} + +Option +Message::body_html() const +{ + if (!load_mime_message()) + return {}; + + return priv_->body_html; +} + +Contacts +Message::all_contacts() const +{ + Contacts contacts; + + if (!load_mime_message()) + return contacts; /* empty */ + + for (auto&& ctype: { + MimeMessage::AddressType::Sender, + MimeMessage::AddressType::From, + MimeMessage::AddressType::ReplyTo, + MimeMessage::AddressType::To, + MimeMessage::AddressType::Cc, + MimeMessage::AddressType::Bcc}) { + auto addrs{priv_->mime_msg->addresses(ctype)}; + std::move(addrs.begin(), addrs.end(), std::back_inserter(contacts)); + } + + return contacts; +} + +const std::vector& +Message::parts() const +{ + if (!load_mime_message()) { + static std::vector empty; + return empty; + } + + return priv_->parts; +} + + +/* + * tests + */ + +#ifdef BUILD_TESTS + +/* + * test message 1 + */ + +static void +test_message_mailing_list() +{ + constexpr const char *test_message_1 = +R"(Return-Path: +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F + for ; Mon, 4 Aug 2008 21:49:34 +0300 (EEST) +Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +From: anon@example.com +To: sqlite-dev@sqlite.org +Mime-Version: 1.0 (Apple Message framework v926) +Date: Mon, 4 Aug 2008 11:40:49 +0200 +X-Mailer: Apple Mail (2.926) +Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec +Precedence: list +Reply-To: sqlite-dev@sqlite.org +List-Id: +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: sqlite-dev-bounces@sqlite.org +Content-Length: 639 + +Inside sqlite3VdbeExec there is a very big switch statement. +In order to increase performance with few modifications to the +original code, why not use this technique ? +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html + +With a properly defined "instructions" array, instead of the switch +statement you can use something like: +goto * instructions[pOp->opcode]; +)"; + auto message{Message::make_from_string(test_message_1)}; + g_assert_true(!!message); + + g_assert_true(message->bcc().empty()); + + g_assert_true(!message->body_html()); + assert_equal(message->body_text().value_or(""), +R"(Inside sqlite3VdbeExec there is a very big switch statement. +In order to increase performance with few modifications to the +original code, why not use this technique ? +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html + +With a properly defined "instructions" array, instead of the switch +statement you can use something like: +goto * instructions[pOp->opcode]; +)"); + g_assert_true(message->cc().empty()); + g_assert_cmpuint(message->date(), ==, 1217842849); + g_assert_true(message->flags() == (Flags::MailingList | Flags::Unread)); + + const auto from{message->from()}; + g_assert_cmpuint(from.size(),==,1); + assert_equal(from.at(0).name, ""); + assert_equal(from.at(0).email, "anon@example.com"); + + assert_equal(message->mailing_list(), "sqlite-dev.sqlite.org"); + assert_equal(message->message_id(), + "83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net"); + + g_assert_true(message->path().empty()); + g_assert_true(message->priority() == Priority::Low); + g_assert_cmpuint(message->size(),==,::strlen(test_message_1)); + + g_assert_true(message->references().empty()); + + assert_equal(message->subject(), + "[sqlite-dev] VM optimization inside sqlite3VdbeExec"); + + const auto to{message->to()}; + g_assert_cmpuint(to.size(),==,1); + assert_equal(to.at(0).name, ""); + assert_equal(to.at(0).email, "sqlite-dev@sqlite.org"); + + assert_equal(message->header("X-Mailer").value_or(""), "Apple Mail (2.926)"); + + auto all_contacts{message->all_contacts()}; + g_assert_cmpuint(all_contacts.size(), ==, 4); + seq_sort(all_contacts, [](auto&& c1, auto&& c2){return c1.email < c2.email; }); + assert_equal(all_contacts[0].email, "anon@example.com"); + assert_equal(all_contacts[1].email, "sqlite-dev-bounces@sqlite.org"); + assert_equal(all_contacts[2].email, "sqlite-dev@sqlite.org"); + assert_equal(all_contacts[3].email, "sqlite-dev@sqlite.org"); +} + + +static void +test_message_attachments(void) +{ + constexpr const char* msg_text = +R"(Return-Path: +Received: from pop.gmail.com [256.85.129.309] + by evergrey with POP3 (fetchmail-6.4.29) + for (single-drop); Thu, 24 Mar 2022 20:12:40 +0200 (EET) +Sender: "Foo, Example" +User-agent: mu4e 1.7.10; emacs 29.0.50 +From: "Foo Example" +To: bar@example.com +Subject: =?utf-8?B?w6R0dMOkY2htZcOxdHM=?= +Date: Thu, 24 Mar 2022 20:04:39 +0200 +Organization: ACME Inc. +Message-ID: <87a6dfw7bg.fsf@example.com> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=-=-=" + +--=-=-= +Content-Type: text/plain + +Hello, +--=-=-= +Content-Type: image/jpeg +Content-Disposition: attachment; filename=file-01.bin +Content-Transfer-Encoding: base64 +Content-Description: test file 1 + +MDAwAQID +--=-=-= +Content-Type: audio/ogg +Content-Disposition: inline; filename=file-02.bin +Content-Transfer-Encoding: base64 + +MDA0BQYH +--=-=-= +Content-Type: text/plain + +World! +--=-=-=-- +)"; + + auto message{Message::make_from_string(msg_text)}; + g_assert_true(!!message); + + g_assert_true(message->bcc().empty()); + g_assert_true(!message->body_html()); + assert_equal(message->body_text().value_or(""), +R"(Hello,World!)"); + + g_assert_true(message->cc().empty()); + g_assert_cmpuint(message->date(), ==, 1648145079); + g_assert_true(message->flags() == (Flags::HasAttachment|Flags::Unread)); + + const auto from{message->from()}; + g_assert_cmpuint(from.size(),==,1); + assert_equal(from.at(0).name, "Foo Example"); + assert_equal(from.at(0).email, "foo@example.com"); + + g_assert_true(message->path().empty()); + g_assert_true(message->priority() == Priority::Normal); + g_assert_cmpuint(message->size(),==,::strlen(msg_text)); + + assert_equal(message->subject(), "ättächmeñts"); + + g_assert_cmpuint(message->parts().size(),==,4); + { + auto&& part{message->parts().at(0)}; + g_assert_false(!!part.filename()); + assert_equal(part.mime_type().value(), "text/plain"); + assert_equal(part.to_string().value(), "Hello,"); + } + { + auto&& part{message->parts().at(1)}; + assert_equal(part.filename().value(), "file-01.bin"); + assert_equal(part.mime_type().value(), "image/jpeg"); + // file consist of 6 bytes "000" 0x01,0x02.0x03. + assert_equal(part.to_string().value(), "000\001\002\003"); + } + { + auto&& part{message->parts().at(2)}; + assert_equal(part.filename().value(), "file-02.bin"); + assert_equal(part.mime_type().value(), "audio/ogg"); + // file consist of the string "004" followed by 0x5,0x6,0x7. + assert_equal(part.to_string().value(), "004\005\006\007"); + } + { + auto&& part{message->parts().at(3)}; + g_assert_false(!!part.filename()); + g_assert_true(!!part.mime_type()); + assert_equal(part.mime_type().value(), "text/plain"); + assert_equal(part.to_string().value(), "World!"); + } +} + +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/message/message/mailing-list", + test_message_mailing_list); + g_test_add_func("/message/message/attachments", + test_message_attachments); + + return g_test_run(); +} + +#endif /*BUILD_TESTS*/ diff --git a/lib/message/mu-message.hh b/lib/message/mu-message.hh index 81016bb2..72e44a95 100644 --- a/lib/message/mu-message.hh +++ b/lib/message/mu-message.hh @@ -23,19 +23,27 @@ #include #include #include - +#include "utils/mu-option.hh" #include "mu-contact.hh" #include "mu-priority.hh" #include "mu-flags.hh" #include "mu-fields.hh" #include "mu-document.hh" +#include "mu-message-part.hh" -struct _GMimeMessage; +#include "utils/mu-result.hh" namespace Mu { class Message { public: + /** + * Move CTOR + * + * @param some other message + */ + Message(Message&& msg); + /** * Construct a message based on a path * @@ -44,57 +52,52 @@ public: * ~/Maildir/foo/bar/cur/msg, the maildir would be foo/bar; you can * pass NULL for this parameter, in which case some maildir-specific * information is not available. + * + * @return a message or an error */ - Message(const std::string& path, const std::string& mdir); + static Result make_from_path(const std::string& path, const std::string& mdir) try { + return Ok(Message{path, mdir}); + } catch (Error& err) { + return Err(err); + } catch (...) { + return Err(Mu::Error(Error::Code::Message, "failed to create message")); + } + /** * Construct a message based on a Message::Document * * @param doc - */ - Message(Document& doc): doc_{doc} {} - - /** - * Copy CTOR * - * @param rhs a Message + * @return a message or an error */ - Message(const Message& rhs) { - *this = rhs; + static Result make_from_document(Document& doc) try { + return Ok(Message{doc}); + } catch (Error& err) { + return Err(err); + } catch (...) { + return Err(Mu::Error(Error::Code::Message, "failed to create message")); } /** - * Move CTOR + * Construct a message from a string. This is mostly useful for testing. * - * @param rhs a Message + * @param text message text + * + * @return a message or an error */ - - Message(Message&& rhs) { - *this = std::move(rhs); + static Result make_from_string(const std::string& text) try { + return Ok(Message{text}); + } catch (Error& err) { + return Err(err); + } catch (...) { + return Err(Mu::Error(Error::Code::Message, "failed to create message")); } /** * DTOR - * */ ~Message(); -/** - * Copy assignment operator - * - * @param rhs some message - * - * @return a message ref - */ - Message& operator=(const Message& rhs); - - /** - * Move assignment operator - * - * @param rhs some message - * - * @return a message ref - */ - Message& operator=(Message&& rhs); /** * Get the document. @@ -102,7 +105,7 @@ public: * * @return document */ - const Document& document() const { return doc_; } + const Document& document() const; /** * Get the file system path of this message @@ -110,50 +113,50 @@ public: * @return the path of this Message or NULL in case of error. * the returned string should *not* be modified or freed. */ - std::string path() const { return doc_.string_value(Field::Id::Path); } + std::string path() const { return document().string_value(Field::Id::Path); } /** * Get the sender (From:) of this message * * @return the sender(s) of this Message */ - Contacts from() const { return doc_.contacts_value(Field::Id::From); } + Contacts from() const { return document().contacts_value(Field::Id::From); } /** * Get the recipient(s) (To:) for this message * * @return recipients */ - Contacts to() const { return doc_.contacts_value(Field::Id::To); } + Contacts to() const { return document().contacts_value(Field::Id::To); } /** * Get the recipient(s) (Cc:) for this message * * @return recipients */ - Contacts cc() const { return doc_.contacts_value(Field::Id::Cc); } - + Contacts cc() const { return document().contacts_value(Field::Id::Cc); } /** * Get the recipient(s) (Bcc:) for this message * * @return recipients */ - Contacts bcc() const { return doc_.contacts_value(Field::Id::Bcc); } + Contacts bcc() const { return document().contacts_value(Field::Id::Bcc); } + /** * Get the maildir this message lives in; ie, if the path is * ~/Maildir/foo/bar/cur/msg, the maildir would be foo/bar * * @return the maildir requested or empty */ - std::string maildir() const { return doc_.string_value(Field::Id::Maildir); } + std::string maildir() const { return document().string_value(Field::Id::Maildir); } /** * Get the subject of this message * * @return the subject of this Message */ - std::string subject() const { return doc_.string_value(Field::Id::Subject); } + std::string subject() const { return document().string_value(Field::Id::Subject); } /** * Get the Message-Id of this message @@ -161,7 +164,7 @@ public: * @return the Message-Id of this message (without the enclosing <>), or * a fake message-id for messages that don't have them */ - std::string message_id() const { return doc_.string_value(Field::Id::MessageId);} + std::string message_id() const { return document().string_value(Field::Id::MessageId);} /** * get the mailing list for a message, i.e. the mailing-list @@ -170,7 +173,7 @@ public: * @return the mailing list id for this message (without the enclosing <>) * or NULL in case of error or if there is none. */ - std::string mailing_list() const { return doc_.string_value(Field::Id::MailingList);} + std::string mailing_list() const { return document().string_value(Field::Id::MailingList);} /** * get the message date/time (the Date: field) as time_t, using UTC @@ -178,14 +181,14 @@ public: * @return message date/time or 0 in case of error or if there * is no such header. */ - time_t date() const { return static_cast(doc_.integer_value(Field::Id::Date)); } + ::time_t date() const { return static_cast(document().integer_value(Field::Id::Date)); } /** * get the flags for this message * * @return the file/content flags */ - Flags flags() const { return doc_.flags_value(); } + Flags flags() const { return document().flags_value(); } /** * get the message priority for this message. The X-Priority, X-MSMailPriority, @@ -194,14 +197,14 @@ public: * * @return the message priority */ - Priority priority() const { return doc_.priority_value(); } + Priority priority() const { return document().priority_value(); } /** * get the file size in bytes of this message * * @return the filesize */ - size_t size() const { return static_cast(doc_.integer_value(Field::Id::Size)); } + size_t size() const { return static_cast(document().integer_value(Field::Id::Size)); } /** * get the list of references (consisting of both the References and @@ -212,7 +215,7 @@ public: * @return a vec with the references for this msg. */ std::vector references() const { - return doc_.string_vec_value(Field::Id::References); + return document().string_vec_value(Field::Id::References); } /** @@ -223,22 +226,82 @@ public: * @return a list with the tags for this msg. Don't modify/free */ std::vector tags() const { - return doc_.string_vec_value(Field::Id::References); + return document().string_vec_value(Field::Id::References); } + + /* + * Below require a file-backed message, which is a relatively slow + * if there isn't one already () + * + */ + + + /** + * Get the text body + * + * @return text body + */ + Option body_text() const; + + /** + * Get the HTML body + * + * @return text body + */ + Option body_html() const; + /** * Get some message-header * * @param header_field name of the header * - * @return the value + * @return the value (UTF-8), or Nothing. */ - std::string header(const std::string& header_field) const; + Option header(const std::string& header_field) const; + /** + * Get all contacts for this message. + * + * @return contacts + */ + Contacts all_contacts() const; + + /** + * Get information about MIME-parts in this message. + * + * @return mime-part info. + */ + using Part = MessagePart; + const std::vector& parts() const; + + /** + * Load the GMime (file) message (for a database-backed message), + * if not already (but see @param reload). + * + * Affects cached-state only, so we still mark this as 'const' + * + * @param reload whether to force reloading (even if already) + * + * @return true if loading worked; false otherwise. + */ + bool load_mime_message(bool reload=false) const; + + /** + * Clear the GMime message. + * + * Affects cached-state only, so we still mark this as 'const' + */ + void unload_mime_message() const; + + struct Private; private: - Document doc_; - mutable struct _GMimeMessage *mime_msg_{}; + Message(const std::string& path, const std::string& mdir); + Message(const std::string& str); + Message(Document& doc); + + std::unique_ptr priv_; }; // Message } // Mu #endif /* MU_MESSAGE_HH__ */ diff --git a/lib/message/mu-mime-object.cc b/lib/message/mu-mime-object.cc new file mode 100644 index 00000000..862dbf79 --- /dev/null +++ b/lib/message/mu-mime-object.cc @@ -0,0 +1,363 @@ +/* +** 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-mime-object.hh" +#include "gmime/gmime-message.h" +#include "mu-utils.hh" +#include +#include + + +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. */ + +void +Mu::init_gmime(void) +{ + // fast path. + static bool gmime_initialized = false; + if (gmime_initialized) + return; + + static std::mutex gmime_lock; + std::lock_guard lock (gmime_lock); + if (gmime_initialized) + return; // already + + g_debug("initializing gmime %u.%u.%u", + gmime_major_version, + gmime_minor_version, + gmime_micro_version); + + g_mime_init(); + gmime_initialized = true; + + std::atexit([] { + g_debug("shutting down gmime"); + g_mime_shutdown(); + gmime_initialized = false; + }); +} + + + +/* + * MimeObject + */ + +Option +MimeObject::header(const std::string& hdr) const noexcept +{ + const char *val{g_mime_object_get_header(self(), hdr.c_str())}; + if (!val) + return Nothing; + if (!g_utf8_validate(val, -1, {})) + return utf8_clean(hdr); + else + return val; +} + + +Option +MimeObject::object_to_string() const noexcept +{ + GMimeStream *stream{g_mime_stream_mem_new()}; + if (!stream) { + g_warning("failed to create mem stream"); + return Nothing; + } + + const auto written = g_mime_object_write_to_stream(self(), {}, stream); + if (written < 0) { + g_warning("failed to write object to stream"); + return Nothing; + } + + std::string buffer; + buffer.resize(written + 1); + g_mime_stream_reset(stream); + + auto bytes{g_mime_stream_read(stream, buffer.data(), written)}; + g_object_unref(stream); + if (bytes < 0) + return Nothing; + + buffer.data()[written]='\0'; + buffer.resize(written); + + return buffer; +} + + +/* + * MimeMessage + */ + + + +static Result +make_from_stream(GMimeStream* &&stream/*consume*/) +{ + GMimeParser *parser{g_mime_parser_new_with_stream(stream)}; + g_object_unref(stream); + if (!parser) + return Err(Error::Code::Message, "cannot create mime parser"); + + GMimeMessage *gmime_msg{g_mime_parser_construct_message(parser, NULL)}; + g_object_unref(parser); + if (!gmime_msg) + return Err(Error::Code::Message, "message seems invalid"); + + auto mime_msg{MimeMessage{std::move(G_OBJECT(gmime_msg))}}; + g_object_unref(gmime_msg); + + return Ok(std::move(mime_msg)); +} + +Result +MimeMessage::make_from_file(const std::string& path) +{ + GError* err{}; + if (auto&& stream{g_mime_stream_file_open(path.c_str(), "r", &err)}; !stream) + return Err(Error::Code::Message, &err, + "failed to open stream for %s", path.c_str()); + else + return make_from_stream(std::move(stream)); +} + +Result +MimeMessage::make_from_string(const std::string& text) +{ + if (auto&& stream{g_mime_stream_mem_new_with_buffer( + text.c_str(), text.length())}; !stream) + return Err(Error::Code::Message, + "failed to open stream for string"); + else + return make_from_stream(std::move(stream)); +} + +Option +MimeMessage::date() const noexcept +{ + GDateTime *dt{g_mime_message_get_date(self())}; + if (!dt) + return Nothing; + else + return g_date_time_to_unix(dt); +} + +Mu::Contacts +MimeMessage::addresses(AddressType atype) const noexcept +{ + auto addrs{g_mime_message_get_addresses( + self(), static_cast(atype))}; + if (!addrs) + return {}; + + + const auto msgtime{date().value_or(0)}; + const auto opt_field_id = std::invoke( + [&]()->Option{ + switch(atype) { + case AddressType::To: + return Field::Id::To; + case AddressType::From: + return Field::Id::From; + case AddressType::Bcc: + return Field::Id::Bcc; + case AddressType::Cc: + return Field::Id::Cc; + default: + return Nothing; + } + }); + + Contacts contacts; + auto lst_len{internet_address_list_length(addrs)}; + contacts.reserve(lst_len); + for (auto i = 0; i != lst_len; ++i) { + + auto&& addr{internet_address_list_get_address(addrs, i)}; + const auto name{internet_address_get_name(addr)}; + + if (G_UNLIKELY(!INTERNET_ADDRESS_IS_MAILBOX(addr))) + continue; + + const auto email{internet_address_mailbox_get_addr ( + INTERNET_ADDRESS_MAILBOX(addr))}; + if (G_UNLIKELY(!email)) + continue; + + contacts.push_back(Contact{email, name ? name : "", + opt_field_id, msgtime}); + } + + return contacts; +} + + + +std::vector +MimeMessage::references() const noexcept +{ + constexpr std::array ref_headers = { + "References", "In-reply-to", + }; + + // is ref already in the list? + auto is_dup = [](auto&& seq, const std::string& ref) { + return seq_find_if(seq, [&](auto&& str) { return ref == str; }) + == seq.cend(); + }; + + std::vector refs; + for (auto&& ref_header: ref_headers) { + + auto hdr{header(ref_header)}; + if (!hdr) + continue; + + GMimeReferences *mime_refs{g_mime_references_parse({}, hdr->c_str())}; + refs.reserve(refs.size() + g_mime_references_length(mime_refs)); + + for (auto i = 0; i != g_mime_references_length(mime_refs); ++i) { + + if (auto&& msgid{g_mime_references_get_message_id(mime_refs, i)}; !msgid) + continue; // invalid + else if (is_dup(refs, msgid)) + continue; // skip dups + else + refs.emplace_back(msgid); + } + g_mime_references_free(mime_refs); + } + + return refs; +} + + +void +MimeMessage::for_each(const ForEachFunc& func) const noexcept +{ + struct CallbackData { const ForEachFunc& func; }; + CallbackData cbd{func}; + + g_mime_message_foreach( + self(), + [] (GMimeObject *parent, GMimeObject *part, gpointer user_data) { + auto cbd{reinterpret_cast(user_data)}; + cbd->func(MimeObject{parent}, MimeObject{part}); + }, &cbd); +} + + + +/* + * MimePart + */ +size_t +MimePart::size() const noexcept +{ + auto wrapper{g_mime_part_get_content(self())}; + if (!wrapper) { + g_warning("failed to get content wrapper"); + return 0; + } + + auto stream{g_mime_data_wrapper_get_stream(wrapper)}; + if (!stream) { + g_warning("failed to get stream"); + return 0; + } + + return static_cast(g_mime_stream_length(stream)); +} + +Option +MimePart::to_string() const noexcept +{ + GMimeDataWrapper *wrapper{g_mime_part_get_content(self())}; + if (!wrapper) { /* this happens with invalid mails */ + g_debug("failed to create data wrapper"); + return Nothing; + } + + GMimeStream *stream{g_mime_stream_mem_new()}; + if (!stream) { + g_warning("failed to create mem stream"); + return Nothing; + } + + + ssize_t buflen{g_mime_data_wrapper_write_to_stream(wrapper, stream)}; + if (buflen <= 0) { /* empty buffer, not an error */ + g_object_unref(stream); + return Nothing; + } + + std::string buffer; + buffer.resize(buflen + 1); + g_mime_stream_reset(stream); + + auto bytes{g_mime_stream_read(stream, buffer.data(), buflen)}; + g_object_unref(stream); + if (bytes < 0) + return Nothing; + + buffer.data()[bytes]='\0'; + buffer.resize(buflen); + + return buffer; +} + + +Result +MimePart::to_file(const std::string& path, bool overwrite) const noexcept +{ + GMimeDataWrapper *wrapper{g_mime_part_get_content(self())}; + if (!wrapper) /* this happens with invalid mails */ + return Err(Error::Code::File, "failed to create data wrapper"); + + + GError *err{}; + GMimeStream *stream{g_mime_stream_fs_open( + path.c_str(), + O_WRONLY | O_CREAT | O_TRUNC |(overwrite ? 0 : O_EXCL), + S_IRUSR|S_IWUSR, + &err)}; + if (!stream) + return Err(Error::Code::File, &err, + "failed to open '%s'", path.c_str()); + + ssize_t written{g_mime_data_wrapper_write_to_stream(wrapper, stream)}; + g_object_unref(stream); + if (written < 0) { + return Err(Error::Code::File, &err, + "failed to write to '%s'", path.c_str()); + } + + return Ok(static_cast(written)); +} diff --git a/lib/message/mu-mime-object.hh b/lib/message/mu-mime-object.hh new file mode 100644 index 00000000..44990b0f --- /dev/null +++ b/lib/message/mu-mime-object.hh @@ -0,0 +1,665 @@ +/* +** 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. +** +*/ + +#ifndef MU_MIME_OBJECT_HH__ +#define MU_MIME_OBJECT_HH__ + +#include +#include +#include +#include +#include "gmime/gmime-application-pkcs7-mime.h" +#include "utils/mu-option.hh" +#include "utils/mu-result.hh" +#include "mu-contact.hh" + +namespace Mu { + +/** + * Initialize gmime (idempotent) + * + */ +void init_gmime(void); + +class Object { +public: + /** + * Default CTOR + * + */ + Object() noexcept: self_{} {} + + /** + * Create an object from a GObject + * + * @param obj a gobject. A ref is added. + */ + Object(GObject* &&obj): self_{g_object_ref(obj)} { + if (!G_IS_OBJECT(obj)) + throw std::runtime_error("not a g-object"); + } + + /** + * Copy CTOR + * + * @param other some other Object + */ + Object(const Object& other) noexcept { *this = other; } + + /** + * Move CTOR + * + * @param other some other Object + */ + Object(Object&& other) noexcept { *this = std::move(other); } + + /** + * operator= + * + * @param other copy some other object + * + * @return *this + */ + Object& operator=(const Object& other) noexcept { + + if (this != &other) { + auto oldself = self_; + self_ = other.self_ ? g_object_ref(other.self_) : nullptr; + if (oldself) + g_object_unref(oldself); + } + return *this; + } + + /** + * operator= + * + * @param other move some object object + * + * @return + */ + Object& operator=(Object&& other) noexcept { + + if (this != &other) { + auto oldself = self_; + self_ = other.self_; + other.self_ = nullptr; + if (oldself) + g_object_unref(oldself); + } + return *this; + } + + /** + * DTOR + */ + virtual ~Object() { + if (self_) { + g_object_unref(self_); + } + } + + /** + * operator bool + * + * @return true if object wraps a GObject, false otherwise + */ + operator bool() const noexcept { return !!self_; } + +protected: + GObject* object() const { return self(); } + + static Option maybe_string(const char *str) noexcept { + if (!str) + return Nothing; + else + return std::string(str); + } + +private: + GObject *self() const { return self_; } + mutable GObject *self_{}; +}; + + +struct MimeContentType: public Object { + + MimeContentType(GMimeContentType *ctype) : Object{G_OBJECT(ctype)} { + if (!GMIME_IS_CONTENT_TYPE(self())) + throw std::runtime_error("not a content-type"); + } + std::string media_type() const { + return g_mime_content_type_get_media_type(self()); + } + std::string media_subtype() const { + return g_mime_content_type_get_media_subtype(self()); + } + bool is_type(const std::string& type, const std::string& subtype) const { + return g_mime_content_type_is_type(self(), type.c_str(), + subtype.c_str()); + } +private: + GMimeContentType* self() const { + return reinterpret_cast(object()); + } +}; + + +/** + * Thin wrapper around a GMimeObject + * + */ +class MimeObject: public Object { +public: + /** + * Construct a new MimeObject. Take a ref on the obj + * + * @param mime_part mime-part pointer + */ + MimeObject(const Object& obj): Object{obj} { + if (!GMIME_IS_OBJECT(self())) + throw std::runtime_error("not a mime-object"); + } + MimeObject(GMimeObject *mobj): Object{G_OBJECT(mobj)} { + if (!GMIME_IS_OBJECT(self())) + throw std::runtime_error("not a mime-object"); + } + + /** + * Get a header from the MimeObject + * + * @param header the header to retrieve + * + * @return header value (UTF-8) or Nothing + */ + Option header(const std::string& header) const noexcept; + + /** + * Get the content type + * + * @return the content-type or Nothing + */ + Option content_type() const noexcept { + auto ct{g_mime_object_get_content_type(self())}; + if (!ct) + return Nothing; + else + return MimeContentType(ct); + } + + + /** + * Write the object to a string. + * + * @return + */ + Option object_to_string() const noexcept; + + /* + * subtypes. + */ + + /** + * Is this a MimePart? + * + * @return true or false + */ + bool is_part() const { return GMIME_IS_PART(self()); } + + /** + * Is this a MimeMultiPart? + * + * @return true or false + */ + bool is_multipart() const { return GMIME_IS_MULTIPART(self());} + + /** + * Is this a MimeMultiPart? + * + * @return true or false + */ + bool is_multipart_encrypted() const { + return GMIME_IS_MULTIPART_ENCRYPTED(self()); + } + + /** + * Is this a MimeMultiPart? + * + * @return true or false + */ + bool is_multipart_signed() const { + return GMIME_IS_MULTIPART_SIGNED(self()); + } + + /** + * Is this a MimeMessage? + * + * @return true or false + */ + bool is_message() const { return GMIME_IS_MESSAGE(self());} + + /** + * Is this a MimeMessagePart? + * + * @return true orf alse + */ + bool is_message_part() const { return GMIME_IS_MESSAGE_PART(self());} + + /** + * Is this a MimeApplicationpkcs7Mime? + * + * @return true orf alse + */ + bool is_mime_application_pkcs7_mime() const { + return GMIME_IS_APPLICATION_PKCS7_MIME(self()); + } + +private: + GMimeObject* self() const { + return reinterpret_cast(object()); + } +}; + + +/** + * Thin wrapper around a GMimeMessage + * + */ +class MimeMessage: public MimeObject { +public: + /** + * Construct a MimeMessage + * + * @param obj an Object of the right type + */ + MimeMessage(const Object& obj): MimeObject(obj) { + if (!is_message()) + throw std::runtime_error("not a mime-message"); + } + + /** + * Make a MimeMessage from a file + * + * @param path path to the file + * + * @return a MimeMessage or an error. + */ + static Result make_from_file (const std::string& path); + + /** + * Make a MimeMessage from a string + * + * @param path path to the file + * + * @return a MimeMessage or an error. + */ + static Result make_from_string (const std::string& text); + + + /** + * Address types + * + */ + enum struct AddressType { + Sender = GMIME_ADDRESS_TYPE_SENDER, + From = GMIME_ADDRESS_TYPE_FROM, + ReplyTo = GMIME_ADDRESS_TYPE_REPLY_TO, + To = GMIME_ADDRESS_TYPE_TO, + Cc = GMIME_ADDRESS_TYPE_CC, + Bcc = GMIME_ADDRESS_TYPE_BCC + }; + + Contacts addresses(AddressType atype) const noexcept; + + /** + * Gets the message-id if it exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option message_id() const noexcept { + return maybe_string(g_mime_message_get_message_id(self())); + } + + + /** + * Gets the message-id if it exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option subject() const noexcept { + return maybe_string(g_mime_message_get_subject(self())); + } + + /** + * Gets the date if it exists, or nullopt otherwise. + * + * @return a time_t value (expressed as a 64-bit number) or nullopt + */ + Option date() const noexcept; + + + /** + * Get the references for this message (including in-reply-to), in the + * order of older..newer; in-reply-to would be the last one. + * + * @return references. + */ + std::vector references() const noexcept; + + + /** + * Callback for for_each(). See GMimeObjectForEachFunc. + * + */ + using ForEachFunc = std::function; + + /** + * Recursively apply func tol all parts of this message + * + * @param func a function + */ + void for_each(const ForEachFunc& func) const noexcept; + +private: + GMimeMessage* self() const { + return reinterpret_cast(object()); + } +}; + +/** + * Thin wrapper around a GMimePart. + * + */ +class MimePart: public MimeObject { +public: + /** + * Construct a MimePart + * + * @param obj an Object of the right type + */ + MimePart(const Object& obj): MimeObject(obj) { + if (!is_part()) + throw std::runtime_error("not a mime-part"); + } + + /** + * Determines whether or not the part is an attachment based on the + * value of the Content-Disposition header. + * + * @return true or false + */ + bool is_attachment() const noexcept { + return g_mime_part_is_attachment(self()); + } + + /** + * Gets the value of the Content-Description for this mime part + * if it exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option content_description() const noexcept { + return maybe_string(g_mime_part_get_content_description(self())); + } + + /** + * Gets the value of the Content-Id for this mime part + * if it exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option content_id() const noexcept { + return maybe_string(g_mime_part_get_content_id(self())); + } + + /** + * Gets the value of the Content-Md5 header for this mime part + * if it exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option content_md5() const noexcept { + return maybe_string(g_mime_part_get_content_md5(self())); + + } + + /** + * Verify the content md5 for the specified mime part. Returns false if + * the mime part does not contain a Content-MD5. + * + * @return true or false + */ + bool verify_content_md5() const noexcept { + return g_mime_part_verify_content_md5(self()); + } + + /** + * Gets the value of the Content-Location for this mime part if it + * exists, or nullopt otherwise. + * + * @return string or nullopt + */ + Option content_location() const noexcept { + return maybe_string(g_mime_part_get_content_location(self())); + } + + /** + * Gets the filename for this mime part if it exists, or nullopt + * otherwise. + * + * @return string or nullopt + */ + Option filename() const noexcept { + return maybe_string(g_mime_part_get_filename(self())); + } + + /** + * Size of content, in bytes + * + * @return size + */ + size_t size() const noexcept; + + /** + * Get as UTF-8 string + * + * @return a string, or NULL. + */ + Option to_string() const noexcept; + + + /** + * Write part to a file + * + * @param path path to file + * @param overwrite if true, overwrite existing file, if it bqexists + * + * @return size of the wrtten file, or an error. + */ + Result to_file(const std::string& path, bool overwrite) + const noexcept; + + + /** + * Types of Content Encoding. + * + */ + enum struct ContentEncoding { + Default = GMIME_CONTENT_ENCODING_DEFAULT, + SevenBit = GMIME_CONTENT_ENCODING_7BIT, + EightBit = GMIME_CONTENT_ENCODING_8BIT, + Binary = GMIME_CONTENT_ENCODING_BINARY, + Base64 = GMIME_CONTENT_ENCODING_BASE64, + QuotedPrintable = GMIME_CONTENT_ENCODING_QUOTEDPRINTABLE, + UuEncode = GMIME_CONTENT_ENCODING_UUENCODE + }; + + /** + * Gets the content encoding of the mime part. + * + * @return the content encoding + */ + ContentEncoding content_encoding() const noexcept { + const auto enc{g_mime_part_get_content_encoding(self())}; + g_return_val_if_fail(enc <= GMIME_CONTENT_ENCODING_UUENCODE, + ContentEncoding::Default); + return static_cast(enc); + } + + + /** + * Types of OpenPGP data + * + */ + enum struct OpenPGPData { + None = GMIME_OPENPGP_DATA_NONE, + Encrypted = GMIME_OPENPGP_DATA_ENCRYPTED, + Signed = GMIME_OPENPGP_DATA_SIGNED, + PublicKey = GMIME_OPENPGP_DATA_PUBLIC_KEY, + PrivateKey = GMIME_OPENPGP_DATA_PRIVATE_KEY, + }; + + /** + * Gets whether or not (and what type) of OpenPGP data is contained + * + * @return OpenGPGData + */ + OpenPGPData openpgp_data() const noexcept { + const auto data{g_mime_part_get_openpgp_data(self())}; + g_return_val_if_fail(data <= GMIME_OPENPGP_DATA_PRIVATE_KEY, + OpenPGPData::None); + return static_cast(data); + } + +private: + GMimePart* self() const { + return reinterpret_cast(object()); + } +}; + + + /** + * Thin wrapper around a GMimeApplicationPkcs7Mime + * + */ +class MimeApplicationPkcs7Mime: public MimePart { +public: + /** + * Construct a MimeApplicationPkcs7Mime + * + * @param obj an Object of the right type + */ + MimeApplicationPkcs7Mime(const Object& obj): MimePart(obj) { + if (!is_mime_application_pkcs7_mime()) + throw std::runtime_error("not a mime-application-pkcs7-mime"); + } + + enum struct SecureMimeType { + CompressedData = GMIME_SECURE_MIME_TYPE_COMPRESSED_DATA, + EnvelopedData = GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA, + SignedData = GMIME_SECURE_MIME_TYPE_SIGNED_DATA, + CertsOnly = GMIME_SECURE_MIME_TYPE_CERTS_ONLY, + Unknown = GMIME_SECURE_MIME_TYPE_UNKNOWN + }; + + SecureMimeType smime_type() const { + return static_cast( + g_mime_application_pkcs7_mime_get_smime_type(self())); + } + +private: + GMimeApplicationPkcs7Mime* self() const { + return reinterpret_cast(object()); + } +}; + + +/** + * Thin wrapper around a GMimeMultiPart + * + */ +class MimeMultipart: public MimeObject { +public: + /** + * Construct a MimeMultipart + * + * @param obj an Object of the right type + */ + MimeMultipart(const Object& obj): MimeObject(obj) { + if (!is_multipart()) + throw std::runtime_error("not a mime-multipart"); + } + +private: + GMimeMultipart* self() const { + return reinterpret_cast(object()); + } +}; + + +/** + * Thin wrapper around a GMimeMultiPartEncrypted + * + */ +class MimeMultipartEncrypted: public MimeMultipart { +public: + /** + * Construct a MimeMultipartEncrypted + * + * @param obj an Object of the right type + */ + MimeMultipartEncrypted(const Object& obj): MimeMultipart(obj) { + if (!is_multipart_encrypted()) + throw std::runtime_error("not a mime-multipart-encrypted"); + } + +private: + GMimeMultipartEncrypted* self() const { + return reinterpret_cast(object()); + } +}; + + +/** + * Thin wrapper around a GMimeMultiPartSigned + * + */ +class MimeMultipartSigned: public MimeMultipart { +public: + /** + * Construct a MimeMultipartSigned + * + * @param obj an Object of the right type + */ + MimeMultipartSigned(const Object& obj): MimeMultipart(obj) { + if (!is_multipart_signed()) + throw std::runtime_error("not a mime-multipart-signed"); + } + +private: + GMimeMultipartSigned* self() const { + return reinterpret_cast(object()); + } +}; + +} // namespace Mu + + +#endif /* MU_MIME_OBJECT_HH__ */