mu/lib/message/mu-mime-object.cc

629 lines
15 KiB
C++
Raw Normal View History

/*
** 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 "utils/mu-utils.hh"
#include <mutex>
#include <fcntl.h>
#include <errno.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;
}
/*
* MimeCryptoContext
*/
Result<size_t>
MimeCryptoContext::import_keys(MimeStream& stream)
{
GError *err{};
auto res = g_mime_crypto_context_import_keys(
self(), stream.self(), &err);
if (res < 0)
return Err(Error::Code::File, &err,
"error importing keys");
return Ok(static_cast<size_t>(res));
}
void
MimeCryptoContext::set_password_request_function(PasswordRequestFunc pw_func)
{
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 {
MimeStream mstream{response};
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");
if (g_mkdir_with_parents((testpath + "/.gnupg").c_str(), 0700) != 0)
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();
}
/*
* 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_text(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 cb_data{reinterpret_cast<CallbackData*>(user_data)};
cb_data->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));
}
Result<std::vector<MimeSignature>>
MimeMultipartSigned::verify(VerifyFlags vflags) const noexcept
{
GError *err{};
GMimeSignatureList *siglist = g_mime_multipart_signed_verify(
self(),
static_cast<GMimeVerifyFlags>(vflags),
&err);
if (!siglist)
return Err(Error::Code::Crypto, &err, "failed to verify");
std::vector<MimeSignature> sigs;
for (auto i = 0; i != g_mime_signature_list_length(siglist); ++i) {
GMimeSignature *sig = g_mime_signature_list_get_signature(siglist, i);
sigs.emplace_back(MimeSignature(sig));
}
g_object_unref(siglist);
return sigs;
}
/*
* 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)
{
if (g_ascii_strcasecmp(mime_type.c_str(), official_type.c_str()))
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;
}
// Result<std::vector<MimeSignature>>
// MimeMultipartSigned::verify(VerifyFlags vflags, const CryptoContext& ctx) const noexcept
// {
// if (g_mime_multipart_get_count(self()) < 2)
// return Err(Error::Code::Crypto, "cannot verify, not enough subparts");
// const auto proto{content_type_parameter("protocol")};
// const auto sign_proto{ctx.signature_prototol().value_or("<unknown>")};
// if (!proto || !sign_proto || !mime_types_equal(*proto, sign_proto))
// return Err(Error::Code::Crypto, "unsupported protocol");
// const auto sig{MimeObject(g_mime_multipart_get_part(self(), GMIME_MULTIPART_SIGNED_SIGNATURE))};
// if (!sig || !mime_types_equal(sig.mime_type().value_or("<none>"), sign_proto))
// return Err(Error::Code::Crypto, "failed to find matching signature part");
// MimeObject content{g_mime_multipart_get_part(self(), GMIME_MULTIPART_SIGNED_CONTENT)};
// if (!content)
// return Err(Error::Code::Crypto, "cannot find content part");
// MimeFormatOptions fopts{format_options_new()};
// g_mime_format_options_set_newline_format(*fopts, GMIME_NEWLINE_FORMAT_DOS);
// MimeStream stream{g_mime_stream_mem_new()};
// g_mime_object_write_to_stream (content, *fopts, stream);
// g_mime_stream_reset (stream);
// GMimeDataWrapper *wrapper = g_mime_part_get_content(static_cast<GMimePart*>(sig));
// GError *err{};
// GMimeSignatureList *siglist = g_mime_multipart_signed_verify(
// self(),
// static_cast<GMimeVerifyFlags>(vflags),
// &err);
// if (!siglist)
// return Err(Error::Code::Crypto, &err, "failed to verify");
// std::vector<MimeSignature> sigs;
// for (auto i = 0; i != g_mime_signature_list_length(siglist); ++i) {
// GMimeSignature *sig = g_mime_signature_list_get_signature(siglist, i);
// sigs.emplace_back(MimeSignature(sig));
// }
// g_object_unref(siglist);
// return sigs;
// }
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;
}
Mu::Result<MimeMultipartEncrypted::Decrypted>
MimeMultipartEncrypted::decrypt(DecryptFlags flags,
const std::string& session_key) const noexcept
{
GError *err{};
GMimeDecryptResult *dres{};
GMimeObject *obj = g_mime_multipart_encrypted_decrypt(
self(),
static_cast<GMimeDecryptFlags>(flags),
session_key.empty() ? NULL : session_key.c_str(),
&dres,
&err);
if (!obj)
return Err(Error::Code::Crypto, &err, "failed to decrypt");
return Ok(Decrypted{MimeObject{obj}, MimeDecryptResult(dres)});
}