mu-scm: add full-message support (body/header)
Implement support for "header" and "body" procedures, with require loading the message file from disk, and create a foreign object for the message. We keep those alive in a vector, and hook up a finalizer. Update docs & tests as well.
This commit is contained in:
@ -20,7 +20,7 @@ lib_mu_scm=static_library(
|
|||||||
'mu-scm',
|
'mu-scm',
|
||||||
[
|
[
|
||||||
'mu-scm.cc',
|
'mu-scm.cc',
|
||||||
'mu-scm-contact.cc',
|
'mu-scm-message.cc',
|
||||||
'mu-scm-store.cc'
|
'mu-scm-store.cc'
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
** 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-contact.hh"
|
|
||||||
|
|
||||||
using namespace Mu::Scm;
|
|
||||||
|
|
||||||
SCM
|
|
||||||
Mu::Scm::to_scm(const Contact& contact)
|
|
||||||
{
|
|
||||||
static SCM email{scm_from_utf8_symbol("email")};
|
|
||||||
static SCM name{scm_from_utf8_symbol("name")};
|
|
||||||
|
|
||||||
SCM alist = scm_acons(email, to_scm(contact.email), SCM_EOL);
|
|
||||||
if (!contact.name.empty())
|
|
||||||
alist = scm_acons(name, to_scm(contact.name), alist);
|
|
||||||
|
|
||||||
return alist;
|
|
||||||
}
|
|
||||||
145
scm/mu-scm-message.cc
Normal file
145
scm/mu-scm-message.cc
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
** 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 <mutex>
|
||||||
|
|
||||||
|
using namespace Mu;
|
||||||
|
using namespace Mu::Scm;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
static SCM message_type;
|
||||||
|
static bool initialized;
|
||||||
|
|
||||||
|
std::mutex map_lock;
|
||||||
|
|
||||||
|
constexpr auto max_message_map_size{512};
|
||||||
|
|
||||||
|
struct MessageObject {
|
||||||
|
const Message message;
|
||||||
|
SCM foreign_object{};
|
||||||
|
};
|
||||||
|
using MessageMap = std::unordered_map<std::string, MessageObject>;
|
||||||
|
static MessageMap message_map;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Message&
|
||||||
|
to_message(SCM scm)
|
||||||
|
{
|
||||||
|
scm_assert_foreign_object_type(message_type, scm);
|
||||||
|
return *reinterpret_cast<Message*>(scm_foreign_object_ref(scm, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
finalize_message(SCM scm)
|
||||||
|
{
|
||||||
|
std::unique_lock lock{map_lock};
|
||||||
|
const auto msg = reinterpret_cast<const Message*>(scm_foreign_object_ref(scm, 0));
|
||||||
|
//mu_debug("finalizing message @ {}", msg->path());
|
||||||
|
if (const auto n = message_map.erase(msg->path()); n != 1)
|
||||||
|
mu_warning("huh?! deleted {}", n);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SCM
|
||||||
|
subr_message_object_make(SCM message_path_scm)
|
||||||
|
{
|
||||||
|
// message objects eat fds, tickle the gc... letting it handle it
|
||||||
|
// automatically is not soon enough.
|
||||||
|
if (message_map.size() >= 0.8 * max_message_map_size)
|
||||||
|
scm_gc();
|
||||||
|
|
||||||
|
std::unique_lock lock{map_lock};
|
||||||
|
|
||||||
|
// qttempt to give an good error message rather then getting something
|
||||||
|
// from GMime)
|
||||||
|
if (message_map.size() >= max_message_map_size)
|
||||||
|
raise_error("failure", "message-object",
|
||||||
|
"too many open messages");
|
||||||
|
|
||||||
|
// if we already have the message in our map, return it.
|
||||||
|
auto path{from_scm<std::string>(message_path_scm)};
|
||||||
|
if (const auto it = message_map.find(path); it != message_map.end())
|
||||||
|
return it->second.foreign_object;
|
||||||
|
|
||||||
|
// don't have it yet; attempt to create one
|
||||||
|
if (auto res{Message::make_from_path(path)}; !res) {
|
||||||
|
raise_error("failure", "message-object",
|
||||||
|
"failed to create message from {}: {}", path, res.error());
|
||||||
|
return SCM_BOOL_F;
|
||||||
|
} else {
|
||||||
|
// create a new object, store it in our map and return the foreign ptr.
|
||||||
|
auto it = message_map.emplace(std::move(path), std::move(*res));
|
||||||
|
return it.first->second.foreign_object = scm_make_foreign_object_1(
|
||||||
|
message_type, const_cast<Message*>(&it.first->second.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static SCM
|
||||||
|
subr_message_body(SCM message_scm, SCM html_scm)
|
||||||
|
{
|
||||||
|
const auto& message{to_message(message_scm)};
|
||||||
|
const auto html{from_scm<bool>(html_scm)};
|
||||||
|
if (const auto body{html ? message.body_html() : message.body_text()}; body)
|
||||||
|
return to_scm(*body);
|
||||||
|
else
|
||||||
|
return SCM_BOOL_F;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SCM
|
||||||
|
subr_message_header(SCM message_scm, SCM field_scm)
|
||||||
|
{
|
||||||
|
const auto& message{to_message(message_scm)};
|
||||||
|
const auto field{from_scm<std::string>(field_scm)};
|
||||||
|
|
||||||
|
if (const auto val{message.header(field)}; val)
|
||||||
|
return to_scm(*val);
|
||||||
|
else
|
||||||
|
return SCM_BOOL_F;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
init_subrs()
|
||||||
|
{
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Wcast-function-type"
|
||||||
|
scm_c_define_gsubr("message-object-make", 1/*req*/, 0/*opt*/, 0/*rst*/,
|
||||||
|
reinterpret_cast<scm_t_subr>(subr_message_object_make));
|
||||||
|
scm_c_define_gsubr("message-body", 2/*req*/, 0/*opt*/, 0/*rst*/,
|
||||||
|
reinterpret_cast<scm_t_subr>(subr_message_body));
|
||||||
|
scm_c_define_gsubr("message-header",2/*req*/, 0/*opt*/, 0/*rst*/,
|
||||||
|
reinterpret_cast<scm_t_subr>(subr_message_header));
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
Mu::Scm::init_message()
|
||||||
|
{
|
||||||
|
if (initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
message_type = scm_make_foreign_object_type(
|
||||||
|
make_symbol("message"),
|
||||||
|
scm_list_1(make_symbol("data")),
|
||||||
|
finalize_message);
|
||||||
|
|
||||||
|
init_subrs();
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
@ -17,8 +17,7 @@
|
|||||||
**
|
**
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "mu-scm-store.hh"
|
#include "mu-scm-types.hh"
|
||||||
#include "mu-scm-contact.hh"
|
|
||||||
|
|
||||||
using namespace Mu;
|
using namespace Mu;
|
||||||
using namespace Mu::Scm;
|
using namespace Mu::Scm;
|
||||||
@ -154,3 +153,17 @@ Mu::Scm::init_store(const Store& store)
|
|||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SCM
|
||||||
|
Mu::Scm::to_scm(const Contact& contact)
|
||||||
|
{
|
||||||
|
static SCM email{scm_from_utf8_symbol("email")};
|
||||||
|
static SCM name{scm_from_utf8_symbol("name")};
|
||||||
|
|
||||||
|
SCM alist = scm_acons(email, to_scm(contact.email), SCM_EOL);
|
||||||
|
if (!contact.name.empty())
|
||||||
|
alist = scm_acons(name, to_scm(contact.name), alist);
|
||||||
|
|
||||||
|
return alist;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
/*
|
|
||||||
** 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.
|
|
||||||
**
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef MU_SCM_STORE_HH
|
|
||||||
#define MU_SCM_STORE_HH
|
|
||||||
|
|
||||||
#include "lib/mu-store.hh"
|
|
||||||
#include "mu-scm.hh"
|
|
||||||
|
|
||||||
namespace Mu::Scm {
|
|
||||||
void init_store(const Mu::Store& store);
|
|
||||||
} // Mu::Scm
|
|
||||||
|
|
||||||
#endif /*MU_SCM_STORE_HH*/
|
|
||||||
@ -54,6 +54,22 @@
|
|||||||
(test-end "test-mfind"))
|
(test-end "test-mfind"))
|
||||||
|
|
||||||
|
|
||||||
|
(define (test-message-full)
|
||||||
|
(test-begin "test-message-full")
|
||||||
|
|
||||||
|
(let ((msg (cadr (mfind ""))))
|
||||||
|
(test-equal "Motörhead" (header msg "Subject"))
|
||||||
|
(test-equal "Mü <testmu@testmu.xx>" (header msg "From"))
|
||||||
|
(test-equal #f (header msg "Bla"))
|
||||||
|
|
||||||
|
(test-equal (string-append "\nTest for issue #38, where apparently searching for "
|
||||||
|
"accented words in subject,\nto etc. fails.\n\n"
|
||||||
|
"What about here? Queensrÿche. Mötley Crüe.\n\n\n")
|
||||||
|
(body msg))
|
||||||
|
(test-equal #f (body msg #:html? #t))
|
||||||
|
|
||||||
|
(test-end "test-message-full")))
|
||||||
|
|
||||||
(define (test-misc)
|
(define (test-misc)
|
||||||
(let ((opts (options)))
|
(let ((opts (options)))
|
||||||
(test-assert (>= (length opts) 4))
|
(test-assert (>= (length opts) 4))
|
||||||
@ -79,6 +95,7 @@
|
|||||||
(test-basic)
|
(test-basic)
|
||||||
(test-basic-mfind)
|
(test-basic-mfind)
|
||||||
(test-mfind)
|
(test-mfind)
|
||||||
|
(test-message-full)
|
||||||
(test-misc)
|
(test-misc)
|
||||||
(test-helpers)
|
(test-helpers)
|
||||||
|
|
||||||
|
|||||||
@ -17,13 +17,30 @@
|
|||||||
**
|
**
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef MU_SCM_CONTACT_HH
|
#ifndef MU_SCM_TYPES_HH
|
||||||
#define MU_SCM_CONTACT_HH
|
#define MU_SCM_TYPES_HH
|
||||||
|
|
||||||
|
#include "lib/mu-store.hh"
|
||||||
#include "message/mu-contact.hh"
|
#include "message/mu-contact.hh"
|
||||||
|
|
||||||
#include "mu-scm.hh"
|
#include "mu-scm.hh"
|
||||||
|
|
||||||
namespace Mu::Scm {
|
namespace Mu::Scm {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize SCM/Store support.
|
||||||
|
*
|
||||||
|
* @param store a store
|
||||||
|
*/
|
||||||
|
void init_store(const Mu::Store& store);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize SCM/Message support.
|
||||||
|
*
|
||||||
|
* @param store a store
|
||||||
|
*/
|
||||||
|
void init_message();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Contact to an SCM
|
* Convert a Contact to an SCM
|
||||||
*
|
*
|
||||||
@ -35,4 +52,4 @@ SCM to_scm(const Contact& contact);
|
|||||||
|
|
||||||
} // Mu::Scm
|
} // Mu::Scm
|
||||||
|
|
||||||
#endif /*MU_SCM_CONTACT_HH*/
|
#endif /*MU_SCM_TYPES_HH*/
|
||||||
@ -25,8 +25,7 @@
|
|||||||
#include "mu-utils.hh"
|
#include "mu-utils.hh"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
#include "mu-scm-contact.hh"
|
#include "mu-scm-types.hh"
|
||||||
#include "mu-scm-store.hh"
|
|
||||||
|
|
||||||
using namespace Mu;
|
using namespace Mu;
|
||||||
using namespace Mu::Scm;
|
using namespace Mu::Scm;
|
||||||
@ -62,6 +61,7 @@ init_module_mu(void* _data)
|
|||||||
{
|
{
|
||||||
init_options(config->options);
|
init_options(config->options);
|
||||||
init_store(config->store);
|
init_store(config->store);
|
||||||
|
init_message();
|
||||||
}
|
}
|
||||||
|
|
||||||
static const Result<std::string>
|
static const Result<std::string>
|
||||||
|
|||||||
@ -61,6 +61,7 @@ namespace Mu::Scm {
|
|||||||
*/
|
*/
|
||||||
Result<void> run(const Config& conf);
|
Result<void> run(const Config& conf);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helpers
|
* Helpers
|
||||||
*
|
*
|
||||||
@ -225,18 +226,4 @@ namespace Mu::Scm {
|
|||||||
/**@}*/
|
/**@}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SCM formatter, for use with fmt
|
|
||||||
*
|
|
||||||
* @param scm some object
|
|
||||||
*
|
|
||||||
* @return string representation of scm
|
|
||||||
*/
|
|
||||||
// static inline std::string format_as(SCM scm) {
|
|
||||||
// return Mu::Scm::from_scm<std::string>(scm_object_to_string(scm, SCM_UNSPECIFIED));
|
|
||||||
// }
|
|
||||||
// XXX doesn't work:
|
|
||||||
// "static assertion failed: Formatting of non-void pointers is disallowed"
|
|
||||||
|
|
||||||
#endif /*MU_SCM_HH*/
|
#endif /*MU_SCM_HH*/
|
||||||
|
|||||||
@ -68,6 +68,10 @@
|
|||||||
cc
|
cc
|
||||||
bcc
|
bcc
|
||||||
|
|
||||||
|
;; message-body
|
||||||
|
body
|
||||||
|
header
|
||||||
|
|
||||||
;; misc
|
;; misc
|
||||||
options
|
options
|
||||||
|
|
||||||
@ -120,14 +124,30 @@ If not found, return #f."
|
|||||||
|
|
||||||
;; Message
|
;; Message
|
||||||
;;
|
;;
|
||||||
;; A <message> is created from a message plist.
|
;; A <message> has two slots:
|
||||||
|
;; plist --> this is the message sexp cached in the database;
|
||||||
|
;; for each message (for mu4e, but we reuse here)
|
||||||
|
;; object--> wraps a Mu::Message* as a "foreign object"
|
||||||
|
;;
|
||||||
|
;; generally the plist is a bit cheaper, since the mu-message
|
||||||
|
;; captures a file-deescriptor.
|
||||||
|
|
||||||
;; In mu, we have store a plist sexp for each message in the database,
|
|
||||||
;; for use with mu4e. But, that very plist is useful here as well.
|
|
||||||
(define-class <message> ()
|
(define-class <message> ()
|
||||||
(plist #:init-keyword #:plist #:getter plist))
|
(plist #:init-value #f #:init-keyword #:plist)
|
||||||
|
(object #:init-value #f #:init-keyword #:object))
|
||||||
|
|
||||||
;; using the plist as-is makes for O(n) access to the various fields
|
(define-method (plist (message <message>))
|
||||||
|
"Get the PLIST for this MESSAGE."
|
||||||
|
(slot-ref message 'plist))
|
||||||
|
|
||||||
|
(define-method (object (message <message>))
|
||||||
|
"Get the foreign object for this MESSAGE.
|
||||||
|
If MESSAGE does not have such an object yet, crate it from the
|
||||||
|
path of the message."
|
||||||
|
(if (not (slot-ref message 'object))
|
||||||
|
(slot-set! message 'object
|
||||||
|
(message-object-make (path message))))
|
||||||
|
(slot-ref message 'object))
|
||||||
|
|
||||||
(define-method (find-field (message <message>) field)
|
(define-method (find-field (message <message>) field)
|
||||||
(plist-find (plist message) field))
|
(plist-find (plist message) field))
|
||||||
@ -200,7 +220,7 @@ Return #f otherwise."
|
|||||||
(find-field message ':flags))
|
(find-field message ':flags))
|
||||||
|
|
||||||
(define-method (flag? (message <message>) flag)
|
(define-method (flag? (message <message>) flag)
|
||||||
"Does MESSAGE have FLAG?."
|
"Does MESSAGE have FLAG?"
|
||||||
(let ((flags
|
(let ((flags
|
||||||
(find-field message ':flags)))
|
(find-field message ':flags)))
|
||||||
(if flags
|
(if flags
|
||||||
@ -289,6 +309,18 @@ not found."
|
|||||||
#f if not found."
|
#f if not found."
|
||||||
(find-contact-field message ':bcc))
|
(find-contact-field message ':bcc))
|
||||||
|
|
||||||
|
(define* (body message #:key (html? #f))
|
||||||
|
"Get the MESSAGE body or #f if not found.
|
||||||
|
If #:html is non-#f, instead search for the HTML body.
|
||||||
|
Requires the full message."
|
||||||
|
(message-body (object message) html?))
|
||||||
|
|
||||||
|
(define-method (header (message <message>) (field <string>))
|
||||||
|
"Get the raw MESSAGE header FIELD or #f if not found.
|
||||||
|
FIELD is case-insensitive and should not have the ':' suffix.
|
||||||
|
Requires the full message."
|
||||||
|
(message-header (object message) field))
|
||||||
|
|
||||||
;; Store
|
;; Store
|
||||||
;;
|
;;
|
||||||
;; Note: we have a %default-store, which is the store we opened during
|
;; Note: we have a %default-store, which is the store we opened during
|
||||||
@ -322,7 +354,7 @@ not found."
|
|||||||
(max-results #f))
|
(max-results #f))
|
||||||
"Find messages matching some query.
|
"Find messages matching some query.
|
||||||
|
|
||||||
The query is mandatory, the other (keyword) arguments are optional. | |||||||