From 4c4fb1759f2991f11d8887e35ac5b70e87ef9b5c Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Sat, 19 Mar 2022 18:56:10 +0200 Subject: [PATCH] message: move to lib/message, update naming Basically, move/rename lib/mu-message* to lib/mu-*. Add the beginnings of a Message class. --- guile/mu-guile-message.cc | 23 +- lib/meson.build | 12 +- lib/message/meson.build | 85 ++++ .../mu-contact.cc} | 51 ++- .../mu-contact.hh} | 48 +- .../mu-document.cc} | 128 +++--- .../mu-document.hh} | 92 ++-- .../mu-fields.cc} | 48 +- .../mu-fields.hh} | 268 +++++------ lib/message/mu-flags.cc | 170 +++++++ .../mu-flags.hh} | 80 ++-- lib/message/mu-message.cc | 425 ++++++++++++++++++ lib/message/mu-message.hh | 244 ++++++++++ .../mu-priority.cc} | 32 +- .../mu-priority.hh} | 44 +- lib/mu-message-flags.cc | 170 ------- lib/mu-message.hh | 40 -- lib/tests/meson.build | 28 +- 18 files changed, 1363 insertions(+), 625 deletions(-) create mode 100644 lib/message/meson.build rename lib/{mu-message-contact.cc => message/mu-contact.cc} (78%) rename lib/{mu-message-contact.hh => message/mu-contact.hh} (76%) rename lib/{mu-message-document.cc => message/mu-document.cc} (61%) rename lib/{mu-message-document.hh => message/mu-document.hh} (61%) rename lib/{mu-message-fields.cc => message/mu-fields.cc} (66%) rename lib/{mu-message-fields.hh => message/mu-fields.hh} (67%) create mode 100644 lib/message/mu-flags.cc rename lib/{mu-message-flags.hh => message/mu-flags.hh} (73%) create mode 100644 lib/message/mu-message.cc create mode 100644 lib/message/mu-message.hh rename lib/{mu-message-priority.cc => message/mu-priority.cc} (62%) rename lib/{mu-message-priority.hh => message/mu-priority.hh} (68%) delete mode 100644 lib/mu-message-flags.cc delete mode 100644 lib/mu-message.hh diff --git a/guile/mu-guile-message.cc b/guile/mu-guile-message.cc index 25fd12dc..a80f4d50 100644 --- a/guile/mu-guile-message.cc +++ b/guile/mu-guile-message.cc @@ -18,7 +18,7 @@ */ #include "mu-guile-message.hh" #include "libguile/scm.h" -#include "mu-message-flags.hh" +#include "mu-message.hh" #include #include @@ -213,12 +213,12 @@ SCM_DEFINE(get_field, #undef FUNC_NAME static SCM -contacts_to_list(MuMsg *msg, Mu::MessageContact::Type mtype) +contacts_to_list(MuMsg *msg, std::optional field_id) { SCM list{SCM_EOL}; - const auto contacts{mu_msg_get_contacts(msg, mtype)}; - for (auto&& contact: mu_msg_get_contacts(msg, mtype)) { + const auto contacts{mu_msg_get_contacts(msg, field_id)}; + for (auto&& contact: mu_msg_get_contacts(msg, field_id)) { SCM item{scm_list_1( scm_cons(mu_guile_scm_from_str(contact.name.c_str()), mu_guile_scm_from_str(contact.email.c_str())))}; @@ -239,7 +239,7 @@ SCM_DEFINE(get_contacts, { MuMsgWrapper* msgwrap; SCM list; - Mu::MessageContact::Type mtype; + MU_GUILE_INITIALIZED_OR_ERROR; @@ -252,17 +252,18 @@ SCM_DEFINE(get_contacts, if (CONTACT_TYPE == SCM_BOOL_F) return SCM_UNSPECIFIED; /* nothing to do */ + std::optional field_id; if (CONTACT_TYPE == SCM_BOOL_T) - mtype = Mu::MessageContact::Type::Unknown; /* get all */ + field_id = {}; /* get all */ else { if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_TO)) - mtype = Mu::MessageContact::Type::To; + field_id = Field::Id::To; else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_CC)) - mtype = Mu::MessageContact::Type::Cc; + field_id = Field::Id::Cc; else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_BCC)) - mtype = Mu::MessageContact::Type::Bcc; + field_id = Field::Id::Bcc; else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_FROM)) - mtype = Mu::MessageContact::Type::From; + field_id = Field::Id::From; else { mu_guile_error(FUNC_NAME, 0, "invalid contact type", SCM_UNDEFINED); return SCM_UNSPECIFIED; @@ -273,7 +274,7 @@ SCM_DEFINE(get_contacts, #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-function-type" - list = contacts_to_list(msgwrap->_msg, mtype); + list = contacts_to_list(msgwrap->_msg, field_id); #pragma GCC diagnostic pop /* explicitly close the file backend, so we won't run out of fds */ diff --git a/lib/meson.build b/lib/meson.build index 133da156..930dc0b8 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -16,6 +16,7 @@ subdir('utils') +subdir('message') subdir('index') lib_mu=static_library( @@ -61,16 +62,6 @@ lib_mu=static_library( 'mu-msg-sexp.cc', 'mu-msg.cc', 'mu-msg.hh', - 'mu-message-contact.hh', - 'mu-message-contact.cc', - 'mu-message-document.cc', - 'mu-message-document.hh', - 'mu-message-fields.hh', - 'mu-message-fields.cc', - 'mu-message-flags.hh', - 'mu-message-flags.cc', - 'mu-message-priority.hh', - 'mu-message-priority.cc' ], dependencies: [ glib_dep, @@ -80,6 +71,7 @@ lib_mu=static_library( guile_dep, config_h_dep, lib_mu_utils_dep, + lib_mu_message_dep, lib_mu_index_dep ], install: false) diff --git a/lib/message/meson.build b/lib/message/meson.build new file mode 100644 index 00000000..ff36e8f4 --- /dev/null +++ b/lib/message/meson.build @@ -0,0 +1,85 @@ +## Copyright (C) 2021 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 of the License, 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. + + +lib_mu_message=static_library( + 'mu-message', + [ + 'mu-message.cc', + 'mu-message.hh', + 'mu-contact.hh', + 'mu-contact.cc', + 'mu-document.cc', + 'mu-document.hh', + 'mu-fields.hh', + 'mu-fields.cc', + 'mu-flags.hh', + 'mu-flags.cc', + 'mu-priority.hh', + 'mu-priority.cc' + ], + dependencies: [ + glib_dep, + gmime_dep, + xapian_dep, + config_h_dep, + 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, + include_directories: + include_directories(['.', '..', xapian_incs])) + +# +# tests +# + +test('test-contact', + executable('test-contact', + 'mu-contact.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-document', + executable('test-document', + 'mu-document.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-fields', + executable('test-fields', + 'mu-fields.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) + +test('test-flags', + executable('test-flags', + 'mu-flags.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', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_message_dep])) diff --git a/lib/mu-message-contact.cc b/lib/message/mu-contact.cc similarity index 78% rename from lib/mu-message-contact.cc rename to lib/message/mu-contact.cc index c7c50e93..ee6df9dc 100644 --- a/lib/mu-message-contact.cc +++ b/lib/message/mu-contact.cc @@ -17,17 +17,16 @@ ** */ -#include "mu-message-contact.hh" +#include "mu-contact.hh" #include "mu-message.hh" #include #include using namespace Mu; -using namespace Mu::Message; std::string -MessageContact::display_name() const +Contact::display_name() const { if (name.empty()) return email; @@ -35,11 +34,11 @@ MessageContact::display_name() const return name + " <" + email + '>'; } -Mu::MessageContacts -Mu::make_message_contacts(InternetAddressList* addr_lst, - Field::Id field_id, ::time_t message_date) +Mu::Contacts +Mu::make_contacts(InternetAddressList* addr_lst, + Field::Id field_id, ::time_t message_date) { - MessageContacts contacts; + Contacts contacts; size_t num{}; g_return_val_if_fail(addr_lst, contacts); @@ -59,7 +58,7 @@ Mu::make_message_contacts(InternetAddressList* addr_lst, if (G_UNLIKELY(!email)) continue; - contacts.push_back(MessageContact{email, name ? name : "", + contacts.push_back(Contact{email, name ? name : "", field_id, message_date}); ++num; } @@ -68,10 +67,10 @@ Mu::make_message_contacts(InternetAddressList* addr_lst, } -Mu::MessageContacts -Mu::make_message_contacts(const std::string& addrs, - Field::Id field_id, - ::time_t message_date) +Mu::Contacts +Mu::make_contacts(const std::string& addrs, + Field::Id field_id, + ::time_t message_date) { auto addr_list = internet_address_list_parse(NULL, addrs.c_str()); if (!addr_list) { @@ -79,7 +78,7 @@ Mu::make_message_contacts(const std::string& addrs, return {}; } - auto contacts{make_message_contacts(addr_list, field_id, message_date)}; + auto contacts{make_contacts(addr_list, field_id, message_date)}; g_object_unref(addr_list); return contacts; @@ -107,7 +106,7 @@ Mu::lowercase_hash(const std::string& s) static void test_ctor_foo() { - MessageContact c{ + Contact c{ "foo@example.com", "Foo Bar", Field::Id::Bcc, @@ -126,7 +125,7 @@ test_ctor_foo() static void test_ctor_blinky() { - MessageContact c{ + Contact c{ "bar@example.com", "Blinky", 1645215014, @@ -148,7 +147,7 @@ test_ctor_blinky() static void test_ctor_cleanup() { - MessageContact c{ + Contact c{ "bar@example.com", "Bli\nky", 1645215014, @@ -179,12 +178,12 @@ test_make_contacts() internet_address_list_parse(NULL, str)}; g_assert_true(lst); - const auto addrs{make_message_contacts(lst, Field::Id::Cc, 54321 )}; + const auto addrs{make_contacts(lst, Field::Id::Cc, 54321 )}; g_object_unref(lst); g_assert_cmpuint(addrs.size(),==,3); - const auto addrs2{make_message_contacts(str, Field::Id::To, 12345 )}; + const auto addrs2{make_contacts(str, Field::Id::To, 12345 )}; g_assert_cmpuint(addrs2.size(),==,3); assert_equal(addrs2[0].name, "Abc"); @@ -202,7 +201,7 @@ test_make_contacts_2() "De\nf , " "\tGhi "; - const auto addrs2{make_message_contacts(str, Field::Id::Bcc, 12345 )}; + const auto addrs2{make_contacts(str, Field::Id::Bcc, 12345 )}; g_assert_cmpuint(addrs2.size(),==,3); assert_equal(addrs2[0].name, "Äbc"); @@ -222,7 +221,7 @@ test_make_contacts_err() InternetAddressList *lst{ internet_address_list_parse(NULL, "")}; g_assert_false(lst); - const auto addrs{make_message_contacts("", Field::Id::To, 77777)}; + const auto addrs{make_contacts("", Field::Id::To, 77777)}; g_assert_true(addrs.empty()); } @@ -232,12 +231,12 @@ main(int argc, char* argv[]) g_test_init(&argc, &argv, NULL); g_mime_init(); - g_test_add_func("/message-contacts/ctor-foo", test_ctor_foo); - g_test_add_func("/message-contacts/ctor-blinky", test_ctor_blinky); - g_test_add_func("/message-contacts/ctor-cleanup", test_ctor_cleanup); - g_test_add_func("/message-contacts/make-contacts", test_make_contacts); - g_test_add_func("/message-contacts/make-contacts-2", test_make_contacts_2); - g_test_add_func("/message-contacts/make-contacts-err", test_make_contacts_err); + g_test_add_func("/message/contact/ctor-foo", test_ctor_foo); + g_test_add_func("/message/contact/ctor-blinky", test_ctor_blinky); + g_test_add_func("/message/contact/ctor-cleanup", test_ctor_cleanup); + g_test_add_func("/message/contact/make-contacts", test_make_contacts); + g_test_add_func("/message/contact/make-contacts-2", test_make_contacts_2); + g_test_add_func("/message/contact/make-contacts-err", test_make_contacts_err); return g_test_run(); } diff --git a/lib/mu-message-contact.hh b/lib/message/mu-contact.hh similarity index 76% rename from lib/mu-message-contact.hh rename to lib/message/mu-contact.hh index a1742287..a5e2d597 100644 --- a/lib/mu-message-contact.hh +++ b/lib/message/mu-contact.hh @@ -29,7 +29,7 @@ #include #include -#include "mu-message-fields.hh" +#include "mu-fields.hh" struct _InternetAddressList; @@ -44,24 +44,24 @@ namespace Mu { */ size_t lowercase_hash(const std::string& s); -struct MessageContact { +struct Contact { /** - * Construct a new MessageContact + * Construct a new Contact * * @param email_ email address * @param name_ name or empty * @param field_id_ contact field id, or {} * @param message_date_ data for the message for this contact */ - MessageContact(const std::string& email_, const std::string& name_ = "", - std::optional field_id_ = {}, + Contact(const std::string& email_, const std::string& name_ = "", + std::optional field_id_ = {}, time_t message_date_ = 0) : email{email_}, name{name_}, field_id{field_id_}, message_date{message_date_}, personal{}, frequency{1}, tstamp{} { cleanup_name(); } /** - * Construct a new MessageContact + * Construct a new Contact * * @param email_ email address * @param name_ name or empty @@ -70,7 +70,7 @@ struct MessageContact { * @param freq_ how often was this contact seen? * @param tstamp_ timestamp for last change */ - MessageContact(const std::string& email_, const std::string& name_, + Contact(const std::string& email_, const std::string& name_, time_t message_date_, bool personal_, size_t freq_, int64_t tstamp_) : email{email_}, name{name_}, field_id{}, @@ -91,11 +91,11 @@ struct MessageContact { /** * Operator==; based on the hash values (ie. lowercase e-mail address) * - * @param rhs some other MessageContact + * @param rhs some other Contact * * @return true orf false. */ - bool operator== (const MessageContact& rhs) const noexcept { + bool operator== (const Contact& rhs) const noexcept { return hash() == rhs.hash(); } @@ -120,7 +120,7 @@ struct MessageContact { 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 */ + 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? */ @@ -135,43 +135,43 @@ private: } }; -using MessageContacts = std::vector; +using Contacts = std::vector; /** - * Create a sequence of MessageContact objects from an InternetAddressList + * Create a sequence of Contact objects from an InternetAddressList * * @param addr_lst an address list * @param field_id the field_id for message field for these addresses * @param message_date the date of the message from which the InternetAddressList * originates. * - * @return a sequence of MessageContact objects. + * @return a sequence of Contact objects. */ -MessageContacts -make_message_contacts(/*const*/ struct _InternetAddressList* addr_lst, - MessageField::Id field_id, ::time_t message_date); +Contacts +make_contacts(/*const*/ struct _InternetAddressList* addr_lst, + Field::Id field_id, ::time_t message_date); /** - * Create a sequence of MessageContact objects from an InternetAddressList + * Create a sequence of Contact objects from an InternetAddressList * * @param addrs a string with one more valid addresses (as per internet_address_list_parse()) * @param field_id the field_id for message field for these addresses * @param message_date the date of the message from which the addresses originate * - * @return a sequence of MessageContact objects. + * @return a sequence of Contact objects. */ -MessageContacts -make_message_contacts(const std::string& addrs, - MessageField::Id field_id, ::time_t message_date); +Contacts +make_contacts(const std::string& addrs, + Field::Id field_id, ::time_t message_date); } // namespace Mu /** * Implement our hash int std:: */ -template<> struct std::hash { - std::size_t operator()(const Mu::MessageContact& c) const noexcept { +template<> struct std::hash { + std::size_t operator()(const Mu::Contact& c) const noexcept { return c.hash(); } }; -#endif /* MU_MESSAGE_CONTACT_HH__ */ +#endif /* MU_CONTACT_HH__ */ diff --git a/lib/mu-message-document.cc b/lib/message/mu-document.cc similarity index 61% rename from lib/mu-message-document.cc rename to lib/message/mu-document.cc index b3df1394..494eddfe 100644 --- a/lib/mu-message-document.cc +++ b/lib/message/mu-document.cc @@ -17,7 +17,7 @@ ** */ -#include "mu-message-document.hh" +#include "mu-document.hh" #include "mu-message.hh" #include @@ -31,12 +31,10 @@ using namespace Mu; -using namespace Mu::Message; constexpr char SepaChar1 = 0xfe; constexpr char SepaChar2 = 0xff; - static void add_index_term(Xapian::Document& doc, const Field& field, const std::string& val) { @@ -58,39 +56,39 @@ maybe_add_term(Xapian::Document& doc, const Field& field, const std::string& val } void -MessageDocument::add(Field::Id id, const std::string& val) +Document::add(Field::Id id, const std::string& val) { - const auto field{message_field(id)}; + const auto field{field_from_id(id)}; if (field.is_value()) - doc_.add_value(field.value_no(), val); + xdoc_.add_value(field.value_no(), val); - maybe_add_term(doc_, field, val); + maybe_add_term(xdoc_, field, val); } void -MessageDocument::add(Field::Id id, const std::vector& vals) +Document::add(Field::Id id, const std::vector& vals) { - const auto field{message_field(id)}; + const auto field{field_from_id(id)}; if (field.is_value()) - doc_.add_value(field.value_no(), Mu::join(vals, SepaChar1)); + xdoc_.add_value(field.value_no(), Mu::join(vals, SepaChar1)); std::for_each(vals.begin(), vals.end(), - [&](const auto& val) { maybe_add_term(doc_, field, val); }); + [&](const auto& val) { maybe_add_term(xdoc_, field, val); }); } std::vector -MessageDocument::string_vec_value(MessageField::Id field_id) const noexcept +Document::string_vec_value(Field::Id field_id) const noexcept { return Mu::split(string_value(field_id), SepaChar1); } void -MessageDocument::add(Field::Id id, const Contacts& contacts) +Document::add(Field::Id id, const Contacts& contacts) { - const auto field{message_field(id)}; + const auto field{field_from_id(id)}; std::vector cvec; const std::string sepa2(1, SepaChar2); @@ -99,19 +97,19 @@ MessageDocument::add(Field::Id id, const Contacts& contacts) if (!contact.field_id || *contact.field_id != id) continue; - doc_.add_term(contact.email); + xdoc_.add_term(contact.email); if (!contact.name.empty()) - add_index_term(doc_, field, contact.name); + add_index_term(xdoc_, field, contact.name); cvec.emplace_back(contact.email + sepa2 + contact.name); } if (!cvec.empty()) - doc_.add_value(field.value_no(), join(cvec, SepaChar1)); + xdoc_.add_value(field.value_no(), join(cvec, SepaChar1)); } Contacts -MessageDocument::contacts_value(Field::Id id) const noexcept +Document::contacts_value(Field::Id id) const noexcept { const auto vals{string_vec_value(id)}; Contacts contacts; @@ -152,7 +150,7 @@ string_to_integer(const std::string& str) } void -MessageDocument::add(Field::Id id, int64_t val) +Document::add(Field::Id id, int64_t val) { /* * Xapian stores everything (incl. numbers) as strings. @@ -161,51 +159,51 @@ MessageDocument::add(Field::Id id, int64_t val) * length; such that the strings are sorted in the numerical order. */ - const auto field{message_field(id)}; + const auto field{field_from_id(id)}; if (field.is_value()) - doc_.add_value(field.value_no(), integer_to_string(val)); + xdoc_.add_value(field.value_no(), integer_to_string(val)); /* terms are not supported for numerical fields */ } int64_t -MessageDocument::integer_value(MessageField::Id field_id) const noexcept +Document::integer_value(Field::Id field_id) const noexcept { return string_to_integer(string_value(field_id)); } void -MessageDocument::add(Priority prio) +Document::add(Priority prio) { - constexpr auto field{message_field(Field::Id::Priority)}; + constexpr auto field{field_from_id(Field::Id::Priority)}; - doc_.add_value(field.value_no(), std::string(1, to_char(prio))); - doc_.add_boolean_term(field.xapian_term(to_char(prio))); + xdoc_.add_value(field.value_no(), std::string(1, to_char(prio))); + xdoc_.add_boolean_term(field.xapian_term(to_char(prio))); } Priority -MessageDocument::priority_value() const noexcept +Document::priority_value() const noexcept { const auto val{string_value(Field::Id::Priority)}; - return message_priority_from_char(val.empty() ? 'n' : val[0]); + return priority_from_char(val.empty() ? 'n' : val[0]); } void -MessageDocument::add(Flags flags) +Document::add(Flags flags) { - constexpr auto field{message_field(Field::Id::Flags)}; + constexpr auto field{field_from_id(Field::Id::Flags)}; - doc_.add_value(field.value_no(), integer_to_string(static_cast(flags))); - message_flag_infos_for_each([&](auto&& flag_info) { + xdoc_.add_value(field.value_no(), integer_to_string(static_cast(flags))); + flag_infos_for_each([&](auto&& flag_info) { if (any_of(flag_info.flag & flags)) - doc_.add_boolean_term(field.xapian_term(flag_info.shortcut_lower())); + xdoc_.add_boolean_term(field.xapian_term(flag_info.shortcut_lower())); }); } Flags -MessageDocument::flags_value() const noexcept +Document::flags_value() const noexcept { return static_cast(integer_value(Field::Id::Flags)); } @@ -227,37 +225,37 @@ MessageDocument::flags_value() const noexcept -static const MessageContacts test_contacts = {{ - MessageContact{"john@example.com", "John", Field::Id::Bcc}, - MessageContact{"ringo@example.com", "Ringo", Field::Id::Bcc}, - MessageContact{"paul@example.com", "Paul", Field::Id::Cc}, - MessageContact{"george@example.com", "George", Field::Id::Cc}, - MessageContact{"james@example.com", "James", Field::Id::From}, - MessageContact{"lars@example.com", "Lars", Field::Id::To}, - MessageContact{"kirk@example.com", "Kirk", Field::Id::To}, - MessageContact{"jason@example.com", "Jason", Field::Id::To} +static const Contacts test_contacts = {{ + Contact{"john@example.com", "John", Field::Id::Bcc}, + Contact{"ringo@example.com", "Ringo", Field::Id::Bcc}, + Contact{"paul@example.com", "Paul", Field::Id::Cc}, + Contact{"george@example.com", "George", Field::Id::Cc}, + Contact{"james@example.com", "James", Field::Id::From}, + Contact{"lars@example.com", "Lars", Field::Id::To}, + Contact{"kirk@example.com", "Kirk", Field::Id::To}, + Contact{"jason@example.com", "Jason", Field::Id::To} }}; static void test_bcc() { { - MessageDocument doc; + Document doc; doc.add(Field::Id::Bcc, test_contacts); - MessageContacts expected_contacts = {{ - MessageContact{"john@example.com", "John", Field::Id::Bcc}, - MessageContact{"ringo@example.com", "Ringo", Field::Id::Bcc}, + Contacts expected_contacts = {{ + Contact{"john@example.com", "John", Field::Id::Bcc}, + Contact{"ringo@example.com", "Ringo", Field::Id::Bcc}, }}; const auto actual_contacts = doc.contacts_value(Field::Id::Bcc); assert_same_contacts(expected_contacts, actual_contacts); } { - MessageDocument doc; - MessageContacts contacts = {{ - MessageContact{"john@example.com", "John Lennon", Field::Id::Bcc}, - MessageContact{"ringo@example.com", "Ringo", Field::Id::Bcc}, + Document doc; + Contacts contacts = {{ + Contact{"john@example.com", "John Lennon", Field::Id::Bcc}, + Contact{"ringo@example.com", "Ringo", Field::Id::Bcc}, }}; doc.add(Field::Id::Bcc, contacts); auto db = Xapian::InMemory::open(); @@ -271,12 +269,12 @@ test_bcc() static void test_cc() { - MessageDocument doc; + Document doc; doc.add(Field::Id::Cc, test_contacts); - MessageContacts expected_contacts = {{ - MessageContact{"paul@example.com", "Paul", Field::Id::Cc}, - MessageContact{"george@example.com", "George", Field::Id::Cc} + Contacts expected_contacts = {{ + Contact{"paul@example.com", "Paul", Field::Id::Cc}, + Contact{"george@example.com", "George", Field::Id::Cc} }}; const auto actual_contacts = doc.contacts_value(Field::Id::Cc); @@ -287,11 +285,11 @@ test_cc() static void test_from() { - MessageDocument doc; + Document doc; doc.add(Field::Id::From, test_contacts); - MessageContacts expected_contacts = {{ - MessageContact{"james@example.com", "James", Field::Id::From}, + Contacts expected_contacts = {{ + Contact{"james@example.com", "James", Field::Id::From}, }}; const auto actual_contacts = doc.contacts_value(Field::Id::From); @@ -301,13 +299,13 @@ test_from() static void test_to() { - MessageDocument doc; + Document doc; doc.add(Field::Id::To, test_contacts); - MessageContacts expected_contacts = {{ - MessageContact{"lars@example.com", "Lars", Field::Id::To}, - MessageContact{"kirk@example.com", "Kirk", Field::Id::To}, - MessageContact{"jason@example.com", "Jason", Field::Id::To} + Contacts expected_contacts = {{ + Contact{"lars@example.com", "Lars", Field::Id::To}, + Contact{"kirk@example.com", "Kirk", Field::Id::To}, + Contact{"jason@example.com", "Jason", Field::Id::To} }}; const auto actual_contacts = doc.contacts_value(Field::Id::To); @@ -319,13 +317,13 @@ static void test_size() { { - MessageDocument doc; + Document doc; doc.add(Field::Id::Size, 12345); g_assert_cmpuint(doc.integer_value(Field::Id::Size),==,12345); } { - MessageDocument doc; + Document doc; g_assert_cmpuint(doc.integer_value(Field::Id::Size),==,0); } } diff --git a/lib/mu-message-document.hh b/lib/message/mu-document.hh similarity index 61% rename from lib/mu-message-document.hh rename to lib/message/mu-document.hh index 88aa2fdf..5ffbab49 100644 --- a/lib/mu-message-document.hh +++ b/lib/message/mu-document.hh @@ -1,5 +1,4 @@ -/* -** 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 @@ -17,8 +16,8 @@ ** */ -#ifndef MU_MESSAGE_DOCUMENT_HH__ -#define MU_MESSAGE_DOCUMENT_HH__ +#ifndef MU_DOCUMENT_HH__ +#define MU_DOCUMENT_HH__ #include #include @@ -26,49 +25,73 @@ #include #include "utils/mu-xapian-utils.hh" -#include "mu-message-fields.hh" -#include "mu-message-priority.hh" -#include "mu-message-flags.hh" -#include "mu-message-contact.hh" +#include "mu-fields.hh" +#include "mu-priority.hh" +#include "mu-flags.hh" +#include "mu-contact.hh" namespace Mu { /** - * A MessageDocument describes the information about a message that is + * A Document describes the information about a message that is * or can be stored in the database. * */ -class MessageDocument { +class Document { public: /** * Construct a message for a new Xapian Document * */ - MessageDocument() {} + Document() {} /** * Construct a message document based on on existing Xapian document. * * @param doc */ - MessageDocument(const Xapian::Document& doc): doc_{doc} {} + Document(const Xapian::Document& doc): xdoc_{doc} {} /** * Copy CTOR */ - MessageDocument(const MessageDocument&) = default; - + Document(const Document& rhs) { *this = rhs; } /** * Move CTOR * */ - MessageDocument(MessageDocument&&) = default; + Document(Document&& rhs) {*this = std::move(rhs); } /** * Get a reference to the underlying Xapian document. * */ - const Xapian::Document& xapian_document() const { return doc_; } + const Xapian::Document& xapian_document() const { return xdoc_; } + + /* Copy assignment operator + * + * @param rhs some message + * + * @return a message ref + */ + Document& operator=(const Document& rhs) { + if (this != &rhs) + xdoc_ = rhs.xdoc_; + return *this; + } + + /** + * Move assignment operator + * + * @param rhs some message + * + * @return a message ref + */ + Document& operator=(Document&& rhs) { + if (this != &rhs) + xdoc_ = std::move(rhs.xdoc_); + return *this; + } /* @@ -81,7 +104,7 @@ public: * @param field_id field id * @param val string value */ - void add(MessageField::Id field_id, const std::string& val); + void add(Field::Id field_id, const std::string& val); /** * Add a string-vec value to the document @@ -89,7 +112,7 @@ public: * @param field_id field id * @param val string-vec value */ - void add(MessageField::Id field_id, const std::vector& vals); + void add(Field::Id field_id, const std::vector& vals); /** @@ -98,7 +121,7 @@ public: * @param field_id field id * @param contacts message contacts */ - void add(MessageField::Id id, const MessageContacts& contacts); + void add(Field::Id id, const Contacts& contacts); /** * Add an integer value to the document @@ -106,14 +129,14 @@ public: * @param field_id field id * @param val integer value */ - void add(MessageField::Id field_id, int64_t val); + void add(Field::Id field_id, int64_t val); /** * Add a message priority to the document * * @param prio priority */ - void add(MessagePriority prio); + void add(Priority prio); /** @@ -121,7 +144,7 @@ public: * * @param flags mesage flags. */ - void add(MessageFlags flags); + void add(Flags flags); /* * Retrieving values @@ -135,9 +158,9 @@ public: * * @return a string (empty if not found) */ - std::string string_value(MessageField::Id field_id) const noexcept { + std::string string_value(Field::Id field_id) const noexcept { return xapian_try([&]{ - return doc_.get_value(message_field(field_id).value_no()); + return xdoc_.get_value(field_from_id(field_id).value_no()); }, std::string{}); } /** @@ -147,7 +170,7 @@ public: * * @return a string list */ - std::vector string_vec_value(MessageField::Id field_id) const noexcept; + std::vector string_vec_value(Field::Id field_id) const noexcept; /** @@ -157,7 +180,7 @@ public: * * @return an integer or 0 if not found. */ - int64_t integer_value(MessageField::Id field_id) const noexcept; + int64_t integer_value(Field::Id field_id) const noexcept; /** @@ -167,15 +190,14 @@ public: * * @return an integer or 0 if not found. */ - MessageContacts contacts_value(MessageField::Id id) const noexcept; - + Contacts contacts_value(Field::Id id) const noexcept; /** * Get the priority * * @return the message priority */ - MessagePriority priority_value() const noexcept; + Priority priority_value() const noexcept; /** * Get the message flags @@ -183,18 +205,12 @@ public: * * @return flags */ - MessageFlags flags_value() const noexcept; - + Flags flags_value() const noexcept; private: - Xapian::Document doc_; + Xapian::Document xdoc_; }; - - } // namepace Mu - - - -#endif /* MU_MESSAGE_DOCUMENT_HH__ */ +#endif /* MU_DOCUMENT_HH__ */ diff --git a/lib/mu-message-fields.cc b/lib/message/mu-fields.cc similarity index 66% rename from lib/mu-message-fields.cc rename to lib/message/mu-fields.cc index 5e1e5880..3d8e5bb9 100644 --- a/lib/mu-message-fields.cc +++ b/lib/message/mu-fields.cc @@ -17,24 +17,24 @@ ** */ -#include "mu-message-fields.hh" -#include "mu-message-flags.hh" +#include "mu-fields.hh" +#include "mu-flags.hh" using namespace Mu; std::string -MessageField::xapian_term(const std::string& s) const +Field::xapian_term(const std::string& s) const { return std::string(1U, xapian_prefix()) + s; } std::string -MessageField::xapian_term(std::string_view sv) const +Field::xapian_term(std::string_view sv) const { return std::string(1U, xapian_prefix()) + std::string{sv}; } std::string -MessageField::xapian_term(char c) const +Field::xapian_term(char c) const { return std::string(1U, xapian_prefix()) + c; } @@ -45,22 +45,22 @@ MessageField::xapian_term(char c) const * compile-time checks */ constexpr bool -validate_message_field_ids() +validate_field_ids() { - for (auto id = 0U; id != MessageField::id_size(); ++id) { - const auto field_id = static_cast(id); - if (message_field(field_id).id != field_id) + for (auto id = 0U; id != Field::id_size(); ++id) { + const auto field_id = static_cast(id); + if (field_from_id(field_id).id != field_id) return false; } return true; } constexpr bool -validate_message_field_shortcuts() +validate_field_shortcuts() { - for (auto id = 0U; id != MessageField::id_size(); ++id) { - const auto field_id = static_cast(id); - const auto shortcut = message_field(field_id).shortcut; + for (auto id = 0U; id != Field::id_size(); ++id) { + const auto field_id = static_cast(id); + const auto shortcut = field_from_id(field_id).shortcut; if (shortcut != 0 && (shortcut < 'a' || shortcut > 'z')) return false; @@ -70,9 +70,9 @@ validate_message_field_shortcuts() constexpr /*static*/ bool -validate_message_field_flags() +validate_field_flags() { - for (auto&& field: MessageFields) { + for (auto&& field: Fields) { /* - A field has at most one of Indexable, HasTerms, IsXapianBoolean and IsContact. */ size_t flagnum{}; @@ -108,29 +108,29 @@ validate_message_field_flags() static void test_ids() { - static_assert(validate_message_field_ids()); + static_assert(validate_field_ids()); } [[maybe_unused]] static void test_shortcuts() { - static_assert(validate_message_field_shortcuts()); + static_assert(validate_field_shortcuts()); } [[maybe_unused]] static void test_prefix() { - static_assert(message_field(MessageField::Id::Subject).xapian_prefix() == 'S'); - static_assert(message_field(MessageField::Id::BodyHtml).xapian_prefix() == 0); + static_assert(field_from_id(Field::Id::Subject).xapian_prefix() == 'S'); + static_assert(field_from_id(Field::Id::BodyHtml).xapian_prefix() == 0); } [[maybe_unused]] static void test_field_flags() { - static_assert(validate_message_field_flags()); + static_assert(validate_field_flags()); } #ifdef BUILD_TESTS @@ -141,11 +141,11 @@ test_xapian_term() using namespace std::string_literals; using namespace std::literals; - assert_equal(message_field(MessageField::Id::Subject).xapian_term(""s), "S"); - assert_equal(message_field(MessageField::Id::Subject).xapian_term("boo"s), "Sboo"); + assert_equal(field_from_id(Field::Id::Subject).xapian_term(""s), "S"); + assert_equal(field_from_id(Field::Id::Subject).xapian_term("boo"s), "Sboo"); - assert_equal(message_field(MessageField::Id::From).xapian_term('x'), "Fx"); - assert_equal(message_field(MessageField::Id::To).xapian_term("boo"sv), "Tboo"); + assert_equal(field_from_id(Field::Id::From).xapian_term('x'), "Fx"); + assert_equal(field_from_id(Field::Id::To).xapian_term("boo"sv), "Tboo"); } int diff --git a/lib/mu-message-fields.hh b/lib/message/mu-fields.hh similarity index 67% rename from lib/mu-message-fields.hh rename to lib/message/mu-fields.hh index cc7d1df5..add13e97 100644 --- a/lib/mu-message-fields.hh +++ b/lib/message/mu-fields.hh @@ -17,8 +17,8 @@ ** */ -#ifndef MU_MESSAGE_FIELDS_HH__ -#define MU_MESSAGE_FIELDS_HH__ +#ifndef MU_FIELDS_HH__ +#define MU_FIELDS_HH__ #include #include @@ -30,11 +30,11 @@ namespace Mu { -struct MessageField { +struct Field { /** * Field Ids. * - * Note, the Ids are also used as indices in the MessageFields array, + * Note, the Ids are also used as indices in the Fields array, * so their numerical values must be 0...Count. * */ @@ -55,6 +55,7 @@ struct MessageField { Path, /**< File-system Path */ Subject, /**< Message subject */ To, /**< To: recipient */ + Uid, /**< Unique id for message (based on path) */ /* * string list items... */ @@ -75,7 +76,7 @@ struct MessageField { /* * */ - _count_ /**< Number of MessageFieldIds */ + _count_ /**< Number of FieldIds */ }; /** @@ -165,6 +166,10 @@ struct MessageField { constexpr bool is_boolean_term() const { return any_of(Flag::BooleanTerm); } constexpr bool is_normal_term() const { return any_of(Flag::NormalTerm); } + constexpr bool is_searchable() const { return is_indexable_term() || + is_boolean_term() || + is_normal_term(); } + constexpr bool is_value() const { return any_of(Flag::Value); } constexpr bool is_contact() const { return any_of(Flag::Contact); } @@ -172,6 +177,7 @@ struct MessageField { constexpr bool do_not_cache() const { return any_of(Flag::DoNotCache); } + /** * Field members * @@ -199,254 +205,264 @@ struct MessageField { std::string xapian_term(char c) const; }; -MU_ENABLE_BITOPS(MessageField::Flag); +MU_ENABLE_BITOPS(Field::Flag); /** * Sequence of _all_ message fields */ -static constexpr std::array - MessageFields = { +static constexpr std::array + Fields = { { // Bcc { - MessageField::Id::Bcc, - MessageField::Type::String, + Field::Id::Bcc, + Field::Type::String, "bcc", "Blind carbon-copy recipient", "bcc:foo@example.com", 'h', - MessageField::Flag::GMime | - MessageField::Flag::Contact | - MessageField::Flag::Value + Field::Flag::GMime | + Field::Flag::Contact | + Field::Flag::Value }, // HTML Body { - MessageField::Id::BodyHtml, - MessageField::Type::String, + Field::Id::BodyHtml, + Field::Type::String, "body", "Message html body", {}, {}, - MessageField::Flag::GMime | - MessageField::Flag::DoNotCache + Field::Flag::GMime | + Field::Flag::DoNotCache }, // Body { - MessageField::Id::BodyText, - MessageField::Type::String, + Field::Id::BodyText, + Field::Type::String, "body", "Message plain-text body", "body:capybara", // example 'b', - MessageField::Flag::GMime | - MessageField::Flag::IndexableTerm | - MessageField::Flag::DoNotCache + Field::Flag::GMime | + Field::Flag::IndexableTerm | + Field::Flag::DoNotCache }, // Cc { - MessageField::Id::Cc, - MessageField::Type::String, + Field::Id::Cc, + Field::Type::String, "cc", "Carbon-copy recipient", "cc:quinn@example.com", 'c', - MessageField::Flag::GMime | - MessageField::Flag::Contact | - MessageField::Flag::Value}, + Field::Flag::GMime | + Field::Flag::Contact | + Field::Flag::Value}, // Embed { - MessageField::Id::EmbeddedText, - MessageField::Type::String, + Field::Id::EmbeddedText, + Field::Type::String, "embed", "Embedded text", "embed:war OR embed:peace", 'e', - MessageField::Flag::GMime | - MessageField::Flag::IndexableTerm | - MessageField::Flag::DoNotCache}, + Field::Flag::GMime | + Field::Flag::IndexableTerm | + Field::Flag::DoNotCache}, // File { - MessageField::Id::File, - MessageField::Type::String, + Field::Id::File, + Field::Type::String, "file", "Attachment file name", "file:/image\\.*.jpg/", 'j', - MessageField::Flag::GMime | - MessageField::Flag::NormalTerm | - MessageField::Flag::DoNotCache}, + Field::Flag::GMime | + Field::Flag::NormalTerm | + Field::Flag::DoNotCache}, // From { - MessageField::Id::From, - MessageField::Type::String, + Field::Id::From, + Field::Type::String, "from", "Message sender", "from:jimbo", 'f', - MessageField::Flag::GMime | - MessageField::Flag::Contact | - MessageField::Flag::Value}, + Field::Flag::GMime | + Field::Flag::Contact | + Field::Flag::Value}, // Maildir { - MessageField::Id::Maildir, - MessageField::Type::String, + Field::Id::Maildir, + Field::Type::String, "maildir", "Maildir path for message", "maildir:/private/archive", 'm', - MessageField::Flag::GMime | - MessageField::Flag::NormalTerm | - MessageField::Flag::Value}, + Field::Flag::GMime | + Field::Flag::NormalTerm | + Field::Flag::Value}, // MIME { - MessageField::Id::Mime, - MessageField::Type::String, + Field::Id::Mime, + Field::Type::String, "mime", "Attachment MIME-type", "mime:image/jpeg", 'y', - MessageField::Flag::NormalTerm}, + Field::Flag::NormalTerm}, // Message-ID { - MessageField::Id::MessageId, - MessageField::Type::String, + Field::Id::MessageId, + Field::Type::String, "msgid", "Attachment MIME-type", "mime:image/jpeg", 'i', - MessageField::Flag::GMime | - MessageField::Flag::NormalTerm | - MessageField::Flag::Value}, + Field::Flag::GMime | + Field::Flag::NormalTerm | + Field::Flag::Value}, // Path { - MessageField::Id::Path, - MessageField::Type::String, + Field::Id::Path, + Field::Type::String, "path", "File system path to message", {}, 'p', - MessageField::Flag::GMime | - MessageField::Flag::BooleanTerm | - MessageField::Flag::Value}, + Field::Flag::GMime | + Field::Flag::BooleanTerm | + Field::Flag::Value}, // Subject { - MessageField::Id::Subject, - MessageField::Type::String, + Field::Id::Subject, + Field::Type::String, "subject", "Message subject", "subject:wombat", 's', - MessageField::Flag::GMime | - MessageField::Flag::Value | - MessageField::Flag::IndexableTerm}, + Field::Flag::GMime | + Field::Flag::Value | + Field::Flag::IndexableTerm}, // To { - MessageField::Id::To, - MessageField::Type::String, + Field::Id::To, + Field::Type::String, "to", "Message recipient", "to:flimflam@example.com", 't', - MessageField::Flag::GMime | - MessageField::Flag::Contact | - MessageField::Flag::Value + Field::Flag::GMime | + Field::Flag::Contact | + Field::Flag::Value }, + // UID (internal) + { + Field::Id::Uid, + Field::Type::String, + "uid", + "Message recipient", + {}, + 'u', + Field::Flag::NormalTerm}, + // References { - MessageField::Id::References, - MessageField::Type::StringList, + Field::Id::References, + Field::Type::StringList, "refs", "Message references to other messages", {}, 'r', - MessageField::Flag::GMime | - MessageField::Flag::Value + Field::Flag::GMime | + Field::Flag::Value }, // Tags { - MessageField::Id::Tags, - MessageField::Type::StringList, + Field::Id::Tags, + Field::Type::StringList, "tag", "Message tags", "tag:projectx", 'x', - MessageField::Flag::GMime | - MessageField::Flag::NormalTerm | - MessageField::Flag::Value + Field::Flag::GMime | + Field::Flag::NormalTerm | + Field::Flag::Value }, // Date { - MessageField::Id::Date, - MessageField::Type::TimeT, + Field::Id::Date, + Field::Type::TimeT, "date", "Message date", "date:20220101..20220505", 'd', - MessageField::Flag::GMime | - MessageField::Flag::Value | - MessageField::Flag::Range + Field::Flag::GMime | + Field::Flag::Value | + Field::Flag::Range }, // Flags { - MessageField::Id::Flags, - MessageField::Type::Integer, + Field::Id::Flags, + Field::Type::Integer, "flag", "Message properties", "flag:unread", 'g', - MessageField::Flag::GMime | - MessageField::Flag::NormalTerm | - MessageField::Flag::Value + Field::Flag::GMime | + Field::Flag::NormalTerm | + Field::Flag::Value }, // Priority { - MessageField::Id::Priority, - MessageField::Type::Integer, + Field::Id::Priority, + Field::Type::Integer, "prio", "Priority", "prio:high", 'p', - MessageField::Flag::GMime | - MessageField::Flag::NormalTerm | - MessageField::Flag::Value + Field::Flag::GMime | + Field::Flag::NormalTerm | + Field::Flag::Value }, // Size { - MessageField::Id::Size, - MessageField::Type::ByteSize, + Field::Id::Size, + Field::Type::ByteSize, "size", "Message size in bytes", "size:1M..5M", 'z', - MessageField::Flag::GMime | - MessageField::Flag::Value | - MessageField::Flag::Range + Field::Flag::GMime | + Field::Flag::Value | + Field::Flag::Range }, // Mailing List { - MessageField::Id::MailingList, - MessageField::Type::String, + Field::Id::MailingList, + Field::Type::String, "list", "Mailing list (List-Id:)", "list:mu-discuss.googlegroups.com", 'v', - MessageField::Flag::GMime | - MessageField::Flag::NormalTerm | - MessageField::Flag::Value + Field::Flag::GMime | + Field::Flag::NormalTerm | + Field::Flag::Value }, // ThreadId { - MessageField::Id::ThreadId, - MessageField::Type::String, + Field::Id::ThreadId, + Field::Type::String, "thread", "Thread a message belongs to", {}, 'w', - MessageField::Flag::NormalTerm + Field::Flag::NormalTerm }, }}; @@ -461,10 +477,10 @@ static constexpr std::array * * @return ref of the message field. */ -constexpr const MessageField& -message_field(MessageField::Id id) +constexpr const Field& +field_from_id(Field::Id id) { - return MessageFields.at(static_cast(id)); + return Fields.at(static_cast(id)); } /** @@ -473,8 +489,8 @@ message_field(MessageField::Id id) * @param func some callable */ template -void message_field_for_each(Func&& func) { - for (const auto& field: MessageFields) +void field_for_each(Func&& func) { + for (const auto& field: Fields) func(field); } @@ -486,10 +502,10 @@ void message_field_for_each(Func&& func) { * @return a message-field id, or nullopt if not found. */ template -std::optional message_field_find_if(Pred&& pred) { - for (auto&& field: MessageFields) +std::optional field_find_if(Pred&& pred) { + for (auto&& field: Fields) if (pred(field)) - return field.id; + return field; return std::nullopt; } @@ -501,37 +517,37 @@ std::optional message_field_find_if(Pred&& pred) { * @return the message-field-id or nullopt. */ static inline -std::optional message_field_id(char shortcut) { - return message_field_find_if([&](auto&& field ){ +std::optional field_from_shortcut(char shortcut) { + return field_find_if([&](auto&& field){ return field.shortcut == shortcut; }); } static inline -std::optional message_field_id(const std::string& name) { +std::optional field_from_name(const std::string& name) { if (name.length() == 1) - return message_field_id(name[0]); + return field_from_shortcut(name[0]); else - return message_field_find_if([&](auto&& field){ + return field_find_if([&](auto&& field){ return field.name == name; }); } /** - * Get the MessageField::Id for some number, or nullopt if it does not match + * Get the Field::Id for some number, or nullopt if it does not match * * @param id an id number * - * @return MessageField::Id or nullopt + * @return Field::Id or nullopt */ static inline -std::optional message_field_id(size_t id) +std::optional field_from_number(size_t id) { - if (id >= static_cast(MessageField::Id::_count_)) + if (id >= static_cast(Field::Id::_count_)) return std::nullopt; else - return static_cast(id); + return field_from_id(static_cast(id)); } } // namespace Mu -#endif /* MU_MESSAGE_FIELDS_HH__ */ +#endif /* MU_FIELDS_HH__ */ diff --git a/lib/message/mu-flags.cc b/lib/message/mu-flags.cc new file mode 100644 index 00000000..05a5b0b3 --- /dev/null +++ b/lib/message/mu-flags.cc @@ -0,0 +1,170 @@ +/* +** 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. +** +*/ + +/* + * implementation is almost completely in the header; here we just add some + * compile-time tests. + */ + +#include "mu-flags.hh" + +using namespace Mu; + +std::string +Mu::flags_to_string(Flags flags) +{ + std::string str; + + for (auto&& info: AllMessageFlagInfos) + if (any_of(info.flag & flags)) + str+=info.shortcut; + + return str; +} + + +/* + * flags & flag-info + */ +constexpr bool +validate_message_info_flags() +{ + for (auto id = 0U; id != AllMessageFlagInfos.size(); ++id) { + const auto flag = static_cast(1 << id); + if (flag != AllMessageFlagInfos[id].flag) + return false; + } + return true; +} + + +/* + * tests... also build as runtime-tests, so we can get coverage info + */ +#ifdef BUILD_TESTS +#define static_assert g_assert_true +#endif /*BUILD_TESTS*/ + +[[maybe_unused]] static void +test_basic() +{ + static_assert(AllMessageFlagInfos.size() == + __builtin_ctz(static_cast(Flags::_final_))); + static_assert(validate_message_info_flags()); + + static_assert(!!flag_info(Flags::Encrypted)); + static_assert(!flag_info(Flags::None)); + static_assert(!flag_info(static_cast(0))); + static_assert(!flag_info(static_cast(1<flag == Flags::Draft); + static_assert(flag_info('l')->flag == Flags::MailingList); + static_assert(!flag_info('q')); + + static_assert(flag_info("trashed")->flag == Flags::Trashed); + static_assert(flag_info("attach")->flag == Flags::HasAttachment); + static_assert(!flag_info("fnorb")); + + + static_assert(flag_info('D')->shortcut_lower() == 'd'); + static_assert(flag_info('u')->shortcut_lower() == 'u'); +} + +/* + * flags_from_expr + */ +[[maybe_unused]] static void +test_flags_from_expr() +{ + static_assert(flags_from_absolute_expr("SRP").value() == + (Flags::Seen | Flags::Replied | Flags::Passed)); + static_assert(flags_from_absolute_expr("Faul").value() == + (Flags::Flagged | Flags::Unread | + Flags::HasAttachment | Flags::MailingList)); + + static_assert(!flags_from_absolute_expr("DRT?")); + static_assert(flags_from_absolute_expr("DRT?", true/*ignore invalid*/).value() == + (Flags::Draft | Flags::Replied | + Flags::Trashed)); + static_assert(flags_from_absolute_expr("DFPNxulabcdef", true/*ignore invalid*/).value() == + (Flags::Draft|Flags::Flagged|Flags::Passed| + Flags::New | Flags::Encrypted | + Flags::Unread | Flags::MailingList | + Flags::HasAttachment)); +} + + +/* + * flags_from_delta_expr + */ +[[maybe_unused]] static void +test_flags_from_delta_expr() +{ + static_assert(flags_from_delta_expr( + "+S-u-N", Flags::New|Flags::Unread).value() == + Flags::Seen); + static_assert(flags_from_delta_expr("+R+P-F", Flags::Seen).value() == + (Flags::Seen|Flags::Passed|Flags::Replied)); + /* '-B' is invalid */ + static_assert(!flags_from_delta_expr("+R+P-B", Flags::Seen)); + /* '-B' is invalid, but ignore invalid */ + static_assert(flags_from_delta_expr("+R+P-B", Flags::Seen, true) == + (Flags::Replied|Flags::Passed|Flags::Seen)); + static_assert(flags_from_delta_expr("+F+T-S", Flags::None, true).value() == + (Flags::Flagged|Flags::Trashed)); +} + +/* + * flags_filter + */ +[[maybe_unused]] static void +test_flags_filter() +{ + static_assert(flags_filter(flags_from_absolute_expr( + "DFPNxulabcdef", true/*ignore invalid*/).value(), + MessageFlagCategory::Mailfile) == + (Flags::Draft|Flags::Flagged|Flags::Passed)); +} + + +#ifdef BUILD_TESTS +int +main(int argc, char* argv[]) +{ + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/message/flags/basic", test_basic); + g_test_add_func("/message/flags/flag-info", test_flag_info); + g_test_add_func("/message/flags/flags-from-absolute-expr", + test_flags_from_expr); + g_test_add_func("/message/flags/flags-from-delta-expr", + test_flags_from_delta_expr); + g_test_add_func("/message/flags/flags-filter", + test_flags_filter); + + return g_test_run(); +} +#endif /*BUILD_TESTS*/ diff --git a/lib/mu-message-flags.hh b/lib/message/mu-flags.hh similarity index 73% rename from lib/mu-message-flags.hh rename to lib/message/mu-flags.hh index dd9cd232..f424b5a1 100644 --- a/lib/mu-message-flags.hh +++ b/lib/message/mu-flags.hh @@ -17,8 +17,8 @@ ** */ -#ifndef MU_MESSAGE_FLAGS_HH__ -#define MU_MESSAGE_FLAGS_HH__ +#ifndef MU_FLAGS_HH__ +#define MU_FLAGS_HH__ #include #include @@ -28,7 +28,7 @@ namespace Mu { -enum struct MessageFlags { +enum struct Flags { None = 0, /**< No flags */ /** * next 6 are seen in the file-info part of maildir message file @@ -69,7 +69,7 @@ enum struct MessageFlags { */ _final_ = 1 << 12 }; -MU_ENABLE_BITOPS(MessageFlags); +MU_ENABLE_BITOPS(Flags); /** * Message flags category @@ -88,7 +88,7 @@ enum struct MessageFlagCategory { * */ struct MessageFlagInfo { - MessageFlags flag; /**< The message flag */ + Flags flag; /**< The message flag */ char shortcut; /**< Shortcut character */ std::string_view name; /**< Name of the flag */ MessageFlagCategory category; /**< Flag category */ @@ -108,24 +108,24 @@ struct MessageFlagInfo { * Array of all flag information. */ constexpr std::array AllMessageFlagInfos = {{ - MessageFlagInfo{MessageFlags::Draft, 'D', "draft", MessageFlagCategory::Mailfile}, - MessageFlagInfo{MessageFlags::Flagged, 'F', "flagged", MessageFlagCategory::Mailfile}, - MessageFlagInfo{MessageFlags::Passed, 'P', "passed", MessageFlagCategory::Mailfile}, - MessageFlagInfo{MessageFlags::Replied, 'R', "replied", MessageFlagCategory::Mailfile}, - MessageFlagInfo{MessageFlags::Seen, 'S', "seen", MessageFlagCategory::Mailfile}, - MessageFlagInfo{MessageFlags::Trashed, 'T', "trashed", MessageFlagCategory::Mailfile}, + MessageFlagInfo{Flags::Draft, 'D', "draft", MessageFlagCategory::Mailfile}, + MessageFlagInfo{Flags::Flagged, 'F', "flagged", MessageFlagCategory::Mailfile}, + MessageFlagInfo{Flags::Passed, 'P', "passed", MessageFlagCategory::Mailfile}, + MessageFlagInfo{Flags::Replied, 'R', "replied", MessageFlagCategory::Mailfile}, + MessageFlagInfo{Flags::Seen, 'S', "seen", MessageFlagCategory::Mailfile}, + MessageFlagInfo{Flags::Trashed, 'T', "trashed", MessageFlagCategory::Mailfile}, - MessageFlagInfo{MessageFlags::New, 'N', "new", MessageFlagCategory::Maildir}, + MessageFlagInfo{Flags::New, 'N', "new", MessageFlagCategory::Maildir}, - MessageFlagInfo{MessageFlags::Signed, 'z', "signed", MessageFlagCategory::Content}, - MessageFlagInfo{MessageFlags::Encrypted, 'x', "encrypted", + MessageFlagInfo{Flags::Signed, 'z', "signed", MessageFlagCategory::Content}, + MessageFlagInfo{Flags::Encrypted, 'x', "encrypted", MessageFlagCategory::Content}, - MessageFlagInfo{MessageFlags::HasAttachment, 'a', "attach", + MessageFlagInfo{Flags::HasAttachment, 'a', "attach", MessageFlagCategory::Content}, - MessageFlagInfo{MessageFlags::Unread, 'u', "unread", MessageFlagCategory::Pseudo}, + MessageFlagInfo{Flags::Unread, 'u', "unread", MessageFlagCategory::Pseudo}, - MessageFlagInfo{MessageFlags::MailingList, 'l', "list", MessageFlagCategory::Content}, + MessageFlagInfo{Flags::MailingList, 'l', "list", MessageFlagCategory::Content}, }}; @@ -135,7 +135,7 @@ constexpr std::array AllMessageFlagInfos = {{ * @param func some callable */ template -constexpr void message_flag_infos_for_each(Func&& func) +constexpr void flag_infos_for_each(Func&& func) { for (auto&& info: AllMessageFlagInfos) func(info); @@ -149,9 +149,9 @@ constexpr void message_flag_infos_for_each(Func&& func) * @return the MessageFlagInfo, or std::nullopt in case of error. */ constexpr const std::optional -message_flag_info(MessageFlags flag) +flag_info(Flags flag) { - constexpr auto upper = static_cast(MessageFlags::_final_); + constexpr auto upper = static_cast(Flags::_final_); const auto val = static_cast(flag); if (__builtin_popcount(val) != 1 || val >= upper) @@ -168,7 +168,7 @@ message_flag_info(MessageFlags flag) * @return the MessageFlagInfo */ constexpr const std::optional -message_flag_info(char shortcut) +flag_info(char shortcut) { for (auto&& info : AllMessageFlagInfos) if (info.shortcut == shortcut) @@ -185,7 +185,7 @@ message_flag_info(char shortcut) * @return the MessageFlagInfo */ constexpr const std::optional -message_flag_info(std::string_view name) +flag_info(std::string_view name) { for (auto&& info : AllMessageFlagInfos) if (info.name == name) @@ -207,15 +207,15 @@ message_flag_info(std::string_view name) * @param ignore_invalid if @true, ignore invalid flags, otherwise return * nullopt if an invalid flag is encountered * - * @return the (OR'ed) flags or MessageFlags::None + * @return the (OR'ed) flags or Flags::None */ -constexpr std::optional -message_flags_from_absolute_expr(std::string_view expr, bool ignore_invalid = false) +constexpr std::optional +flags_from_absolute_expr(std::string_view expr, bool ignore_invalid = false) { - MessageFlags flags{MessageFlags::None}; + Flags flags{Flags::None}; for (auto&& kar : expr) { - if (const auto& info{message_flag_info(kar)}; !info) { + if (const auto& info{flag_info(kar)}; !info) { if (!ignore_invalid) return std::nullopt; } else @@ -242,15 +242,15 @@ message_flags_from_absolute_expr(std::string_view expr, bool ignore_invalid = fa * * @return new flags, or nullopt in case of error */ -constexpr std::optional -message_flags_from_delta_expr(std::string_view expr, MessageFlags flags, +constexpr std::optional +flags_from_delta_expr(std::string_view expr, Flags flags, bool ignore_invalid = false) { if (expr.size() % 2 != 0) return std::nullopt; for (auto u = 0U; u != expr.size(); u += 2) { - if (const auto& info{message_flag_info(expr[u + 1])}; !info) { + if (const auto& info{flag_info(expr[u + 1])}; !info) { if (!ignore_invalid) return std::nullopt; } else { @@ -276,18 +276,18 @@ message_flags_from_delta_expr(std::string_view expr, MessageFlags flags, * * @return either messages flags or std::nullopt in case of error. */ -constexpr std::optional -message_flags_from_expr(std::string_view expr, - std::optional flags = std::nullopt) +constexpr std::optional +flags_from_expr(std::string_view expr, + std::optional flags = std::nullopt) { if (expr.empty()) return std::nullopt; if (expr[0] == '+' || expr[0] == '-') - return message_flags_from_delta_expr( - expr, flags.value_or(MessageFlags::None), true); + return flags_from_delta_expr( + expr, flags.value_or(Flags::None), true); else - return message_flags_from_absolute_expr(expr, true); + return flags_from_absolute_expr(expr, true); } /** @@ -298,8 +298,8 @@ message_flags_from_expr(std::string_view expr, * * @return filter flags */ -constexpr MessageFlags -message_flags_filter(MessageFlags flags, MessageFlagCategory cat) +constexpr Flags +flags_filter(Flags flags, MessageFlagCategory cat) { for (auto&& info : AllMessageFlagInfos) if (info.category != cat) @@ -314,8 +314,8 @@ message_flags_filter(MessageFlags flags, MessageFlagCategory cat) * * @return string as a sequence of message-flag shortcuts */ -std::string message_flags_to_string(MessageFlags flags); +std::string flags_to_string(Flags flags); } // namespace Mu -#endif /* MU_MESSAGE_FLAGS_HH__ */ +#endif /* MU_FLAGS_HH__ */ diff --git a/lib/message/mu-message.cc b/lib/message/mu-message.cc new file mode 100644 index 00000000..6f475415 --- /dev/null +++ b/lib/message/mu-message.cc @@ -0,0 +1,425 @@ +/* +** 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; +} diff --git a/lib/message/mu-message.hh b/lib/message/mu-message.hh new file mode 100644 index 00000000..81016bb2 --- /dev/null +++ b/lib/message/mu-message.hh @@ -0,0 +1,244 @@ +/* +** 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_HH__ +#define MU_MESSAGE_HH__ + +#include +#include +#include + +#include "mu-contact.hh" +#include "mu-priority.hh" +#include "mu-flags.hh" +#include "mu-fields.hh" +#include "mu-document.hh" + +struct _GMimeMessage; + +namespace Mu { + +class Message { +public: + /** + * Construct a message based on a path + * + * @param path path to message + * @param mdir the maildir for this message; ie, if the path is + * ~/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. + */ + Message(const std::string& path, const std::string& mdir); + + /** + * Construct a message based on a Message::Document + * + * @param doc + */ + Message(Document& doc): doc_{doc} {} + + /** + * Copy CTOR + * + * @param rhs a Message + */ + Message(const Message& rhs) { + *this = rhs; + } + + /** + * Move CTOR + * + * @param rhs a Message + */ + + Message(Message&& rhs) { + *this = std::move(rhs); + } + + /** + * 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. + * + * + * @return document + */ + const Document& document() const { return doc_; } + + /** + * Get the file system path of this message + * + * @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); } + + /** + * Get the sender (From:) of this message + * + * @return the sender(s) of this Message + */ + Contacts from() const { return doc_.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); } + + /** + * Get the recipient(s) (Cc:) for this message + * + * @return recipients + */ + Contacts cc() const { return doc_.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); } + + /** + * 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); } + + /** + * Get the subject of this message + * + * @return the subject of this Message + */ + std::string subject() const { return doc_.string_value(Field::Id::Subject); } + + /** + * Get the Message-Id of this message + * + * @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);} + + /** + * get the mailing list for a message, i.e. the mailing-list + * identifier in the List-Id header. + * + * @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);} + + /** + * get the message date/time (the Date: field) as time_t, using UTC + * + * @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)); } + + /** + * get the flags for this message + * + * @return the file/content flags + */ + Flags flags() const { return doc_.flags_value(); } + + /** + * get the message priority for this message. The X-Priority, X-MSMailPriority, + * Importance and Precedence header are checked, in that order. if no known or + * explicit priority is set, Priority::Id::Normal is assumed + * + * @return the message priority + */ + Priority priority() const { return doc_.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)); } + + /** + * get the list of references (consisting of both the References and + * In-Reply-To fields), with the oldest first and the direct parent as + * the last one. Note, any reference (message-id) will appear at most + * once, duplicates are filtered out. + * + * @return a vec with the references for this msg. + */ + std::vector references() const { + return doc_.string_vec_value(Field::Id::References); + } + + /** + * get the list of tags (ie., X-Label) + * + * @param msg a valid MuMsg + * + * @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); + } + + /** + * Get some message-header + * + * @param header_field name of the header + * + * @return the value + */ + std::string header(const std::string& header_field) const; + +private: + Document doc_; + mutable struct _GMimeMessage *mime_msg_{}; + +}; // Message +} // Mu +#endif /* MU_MESSAGE_HH__ */ diff --git a/lib/mu-message-priority.cc b/lib/message/mu-priority.cc similarity index 62% rename from lib/mu-message-priority.cc rename to lib/message/mu-priority.cc index 4f4aea46..9b57cea9 100644 --- a/lib/mu-message-priority.cc +++ b/lib/message/mu-priority.cc @@ -17,17 +17,14 @@ ** */ -#ifndef MU_MESSAGE_PRIORITY_CC__ -#define MU_MESSAGE_PRIORITY_CC__ - -#include "mu-message-priority.hh" +#include "mu-priority.hh" using namespace Mu; std::string -Mu::to_string(MessagePriority prio) +Mu::to_string(Priority prio) { - return std::string{message_priority_name(prio)}; + return std::string{priority_name(prio)}; } /* @@ -41,26 +38,26 @@ Mu::to_string(MessagePriority prio) [[maybe_unused]] static void test_priority_to_char() { - static_assert(to_char(MessagePriority::Low) == 'l'); - static_assert(to_char(MessagePriority::Normal) == 'n'); - static_assert(to_char(MessagePriority::High) == 'h'); + static_assert(to_char(Priority::Low) == 'l'); + static_assert(to_char(Priority::Normal) == 'n'); + static_assert(to_char(Priority::High) == 'h'); } [[maybe_unused]] static void test_priority_from_char() { - static_assert(message_priority_from_char('l') == MessagePriority::Low); - static_assert(message_priority_from_char('n') == MessagePriority::Normal); - static_assert(message_priority_from_char('h') == MessagePriority::High); - static_assert(message_priority_from_char('x') == MessagePriority::Normal); + static_assert(priority_from_char('l') == Priority::Low); + static_assert(priority_from_char('n') == Priority::Normal); + static_assert(priority_from_char('h') == Priority::High); + static_assert(priority_from_char('x') == Priority::Normal); } [[maybe_unused]] static void test_priority_name() { - static_assert(message_priority_name(MessagePriority::Low) == "low"); - static_assert(message_priority_name(MessagePriority::Normal) == "normal"); - static_assert(message_priority_name(MessagePriority::High) == "high"); + static_assert(priority_name(Priority::Low) == "low"); + static_assert(priority_name(Priority::Normal) == "normal"); + static_assert(priority_name(Priority::High) == "high"); } @@ -77,6 +74,3 @@ main(int argc, char* argv[]) return g_test_run(); } #endif /*BUILD_TESTS*/ - - -#endif /* MU_MESSAGE_PRIORITY_CC__ */ diff --git a/lib/mu-message-priority.hh b/lib/message/mu-priority.hh similarity index 68% rename from lib/mu-message-priority.hh rename to lib/message/mu-priority.hh index 5ede4779..af76eceb 100644 --- a/lib/mu-message-priority.hh +++ b/lib/message/mu-priority.hh @@ -17,13 +17,13 @@ ** */ -#ifndef MU_MESSAGE_PRIORITY_HH__ -#define MU_MESSAGE_PRIORITY_HH__ +#ifndef MU_PRIORITY_HH__ +#define MU_PRIORITY_HH__ #include #include #include -#include "mu-message-fields.hh" +#include "mu-fields.hh" namespace Mu { /** @@ -35,7 +35,7 @@ namespace Mu { * The priority ids * */ -enum struct MessagePriority : char { +enum struct Priority : char { Low = 'l', /**< Low priority */ Normal = 'n', /**< Normal priority */ High = 'h', /**< High priority */ @@ -44,8 +44,8 @@ enum struct MessagePriority : char { /** * Sequence of all message priorities. */ -static constexpr std::array AllMessagePriorities = { - MessagePriority::Low, MessagePriority::Normal, MessagePriority::High}; +static constexpr std::array AllMessagePriorities = { + Priority::Low, Priority::Normal, Priority::High}; /** * Get the char for some priority @@ -55,7 +55,7 @@ static constexpr std::array AllMessagePriorities = { * @return the char */ constexpr char -to_char(MessagePriority prio) +to_char(Priority prio) { return static_cast(prio); } @@ -66,17 +66,17 @@ to_char(MessagePriority prio) * * @param c some character */ -constexpr MessagePriority -message_priority_from_char(char c) +constexpr Priority +priority_from_char(char c) { switch (c) { case 'l': - return MessagePriority::Low; + return Priority::Low; case 'h': - return MessagePriority::High; + return Priority::High; case 'n': default: - return MessagePriority::Normal; + return Priority::Normal; } } @@ -86,14 +86,14 @@ message_priority_from_char(char c) * @return the name */ constexpr std::string_view -message_priority_name(MessagePriority prio) +priority_name(Priority prio) { switch (prio) { - case MessagePriority::Low: + case Priority::Low: return "low"; - case MessagePriority::High: + case Priority::High: return "high"; - case MessagePriority::Normal: + case Priority::Normal: default: return "normal"; } @@ -105,12 +105,12 @@ message_priority_name(MessagePriority prio) * @return the name */ constexpr const char* -message_priority_name_c_str(MessagePriority prio) +priority_name_c_str(Priority prio) { switch (prio) { - case MessagePriority::Low: return "low"; - case MessagePriority::High: return "high"; - case MessagePriority::Normal: + case Priority::Low: return "low"; + case Priority::High: return "high"; + case Priority::Normal: default: return "normal"; } } @@ -122,8 +122,8 @@ message_priority_name_c_str(MessagePriority prio) * * @return a string */ -std::string to_string(MessagePriority prio); +std::string to_string(Priority prio); } // namespace Mu -#endif /*MU_MESSAGE_PRIORITY_HH_*/ +#endif /*MU_PRIORITY_HH_*/ diff --git a/lib/mu-message-flags.cc b/lib/mu-message-flags.cc deleted file mode 100644 index 284d7001..00000000 --- a/lib/mu-message-flags.cc +++ /dev/null @@ -1,170 +0,0 @@ -/* -** 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. -** -*/ - -/* - * implementation is almost completely in the header; here we just add some - * compile-time tests. - */ - -#include "mu-message-flags.hh" - -using namespace Mu; - -std::string -Mu::message_flags_to_string(MessageFlags flags) -{ - std::string str; - - for (auto&& info: AllMessageFlagInfos) - if (any_of(info.flag & flags)) - str+=info.shortcut; - - return str; -} - - -/* - * flags & flag-info - */ -constexpr bool -validate_message_info_flags() -{ - for (auto id = 0U; id != AllMessageFlagInfos.size(); ++id) { - const auto flag = static_cast(1 << id); - if (flag != AllMessageFlagInfos[id].flag) - return false; - } - return true; -} - - -/* - * tests... also build as runtime-tests, so we can get coverage info - */ -#ifdef BUILD_TESTS -#define static_assert g_assert_true -#endif /*BUILD_TESTS*/ - -[[maybe_unused]] static void -test_basic() -{ - static_assert(AllMessageFlagInfos.size() == - __builtin_ctz(static_cast(MessageFlags::_final_))); - static_assert(validate_message_info_flags()); - - static_assert(!!message_flag_info(MessageFlags::Encrypted)); - static_assert(!message_flag_info(MessageFlags::None)); - static_assert(!message_flag_info(static_cast(0))); - static_assert(!message_flag_info(static_cast(1<flag == MessageFlags::Draft); - static_assert(message_flag_info('l')->flag == MessageFlags::MailingList); - static_assert(!message_flag_info('q')); - - static_assert(message_flag_info("trashed")->flag == MessageFlags::Trashed); - static_assert(message_flag_info("attach")->flag == MessageFlags::HasAttachment); - static_assert(!message_flag_info("fnorb")); - - - static_assert(message_flag_info('D')->shortcut_lower() == 'd'); - static_assert(message_flag_info('u')->shortcut_lower() == 'u'); -} - -/* - * message_flags_from_expr - */ -[[maybe_unused]] static void -test_message_flags_from_expr() -{ - static_assert(message_flags_from_absolute_expr("SRP").value() == - (MessageFlags::Seen | MessageFlags::Replied | MessageFlags::Passed)); - static_assert(message_flags_from_absolute_expr("Faul").value() == - (MessageFlags::Flagged | MessageFlags::Unread | - MessageFlags::HasAttachment | MessageFlags::MailingList)); - - static_assert(!message_flags_from_absolute_expr("DRT?")); - static_assert(message_flags_from_absolute_expr("DRT?", true/*ignore invalid*/).value() == - (MessageFlags::Draft | MessageFlags::Replied | - MessageFlags::Trashed)); - static_assert(message_flags_from_absolute_expr("DFPNxulabcdef", true/*ignore invalid*/).value() == - (MessageFlags::Draft|MessageFlags::Flagged|MessageFlags::Passed| - MessageFlags::New | MessageFlags::Encrypted | - MessageFlags::Unread | MessageFlags::MailingList | - MessageFlags::HasAttachment)); -} - - -/* - * message_flags_from_delta_expr - */ -[[maybe_unused]] static void -test_message_flags_from_delta_expr() -{ - static_assert(message_flags_from_delta_expr( - "+S-u-N", MessageFlags::New|MessageFlags::Unread).value() == - MessageFlags::Seen); - static_assert(message_flags_from_delta_expr("+R+P-F", MessageFlags::Seen).value() == - (MessageFlags::Seen|MessageFlags::Passed|MessageFlags::Replied)); - /* '-B' is invalid */ - static_assert(!message_flags_from_delta_expr("+R+P-B", MessageFlags::Seen)); - /* '-B' is invalid, but ignore invalid */ - static_assert(message_flags_from_delta_expr("+R+P-B", MessageFlags::Seen, true) == - (MessageFlags::Replied|MessageFlags::Passed|MessageFlags::Seen)); - static_assert(message_flags_from_delta_expr("+F+T-S", MessageFlags::None, true).value() == - (MessageFlags::Flagged|MessageFlags::Trashed)); -} - -/* - * message_flags_filter - */ -[[maybe_unused]] static void -test_message_flags_filter() -{ - static_assert(message_flags_filter(message_flags_from_absolute_expr( - "DFPNxulabcdef", true/*ignore invalid*/).value(), - MessageFlagCategory::Mailfile) == - (MessageFlags::Draft|MessageFlags::Flagged|MessageFlags::Passed)); -} - - -#ifdef BUILD_TESTS -int -main(int argc, char* argv[]) -{ - g_test_init(&argc, &argv, NULL); - - g_test_add_func("/message/flags/basic", test_basic); - g_test_add_func("/message/flags/flag-info", test_message_flag_info); - g_test_add_func("/message/flags/flags-from-absolute-expr", - test_message_flags_from_expr); - g_test_add_func("/message/flags/flags-from-delta-expr", - test_message_flags_from_delta_expr); - g_test_add_func("/message/flags/flags-filter", - test_message_flags_filter); - - return g_test_run(); -} -#endif /*BUILD_TESTS*/ diff --git a/lib/mu-message.hh b/lib/mu-message.hh deleted file mode 100644 index cea716ba..00000000 --- a/lib/mu-message.hh +++ /dev/null @@ -1,40 +0,0 @@ -/* -** 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_HH__ -#define MU_MESSAGE_HH__ - -#include "mu-message-contact.hh" -#include "mu-message-priority.hh" -#include "mu-message-flags.hh" -#include "mu-message-fields.hh" - -namespace Mu { - -namespace Message { - -using Contact = MessageContact; -using Contacts = MessageContacts; -using Priority = MessagePriority; -using Flags = MessageFlags; -using Field = MessageField; - -} // Message -} // Mu -#endif /* MU_MESSAGE_HH__ */ diff --git a/lib/tests/meson.build b/lib/tests/meson.build index ac040cc9..6c1cc8b9 100644 --- a/lib/tests/meson.build +++ b/lib/tests/meson.build @@ -18,17 +18,18 @@ # tests # - -test('test-message-flags', - executable('test-message-flags', - '../mu-message-flags.cc', +test('test-message-contact', + executable('test-message-contact', + '../mu-message-contact.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, gmime_dep, lib_mu_dep, lib_test_mu_common_dep])) -test('test-message-priority', - executable('test-message-priority', - '../mu-message-priority.cc', + + +test('test-message-document', + executable('test-message-document', + '../mu-message-document.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, gmime_dep, lib_mu_dep, @@ -42,9 +43,16 @@ test('test-message-fields', dependencies: [glib_dep, gmime_dep, lib_mu_dep, lib_test_mu_common_dep])) -test('test-message-contact', - executable('test-message-contact', - '../mu-message-contact.cc', +test('test-message-flags', + executable('test-message-flags', + '../mu-message-flags.cc', + install: false, + cpp_args: ['-DBUILD_TESTS'], + dependencies: [glib_dep, gmime_dep, lib_mu_dep, + lib_test_mu_common_dep])) +test('test-message-priority', + executable('test-message-priority', + '../mu-message-priority.cc', install: false, cpp_args: ['-DBUILD_TESTS'], dependencies: [glib_dep, gmime_dep, lib_mu_dep,