Take an optional index parameter for a *subpart*. This is for the case where we save attachments from a message (in particular, when forwarding). We can't save them in the same directory for the (rare) case when there are multiple attachments with the same name. And we don't want to uniquify the name, since that shows up in e.g. the forwarded file name. This can be solved by saving each in their own indexed subdir.
839 lines
20 KiB
C++
839 lines
20 KiB
C++
/*
|
|
** 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.hh"
|
|
#include "gmime/gmime-references.h"
|
|
#include "gmime/gmime-stream-mem.h"
|
|
#include "mu-maildir.hh"
|
|
|
|
#include <array>
|
|
#include <string>
|
|
#include <regex>
|
|
#include <utils/mu-util.h>
|
|
#include <utils/mu-utils.hh>
|
|
#include <utils/mu-error.hh>
|
|
#include <utils/mu-option.hh>
|
|
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <cstdlib>
|
|
|
|
#include <glib.h>
|
|
#include <glib/gstdio.h>
|
|
#include <gmime/gmime.h>
|
|
|
|
#include "gmime/gmime-message.h"
|
|
#include "mu-mime-object.hh"
|
|
|
|
using namespace Mu;
|
|
|
|
struct Message::Private {
|
|
Private(Message::Options options): opts{options} {}
|
|
|
|
Message::Options opts;
|
|
Document doc;
|
|
mutable Option<MimeMessage> mime_msg;
|
|
|
|
Flags flags{};
|
|
Option<std::string> mailing_list;
|
|
std::vector<Part> parts;
|
|
|
|
::time_t ctime{};
|
|
|
|
std::string cache_path;
|
|
/*
|
|
* we only need to index these, so we don't
|
|
* really need these copy if we re-arrange things
|
|
* a bit
|
|
*/
|
|
Option<std::string> body_txt;
|
|
Option<std::string> body_html;
|
|
Option<std::string> embedded;
|
|
};
|
|
|
|
|
|
static void fill_document(Message::Private& priv);
|
|
|
|
static Result<struct stat>
|
|
get_statbuf(const std::string& path)
|
|
{
|
|
if (!g_path_is_absolute(path.c_str()))
|
|
return Err(Error::Code::File, "path '%s' is not absolute",
|
|
path.c_str());
|
|
if (::access(path.c_str(), R_OK) != 0)
|
|
return Err(Error::Code::File, "file @ '%s' is not readable",
|
|
path.c_str());
|
|
|
|
struct stat statbuf{};
|
|
if (::stat(path.c_str(), &statbuf) < 0)
|
|
return Err(Error::Code::File, "cannot stat %s: %s", path.c_str(),
|
|
g_strerror(errno));
|
|
|
|
if (!S_ISREG(statbuf.st_mode))
|
|
return Err(Error::Code::File, "not a regular file: %s", path.c_str());
|
|
|
|
return Ok(std::move(statbuf));
|
|
}
|
|
|
|
|
|
Message::Message(const std::string& path, Message::Options opts):
|
|
priv_{std::make_unique<Private>(opts)}
|
|
{
|
|
const auto statbuf{get_statbuf(path)};
|
|
if (!statbuf)
|
|
throw statbuf.error();
|
|
|
|
priv_->ctime = statbuf->st_ctime;
|
|
|
|
init_gmime();
|
|
if (auto msg{MimeMessage::make_from_file(path)}; !msg)
|
|
throw msg.error();
|
|
else
|
|
priv_->mime_msg = std::move(msg.value());
|
|
|
|
auto xpath{to_string_opt_gchar(g_canonicalize_filename(path.c_str(), NULL))};
|
|
if (xpath)
|
|
priv_->doc.add(Field::Id::Path, std::move(xpath.value()));
|
|
|
|
priv_->doc.add(Field::Id::Size, static_cast<int64_t>(statbuf->st_size));
|
|
|
|
// rest of the fields
|
|
fill_document(*priv_);
|
|
}
|
|
|
|
Message::Message(const std::string& text, const std::string& path,
|
|
Message::Options opts):
|
|
priv_{std::make_unique<Private>(opts)}
|
|
{
|
|
if (!path.empty()) {
|
|
auto xpath{to_string_opt_gchar(g_canonicalize_filename(path.c_str(), {}))};
|
|
if (xpath)
|
|
priv_->doc.add(Field::Id::Path, std::move(xpath.value()));
|
|
}
|
|
|
|
priv_->doc.add(Field::Id::Size, static_cast<int64_t>(text.size()));
|
|
|
|
init_gmime();
|
|
if (auto msg{MimeMessage::make_from_text(text)}; !msg)
|
|
throw msg.error();
|
|
else
|
|
priv_->mime_msg = std::move(msg.value());
|
|
|
|
fill_document(*priv_);
|
|
}
|
|
|
|
|
|
Message::Message(Message&& other) noexcept
|
|
{
|
|
*this = std::move(other);
|
|
}
|
|
|
|
Message&
|
|
Message::operator=(Message&& other) noexcept
|
|
{
|
|
if (this != &other)
|
|
priv_ = std::move(other.priv_);
|
|
|
|
return *this;
|
|
}
|
|
|
|
Message::Message(Document&& doc):
|
|
priv_{std::make_unique<Private>(Message::Options::None)}
|
|
{
|
|
priv_->doc = std::move(doc);
|
|
}
|
|
|
|
|
|
Message::~Message() = default;
|
|
|
|
const Mu::Document&
|
|
Message::document() const
|
|
{
|
|
return priv_->doc;
|
|
}
|
|
|
|
|
|
unsigned
|
|
Message::docid() const
|
|
{
|
|
return priv_->doc.xapian_document().get_docid();
|
|
}
|
|
|
|
|
|
const Mu::Sexp::List&
|
|
Message::to_sexp_list() const
|
|
{
|
|
return priv_->doc.sexp_list();
|
|
}
|
|
|
|
void
|
|
Message::update_cached_sexp()
|
|
{
|
|
priv_->doc.update_cached_sexp();
|
|
}
|
|
|
|
Result<void>
|
|
Message::set_maildir(const std::string& maildir)
|
|
{
|
|
/* sanity check a little bit */
|
|
|
|
if (maildir.empty() ||
|
|
maildir.at(0) != '/' ||
|
|
(maildir.size() > 1 && maildir.at(maildir.length()-1) == '/'))
|
|
return Err(Error::Code::Message,
|
|
"'%s' is not a valid maildir", maildir.c_str());
|
|
|
|
const auto path{document().string_value(Field::Id::Path)};
|
|
if (path == maildir || path.find(maildir) == std::string::npos)
|
|
return Err(Error::Code::Message,
|
|
"'%s' is not a valid maildir for message @ %s",
|
|
maildir.c_str(), path.c_str());
|
|
|
|
priv_->doc.remove(Field::Id::Maildir);
|
|
priv_->doc.add(Field::Id::Maildir, maildir);
|
|
|
|
return Ok();
|
|
}
|
|
|
|
void
|
|
Message::set_flags(Flags flags)
|
|
{
|
|
priv_->doc.remove(Field::Id::Flags);
|
|
priv_->doc.add(flags);
|
|
}
|
|
|
|
bool
|
|
Message::load_mime_message(bool reload) const
|
|
{
|
|
if (priv_->mime_msg && !reload)
|
|
return true;
|
|
|
|
const auto path{document().string_value(Field::Id::Path)};
|
|
if (auto mime_msg{MimeMessage::make_from_file(path)}; !mime_msg) {
|
|
g_warning("failed to load '%s': %s",
|
|
path.c_str(), mime_msg.error().what());
|
|
return false;
|
|
} else {
|
|
priv_->mime_msg = std::move(mime_msg.value());
|
|
fill_document(*priv_);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void
|
|
Message::unload_mime_message() const
|
|
{
|
|
priv_->mime_msg = Nothing;
|
|
}
|
|
|
|
bool
|
|
Message::has_mime_message() const
|
|
{
|
|
return !!priv_->mime_msg;
|
|
}
|
|
|
|
|
|
static Priority
|
|
get_priority(const MimeMessage& mime_msg)
|
|
{
|
|
constexpr std::array<std::pair<std::string_view, Priority>, 10>
|
|
prio_alist = {{
|
|
{"high", Priority::High},
|
|
{"1", Priority::High},
|
|
{"2", Priority::High},
|
|
|
|
{"normal", Priority::Normal},
|
|
{"3", Priority::Normal},
|
|
|
|
{"low", Priority::Low},
|
|
{"list", Priority::Low},
|
|
{"bulk", Priority::Low},
|
|
{"4", Priority::Low},
|
|
{"5", Priority::Low}
|
|
}};
|
|
|
|
const auto opt_str = mime_msg.header("Precedence")
|
|
.disjunction(mime_msg.header("X-Priority"))
|
|
.disjunction(mime_msg.header("Importance"));
|
|
|
|
if (!opt_str)
|
|
return Priority::Normal;
|
|
|
|
const auto it = seq_find_if(prio_alist, [&](auto&& item) {
|
|
return g_ascii_strncasecmp(item.first.data(), opt_str->c_str(),
|
|
item.first.size()) == 0; });
|
|
|
|
return it == prio_alist.cend() ? Priority::Normal : it->second;
|
|
}
|
|
|
|
|
|
/* see: http://does-not-exist.org/mail-archives/mutt-dev/msg08249.html */
|
|
static std::vector<std::string>
|
|
extract_tags(const MimeMessage& mime_msg)
|
|
{
|
|
constexpr std::array<std::pair<const char*, char>, 3> tag_headers = {{
|
|
{"X-Label", ' '}, {"X-Keywords", ','}, {"Keywords", ' '}
|
|
}};
|
|
static const auto strip_rx{std::regex("^\\s+| +$|( )\\s+")};
|
|
|
|
std::vector<std::string> tags;
|
|
seq_for_each(tag_headers, [&](auto&& item) {
|
|
if (auto&& hdr = mime_msg.header(item.first); hdr) {
|
|
for (auto&& tagval : split(*hdr, item.second)) {
|
|
tags.emplace_back(
|
|
std::regex_replace(tagval, strip_rx, "$1"));
|
|
}
|
|
}
|
|
});
|
|
|
|
return tags;
|
|
}
|
|
|
|
static Option<std::string>
|
|
get_mailing_list(const MimeMessage& mime_msg)
|
|
{
|
|
char *dechdr, *res;
|
|
const char *b, *e;
|
|
|
|
const auto hdr{mime_msg.header("List-Id")};
|
|
if (!hdr)
|
|
return {};
|
|
|
|
dechdr = g_mime_utils_header_decode_phrase(NULL, hdr->c_str());
|
|
if (!dechdr)
|
|
return {};
|
|
|
|
e = NULL;
|
|
b = ::strchr(dechdr, '<');
|
|
if (b)
|
|
e = strchr(b, '>');
|
|
|
|
if (b && e)
|
|
res = g_strndup(b + 1, e - b - 1);
|
|
else
|
|
res = g_strdup(dechdr);
|
|
|
|
g_free(dechdr);
|
|
|
|
return to_string_opt_gchar(std::move(res));
|
|
}
|
|
|
|
static bool /* heuristic */
|
|
looks_like_attachment(const MimeObject& parent,
|
|
const MimePart& part, const MimeContentType& ctype)
|
|
{
|
|
constexpr std::array<std::pair<const char*, const char*>, 4> att_types = {{
|
|
{"image", "*"},
|
|
{"audio", "*"},
|
|
{"application", "*"},
|
|
{"application", "x-patch"}
|
|
}};
|
|
|
|
if (parent) { /* crypto multipart children are not considered attachments */
|
|
if (const auto parent_ctype{parent.content_type()}; parent_ctype) {
|
|
if (parent_ctype->is_type("multipart", "signed") ||
|
|
parent_ctype->is_type("multipart", "encrypted"))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* we also consider patches, images, audio, and non-pgp-signature
|
|
* application attachments to be attachments... */
|
|
if (ctype.is_type("*", "pgp-signature"))
|
|
return false; /* don't consider as a signature */
|
|
|
|
if (ctype.is_type("text", "*") &&
|
|
(ctype.is_type("*", "plain") || ctype.is_type("*", "html")))
|
|
return false; /* not a signature */
|
|
|
|
/* if not one of those special types, consider it any attachment
|
|
* if it says so */
|
|
if (part.is_attachment())
|
|
return true;
|
|
|
|
const auto it = seq_find_if(att_types, [&](auto&& item){
|
|
return ctype.is_type(item.first, item.second);
|
|
});
|
|
return it != att_types.cend(); /* if found, it's an attachment */
|
|
}
|
|
|
|
static void
|
|
append_text(Option<std::string>& str, Option<std::string> app)
|
|
{
|
|
if (!str)
|
|
str = app;
|
|
else if (app)
|
|
str.value() += app.value();
|
|
}
|
|
|
|
static void
|
|
accumulate_text(const MimePart& part, Message::Private& info,
|
|
const MimeContentType& ctype)
|
|
{
|
|
if (!ctype.is_type("text", "*"))
|
|
return; /* not a text type */
|
|
|
|
if (part.is_attachment())
|
|
append_text(info.embedded, part.to_string());
|
|
else if (ctype.is_type("text", "plain"))
|
|
append_text(info.body_txt, part.to_string());
|
|
else if (ctype.is_type("text", "html"))
|
|
append_text(info.body_html, part.to_string());
|
|
}
|
|
|
|
static void
|
|
process_part(const MimeObject& parent, const MimePart& part,
|
|
Message::Private& info)
|
|
{
|
|
const auto ctype{part.content_type()};
|
|
if (!ctype)
|
|
return;
|
|
|
|
if (looks_like_attachment(parent, part, *ctype))
|
|
info.flags |= Flags::HasAttachment;
|
|
|
|
// if there are text parts, gather.
|
|
accumulate_text(part, info, *ctype);
|
|
}
|
|
|
|
|
|
static void
|
|
process_message_part(const MimeMessagePart& msg_part,
|
|
Message::Private& info)
|
|
{
|
|
auto submsg{msg_part.get_message()};
|
|
if (!submsg)
|
|
return;
|
|
|
|
submsg->for_each([&](auto&& parent, auto&& child_obj) {
|
|
|
|
/* XXX: we only handle one level */
|
|
|
|
if (!child_obj.is_part())
|
|
return;
|
|
|
|
const auto ctype{child_obj.content_type()};
|
|
if (!ctype || !ctype->is_type("text", "*"))
|
|
return;
|
|
|
|
append_text(info.embedded, MimePart{child_obj}.to_string());
|
|
});
|
|
}
|
|
|
|
static void
|
|
handle_object(const MimeObject& parent,
|
|
const MimeObject& obj, Message::Private& info);
|
|
|
|
|
|
static void
|
|
handle_encrypted(const MimeMultipartEncrypted& part, Message::Private& info)
|
|
{
|
|
if (!any_of(info.opts & Message::Options::Decrypt)) {
|
|
/* just added to the list */
|
|
info.parts.emplace_back(part);
|
|
return;
|
|
}
|
|
|
|
const auto proto{part.content_type_parameter("protocol").value_or("unknown")};
|
|
const auto ctx = MimeCryptoContext::make(proto);
|
|
if (!ctx) {
|
|
g_warning("failed to create context for protocol <%s>",
|
|
proto.c_str());
|
|
return;
|
|
}
|
|
|
|
auto res{part.decrypt(*ctx)};
|
|
if (!res) {
|
|
g_warning("failed to decrypt: %s", res.error().what());
|
|
return;
|
|
}
|
|
|
|
if (res->first.is_multipart()) {
|
|
MimeMultipart{res->first}.for_each(
|
|
[&](auto&& parent, auto&& child_obj) {
|
|
handle_object(parent, child_obj, info);
|
|
});
|
|
|
|
} else
|
|
handle_object(part, res->first, info);
|
|
}
|
|
|
|
|
|
static void
|
|
handle_object(const MimeObject& parent,
|
|
const MimeObject& obj, Message::Private& info)
|
|
{
|
|
/* if it's an encrypted part we should decrypt, recurse */
|
|
if (obj.is_multipart_encrypted())
|
|
handle_encrypted(MimeMultipartEncrypted{obj}, info);
|
|
else if (obj.is_part() ||
|
|
obj.is_message_part() ||
|
|
obj.is_multipart_signed() ||
|
|
obj.is_multipart_encrypted())
|
|
info.parts.emplace_back(obj);
|
|
|
|
if (obj.is_part())
|
|
process_part(parent, obj, info);
|
|
else if (obj.is_message_part())
|
|
process_message_part(obj, info);
|
|
else if (obj.is_multipart_signed())
|
|
info.flags |= Flags::Signed;
|
|
else if (obj.is_multipart_encrypted()) {
|
|
/* FIXME: An encrypted part might be signed at the same time.
|
|
* In that case the signed flag is lost. */
|
|
info.flags |= Flags::Encrypted;
|
|
} else if (obj.is_mime_application_pkcs7_mime()) {
|
|
MimeApplicationPkcs7Mime smime(obj);
|
|
switch (smime.smime_type()) {
|
|
case Mu::MimeApplicationPkcs7Mime::SecureMimeType::SignedData:
|
|
info.flags |= Flags::Signed;
|
|
break;
|
|
case Mu::MimeApplicationPkcs7Mime::SecureMimeType::EnvelopedData:
|
|
info.flags |= Flags::Encrypted;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This message -- recursively walk through message, and initialize some
|
|
* other values that depend on another.
|
|
*
|
|
* @param mime_msg
|
|
* @param path
|
|
* @param info
|
|
*/
|
|
static void
|
|
process_message(const MimeMessage& mime_msg, const std::string& path,
|
|
Message::Private& info)
|
|
{
|
|
/* only have file-flags when there's a path. */
|
|
if (!path.empty()) {
|
|
info.flags = flags_from_path(path).value_or(Flags::None);
|
|
/* pseudo-flag --> unread means either NEW or NOT SEEN, just
|
|
* for searching convenience */
|
|
if (any_of(info.flags & Flags::New) || none_of(info.flags & Flags::Seen))
|
|
info.flags |= Flags::Unread;
|
|
}
|
|
|
|
// parts
|
|
mime_msg.for_each([&](auto&& parent, auto&& child_obj) {
|
|
handle_object(parent, child_obj, info);
|
|
});
|
|
|
|
// get the mailing here, and use it do update flags, too.
|
|
info.mailing_list = get_mailing_list(mime_msg);
|
|
if (info.mailing_list)
|
|
info.flags |= Flags::MailingList;
|
|
}
|
|
|
|
static Mu::Result<std::string>
|
|
calculate_sha256(const std::string& path)
|
|
{
|
|
g_autoptr(GChecksum) checksum{g_checksum_new(G_CHECKSUM_SHA256)};
|
|
|
|
FILE *file{::fopen(path.c_str(), "r")};
|
|
if (!file)
|
|
return Err(Error{Error::Code::File, "failed to open %s: %s",
|
|
path.c_str(), ::strerror(errno)});
|
|
|
|
std::array<uint8_t, 4096> buf{};
|
|
while (true) {
|
|
const auto n = ::fread(buf.data(), 1, buf.size(), file);
|
|
if (n == 0)
|
|
break;
|
|
g_checksum_update(checksum, buf.data(), n);
|
|
}
|
|
|
|
bool has_err = ::ferror(file) != 0;
|
|
::fclose(file);
|
|
|
|
if (has_err)
|
|
return Err(Error{Error::Code::File, "failed to read %s", path.c_str()});
|
|
|
|
return Ok(g_checksum_get_string(checksum));
|
|
}
|
|
|
|
/**
|
|
* Get a fake-message-id for a message without one.
|
|
*
|
|
* @param path message path
|
|
*
|
|
* @return a fake message-id
|
|
*/
|
|
static std::string
|
|
fake_message_id(const std::string& path)
|
|
{
|
|
constexpr auto mu_suffix{"@mu.id"};
|
|
|
|
// not a very good message-id, only for testing.
|
|
if (path.empty() || ::access(path.c_str(), R_OK) != 0)
|
|
return format("%08x%s", g_str_hash(path.c_str()), mu_suffix);
|
|
if (const auto sha256_res{calculate_sha256(path)}; !sha256_res)
|
|
return format("%08x%s", g_str_hash(path.c_str()), mu_suffix);
|
|
else
|
|
return format("%s%s", sha256_res.value().c_str(), mu_suffix);
|
|
}
|
|
|
|
/* many of the doc.add(fiels ....) automatically update the sexp-list as well;
|
|
* however, there are some _extra_ values in the sexp-list that are not
|
|
* based on a field. So we add them here.
|
|
*/
|
|
|
|
|
|
|
|
static void
|
|
doc_add_list_post(Document& doc, const MimeMessage& mime_msg)
|
|
{
|
|
/* some mailing lists do not set the reply-to; see pull #1278. So for
|
|
* those cases, check the List-Post address and use that instead */
|
|
|
|
GMatchInfo* minfo;
|
|
GRegex* rx;
|
|
const auto list_post{mime_msg.header("List-Post")};
|
|
if (!list_post)
|
|
return;
|
|
|
|
rx = g_regex_new("<?mailto:([a-z0-9!@#$%&'*+-/=?^_`{|}~]+)>?",
|
|
G_REGEX_CASELESS, (GRegexMatchFlags)0, {});
|
|
g_return_if_fail(rx);
|
|
|
|
Contacts contacts;
|
|
if (g_regex_match(rx, list_post->c_str(), (GRegexMatchFlags)0, &minfo)) {
|
|
auto address = (char*)g_match_info_fetch(minfo, 1);
|
|
contacts.push_back(Contact(address));
|
|
g_free(address);
|
|
}
|
|
|
|
g_match_info_free(minfo);
|
|
g_regex_unref(rx);
|
|
|
|
doc.add_extra_contacts(":list-post", contacts);
|
|
}
|
|
|
|
static void
|
|
doc_add_reply_to(Document& doc, const MimeMessage& mime_msg)
|
|
{
|
|
doc.add_extra_contacts(":reply-to", mime_msg.contacts(Contact::Type::ReplyTo));
|
|
}
|
|
|
|
static void
|
|
fill_document(Message::Private& priv)
|
|
{
|
|
/* hunt & gather info from message tree */
|
|
Document& doc{priv.doc};
|
|
MimeMessage& mime_msg{priv.mime_msg.value()};
|
|
|
|
const auto path{doc.string_value(Field::Id::Path)};
|
|
const auto refs{mime_msg.references()};
|
|
const auto message_id{mime_msg.message_id().value_or(fake_message_id(path))};
|
|
|
|
process_message(mime_msg, path, priv);
|
|
|
|
doc_add_list_post(doc, mime_msg); /* only in sexp */
|
|
doc_add_reply_to(doc, mime_msg); /* only in sexp */
|
|
|
|
field_for_each([&](auto&& field) {
|
|
/* insist on expliclity handling each */
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic error "-Wswitch"
|
|
switch(field.id) {
|
|
case Field::Id::Bcc:
|
|
doc.add(field.id, mime_msg.contacts(Contact::Type::Bcc));
|
|
break;
|
|
case Field::Id::BodyText:
|
|
doc.add(field.id, priv.body_txt);
|
|
|
|
break;
|
|
case Field::Id::Cc:
|
|
doc.add(field.id, mime_msg.contacts(Contact::Type::Cc));
|
|
break;
|
|
case Field::Id::Changed:
|
|
doc.add(field.id, priv.ctime);
|
|
break;
|
|
case Field::Id::Date:
|
|
doc.add(field.id, mime_msg.date());
|
|
break;
|
|
case Field::Id::EmbeddedText:
|
|
doc.add(field.id, priv.embedded);
|
|
break;
|
|
case Field::Id::File:
|
|
for (auto&& part: priv.parts)
|
|
doc.add(field.id, part.raw_filename());
|
|
break;
|
|
case Field::Id::Flags:
|
|
doc.add(priv.flags);
|
|
break;
|
|
case Field::Id::From:
|
|
doc.add(field.id, mime_msg.contacts(Contact::Type::From));
|
|
break;
|
|
case Field::Id::Maildir: /* already */
|
|
break;
|
|
case Field::Id::MailingList:
|
|
doc.add(field.id, priv.mailing_list);
|
|
break;
|
|
case Field::Id::MessageId:
|
|
doc.add(field.id, message_id);
|
|
break;
|
|
case Field::Id::MimeType:
|
|
for (auto&& part: priv.parts)
|
|
doc.add(field.id, part.mime_type());
|
|
break;
|
|
case Field::Id::Path: /* already */
|
|
break;
|
|
case Field::Id::Priority:
|
|
doc.add(get_priority(mime_msg));
|
|
break;
|
|
case Field::Id::References:
|
|
if (!refs.empty())
|
|
doc.add(field.id, refs);
|
|
break;
|
|
case Field::Id::Size: /* already */
|
|
break;
|
|
case Field::Id::Subject:
|
|
doc.add(field.id, mime_msg.subject());
|
|
break;
|
|
case Field::Id::Tags:
|
|
if (auto&& tags{extract_tags(mime_msg)}; !tags.empty())
|
|
doc.add(field.id, tags);
|
|
break;
|
|
case Field::Id::ThreadId:
|
|
// either the oldest reference, or otherwise the message id
|
|
doc.add(field.id, refs.empty() ? message_id : refs.at(0));
|
|
break;
|
|
case Field::Id::To:
|
|
doc.add(field.id, mime_msg.contacts(Contact::Type::To));
|
|
break;
|
|
/* internal fields */
|
|
case Field::Id::XBodyHtml:
|
|
doc.add(field.id, priv.body_html);
|
|
break;
|
|
/* ignore */
|
|
case Field::Id::_count_:
|
|
break;
|
|
}
|
|
#pragma GCC diagnostic pop
|
|
|
|
});
|
|
}
|
|
|
|
Option<std::string>
|
|
Message::header(const std::string& header_field) const
|
|
{
|
|
if (!load_mime_message())
|
|
return Nothing;
|
|
|
|
return priv_->mime_msg->header(header_field);
|
|
}
|
|
|
|
Option<std::string>
|
|
Message::body_text() const
|
|
{
|
|
if (!load_mime_message())
|
|
return {};
|
|
|
|
return priv_->body_txt;
|
|
}
|
|
|
|
Option<std::string>
|
|
Message::body_html() const
|
|
{
|
|
if (!load_mime_message())
|
|
return {};
|
|
|
|
return priv_->body_html;
|
|
}
|
|
|
|
Contacts
|
|
Message::all_contacts() const
|
|
{
|
|
Contacts contacts;
|
|
|
|
if (!load_mime_message())
|
|
return contacts; /* empty */
|
|
|
|
return priv_->mime_msg->contacts(Contact::Type::None); /* get all types */
|
|
}
|
|
|
|
const std::vector<Message::Part>&
|
|
Message::parts() const
|
|
{
|
|
if (!load_mime_message()) {
|
|
static std::vector<Message::Part> empty;
|
|
return empty;
|
|
}
|
|
|
|
return priv_->parts;
|
|
}
|
|
|
|
Result<std::string>
|
|
Message::cache_path(Option<size_t> index) const
|
|
{
|
|
/* create tmpdir for this message, if needed */
|
|
if (priv_->cache_path.empty()) {
|
|
GError *err{};
|
|
auto tpath{to_string_opt_gchar(g_dir_make_tmp("mu-cache-XXXXXX", &err))};
|
|
if (!tpath)
|
|
return Err(Error::Code::File, &err, "failed to create temp dir");
|
|
|
|
priv_->cache_path = std::move(tpath.value());
|
|
}
|
|
|
|
if (index) {
|
|
GError *err{};
|
|
auto tpath = format("%s/%zu", priv_->cache_path.c_str(), *index);
|
|
if (g_mkdir(tpath.c_str(), 0700) != 0)
|
|
return Err(Error::Code::File, &err,
|
|
"failed to create cache dir '%s'; err=%d",
|
|
tpath.c_str(), errno);
|
|
return Ok(std::move(tpath));
|
|
} else
|
|
|
|
return Ok(std::string{priv_->cache_path});
|
|
}
|
|
|
|
|
|
Result<void>
|
|
Message::update_after_move(const std::string& new_path,
|
|
const std::string& new_maildir,
|
|
Flags new_flags)
|
|
{
|
|
const auto statbuf{get_statbuf(new_path)};
|
|
if (!statbuf)
|
|
return Err(statbuf.error());
|
|
else
|
|
priv_->ctime = statbuf->st_ctime;
|
|
|
|
priv_->doc.remove(Field::Id::Path);
|
|
priv_->doc.remove(Field::Id::Changed);
|
|
|
|
priv_->doc.add(Field::Id::Path, new_path);
|
|
priv_->doc.add(Field::Id::Changed, priv_->ctime);
|
|
|
|
set_flags(new_flags);
|
|
|
|
if (const auto res = set_maildir(new_maildir); !res)
|
|
return res;
|
|
|
|
return Ok();
|
|
}
|