2022-03-26 15:19:08 +01:00
|
|
|
|
/*
|
|
|
|
|
** 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"
|
2022-03-26 16:16:46 +01:00
|
|
|
|
#include "utils/mu-utils.hh"
|
2022-03-26 15:19:08 +01:00
|
|
|
|
#include <mutex>
|
2022-04-28 21:55:36 +02:00
|
|
|
|
#include <regex>
|
2022-03-26 15:19:08 +01:00
|
|
|
|
#include <fcntl.h>
|
2022-04-09 00:20:33 +02:00
|
|
|
|
#include <errno.h>
|
2022-03-26 15:19:08 +01:00
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2022-05-06 21:09:50 +02:00
|
|
|
|
std::string
|
|
|
|
|
Mu::address_rfc2047(const Contact& contact)
|
|
|
|
|
{
|
|
|
|
|
init_gmime();
|
|
|
|
|
|
|
|
|
|
InternetAddress *addr =
|
|
|
|
|
internet_address_mailbox_new(contact.name.c_str(),
|
|
|
|
|
contact.email.c_str());
|
|
|
|
|
|
|
|
|
|
std::string encoded = to_string_gchar(
|
|
|
|
|
internet_address_to_string(addr, {}, true));
|
|
|
|
|
|
|
|
|
|
g_object_unref(addr);
|
|
|
|
|
|
|
|
|
|
return encoded;
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-26 15:19:08 +01:00
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* MimeObject
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
Option<std::string>
|
|
|
|
|
MimeObject::header(const std::string& hdr) const noexcept
|
|
|
|
|
{
|
2022-04-28 21:55:36 +02:00
|
|
|
|
if (auto val{g_mime_object_get_header(self(), hdr.c_str())}; !val)
|
2022-03-26 15:19:08 +01:00
|
|
|
|
return Nothing;
|
2022-04-28 21:55:36 +02:00
|
|
|
|
else if (!g_utf8_validate(val, -1, {}))
|
|
|
|
|
return utf8_clean(val);
|
2022-03-26 15:19:08 +01:00
|
|
|
|
else
|
2022-04-28 21:55:36 +02:00
|
|
|
|
return std::string{val};
|
2022-03-26 15:19:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2022-06-10 22:27:11 +02:00
|
|
|
|
std::vector<std::pair<std::string, std::string>>
|
|
|
|
|
MimeObject::headers() const noexcept
|
|
|
|
|
{
|
|
|
|
|
GMimeHeaderList *lst;
|
|
|
|
|
|
|
|
|
|
lst = g_mime_object_get_header_list(self()); /* _not_ owned */
|
|
|
|
|
if (!lst)
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
std::vector<std::pair<std::string, std::string>> hdrs;
|
|
|
|
|
const auto hdr_num{g_mime_header_list_get_count(lst)};
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i != hdr_num; ++i) {
|
|
|
|
|
GMimeHeader *hdr{g_mime_header_list_get_header_at(lst, i)};
|
|
|
|
|
if (!hdr) /* ^^^ _not_ owned */
|
|
|
|
|
continue;
|
|
|
|
|
const auto name{g_mime_header_get_name(hdr)};
|
|
|
|
|
const auto val{g_mime_header_get_value(hdr)};
|
|
|
|
|
if (!name || !val)
|
|
|
|
|
continue;
|
|
|
|
|
hdrs.emplace_back(name, val);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return hdrs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
Result<size_t>
|
|
|
|
|
MimeObject::write_to_stream(const MimeFormatOptions& f_opts,
|
|
|
|
|
MimeStream& stream) const
|
|
|
|
|
{
|
|
|
|
|
auto written = g_mime_object_write_to_stream(self(), f_opts.get(),
|
|
|
|
|
GMIME_STREAM(stream.object()));
|
|
|
|
|
if (written < 0)
|
|
|
|
|
return Err(Error::Code::File, "failed to write mime-object to stream");
|
|
|
|
|
else
|
|
|
|
|
return Ok(static_cast<size_t>(written));
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-26 15:19:08 +01:00
|
|
|
|
Option<std::string>
|
2022-04-18 20:56:49 +02:00
|
|
|
|
MimeObject::to_string_opt() const noexcept
|
2022-03-26 15:19:08 +01:00
|
|
|
|
{
|
2022-04-18 20:56:49 +02:00
|
|
|
|
auto stream{MimeStream::make_mem()};
|
2022-03-26 15:19:08 +01:00
|
|
|
|
if (!stream) {
|
|
|
|
|
g_warning("failed to create mem stream");
|
|
|
|
|
return Nothing;
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
const auto written = g_mime_object_write_to_stream(
|
|
|
|
|
self(), {}, GMIME_STREAM(stream.object()));
|
2022-03-26 15:19:08 +01:00
|
|
|
|
if (written < 0) {
|
|
|
|
|
g_warning("failed to write object to stream");
|
|
|
|
|
return Nothing;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string buffer;
|
|
|
|
|
buffer.resize(written + 1);
|
2022-04-18 20:56:49 +02:00
|
|
|
|
stream.reset();
|
2022-03-26 15:19:08 +01:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
auto bytes{g_mime_stream_read(GMIME_STREAM(stream.object()),
|
|
|
|
|
buffer.data(), written)};
|
2022-03-26 15:19:08 +01:00
|
|
|
|
if (bytes < 0)
|
|
|
|
|
return Nothing;
|
|
|
|
|
|
|
|
|
|
buffer.data()[written]='\0';
|
|
|
|
|
buffer.resize(written);
|
|
|
|
|
|
|
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-09 00:20:33 +02:00
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* MimeCryptoContext
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
Result<size_t>
|
|
|
|
|
MimeCryptoContext::import_keys(MimeStream& stream)
|
|
|
|
|
{
|
|
|
|
|
GError *err{};
|
|
|
|
|
auto res = g_mime_crypto_context_import_keys(
|
2022-04-18 20:56:49 +02:00
|
|
|
|
self(), GMIME_STREAM(stream.object()), &err);
|
2022-04-09 00:20:33 +02:00
|
|
|
|
|
|
|
|
|
if (res < 0)
|
|
|
|
|
return Err(Error::Code::File, &err,
|
|
|
|
|
"error importing keys");
|
|
|
|
|
|
|
|
|
|
return Ok(static_cast<size_t>(res));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void
|
2022-04-18 20:56:49 +02:00
|
|
|
|
MimeCryptoContext::set_request_password(PasswordRequestFunc pw_func)
|
2022-04-09 00:20:33 +02:00
|
|
|
|
{
|
|
|
|
|
static auto request_func = pw_func;
|
|
|
|
|
|
|
|
|
|
g_mime_crypto_context_set_request_password(
|
|
|
|
|
self(),
|
|
|
|
|
[](GMimeCryptoContext *ctx,
|
|
|
|
|
const char *user_id,
|
|
|
|
|
const char *prompt,
|
|
|
|
|
gboolean reprompt,
|
|
|
|
|
GMimeStream *response,
|
|
|
|
|
GError **err) -> gboolean {
|
2022-04-18 20:56:49 +02:00
|
|
|
|
MimeStream mstream{MimeStream::make_from_stream(response)};
|
|
|
|
|
|
2022-04-09 00:20:33 +02:00
|
|
|
|
auto res = request_func(MimeCryptoContext(ctx),
|
|
|
|
|
std::string{user_id ? user_id : ""},
|
|
|
|
|
std::string{prompt ? prompt : ""},
|
|
|
|
|
!!reprompt,
|
|
|
|
|
mstream);
|
|
|
|
|
if (res)
|
|
|
|
|
return TRUE;
|
|
|
|
|
|
|
|
|
|
res.error().fill_g_error(err);
|
|
|
|
|
return FALSE;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Result<void>
|
|
|
|
|
MimeCryptoContext::setup_gpg_test(const std::string& testpath)
|
|
|
|
|
{
|
|
|
|
|
/* setup clean environment for testing; inspired by gmime */
|
|
|
|
|
|
|
|
|
|
g_setenv ("GNUPGHOME", format("%s/.gnupg", testpath.c_str()).c_str(), 1);
|
|
|
|
|
|
|
|
|
|
/* disable environment variables that gpg-agent uses for pinentry */
|
|
|
|
|
g_unsetenv ("DBUS_SESSION_BUS_ADDRESS");
|
|
|
|
|
g_unsetenv ("DISPLAY");
|
|
|
|
|
g_unsetenv ("GPG_TTY");
|
|
|
|
|
|
2022-04-10 12:18:55 +02:00
|
|
|
|
if (g_mkdir_with_parents((testpath + "/.gnupg").c_str(), 0700) != 0)
|
2022-04-09 00:20:33 +02:00
|
|
|
|
return Err(Error::Code::File,
|
|
|
|
|
"failed to create gnupg dir; err=%d", errno);
|
|
|
|
|
|
|
|
|
|
auto write_gpgfile=[&](const std::string& fname, const std::string& data)
|
|
|
|
|
-> Result<void> {
|
|
|
|
|
|
|
|
|
|
GError *err{};
|
|
|
|
|
std::string path{format("%s/%s", testpath.c_str(), fname.c_str())};
|
|
|
|
|
if (!g_file_set_contents(path.c_str(), data.c_str(), data.size(), &err))
|
|
|
|
|
return Err(Error::Code::File, &err,
|
|
|
|
|
"failed to write %s", path.c_str());
|
|
|
|
|
else
|
|
|
|
|
return Ok();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// some more elegant way?
|
|
|
|
|
if (auto&& res = write_gpgfile("gpg.conf", "pinentry-mode loopback\n"); !res)
|
|
|
|
|
return res;
|
|
|
|
|
if (auto&& res = write_gpgfile("gpgsm.conf", "disable-crl-checks\n"))
|
|
|
|
|
return res;
|
|
|
|
|
|
|
|
|
|
return Ok();
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-26 15:19:08 +01:00
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* MimeMessage
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static Result<MimeMessage>
|
|
|
|
|
make_from_stream(GMimeStream* &&stream/*consume*/)
|
|
|
|
|
{
|
2022-05-05 00:30:23 +02:00
|
|
|
|
init_gmime();
|
2022-03-26 15:19:08 +01:00
|
|
|
|
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{};
|
2022-05-05 00:30:23 +02:00
|
|
|
|
init_gmime();
|
2022-03-26 15:19:08 +01:00
|
|
|
|
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>
|
2022-04-09 00:20:33 +02:00
|
|
|
|
MimeMessage::make_from_text(const std::string& text)
|
2022-03-26 15:19:08 +01:00
|
|
|
|
{
|
2022-05-05 00:30:23 +02:00
|
|
|
|
init_gmime();
|
2022-03-26 15:19:08 +01:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-01 00:10:07 +02:00
|
|
|
|
constexpr Option<GMimeAddressType>
|
|
|
|
|
address_type(Contact::Type ctype)
|
|
|
|
|
{
|
|
|
|
|
switch(ctype) {
|
|
|
|
|
case Contact::Type::Bcc:
|
|
|
|
|
return GMIME_ADDRESS_TYPE_BCC;
|
|
|
|
|
case Contact::Type::Cc:
|
|
|
|
|
return GMIME_ADDRESS_TYPE_CC;
|
|
|
|
|
case Contact::Type::From:
|
|
|
|
|
return GMIME_ADDRESS_TYPE_FROM;
|
|
|
|
|
case Contact::Type::To:
|
|
|
|
|
return GMIME_ADDRESS_TYPE_TO;
|
|
|
|
|
case Contact::Type::ReplyTo:
|
|
|
|
|
return GMIME_ADDRESS_TYPE_REPLY_TO;
|
|
|
|
|
case Contact::Type::Sender:
|
|
|
|
|
return GMIME_ADDRESS_TYPE_SENDER;
|
2022-06-18 13:35:01 +02:00
|
|
|
|
case Contact::Type::None:
|
2022-05-01 00:10:07 +02:00
|
|
|
|
default:
|
|
|
|
|
return Nothing;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Mu::Contacts
|
|
|
|
|
all_contacts(const MimeMessage& msg)
|
|
|
|
|
{
|
|
|
|
|
Contacts contacts;
|
|
|
|
|
|
|
|
|
|
for (auto&& cctype: {
|
|
|
|
|
Contact::Type::Sender,
|
|
|
|
|
Contact::Type::From,
|
|
|
|
|
Contact::Type::ReplyTo,
|
|
|
|
|
Contact::Type::To,
|
|
|
|
|
Contact::Type::Cc,
|
|
|
|
|
Contact::Type::Bcc
|
|
|
|
|
}) {
|
|
|
|
|
auto addrs{msg.contacts(cctype)};
|
|
|
|
|
std::move(addrs.begin(), addrs.end(),
|
|
|
|
|
std::back_inserter(contacts));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return contacts;
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-26 15:19:08 +01:00
|
|
|
|
Mu::Contacts
|
2022-05-01 00:10:07 +02:00
|
|
|
|
MimeMessage::contacts(Contact::Type ctype) const noexcept
|
2022-03-26 15:19:08 +01:00
|
|
|
|
{
|
2022-05-01 00:10:07 +02:00
|
|
|
|
/* special case: get all */
|
|
|
|
|
if (ctype == Contact::Type::None)
|
|
|
|
|
return all_contacts(*this);
|
|
|
|
|
|
|
|
|
|
const auto atype{address_type(ctype)};
|
|
|
|
|
if (!atype)
|
2022-03-26 15:19:08 +01:00
|
|
|
|
return {};
|
|
|
|
|
|
2022-05-01 00:10:07 +02:00
|
|
|
|
auto addrs{g_mime_message_get_addresses(self(), *atype)};
|
|
|
|
|
if (!addrs)
|
|
|
|
|
return {};
|
2022-03-26 15:19:08 +01:00
|
|
|
|
|
|
|
|
|
const auto msgtime{date().value_or(0)};
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2022-05-01 00:10:07 +02:00
|
|
|
|
contacts.emplace_back(email, name ? name : "", ctype, msgtime);
|
2022-03-26 15:19:08 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2022-06-10 22:27:11 +02:00
|
|
|
|
return seq_some(seq, [&](auto&& str) { return ref == str; });
|
2022-03-26 15:19:08 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
2022-05-01 00:10:07 +02:00
|
|
|
|
const auto msgid{g_mime_references_get_message_id(mime_refs, i)};
|
|
|
|
|
if (!msgid || is_dup(refs, msgid))
|
|
|
|
|
continue; // invalid or skip dups
|
|
|
|
|
|
|
|
|
|
refs.emplace_back(msgid);
|
2022-03-26 15:19:08 +01:00
|
|
|
|
}
|
|
|
|
|
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) {
|
2022-04-09 00:20:33 +02:00
|
|
|
|
auto cb_data{reinterpret_cast<CallbackData*>(user_data)};
|
|
|
|
|
cb_data->func(MimeObject{parent}, MimeObject{part});
|
2022-03-26 15:19:08 +01:00
|
|
|
|
}, &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
|
|
|
|
|
{
|
2022-04-18 20:56:49 +02:00
|
|
|
|
MimeDataWrapper wrapper{g_mime_part_get_content(self())};
|
2022-03-26 15:19:08 +01:00
|
|
|
|
if (!wrapper) /* this happens with invalid mails */
|
|
|
|
|
return Err(Error::Code::File, "failed to create data wrapper");
|
|
|
|
|
|
|
|
|
|
GError *err{};
|
2022-04-18 20:56:49 +02:00
|
|
|
|
auto strm{g_mime_stream_fs_open(path.c_str(),
|
|
|
|
|
O_WRONLY | O_CREAT | O_TRUNC |(overwrite ? 0 : O_EXCL),
|
|
|
|
|
S_IRUSR|S_IWUSR,
|
|
|
|
|
&err)};
|
|
|
|
|
if (!strm)
|
|
|
|
|
return Err(Error::Code::File, &err, "failed to open '%s'", path.c_str());
|
|
|
|
|
|
|
|
|
|
MimeStream stream{MimeStream::make_from_stream(strm)};
|
|
|
|
|
ssize_t written{g_mime_data_wrapper_write_to_stream(
|
|
|
|
|
GMIME_DATA_WRAPPER(wrapper.object()),
|
|
|
|
|
GMIME_STREAM(stream.object()))};
|
2022-03-26 15:19:08 +01:00
|
|
|
|
|
|
|
|
|
if (written < 0) {
|
|
|
|
|
return Err(Error::Code::File, &err,
|
|
|
|
|
"failed to write to '%s'", path.c_str());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Ok(static_cast<size_t>(written));
|
|
|
|
|
}
|
2022-04-09 00:20:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
void
|
|
|
|
|
MimeMultipart::for_each(const ForEachFunc& func) const noexcept
|
|
|
|
|
{
|
|
|
|
|
struct CallbackData { const ForEachFunc& func; };
|
|
|
|
|
CallbackData cbd{func};
|
2022-04-09 00:20:33 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
g_mime_multipart_foreach(
|
|
|
|
|
self(),
|
|
|
|
|
[] (GMimeObject *parent, GMimeObject *part, gpointer user_data) {
|
|
|
|
|
auto cb_data{reinterpret_cast<CallbackData*>(user_data)};
|
|
|
|
|
cb_data->func(MimeObject{parent}, MimeObject{part});
|
|
|
|
|
}, &cbd);
|
2022-04-09 00:20:33 +02:00
|
|
|
|
}
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
|
2022-04-10 12:18:55 +02:00
|
|
|
|
/*
|
|
|
|
|
* we need to be able to pass a crypto-context to the verify(), but
|
|
|
|
|
* g_mime_multipart_signed_verify() doesn't offer that anymore in GMime 3.x.
|
|
|
|
|
*
|
|
|
|
|
* So, add that by reimplementing it a bit (follow the upstream impl)
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static bool
|
|
|
|
|
mime_types_equal (const std::string& mime_type, const std::string& official_type)
|
|
|
|
|
{
|
2022-04-18 20:56:49 +02:00
|
|
|
|
if (g_ascii_strcasecmp(mime_type.c_str(), official_type.c_str()) == 0)
|
2022-04-10 12:18:55 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
const auto slash_pos = official_type.find("/");
|
|
|
|
|
if (slash_pos == std::string::npos || slash_pos == 0)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
/* If the official mime-type's subtype already begins with "x-", then there's
|
|
|
|
|
* nothing else to check. */
|
|
|
|
|
const auto subtype{official_type.substr(slash_pos + 1)};
|
|
|
|
|
if (g_ascii_strncasecmp (subtype.c_str(), "x-", 2) == 0)
|
|
|
|
|
return false;
|
|
|
|
|
const auto supertype{official_type.substr(0, slash_pos - 1)};
|
|
|
|
|
const auto xtype{official_type.substr(0, slash_pos - 1) + "x-" + subtype};
|
|
|
|
|
|
|
|
|
|
/* Check if the "x-" version of the official mime-type matches the
|
|
|
|
|
* supplied mime-type. For example, if the official mime-type is
|
|
|
|
|
* "application/pkcs7-signature", then we also want to match
|
|
|
|
|
* "application/x-pkcs7-signature". */
|
|
|
|
|
return g_ascii_strcasecmp(mime_type.c_str(), xtype.c_str()) == 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
/**
|
|
|
|
|
* A bit of a monster, this impl.
|
|
|
|
|
*
|
|
|
|
|
* It's the transliteration of the g_mime_multipart_signed_verify() which
|
|
|
|
|
* adds the feature of passing in the CryptoContext.
|
|
|
|
|
*
|
|
|
|
|
*/
|
|
|
|
|
Result<std::vector<MimeSignature>>
|
|
|
|
|
MimeMultipartSigned::verify(const MimeCryptoContext& ctx, VerifyFlags vflags) const noexcept
|
|
|
|
|
{
|
|
|
|
|
if (g_mime_multipart_get_count(GMIME_MULTIPART(self())) < 2)
|
|
|
|
|
return Err(Error::Code::Crypto, "cannot verify, not enough subparts");
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
const auto proto{content_type_parameter("protocol")};
|
|
|
|
|
const auto sign_proto{ctx.signature_protocol()};
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
if (!proto || !sign_proto || !mime_types_equal(*proto, *sign_proto))
|
|
|
|
|
return Err(Error::Code::Crypto, "unsupported protocol " +
|
|
|
|
|
proto.value_or("<unknown>"));
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
const auto sig{signed_signature_part()};
|
|
|
|
|
const auto content{signed_content_part()};
|
|
|
|
|
if (!sig || !content)
|
|
|
|
|
return Err(Error::Code::Crypto, "cannot find part");
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
const auto sig_mime_type{sig->mime_type()};
|
|
|
|
|
if (!sig || !mime_types_equal(sig_mime_type.value_or("<none>"), *sign_proto))
|
|
|
|
|
return Err(Error::Code::Crypto, "failed to find matching signature part");
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
MimeFormatOptions fopts{g_mime_format_options_new()};
|
|
|
|
|
g_mime_format_options_set_newline_format(fopts.get(), GMIME_NEWLINE_FORMAT_DOS);
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
MimeStream stream{MimeStream::make_mem()};
|
|
|
|
|
if (auto&& res = content->write_to_stream(fopts, stream); !res)
|
|
|
|
|
return Err(res.error());
|
|
|
|
|
stream.reset();
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
MimeDataWrapper wrapper{g_mime_part_get_content(GMIME_PART(sig->object()))};
|
|
|
|
|
MimeStream sigstream{MimeStream::make_mem()};
|
|
|
|
|
if (auto&& res = wrapper.write_to_stream(sigstream); !res)
|
|
|
|
|
return Err(res.error());
|
|
|
|
|
sigstream.reset();
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
GError *err{};
|
|
|
|
|
GMimeSignatureList *siglist{g_mime_crypto_context_verify(
|
|
|
|
|
GMIME_CRYPTO_CONTEXT(ctx.object()),
|
|
|
|
|
static_cast<GMimeVerifyFlags>(vflags),
|
|
|
|
|
GMIME_STREAM(stream.object()),
|
|
|
|
|
GMIME_STREAM(sigstream.object()),
|
|
|
|
|
{},
|
|
|
|
|
&err)};
|
|
|
|
|
if (!siglist)
|
|
|
|
|
return Err(Error::Code::Crypto, &err, "failed to verify");
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
std::vector<MimeSignature> sigs;
|
|
|
|
|
for (auto i = 0;
|
|
|
|
|
i != g_mime_signature_list_length(siglist); ++i) {
|
2022-05-06 21:09:50 +02:00
|
|
|
|
GMimeSignature *msig = g_mime_signature_list_get_signature(siglist, i);
|
|
|
|
|
sigs.emplace_back(MimeSignature(msig));
|
2022-04-18 20:56:49 +02:00
|
|
|
|
}
|
|
|
|
|
g_object_unref(siglist);
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
return sigs;
|
|
|
|
|
}
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<MimeCertificate>
|
|
|
|
|
MimeDecryptResult::recipients() const noexcept
|
|
|
|
|
{
|
|
|
|
|
GMimeCertificateList *lst{g_mime_decrypt_result_get_recipients(self())};
|
|
|
|
|
if (!lst)
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
std::vector<MimeCertificate> certs;
|
|
|
|
|
for (int i = 0; i != g_mime_certificate_list_length(lst); ++i)
|
|
|
|
|
certs.emplace_back(
|
|
|
|
|
MimeCertificate(
|
|
|
|
|
g_mime_certificate_list_get_certificate(lst, i)));
|
|
|
|
|
|
|
|
|
|
return certs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<MimeSignature>
|
|
|
|
|
MimeDecryptResult::signatures() const noexcept
|
|
|
|
|
{
|
|
|
|
|
GMimeSignatureList *lst{g_mime_decrypt_result_get_signatures(self())};
|
|
|
|
|
if (!lst)
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
std::vector<MimeSignature> sigs;
|
|
|
|
|
for (auto i = 0; i != g_mime_signature_list_length(lst); ++i) {
|
|
|
|
|
GMimeSignature *sig = g_mime_signature_list_get_signature(lst, i);
|
|
|
|
|
sigs.emplace_back(MimeSignature(sig));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sigs;
|
|
|
|
|
}
|
2022-04-18 20:56:49 +02:00
|
|
|
|
/**
|
|
|
|
|
* Like verify, a bit of a monster, this impl.
|
|
|
|
|
*
|
|
|
|
|
* It's the transliteration of the g_mime_multipart_encrypted_decrypt() which
|
|
|
|
|
* adds the feature of passing in the CryptoContext.
|
|
|
|
|
*
|
|
|
|
|
*/
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
|
|
|
|
Mu::Result<MimeMultipartEncrypted::Decrypted>
|
2022-04-18 20:56:49 +02:00
|
|
|
|
MimeMultipartEncrypted::decrypt(const MimeCryptoContext& ctx, DecryptFlags dflags,
|
2022-04-10 12:18:55 +02:00
|
|
|
|
const std::string& session_key) const noexcept
|
|
|
|
|
{
|
2022-04-18 20:56:49 +02:00
|
|
|
|
if (g_mime_multipart_get_count(GMIME_MULTIPART(self())) < 2)
|
|
|
|
|
return Err(Error::Code::Crypto, "cannot decrypted, not enough subparts");
|
|
|
|
|
|
|
|
|
|
const auto proto{content_type_parameter("protocol")};
|
|
|
|
|
const auto enc_proto{ctx.encryption_protocol()};
|
|
|
|
|
|
|
|
|
|
if (!proto || !enc_proto || !mime_types_equal(*proto, *enc_proto))
|
|
|
|
|
return Err(Error::Code::Crypto, "unsupported protocol " +
|
|
|
|
|
proto.value_or("<unknown>"));
|
|
|
|
|
|
|
|
|
|
const auto version{encrypted_version_part()};
|
|
|
|
|
const auto encrypted{encrypted_content_part()};
|
|
|
|
|
if (!version || !encrypted)
|
|
|
|
|
return Err(Error::Code::Crypto, "cannot find part");
|
|
|
|
|
|
|
|
|
|
if (!mime_types_equal(version->mime_type().value_or(""), proto.value()))
|
|
|
|
|
return Err(Error::Code::Crypto,
|
|
|
|
|
"cannot decrypt; unexpected version content-type '%s' != '%s'",
|
|
|
|
|
version->mime_type().value_or("").c_str(),
|
|
|
|
|
proto.value().c_str());
|
|
|
|
|
|
|
|
|
|
if (!mime_types_equal(encrypted->mime_type().value_or(""), "application/octet-stream"))
|
|
|
|
|
return Err(Error::Code::Crypto,
|
|
|
|
|
"cannot decrypt; unexpected encrypted content-type '%s'",
|
|
|
|
|
encrypted->mime_type().value_or("").c_str());
|
|
|
|
|
|
|
|
|
|
const auto content{encrypted->content()};
|
|
|
|
|
auto ciphertext{MimeStream::make_mem()};
|
|
|
|
|
content.write_to_stream(ciphertext);
|
|
|
|
|
ciphertext.reset();
|
|
|
|
|
|
|
|
|
|
auto stream{MimeStream::make_mem()};
|
|
|
|
|
auto filtered{MimeStream::make_filtered(stream)};
|
|
|
|
|
auto filter{g_mime_filter_dos2unix_new(FALSE)};
|
|
|
|
|
g_mime_stream_filter_add(GMIME_STREAM_FILTER(filtered.object()),
|
|
|
|
|
filter);
|
|
|
|
|
g_object_unref(filter);
|
|
|
|
|
|
2022-04-10 12:18:55 +02:00
|
|
|
|
GError *err{};
|
2022-04-18 20:56:49 +02:00
|
|
|
|
GMimeDecryptResult *dres =
|
|
|
|
|
g_mime_crypto_context_decrypt(GMIME_CRYPTO_CONTEXT(ctx.object()),
|
|
|
|
|
static_cast<GMimeDecryptFlags>(dflags),
|
|
|
|
|
session_key.empty() ?
|
|
|
|
|
NULL : session_key.c_str(),
|
|
|
|
|
GMIME_STREAM(ciphertext.object()),
|
|
|
|
|
GMIME_STREAM(filtered.object()),
|
|
|
|
|
&err);
|
|
|
|
|
if (!dres)
|
|
|
|
|
return Err(Error::Code::Crypto, &err, "decryption failed");
|
|
|
|
|
|
|
|
|
|
filtered.flush();
|
|
|
|
|
stream.reset();
|
|
|
|
|
|
|
|
|
|
auto parser{g_mime_parser_new()};
|
|
|
|
|
g_mime_parser_init_with_stream(parser, GMIME_STREAM(stream.object()));
|
|
|
|
|
|
|
|
|
|
auto decrypted{g_mime_parser_construct_part(parser, NULL)};
|
|
|
|
|
g_object_unref(parser);
|
|
|
|
|
if (!decrypted) {
|
|
|
|
|
g_object_unref(dres);
|
|
|
|
|
return Err(Error::Code::Crypto, "failed to parse decrypted part");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Decrypted result = { MimeObject{decrypted}, MimeDecryptResult{dres} };
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
g_object_unref(decrypted);
|
|
|
|
|
g_object_unref(dres);
|
2022-04-10 12:18:55 +02:00
|
|
|
|
|
2022-04-18 20:56:49 +02:00
|
|
|
|
return Ok(std::move(result));
|
2022-04-10 12:18:55 +02:00
|
|
|
|
}
|