mu-scm: implement mime-part handling, refact

Implement accessing the MIME-parts + docs  + test.

Implement saving attachments to file.

Implement creating messages from files.

Refactor / rename functions to be more uniform.
This commit is contained in:
Dirk-Jan C. Binnema
2025-07-02 19:02:33 +03:00
parent 54ec919e8f
commit b02aa57686
10 changed files with 671 additions and 90 deletions

View File

@ -176,6 +176,7 @@ if not get_option('tests').disabled()
config_h_data.set_quoted('LN_PROGRAM', ln.full_path()) config_h_data.set_quoted('LN_PROGRAM', ln.full_path())
testmaildir=join_paths(meson.current_source_dir(), 'testdata') testmaildir=join_paths(meson.current_source_dir(), 'testdata')
config_h_data.set_quoted('MU_TESTDATADIR', testmaildir)
config_h_data.set_quoted('MU_TESTMAILDIR', join_paths(testmaildir, 'testdir')) config_h_data.set_quoted('MU_TESTMAILDIR', join_paths(testmaildir, 'testdir'))
config_h_data.set_quoted('MU_TESTMAILDIR2', join_paths(testmaildir, 'testdir2')) config_h_data.set_quoted('MU_TESTMAILDIR2', join_paths(testmaildir, 'testdir2'))
config_h_data.set_quoted('MU_TESTMAILDIR4', join_paths(testmaildir, 'testdir4')) config_h_data.set_quoted('MU_TESTMAILDIR4', join_paths(testmaildir, 'testdir4'))

View File

@ -21,6 +21,7 @@ lib_mu_scm=static_library(
[ [
'mu-scm.cc', 'mu-scm.cc',
'mu-scm-message.cc', 'mu-scm-message.cc',
'mu-scm-mime.cc',
'mu-scm-store.cc' 'mu-scm-store.cc'
], ],
dependencies: [ dependencies: [

View File

@ -19,7 +19,9 @@
#include "mu-scm-types.hh" #include "mu-scm-types.hh"
#include "message/mu-message.hh" #include "message/mu-message.hh"
#include "message/mu-mime-object.hh"
#include <mutex> #include <mutex>
#include <cstdio>
using namespace Mu; using namespace Mu;
using namespace Mu::Scm; using namespace Mu::Scm;
@ -40,6 +42,7 @@ using MessageMap = std::unordered_map<std::string, MessageObject>;
static MessageMap message_map; static MessageMap message_map;
} }
static const Message& static const Message&
to_message(SCM scm, const char *func, int pos) to_message(SCM scm, const char *func, int pos)
{ {
@ -60,7 +63,10 @@ finalize_message(SCM scm)
} }
static SCM static SCM
subr_message_object_make(SCM message_path_scm) try { subr_cc_message_make(SCM message_path_scm) try {
constexpr auto func{"cc-message-make"};
// message objects eat fds, tickle the gc... letting it handle it // message objects eat fds, tickle the gc... letting it handle it
// automatically is not soon enough. // automatically is not soon enough.
if (message_map.size() >= 0.8 * max_message_map_size) if (message_map.size() >= 0.8 * max_message_map_size)
@ -71,31 +77,37 @@ subr_message_object_make(SCM message_path_scm) try {
// qttempt to give an good error message rather then getting something // qttempt to give an good error message rather then getting something
// from GMime) // from GMime)
if (message_map.size() >= max_message_map_size) if (message_map.size() >= max_message_map_size)
throw ScmError{"make-message", "too many open messages"}; throw ScmError{"cc-make-message", "too many open messages"};
// if we already have the message in our map, return it. // if we already have the message in our map, return it.
auto path{from_scm<std::string>(message_path_scm, "make-message", 1)}; auto path{from_scm<std::string>(message_path_scm, func, 1)};
if (const auto it = message_map.find(path); it != message_map.end()) if (const auto it = message_map.find(path); it != message_map.end())
return it->second.foreign_object; return it->second.foreign_object;
// don't have it yet; attempt to create one // don't have it yet; attempt to create one
if (auto res{Message::make_from_path(path)}; !res) if (auto res{Message::make_from_path(path)}; !res) {
throw ScmError{"make-message", "failed to create message"}; mu_printerrln("{}", res.error().what());
else { throw ScmError{func, "failed to create message"};
} else {
// create a new object, store it in our map and return the foreign ptr. // create a new object, store it in our map and return the foreign ptr.
std::pair<std::string, MessageObject> item {path, MessageObject{std::move(*res), {}}}; std::pair<std::string, MessageObject> item {path,
MessageObject{std::move(*res), {}}};
auto it = message_map.emplace(std::move(item)); auto it = message_map.emplace(std::move(item));
return it.first->second.foreign_object = scm_make_foreign_object_1( return it.first->second.foreign_object = scm_make_foreign_object_1(
message_type, const_cast<Message*>(&it.first->second.message)); message_type,
const_cast<Message*>(&it.first->second.message));
} }
} catch (const ScmError& err) { } catch (const ScmError& err) {
err.throw_scm(); err.throw_scm();
} }
static SCM static SCM
subr_message_body(SCM message_scm, SCM html_scm) try { subr_cc_message_body(SCM message_scm, SCM html_scm) try {
const auto& message{to_message(message_scm, "body", 1)};
const auto html{from_scm<bool>(html_scm, "message-body", 2)}; constexpr auto func{"cc-message-make"};
const auto& message{to_message(message_scm, func, 1)};
const auto html{from_scm<bool>(html_scm, func, 2)};
if (const auto body{html ? message.body_html() : message.body_text()}; body) if (const auto body{html ? message.body_html() : message.body_text()}; body)
return to_scm(*body); return to_scm(*body);
else else
@ -105,9 +117,12 @@ subr_message_body(SCM message_scm, SCM html_scm) try {
} }
static SCM static SCM
subr_message_header(SCM message_scm, SCM field_scm) try { subr_cc_message_header(SCM message_scm, SCM field_scm) try {
const auto& message{to_message(message_scm, "header", 1)};
const auto field{from_scm<std::string>(field_scm, "message-header", 2)}; constexpr auto func{"cc-message-header"};
const auto& message{to_message(message_scm, func, 1)};
const auto field{from_scm<std::string>(field_scm, func, 2)};
if (const auto val{message.header(field)}; val) if (const auto val{message.header(field)}; val)
return to_scm(*val); return to_scm(*val);
@ -117,21 +132,76 @@ subr_message_header(SCM message_scm, SCM field_scm) try {
err.throw_scm(); err.throw_scm();
} }
static SCM
subr_cc_message_plist(SCM message_scm) try {
constexpr auto func{"cc-message-plist"};
const auto& message{to_message(message_scm, func, 1)};
const auto plist{"'" + message.sexp().to_string()};
return scm_c_eval_string(plist.c_str());
} catch (const ScmError& err) {
err.throw_scm();
}
/**
* Get a list of message's MIME-parts
*
* @param message_scm a Message (foreign-object)
*
* @return a list of MIME parts, each is a pair
* ( mime-obj . alist )
* where the mime-obj is the GMimeObject* as a foreign-object,
* and alist is an association list describing the part.
*/
static SCM
subr_cc_message_parts(SCM message_scm) try {
constexpr auto func{"cc-message-parts"};
const auto& message{to_message(message_scm, func, 1)};
const auto& parts{message.parts()};
SCM parts_scm{SCM_EOL};
for (size_t idx = 0; idx != parts.size(); ++idx) {
auto part{parts[idx]};
auto mime_part{GMIME_PART(part.mime_object().object())};
SCM mime_part_scm{to_scm(mime_part)};
SCM alist_scm{to_scm(idx, parts[idx])};
parts_scm = scm_append_x(
scm_list_2(parts_scm,
scm_list_1(
scm_cons(mime_part_scm, alist_scm))));
}
return parts_scm;
} catch (const ScmError& err) {
err.throw_scm();
}
static void static void
init_subrs() init_subrs()
{ {
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type" #pragma GCC diagnostic ignored "-Wcast-function-type"
scm_c_define_gsubr("message-object-make", 1/*req*/, 0/*opt*/, 0/*rst*/, scm_c_define_gsubr("cc-message-make", 1/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_message_object_make)); reinterpret_cast<scm_t_subr>(subr_cc_message_make));
scm_c_define_gsubr("message-body", 2/*req*/, 0/*opt*/, 0/*rst*/, scm_c_define_gsubr("cc-message-body", 2/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_message_body)); reinterpret_cast<scm_t_subr>(subr_cc_message_body));
scm_c_define_gsubr("message-header",2/*req*/, 0/*opt*/, 0/*rst*/, scm_c_define_gsubr("cc-message-header",2/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_message_header)); reinterpret_cast<scm_t_subr>(subr_cc_message_header));
scm_c_define_gsubr("cc-message-parts",1/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_cc_message_parts));
scm_c_define_gsubr("cc-message-plist",1/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_cc_message_plist));
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
} }
void void
Mu::Scm::init_message() Mu::Scm::init_message()
{ {

257
scm/mu-scm-mime.cc Normal file
View File

@ -0,0 +1,257 @@
/*
** Copyright (C) 2025 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-scm-types.hh"
#include "message/mu-message.hh"
#include "message/mu-mime-object.hh"
#include <mutex>
#include <cstdio>
using namespace Mu;
using namespace Mu::Scm;
namespace {
static SCM mime_part_type;
static scm_t_port_type *mime_stream_port_type;
}
static GMimeStream*
from_scm_port(SCM port)
{
return GMIME_STREAM(reinterpret_cast<GMimeStream*>(SCM_STREAM(port)));
}
static size_t
mime_stream_read(SCM port, SCM dst, size_t start, size_t count)
{
auto stream{from_scm_port(port)};
const auto res = g_mime_stream_read(stream,
reinterpret_cast<char*>(SCM_BYTEVECTOR_CONTENTS (dst) + start),
count);
if (res < 0)
scm_misc_error("mime-object-read", "failed to read stream",
SCM_BOOL_F);
return static_cast<size_t>(res);
}
static scm_t_off
mime_stream_seek(SCM port, scm_t_off offset, int whence)
{
auto stream{from_scm_port(port)};
const auto gwhence = [](int w) {
switch(w) {
case SEEK_SET: return GMIME_STREAM_SEEK_SET;
case SEEK_CUR: return GMIME_STREAM_SEEK_CUR;
case SEEK_END: return GMIME_STREAM_SEEK_END;
default:
scm_misc_error("mime-stream-seek", "invalid whence",
SCM_BOOL_F);
return GMIME_STREAM_SEEK_SET; // never reached.
}
}(whence);
const auto res = g_mime_stream_seek(stream, offset, gwhence);
if (res < 0)
scm_misc_error("mime-stream-seek", "invalid seek",
SCM_BOOL_F);
return res;
}
static scm_t_port_type*
make_mime_stream_port_type()
{
auto ptype = scm_make_port_type(const_cast<char*>("mime-stream"), mime_stream_read, {});
scm_set_port_close(ptype, [](SCM port){g_mime_stream_close(from_scm_port(port));});
scm_set_port_needs_close_on_gc(ptype, true);
scm_set_port_seek(ptype, mime_stream_seek);
return ptype;
}
static GMimePart*
part_from_scm(SCM scm, const char *func, int pos)
{
if (!SCM_IS_A_P(scm, mime_part_type))
throw ScmError{ScmError::Id::WrongType, func, pos, scm, "mime-part"};
return GMIME_PART(reinterpret_cast<GMimePart*>(scm_foreign_object_ref(scm, 0)));
}
static GMimeStream*
get_decoded_stream(GMimePart *part)
{
auto wrapper{g_mime_part_get_content(part)};
if (!wrapper)
throw ScmError{"make-make-mime-stream-port",
"failed to create data-wrapper"};
auto stream{g_mime_stream_mem_new()};
if (!stream)
throw ScmError{"make-make-mime-stream-port",
"failed to create mem-stream"};
const auto res{g_mime_data_wrapper_write_to_stream(wrapper, stream)};
if (res < 0) {
g_object_unref(stream);
throw ScmError{"make-make-mime-stream-port",
"failed to write to stream"};
}
return stream;
}
static GMimeStream*
get_stream(GMimePart *part, bool content_only)
{
auto stream = g_mime_stream_mem_new();
if (!stream)
throw ScmError{"make-mime-stream-port",
"failed to create mem-stream"};
ssize_t res{};
if (content_only) // content-only
res = g_mime_object_write_content_to_stream(
GMIME_OBJECT(part), {}, stream);
else // with headers
res = g_mime_object_write_to_stream(
GMIME_OBJECT(part), {}, stream);
if (res < 0) {
g_object_unref(stream);
throw ScmError{"make-mime-stream-port",
"failed to write to stream"};
}
return stream;
}
/**
* Create a port for the mime-part
*
* @param mime_obj mime object (foreign object)
* @param content_only_scm whether to not include headers
* @param decode_scm whether to decode content
* (must be false if decode_scm is true)
*
* @return SCM for mime stream port
*/
static SCM
subr_make_mime_stream_port(SCM mime_part_scm, SCM content_only_scm,
SCM decode_scm)
{
constexpr auto func{"make-mime-stream-port"};
GMimeStream *stream{};
try {
auto part = part_from_scm(mime_part_scm, func, 1);
const auto decode{from_scm<bool>(decode_scm,
func, 2)};
const auto content_only{from_scm<bool>(content_only_scm,
func, 3)};
if (decode)
stream = get_decoded_stream(part);
else
stream = get_stream(part, content_only);
if (const auto res = g_mime_stream_reset(stream); res != 0)
throw ScmError{func, "failed to reset stream"};
return scm_c_make_port(mime_stream_port_type, SCM_RDNG,
reinterpret_cast<scm_t_bits>(stream));
} catch (const ScmError& err) {
if (stream)
g_object_unref(stream);
err.throw_scm();
}
return SCM_UNSPECIFIED;
}
SCM
Mu::Scm::to_scm(GMimePart *part)
{
return scm_make_foreign_object_1(mime_part_type, g_object_ref(part));
}
SCM
Mu::Scm::to_scm(size_t idx, const MessagePart& part)
{
static SCM sym_index{make_symbol("index")};
static SCM sym_content_type{make_symbol("content-type")};
static SCM sym_content_description{make_symbol("content-description")};
static SCM sym_size{make_symbol("size")};
static SCM sym_attachment{make_symbol("attachment?")};
static SCM sym_filename{make_symbol("filename")};
static SCM sym_signed{make_symbol("signed?")};
static SCM sym_encrypted{make_symbol("encrypted?")};
SCM alist = scm_acons(sym_index, to_scm(idx), SCM_EOL);
if (const auto ctype{part.mime_type()}; ctype)
alist = scm_acons(sym_content_type, to_scm(*ctype), alist);
if (const auto cdesc{part.content_description()}; cdesc)
alist = scm_acons(sym_content_description, to_scm(*cdesc), alist);
if (part.is_attachment())
alist = scm_acons(sym_attachment, SCM_BOOL_T, alist);
alist = scm_acons(sym_size, to_scm(part.size()), alist);
if (const auto fname{part.cooked_filename(true/*minmimal*/)}; fname)
alist = scm_acons(sym_filename, to_scm(*fname), alist);
if (part.is_signed())
alist = scm_acons(sym_signed, SCM_BOOL_T, alist);
if (part.is_encrypted())
alist = scm_acons(sym_encrypted, SCM_BOOL_T, alist);
return scm_reverse_x(alist, SCM_EOL); // slightly more convenient
}
static void
init_subrs()
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
scm_c_define_gsubr("cc-mime-make-stream-port",3/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_make_mime_stream_port));
#pragma GCC diagnostic pop
}
void
Mu::Scm::init_mime()
{
mime_part_type = scm_make_foreign_object_type(
make_symbol("g-mime-part"),
scm_list_1(make_symbol("data")),
[](SCM scm) { // finalizer
g_object_unref(
GMIME_PART(reinterpret_cast<GMimePart*>(
scm_foreign_object_ref(scm, 0))));
});
mime_stream_port_type = make_mime_stream_port_type();
init_subrs();
}

View File

@ -39,23 +39,25 @@ to_store(SCM scm, const char *func, int pos)
} }
static SCM static SCM
subr_mcount(SCM store_scm) try { subr_cc_store_mcount(SCM store_scm) try {
return to_scm(to_store(store_scm, "mcount", 1).size()); return to_scm(to_store(store_scm, "cc-store-mcount", 1).size());
} catch (const ScmError& err) { } catch (const ScmError& err) {
err.throw_scm(); err.throw_scm();
} }
static SCM static SCM
subr_cfind(SCM store_scm, SCM pattern_scm, SCM personal_scm, SCM after_scm, SCM max_results_scm) try { subr_cc_store_cfind(SCM store_scm, SCM pattern_scm, SCM personal_scm, SCM after_scm, SCM max_results_scm) try {
constexpr auto func{"cc-store-cfind"};
SCM contacts{SCM_EOL}; SCM contacts{SCM_EOL};
const auto pattern{from_scm<std::string>(pattern_scm, "cfind", 2)}; const auto pattern{from_scm<std::string>(pattern_scm, func, 2)};
const auto personal{from_scm<bool>(personal_scm, "cfind", 3)}; const auto personal{from_scm<bool>(personal_scm, func, 3)};
const auto after{from_scm_with_default(after_scm, 0, "cfind", 4)}; const auto after{from_scm_with_default(after_scm, 0, func, 4)};
// 0 means "unlimited" // 0 means "unlimited"
const size_t maxnum = from_scm_with_default(max_results_scm, 0U, "cfind", 5); const size_t maxnum = from_scm_with_default(max_results_scm, 0U, func, 5);
to_store(store_scm, "cfind", 1).contacts_cache().for_each( to_store(store_scm, func, 1).contacts_cache().for_each(
[&](const auto& contact)->bool { [&](const auto& contact)->bool {
contacts = scm_append_x(scm_list_2(contacts, scm_list_1(to_scm(contact)))); contacts = scm_append_x(scm_list_2(contacts, scm_list_1(to_scm(contact))));
return true; return true;
@ -80,22 +82,24 @@ to_sort_field_id(SCM field, const char *func, int pos)
} }
static SCM static SCM
subr_mfind(SCM store_scm, SCM query_scm, SCM related_scm, SCM skip_dups_scm, subr_cc_store_mfind(SCM store_scm, SCM query_scm, SCM related_scm, SCM skip_dups_scm,
SCM sort_field_scm, SCM reverse_scm, SCM max_results_scm) try { SCM sort_field_scm, SCM reverse_scm, SCM max_results_scm) try {
const auto& store{to_store(store_scm, "mfind", 1)}; constexpr auto func{"cc-store-mfind"};
const auto query{from_scm<std::string>(query_scm, "mfind", 2)};
const auto related(from_scm<bool>(related_scm, "mfind", 3)); const auto& store{to_store(store_scm, func, 1)};
const auto skip_dups(from_scm<bool>(skip_dups_scm, "mfind", 4)); const auto query{from_scm<std::string>(query_scm, func, 2)};
const auto related(from_scm<bool>(related_scm, func, 3));
const auto skip_dups(from_scm<bool>(skip_dups_scm, func, 4));
if (!scm_is_false(sort_field_scm) && !scm_is_symbol(sort_field_scm)) if (!scm_is_false(sort_field_scm) && !scm_is_symbol(sort_field_scm))
throw ScmError{ScmError::Id::WrongType, "mfind", 5, sort_field_scm, "#f or sort-field-symbol"}; throw ScmError{ScmError::Id::WrongType, func, 5, sort_field_scm, "#f or sort-field-symbol"};
const auto sort_field_id = to_sort_field_id(sort_field_scm, "mfind", 5); const auto sort_field_id = to_sort_field_id(sort_field_scm, func, 5);
const auto reverse(from_scm<bool>(reverse_scm, "mfind", 6)); const auto reverse(from_scm<bool>(reverse_scm, func, 6));
// 0 means "unlimited" // 0 means "unlimited"
const size_t maxnum = from_scm_with_default(max_results_scm, 0U, "mfind", 7); const size_t maxnum = from_scm_with_default(max_results_scm, 0U, func, 7);
const QueryFlags qflags = QueryFlags::SkipUnreadable | const QueryFlags qflags = QueryFlags::SkipUnreadable |
(skip_dups ? QueryFlags::SkipDuplicates : QueryFlags::None) | (skip_dups ? QueryFlags::SkipDuplicates : QueryFlags::None) |
@ -107,7 +111,7 @@ subr_mfind(SCM store_scm, SCM query_scm, SCM related_scm, SCM skip_dups_scm,
const auto qres = store.run_query(query, sort_field_id, qflags, maxnum); const auto qres = store.run_query(query, sort_field_id, qflags, maxnum);
if (!qres) if (!qres)
throw ScmError{ScmError::Id::WrongArg, "mfind", 2, query_scm, ""}; throw ScmError{ScmError::Id::WrongArg, func, 2, query_scm, ""};
for (const auto& mi: *qres) { for (const auto& mi: *qres) {
if (auto plist{mi.document()->get_data()}; plist.empty()) if (auto plist{mi.document()->get_data()}; plist.empty())
@ -122,17 +126,19 @@ subr_mfind(SCM store_scm, SCM query_scm, SCM related_scm, SCM skip_dups_scm,
} catch (const ScmError& err) { } catch (const ScmError& err) {
err.throw_scm(); err.throw_scm();
} }
static void static void
init_subrs() init_subrs()
{ {
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type" #pragma GCC diagnostic ignored "-Wcast-function-type"
scm_c_define_gsubr("store-mfind", 7/*req*/, 0/*opt*/, 0/*rst*/, scm_c_define_gsubr("cc-store-mfind", 7/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_mfind)); reinterpret_cast<scm_t_subr>(subr_cc_store_mfind));
scm_c_define_gsubr("store-mcount", 1/*req*/, 0/*opt*/, 0/*rst*/, scm_c_define_gsubr("cc-store-mcount", 1/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_mcount)); reinterpret_cast<scm_t_subr>(subr_cc_store_mcount));
scm_c_define_gsubr("store-cfind", 5/*req*/, 0/*opt*/, 0/*rst*/, scm_c_define_gsubr("cc-store-cfind", 5/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_cfind)); reinterpret_cast<scm_t_subr>(subr_cc_store_cfind));
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
} }

View File

@ -1,6 +1,7 @@
;; unit tests ;; unit tests
(use-modules (mu) (srfi srfi-64)) (use-modules (mu) (srfi srfi-64)
(ice-9 textual-ports))
(define (test-basic) (define (test-basic)
(test-begin "test-basic") (test-begin "test-basic")
@ -47,14 +48,11 @@
(define (test-mfind) (define (test-mfind)
(test-begin "test-mfind") (test-begin "test-mfind")
(let ((msg (car (mfind "" #:sort-field 'date #:reverse? #t)))) (let ((msg (car (mfind "" #:sort-field 'date #:reverse? #t))))
(test-equal "test with multi to and cc" (subject msg) ) (test-equal "test with multi to and cc" (subject msg) )
(test-equal "2016-05-15 16:57:25" (test-equal "2016-05-15 16:57:25"
(time->string (date msg) #:format "%F %T" #:utc? #t))) (time->string (date msg) #:format "%F %T" #:utc? #t)))
(test-end "test-mfind")) (test-end "test-mfind"))
(define (test-message-full) (define (test-message-full)
(test-begin "test-message-full") (test-begin "test-message-full")
@ -91,6 +89,49 @@
(test-equal "3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com" (thread-id msg))) (test-equal "3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com" (thread-id msg)))
(test-end "test-message-more")) (test-end "test-message-more"))
(define (test-message-parts)
(test-begin "test-message-parts")
(let* ((msg (car (mfind "flag:attach")))
(parts (mime-parts msg)))
(test-equal 3 (length parts))
(test-equal
'(((index . 0) (content-type . "text/plain") (size . 1174))
((index . 1) (content-type . "text/x-vcard") (attachment? . #t) (size . 306)
(filename . "mihailim.vcf"))
((index . 2) (content-type . "text/plain") (size . 153)))
(map (lambda (part) (mime-part->alist part)) parts))
(let ((port (make-port (car parts))))
(test-assert (port? port))
(test-assert (input-port? port))
(test-assert (not (port-closed? port)))
(test-equal "Marco Bambini wrote:" (get-string-n port 20)))
(test-end "test-message-parts")))
(define (test-message-new)
(test-begin "test-message-new")
(let ((msg (make-message (format #f "~a/testdir2/Foo/cur/mail5" (getenv "MU_TESTDATADIR"))))
(tmpdir (getenv "MU_TESTTEMPDIR")))
(test-equal "pics for you" (subject msg))
(test-equal
'(((index . 0) (content-type . "text/plain") (size . 27))
((index . 1) (content-type . "image/jpeg") (size . 23881) (filename . "sittingbull.jpg"))
((index . 2) (content-type . "image/jpeg") (size . 21566) (filename . "custer.jpg")))
(map (lambda (part) (mime-part->alist part)) (mime-parts msg)))
(let* ((part (list-ref (mime-parts msg) 1))
(alist (mime-part->alist part))
(fname (format #f "~a/~a" tmpdir (assoc-ref alist 'filename))))
(write-to-file part #:filename fname)
(test-assert (access? fname R_OK))
;; note, the 23881 is the _encoded_ size.
(test-equal 17674 (stat:size (stat fname))))
;;(write-to-file (list-ref (mime-parts msg) 1))
(test-end "test-message-new")))
(define (test-options) (define (test-options)
(test-begin "test-options") (test-begin "test-options")
(let ((opts %options)) (let ((opts %options))
@ -131,6 +172,8 @@
(test-mfind) (test-mfind)
(test-message-full) (test-message-full)
(test-message-more) (test-message-more)
(test-message-parts)
(test-message-new)
(test-options) (test-options)
(test-helpers) (test-helpers)

View File

@ -22,6 +22,7 @@
#include "lib/mu-store.hh" #include "lib/mu-store.hh"
#include "message/mu-contact.hh" #include "message/mu-contact.hh"
#include "message/mu-mime-object.hh"
#include "mu-scm.hh" #include "mu-scm.hh"
@ -41,6 +42,12 @@ void init_store(const Mu::Store& store);
*/ */
void init_message(); void init_message();
/**
* Initialize SCM/MimeObject/Part support
*/
void init_mime();
/** /**
* Convert a Contact to an SCM * Convert a Contact to an SCM
* *
@ -50,6 +57,28 @@ void init_message();
*/ */
SCM to_scm(const Contact& contact); SCM to_scm(const Contact& contact);
/**
* Convert a MessagePart to an SCM (alist)
*
* @param idx index of the message-part
* @param part the part
*
* @return SCM
*/
SCM to_scm(size_t idx, const MessagePart& part);
/**
* Convert a GMimePart* to an SCM (alist)
*
* @param obj a mime part
*
* @return SCM
*/
SCM to_scm(GMimePart *part);
} // Mu::Scm } // Mu::Scm
#endif /*MU_SCM_TYPES_HH*/ #endif /*MU_SCM_TYPES_HH*/

View File

@ -62,6 +62,7 @@ init_module_mu(void* _data)
init_options(config->options); init_options(config->options);
init_store(config->store); init_store(config->store);
init_message(); init_message();
init_mime();
} }
static const Result<std::string> static const Result<std::string>
@ -181,6 +182,9 @@ test_scm_script()
{ {
TempDir tempdir{}; TempDir tempdir{};
const auto MuTestMaildir{ Mu::canonicalize_filename(MU_TESTMAILDIR, "/")}; const auto MuTestMaildir{ Mu::canonicalize_filename(MU_TESTMAILDIR, "/")};
::setenv("MU_TESTTEMPDIR", tempdir.path().c_str(), 1);
auto store{Store::make_new(tempdir.path(), MuTestMaildir)}; auto store{Store::make_new(tempdir.path(), MuTestMaildir)};
assert_valid_result(store); assert_valid_result(store);
@ -207,6 +211,7 @@ int
main(int argc, char* argv[]) main(int argc, char* argv[])
{ {
::setenv("MU_SCM_DIR", MU_SCM_SRCDIR, 1); ::setenv("MU_SCM_DIR", MU_SCM_SRCDIR, 1);
::setenv("MU_TESTDATADIR", MU_TESTDATADIR, 1);
mu_test_init(&argc, &argv); mu_test_init(&argc, &argv);

View File

@ -19,17 +19,20 @@
(define-module (mu) (define-module (mu)
:use-module (oop goops) :use-module (oop goops)
:use-module (system foreign) :use-module (system foreign)
:use-module (rnrs bytevectors)
:use-module (ice-9 optargs) :use-module (ice-9 optargs)
:use-module (ice-9 binary-ports)
#:export ( #:export (
;; classes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
<store> ;; Mime-parts
<mime-part>
mfind mime-part->alist
mcount make-port
cfind write-to-file
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Message
<message> <message>
sexp make-message
date date
last-change last-change
@ -75,14 +78,27 @@
body body
header header
mime-parts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Store
<store>
mfind
mcount
cfind
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Other
;; misc ;; misc
%options %options
;; %preferences ;; %preferences
;; helpers ;; helpers
string->time string->time
time->string)) time->string))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; some helpers for dealing with plists / alists ;; some helpers for dealing with plists / alists
(define (plist-for-each func plist) (define (plist-for-each func plist)
@ -124,6 +140,62 @@ If not found, return #f."
v))))) v)))))
plist) plist)
alist)) alist))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MIME-parts
;;
;; A <mime-object> has two slots:
;; partobj --> wraps a GMimePart* as a "foreign object"
;; alist --> alist with information about some MIME-part
(define-class <mime-part> ()
(mimepart #:init-value #f #:init-keyword #:mimepart)
(alist #:init-value #f #:init-keyword #:alist #:getter mime-part->alist))
(define* (make-port mime-part #:key (content-only? #t) (decode? #t))
"Create a read port for MIME-PART.
If CONTENT-ONLY? is #t, only include the contents, not headers.
If DECODE? is #t, decode the content (from e.g., base64); in that case,
CONTENT-ONLY? is implied to be #t."
(cc-mime-make-stream-port (slot-ref mime-part 'mimepart) content-only? decode?))
(define* (make-output-file mime-part #:key (filename #f) (overwrite? #f))
"Create a port for the file to write MIME-PART to.
FILENAME is the path to the file name. If not specified, use the 'filename'
field in the mime-part and if that does not exist, use 'mime-part-<index>' with
<index> being the number of the mime-part.
OVERWRITE? specifies whether existing files by the same name or overwritten.
Otherwise, trying to overwrite an existing file raises an error."
(let* ((alist (mime-part->alist mime-part))
(filename (or filename
(assoc-ref alist 'filename)
(format #f "mime-part-~d" (assoc-ref alist 'index)))))
;; we need an fd-based port since we want to support overwrite?
(open filename
(logior O_WRONLY O_CREAT O_TRUNC (if overwrite? O_EXCL 0)) #o644)))
(define* (write-to-file mime-part #:key (filename #f) (overwrite? #f))
"Write MIME-PART to a file.
FILENAME is the path to the file name. If not specified, use the 'filename'
field in the mime-part and if that does not exist, use 'mime-part-<index>' with
<index> being the number of the mime-part.
OVERWRITE? specifies whether existing files by the same name or overwritten.
Otherwise, trying to overwrite an existing file raises an error."
(let* ((input (make-port mime-part))
(output (make-output-file mime-part
#:filename filename #:overwrite? overwrite?))
(buf (make-bytevector 4096)) ;; just a guess...
(bytes 0))
(while (not (eof-object? bytes)) ;; XXX do this in a more elegant way.
(set! bytes (get-bytevector-n! input buf 0 (bytevector-length buf)))
(put-bytevector output buf 0 (if (eof-object? bytes) 0 bytes)))
(close input)
(close output)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Message ;; Message
@ -132,25 +204,31 @@ If not found, return #f."
;; plist --> this is the message sexp cached in the database; ;; plist --> this is the message sexp cached in the database;
;; for each message (for mu4e, but we reuse here) ;; for each message (for mu4e, but we reuse here)
;; object--> wraps a Mu::Message* as a "foreign object" ;; object--> wraps a Mu::Message* as a "foreign object"
;; ;; parts --> MIME-parts
;; generally the plist is a bit cheaper, since the mu-message ;; generally the plist is a bit cheaper, since the mu-message
;; captures a file-deescriptor. ;; captures a file-descriptor.
(define-class <message> () (define-class <message> ()
(object #:init-value #f #:init-keyword #:object)
(plist #:init-value #f #:init-keyword #:plist) (plist #:init-value #f #:init-keyword #:plist)
(object #:init-value #f #:init-keyword #:object)) (parts #:init-value #f #:init-keyword #:parts))
(define (make-message path)
"Create a <message> from file at PATH."
(make <message> #:object (cc-message-make path)))
(define-method (plist (message <message>)) (define-method (plist (message <message>))
"Get the PLIST for this MESSAGE." "Get the PLIST for this MESSAGE."
(when (not (slot-ref message 'plist))
(slot-set! message 'plist (cc-message-plist (object message))))
(slot-ref message 'plist)) (slot-ref message 'plist))
(define-method (object (message <message>)) (define-method (object (message <message>))
"Get the foreign object for this MESSAGE. "Get the foreign object for this MESSAGE.
If MESSAGE does not have such an object yet, crate it from the If MESSAGE does not have such an object yet, create it from the
path of the message." path of the message."
(if (not (slot-ref message 'object)) (if (not (slot-ref message 'object))
(slot-set! message 'object (slot-set! message 'object (cc-message-make (path message))))
(message-object-make (path message))))
(slot-ref message 'object)) (slot-ref message 'object))
(define-method (find-field (message <message>) field) (define-method (find-field (message <message>) field)
@ -170,7 +248,7 @@ If LST is #f, return #f."
(+ (ash (car lst) 16) (cadr lst)) (+ (ash (car lst) 16) (cadr lst))
#f)) #f))
;; Accessor for the fields ;; Accessors for the fields
(define-method (subject (message <message>)) (define-method (subject (message <message>))
"Get the subject for MESSAGE or #f if not found." "Get the subject for MESSAGE or #f if not found."
@ -335,16 +413,26 @@ not found."
(find-contact-field message ':bcc)) (find-contact-field message ':bcc))
(define* (body message #:key (html? #f)) (define* (body message #:key (html? #f))
"Get the MESSAGE body or #f if not found. "Get the MESSAGE body or #f if not found
If #:html is non-#f, instead search for the HTML body. If #:html is non-#f, instead search for the HTML body.
Requires the full message." Requires the full message."
(message-body (object message) html?)) (cc-message-body (object message) html?))
(define-method (header (message <message>) (field <string>)) (define-method (header (message <message>) (field <string>))
"Get the raw MESSAGE header FIELD or #f if not found. "Get the raw MESSAGE header FIELD or #f if not found.
FIELD is case-insensitive and should not have the ':' suffix. FIELD is case-insensitive and should not have the ':' suffix.
Requires the full message." Requires the full message."
(message-header (object message) field)) (cc-message-header (object message) field))
(define-method (mime-parts (message <message>))
"Get the MIME-parts for this message.
This is a list of <mime-part> objects."
(let ((msgobj (object message)))
(map (lambda (mimepart-alist)
(make <mime-part>
#:mimepart (car mimepart-alist)
#:alist (cdr mimepart-alist)))
(cc-message-parts msgobj))))
;; Store ;; Store
;; ;;
@ -389,14 +477,14 @@ The query is mandatory, the other (keyword) arguments are optional.
#:max-results max. number of matches. Default: false (unlimited))." #:max-results max. number of matches. Default: false (unlimited))."
(map (lambda (plist) (map (lambda (plist)
(make <message> #:plist plist)) (make <message> #:plist plist))
(store-mfind (store-object store) query (cc-store-mfind (store-object store) query
related? skip-dups? sort-field reverse? max-results))) related? skip-dups? sort-field reverse? max-results)))
(define* (mcount (define* (mcount
#:key #:key
(store %default-store)) (store %default-store))
"Get the number of messages." "Get the number of messages."
(store-mcount (store-object store))) (cc-store-mcount (store-object store)))
(define* (cfind pattern (define* (cfind pattern
#:key #:key
@ -412,7 +500,7 @@ The pattern is mandatory; the other (keyword) arguments are optional.
#:personal? only include 'personal' contacts. Default: all #:personal? only include 'personal' contacts. Default: all
#:after only include contacts last seen time_t: Default all #:after only include contacts last seen time_t: Default all
#:max-results max. number of matches. Default: false (unlimited))." #:max-results max. number of matches. Default: false (unlimited))."
(store-cfind (store-object store) pattern personal? after max-results)) (cc-store-cfind (store-object store) pattern personal? after max-results))
;;; Misc ;;; Misc
@ -467,7 +555,6 @@ The input date format is fixed."
(mktime sbdtime "UTC") (mktime sbdtime "UTC")
(mktime sbdtime))))) (mktime sbdtime)))))
(define* (time->string time-t #:key (format 'preference) (utc? 'preference)) (define* (time->string time-t #:key (format 'preference) (utc? 'preference))
"Convert a time_t (second-since-epoch) value TIME-T into a string. "Convert a time_t (second-since-epoch) value TIME-T into a string.

View File

@ -355,14 +355,15 @@ Example usage:
@node Message @node Message
@section Message @section Message
A message represents the information about some e-mail message whose information A message represents the information about an e-mail message. @t{mu} gets this
has been extracted and store in the @t{mu} store (database). information either from its database (the @t{mu} store), e.g., with @code{mfind}
(see @xref{Store}) or by reading an email message from the file-systems with
@code{make-message}.
You can retrieve lists of @t{<message>} objects with @t{mfind} method, as In many of the examples below we assume there is some @code{message} object,
explained in @xref{Store}. In the following, we use some message-object @t{msg}, e.g. as retrieved through:
e.g.
@lisp @lisp
l(define msg (car (mfind "hello"))) (define msg (car (mfind "hello")))
@end lisp @end lisp
@anchor{full-message} Many of the procedures below use the internal @anchor{full-message} Many of the procedures below use the internal
@ -370,10 +371,18 @@ representation of the message from the database; this re-uses the same
information that @t{mu4e} uses. However, that is not sufficient for all: information that @t{mu4e} uses. However, that is not sufficient for all:
@code{body} and @code{header} need the full message. To get this, it needs to @code{body} and @code{header} need the full message. To get this, it needs to
open the message file from the file-system. Much of this is internal to open the message file from the file-system. Much of this is internal to
@t{mu-scm}, except that full-method-procedures are relatively a bit slower. @t{mu-scm}, except that full-method-procedures are a bit slower relatively to
the database-only ones.
@subsection Basics @subsection Basics
@deffn {Scheme Procedure} make-message path
@end deffn
Create a new message object from a file-system path.
This is a @emph{full message}, unlike the ones you get from a store-query (i.e.,
@code{mfind}).
@deffn {Scheme Procedure} subject message @deffn {Scheme Procedure} subject message
@end deffn @end deffn
Get the message subject, or @t{#f} if there is none. Get the message subject, or @t{#f} if there is none.
@ -438,13 +447,86 @@ If @var{#:html?} is non-@t{#f}, get the HTML-body instead.
This requires the @ref{full-message,,full message}. This requires the @ref{full-message,,full message}.
@deffn {Scheme Procedure} message-id message
@end deffn
Get the message's @t{Message-ID} field, or @t{#f} if there is none.
For example:
@lisp
(message-id msg)
=> "87a15477-dd66-43e5-a722-81c545d6af19@@gmail.com"
@end lisp
@subsection MIME-parts
Messages consist of one or more MIME-parts, which include the body, attachments
and other parts. To get the MIME-parts for a message, you can use the
@code{mime-parts} method on a @code{message}.
@deffn {Scheme Procedure} mime-parts message
@end deffn
Get the MIME-parts for this message, as a list of @code{<mime-part>} objects.
A MIME-parts is an object with a few methods.
@deffn {Scheme Procedure} mime-part->alist mime-part
@end deffn
Get an association list (alist) describing the MIME part.
For example:
@lisp
;; describe the second MIME-part of the first message with an attachment
(mime-part->alist
(cadr (mime-parts
(car (mfind "flag:attach" #:max-results 1)))))
=> ((filename . "emacs.png") (size . 18188) (content-type . "image/png") (index . 1))
@end lisp
Depending on the MIME-part, different fields can be present:
@itemize
@item @t{index}
the index (number) of the part, 0-based
@item @t{mime-type}
the MIME-type of the part
@item @t{size}
the size of the part in bytes. For encoded parts, this is the @emph{encoded} size
@item @t{filename} the filename (for attachments).
This is as specified in the message, but with forward slashes and
control-characters removed or substituted with @t{-}.
@item @t{signed?}
is this part (cryptographically) signed?
@item @t{encrypted?}
is this part encrypted?
@end itemize
@deffn {Scheme Procedure} make-port mime-part [#:content-only? #f]
[#:decode? #t]
@end deffn
Get a read-port for the given MIME-part. Ports are the standard mechanism for
dealing with I/O in Guile; see its documentation for further details.
If @code{content-only?} is true, only include the contents, not headers. If
@code{decode?} is true, decode the content (from e.g., Base-64); in that
case, @code{content-only?} is implied to be #t.
@deffn {Scheme Procedure} write-to-file [#:filename #f] [#:overwrite? #f]
@end deffn
Write MIME-part to file.
Use @code{filename} is the file/path to use for writing; if this is @code{#f},
the name is taken from the @t{filename} property of the MIME-part alist. If that
does not exist, a generic name is chosen.
If @code{overwrite?} is true, overwrite existing files of the same name;
otherwise, raise an error if the file already exists.
@subsection Contacts @subsection Contacts
Message fields @t{To:}, @t{From:}, @t{Cc:} and @t{Bcc:} contain @emph{contacts}. Message fields @t{To:}, @t{From:}, @t{Cc:} and @t{Bcc:} contain @emph{contacts}.
@t{mu-scm} represents those as list of contact-alists, or contacts for short. @t{mu-scm} represents those as list of contact-alists, or contacts for short.
Each contact is an alist with at least an @t{email} and optionally a @t{name} Each contact is an alist with at least an @code{email} and optionally a
field. For instance: @code{name} field. For instance:
@lisp @lisp
(to msg) (to msg)
=> (((name . "Hannibal Smith") (email . "jhs@@example.com")) => (((name . "Hannibal Smith") (email . "jhs@@example.com"))