message: update implementation

Add more of the Message class (and various helpers), which are to replace all
the `mu-msg-*` code.

Add more tests.
This commit is contained in:
Dirk-Jan C. Binnema 2022-03-26 16:19:08 +02:00
parent 55402622b9
commit 37988b5a26
12 changed files with 2056 additions and 412 deletions

View File

@ -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',

View File

@ -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) {

View File

@ -29,6 +29,7 @@
#include <cstdlib>
#include <ctime>
#include <utils/mu-option.hh>
#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> field_id_ = {},
Option<Field::Id> 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; /**< 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; /**< 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<Contact>;
*/
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
/**

View File

@ -69,8 +69,10 @@ Document::add(Field::Id id, const std::string& val)
void
Document::add(Field::Id id, const std::vector<std::string>& 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<std::string> cvec;

View File

@ -29,6 +29,7 @@
#include "mu-priority.hh"
#include "mu-flags.hh"
#include "mu-contact.hh"
#include <utils/mu-option.hh>
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<typename T> void add(Field::Id id, const Option<T>& val) {
if (val)
add(id, val.value());
}
/*
* Retrieving values
*/
/**
* Get a message-field as a string-value
*

View File

@ -24,9 +24,9 @@
#include <string_view>
#include <algorithm>
#include <array>
#include <optional>
#include <xapian.h>
#include <utils/mu-utils.hh>
#include <utils/mu-option.hh>
namespace Mu {
@ -320,7 +320,7 @@ static constexpr std::array<Field, Field::id_size()>
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 <typename Pred>
std::optional<Field> field_find_if(Pred&& pred) {
Option<Field> 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> field_find_if(Pred&& pred) {
* @return the message-field-id or nullopt.
*/
static inline
std::optional<Field> field_from_shortcut(char shortcut) {
Option<Field> field_from_shortcut(char shortcut) {
return field_find_if([&](auto&& field){
return field.shortcut == shortcut;
});
}
static inline
std::optional<Field> field_from_name(const std::string& name) {
Option<Field> 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> field_from_name(const std::string& name) {
* @return Field::Id or nullopt
*/
static inline
std::optional<Field> field_from_number(size_t id)
Option<Field> field_from_number(size_t id)
{
if (id >= static_cast<size_t>(Field::Id::_count_))
return std::nullopt;
return Nothing;
else
return field_from_id(static_cast<Field::Id>(id));
}

View File

@ -0,0 +1,85 @@
/*
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** 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<Mu::MimeObject>(obj)}
{}
MessagePart::MessagePart(const MessagePart& other):
MessagePart(*other.mime_obj)
{}
MessagePart::~MessagePart() = default;
Option<std::string>
MessagePart::filename() const noexcept
{
if (!mime_obj->is_part())
return Nothing;
else
return MimePart(*mime_obj).filename();
}
Option<std::string>
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<std::string>
MessagePart::to_string() const noexcept
{
if (mime_obj->is_part())
return MimePart(*mime_obj).to_string();
else
return mime_obj->object_to_string();
}
Result<size_t>
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);
}

View File

@ -0,0 +1,101 @@
/*
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** 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 <string>
#include <memory>
#include <utils/mu-option.hh>
#include <utils/mu-result.hh>
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<std::string> filename() const noexcept;
/**
* Mime-type for the mime-part (e.g. "text/plain")
*
* @return the mime-part or Nothing if there is none
*/
Option<std::string> 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<std::string> 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<size_t> to_file(const std::string& path, bool overwrite) const noexcept;
struct Private;
private:
std::unique_ptr<MimeObject> mime_obj;
};
} // namespace Mu
#endif /* MU_MESSAGE_PART_HH__ */

File diff suppressed because it is too large Load Diff

View File

@ -23,19 +23,27 @@
#include <memory>
#include <string>
#include <vector>
#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<Message> 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<Message> 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<Message> 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<time_t>(doc_.integer_value(Field::Id::Date)); }
::time_t date() const { return static_cast<time_t>(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<size_t>(doc_.integer_value(Field::Id::Size)); }
size_t size() const { return static_cast<size_t>(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<std::string> 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<std::string> 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<std::string> body_text() const;
/**
* Get the HTML body
*
* @return text body
*/
Option<std::string> 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<std::string> 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<Part>& 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<Private> priv_;
}; // Message
} // Mu
#endif /* MU_MESSAGE_HH__ */

View File

@ -0,0 +1,363 @@
/*
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** 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 <mutex>
#include <fcntl.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. */
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<std::string>
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<std::string>
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<MimeMessage>
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>
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>
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<int64_t>
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<GMimeAddressType>(atype))};
if (!addrs)
return {};
const auto msgtime{date().value_or(0)};
const auto opt_field_id = std::invoke(
[&]()->Option<Field::Id>{
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<std::string>
MimeMessage::references() const noexcept
{
constexpr std::array<const char*, 2> 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<std::string> 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<CallbackData*>(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<size_t>(g_mime_stream_length(stream));
}
Option<std::string>
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<size_t>
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<size_t>(written));
}

View File

@ -0,0 +1,665 @@
/*
** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** 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 <stdexcept>
#include <string>
#include <functional>
#include <gmime/gmime.h>
#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<std::string> 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<GMimeContentType*>(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<std::string> header(const std::string& header) const noexcept;
/**
* Get the content type
*
* @return the content-type or Nothing
*/
Option<MimeContentType> 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<std::string> 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<GMimeObject*>(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<MimeMessage> 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<MimeMessage> 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<std::string> 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<std::string> 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<int64_t> 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<std::string> references() const noexcept;
/**
* Callback for for_each(). See GMimeObjectForEachFunc.
*
*/
using ForEachFunc = std::function<void(const MimeObject& parent,
const MimeObject& part)>;
/**
* 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<GMimeMessage*>(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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<std::string> 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<size_t> 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<ContentEncoding>(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<OpenPGPData>(data);
}
private:
GMimePart* self() const {
return reinterpret_cast<GMimePart*>(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<SecureMimeType>(
g_mime_application_pkcs7_mime_get_smime_type(self()));
}
private:
GMimeApplicationPkcs7Mime* self() const {
return reinterpret_cast<GMimeApplicationPkcs7Mime*>(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<GMimeMultipart*>(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<GMimeMultipartEncrypted*>(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<GMimeMultipartSigned*>(object());
}
};
} // namespace Mu
#endif /* MU_MIME_OBJECT_HH__ */