Merge branch 'wip/djcb/more-scm'

This commit is contained in:
Dirk-Jan C. Binnema
2025-07-12 11:34:38 +03:00
6 changed files with 350 additions and 168 deletions

View File

@ -1,5 +1,5 @@
/* /*
** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ** Copyright (C) 2023-2025 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
** **
** This program is free software; you can redistribute it and/or modify it ** 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 ** under the terms of the GNU General Public License as published by the
@ -39,18 +39,18 @@ namespace Mu {
struct Property { struct Property {
enum struct Id { enum struct Id {
BatchSize, /**< Xapian batch-size */ BatchSize, /**< Xapian batch-size */
Contacts, /**< Cache of contact information */ Contacts, /**< Cache of contact information */
Created, /**< Time of creation */ Created, /**< Time of creation */
IgnoredAddresses,/**< Email addresses ignored for the contacts-cache */ IgnoredAddresses, /**< Email addresses ignored for the contacts-cache */
LastChange, /**< Time of last change */ LastChange, /**< Time of last change */
LastIndex, /**< Time of last index */ LastIndex, /**< Time of last index */
MaxMessageSize, /**< Maximum message size (in bytes) */ MaxMessageSize, /**< Maximum message size (in bytes) */
PersonalAddresses, /**< List of personal e-mail addresses */ PersonalAddresses, /**< List of personal e-mail addresses */
RootMaildir, /**< Root maildir path */ RootMaildir, /**< Root maildir path */
SchemaVersion, /**< Xapian DB schema version */ SchemaVersion, /**< Xapian DB schema version */
SupportNgrams, /**< Support ngrams for indexing & querying SupportNgrams, /**< Support ngrams for indexing & querying
* for e.g. CJK languages */ * for e.g. CJK languages */
/* <private> */ /* <private> */
_count_ /* Number of Ids */ _count_ /* Number of Ids */
}; };
@ -65,6 +65,7 @@ struct Property {
Configurable = 1 << 1, /**< A user-configurable parameter; name Configurable = 1 << 1, /**< A user-configurable parameter; name
* starts with 'conf-' */ * starts with 'conf-' */
Internal = 1 << 2, /**< Mu-internal field */ Internal = 1 << 2, /**< Mu-internal field */
Runtime = 1 << 3, /**< May change at runtime */
}; };
enum struct Type { enum struct Type {
Boolean, /**< Some boolean value */ Boolean, /**< Some boolean value */
@ -132,7 +133,7 @@ public:
{ {
Id::LastChange, Id::LastChange,
Type::Timestamp, Type::Timestamp,
Flags::ReadOnly, Flags::ReadOnly | Flags::Runtime,
MetadataIface::last_change_key, MetadataIface::last_change_key,
{}, {},
"Time when last change occurred" "Time when last change occurred"
@ -140,7 +141,7 @@ public:
{ {
Id::LastIndex, Id::LastIndex,
Type::Timestamp, Type::Timestamp,
Flags::ReadOnly, Flags::ReadOnly | Flags::Runtime,
"last-index", "last-index",
{}, {},
"Time when last indexing operation was completed" "Time when last indexing operation was completed"
@ -222,6 +223,47 @@ public:
return Nothing; return Nothing;
} }
/**
* Get the string-value for prop.
*
* For internal use
*
* @param prop some property
*
* @return a string
*/
std::string get_str(const Property& prop) const {
const auto str = cstore_.metadata(std::string{prop.name});
return str.empty() ? std::string{prop.default_val} : str;
}
/**
* Get the property value decoded based on the type
*
* @param prop_id a property id
*
* @return the value or Nothing
*/
template<Type type>
static constexpr auto decode(const std::string& str) {
if constexpr (type == Type::Number)
return static_cast<size_t>(str.empty() ? 0 : std::atoll(str.c_str()));
if constexpr (type == Type::Boolean)
return static_cast<size_t>(str.empty() ? false :
std::atol(str.c_str()) != 0);
else if constexpr (type == Type::Timestamp)
return static_cast<time_t>(str.empty() ? 0 : std::atoll(str.c_str()));
else if constexpr (type == Type::Path || type == Type::String)
return str;
else if constexpr (type == Type::StringList)
return split(str, SepaChar1);
throw std::logic_error("invalid type");
}
/** /**
* Get the property value of the correct type * Get the property value of the correct type
* *
@ -231,24 +273,8 @@ public:
*/ */
template<Id ID> template<Id ID>
auto get() const { auto get() const {
constexpr auto&& prop{property<ID>()}; constexpr auto& prop{property<ID>()};
const auto str = std::invoke([&]()->std::string { return decode<prop.type>(get_str(prop));
const auto str = cstore_.metadata(std::string{prop.name});
return str.empty() ? std::string{prop.default_val} : str;
});
if constexpr (prop.type == Type::Number)
return static_cast<size_t>(str.empty() ? 0 : std::atoll(str.c_str()));
if constexpr (prop.type == Type::Boolean)
return static_cast<size_t>(str.empty() ? false :
std::atol(str.c_str()) != 0);
else if constexpr (prop.type == Type::Timestamp)
return static_cast<time_t>(str.empty() ? 0 : std::atoll(str.c_str()));
else if constexpr (prop.type == Type::Path || prop.type == Type::String)
return str;
else if constexpr (prop.type == Type::StringList)
return split(str, SepaChar1);
throw std::logic_error("invalid prop " + std::string{prop.name});
} }
/** /**

View File

@ -38,6 +38,57 @@ to_store(SCM scm, const char *func, int pos)
return *reinterpret_cast<Store*>(scm_foreign_object_ref(scm, 0)); return *reinterpret_cast<Store*>(scm_foreign_object_ref(scm, 0));
} }
static SCM
subr_cc_store_alist(SCM store_scm) try {
constexpr auto func{"cc-store-alist"};
SCM alist{SCM_EOL};
const auto& conf{to_store(store_scm, func, 1).config()};
using MuConfig = Mu::Config;
using Type = MuConfig::Type;
for (const auto& prop: Mu::Config::properties) {
// don't expose internal values & values that may change during
// runtime
if (any_of(prop.flags &
(MuConfig::Flags::Internal | MuConfig::Flags::Runtime)))
continue;
const auto str{conf.get_str(prop)};
if (str.empty())
continue;
const auto name{make_symbol(prop.name)};
const auto val = std::invoke([&]() {
switch (prop.type) {
case Type::Number:
return to_scm(MuConfig::decode<Type::Number>(str));
case Type::Boolean:
return to_scm(MuConfig::decode<Type::Boolean>(str));
case Type::Timestamp:
return to_scm(MuConfig::decode<Type::Timestamp>(str));
case Type::Path:
return to_scm(MuConfig::decode<Type::Path>(str));
case Type::String:
return to_scm(MuConfig::decode<Type::String>(str));
case Type::StringList:
return to_scm(MuConfig::decode<Type::StringList>(str));
default:
throw ScmError{ScmError::Id::WrongType, func, 1, store_scm, "store"};
}
});
alist = scm_acons(name, val, alist);
}
return scm_reverse_x(alist, SCM_EOL);
} catch (const ScmError& err) {
err.throw_scm();
}
static SCM static SCM
subr_cc_store_mcount(SCM store_scm) try { subr_cc_store_mcount(SCM store_scm) try {
return to_scm(to_store(store_scm, "cc-store-mcount", 1).size()); return to_scm(to_store(store_scm, "cc-store-mcount", 1).size());
@ -139,6 +190,9 @@ init_subrs()
reinterpret_cast<scm_t_subr>(subr_cc_store_mcount)); reinterpret_cast<scm_t_subr>(subr_cc_store_mcount));
scm_c_define_gsubr("cc-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_cc_store_cfind)); reinterpret_cast<scm_t_subr>(subr_cc_store_cfind));
scm_c_define_gsubr("cc-store-alist", 1/*req*/, 0/*opt*/, 0/*rst*/,
reinterpret_cast<scm_t_subr>(subr_cc_store_alist));
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
} }
@ -156,7 +210,7 @@ Mu::Scm::init_store(const Store& store)
default_store = scm_make_foreign_object_1( default_store = scm_make_foreign_object_1(
store_type, const_cast<Store*>(&store)); store_type, const_cast<Store*>(&store));
scm_c_define("%default-store-object", default_store); scm_c_define("%cc-default-store", default_store);
init_subrs(); init_subrs();
@ -167,8 +221,8 @@ Mu::Scm::init_store(const Store& store)
SCM SCM
Mu::Scm::to_scm(const Contact& contact) Mu::Scm::to_scm(const Contact& contact)
{ {
static SCM email{scm_from_utf8_symbol("email")}; static SCM email{make_symbol("email")};
static SCM name{scm_from_utf8_symbol("name")}; static SCM name{make_symbol("name")};
SCM alist = scm_acons(email, to_scm(contact.email), SCM_EOL); SCM alist = scm_acons(email, to_scm(contact.email), SCM_EOL);
if (!contact.name.empty()) if (!contact.name.empty())

View File

@ -3,14 +3,18 @@
(use-modules (mu) (srfi srfi-64) (use-modules (mu) (srfi srfi-64)
(ice-9 textual-ports)) (ice-9 textual-ports))
(define (test-basic) (define (test-store)
(test-begin "test-basic") (test-begin "test-store")
(test-equal "mcount" 19 (mcount)) (test-equal "mcount" 19 (mcount))
(test-equal "cfind" 29 (length (cfind ""))) (test-equal "cfind" 29 (length (cfind "")))
(test-equal "mfind" 19 (length (mfind ""))) (test-equal "mfind" 19 (length (mfind "")))
(test-end "test-basic")) (let ((info (store->alist)))
(test-equal 50000 (assoc-ref info 'batch-size))
(test-equal 100000000 (assoc-ref info 'max-message-size)))
(test-end "test-store"))
(define (test-basic-mfind) (define (test-basic-mfind)
@ -33,10 +37,8 @@
(let ((recip (car (to msg)))) (let ((recip (car (to msg))))
(test-equal "Bilbo Baggins" (assoc-ref recip 'name)) (test-equal "Bilbo Baggins" (assoc-ref recip 'name))
(test-equal "bilbo@anotherexample.com" (assoc-ref recip 'email))) (test-equal "bilbo@anotherexample.com" (assoc-ref recip 'email)))
;; no date ;; no date
(test-assert (not (date msg))) (test-assert (not (date msg)))
;; flags ;; flags
(test-equal '(unread) (flags msg)) (test-equal '(unread) (flags msg))
(test-assert (unread? msg)) (test-assert (unread? msg))
@ -80,13 +82,21 @@
(references msg)) (references msg))
(test-equal "439C1136.90504@euler.org" (thread-id msg))) (test-equal "439C1136.90504@euler.org" (thread-id msg)))
(let ((msg (car (mfind "subject:\"gcc include search order\"")))) (let* ((msg (car (mfind "subject:\"gcc include search order\"")))
(alist (message->alist msg)))
(test-equal "gcc include search order" (subject msg)) (test-equal "gcc include search order" (subject msg))
(test-equal "klub" (header msg "precedence")) (test-equal "klub" (header msg "precedence"))
(test-equal "gcc-help.gcc.gnu.org" (mailing-list msg)) (test-equal "gcc-help.gcc.gnu.org" (mailing-list msg))
(test-equal #f (references msg)) (test-equal #f (references msg))
(test-equal "3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com" (message-id msg)) (test-equal "3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com" (message-id msg))
(test-equal "3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com" (thread-id msg))) (test-equal "3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com" (thread-id msg))
;; alist
(test-equal "gcc include search order" (assoc-ref alist 'subject))
(test-equal 'normal (assoc-ref alist 'priority))
(test-equal '((email . "anon@example.com") (name . "Mickey Mouse"))
(car (assoc-ref alist 'from))))
(test-end "test-message-more")) (test-end "test-message-more"))
@ -121,10 +131,14 @@
((index . 2) (content-type . "image/jpeg") (size . 21566) (filename . "custer.jpg"))) ((index . 2) (content-type . "image/jpeg") (size . 21566) (filename . "custer.jpg")))
(map (lambda (part) (mime-part->alist part)) (mime-parts msg))) (map (lambda (part) (mime-part->alist part)) (mime-parts msg)))
(test-equal "mime-part-0" (filename (list-ref (mime-parts msg) 0)))
(test-equal "sittingbull.jpg" (filename (list-ref (mime-parts msg) 1)))
(test-equal "custer.jpg" (filename (list-ref (mime-parts msg) 2)))
(let* ((part (list-ref (mime-parts msg) 1)) (let* ((part (list-ref (mime-parts msg) 1))
(alist (mime-part->alist part)) (alist (mime-part->alist part))
(fname (format #f "~a/~a" tmpdir (assoc-ref alist 'filename)))) (fname (format #f "~a/~a" tmpdir (assoc-ref alist 'filename))))
(write-to-file part #:filename fname) (write-to-file part #:path fname)
(test-assert (access? fname R_OK)) (test-assert (access? fname R_OK))
;; note, the 23881 is the _encoded_ size. ;; note, the 23881 is the _encoded_ size.
(test-equal 17674 (stat:size (stat fname)))) (test-equal 17674 (stat:size (stat fname))))
@ -167,7 +181,7 @@
(test-with-runner runner (test-with-runner runner
(test-begin "mu-scm-tests") (test-begin "mu-scm-tests")
(test-basic) (test-store)
(test-basic-mfind) (test-basic-mfind)
(test-mfind) (test-mfind)
(test-message-full) (test-message-full)

View File

@ -221,6 +221,13 @@ namespace Mu::Scm {
return scm_from_utf8_stringn(val.data(), val.size()); return scm_from_utf8_stringn(val.data(), val.size());
else if constexpr (is_char_array_v<Type>|| std::is_same_v<Type, const char*>) else if constexpr (is_char_array_v<Type>|| std::is_same_v<Type, const char*>)
return scm_from_utf8_string(val); return scm_from_utf8_string(val);
else if constexpr (std::is_same_v<Type, std::vector<std::string>>) {
SCM lst{SCM_EOL};
for (const auto& s: val)
lst = scm_append_x(scm_list_2(lst,
scm_list_1(to_scm(s))));
return lst;
}
else if constexpr (std::is_same_v<Type, bool>) else if constexpr (std::is_same_v<Type, bool>)
return scm_from_bool(val); return scm_from_bool(val);
else if constexpr (std::is_same_v<Type, size_t>) else if constexpr (std::is_same_v<Type, size_t>)

View File

@ -28,14 +28,17 @@
<mime-part> <mime-part>
mime-part->alist mime-part->alist
make-port make-port
filename
write-to-file write-to-file
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Message ;; Message
<message> <message>
make-message make-message
message->alist
date date
last-change changed
message-id message-id
path path
@ -86,6 +89,7 @@
mfind mfind
mcount mcount
cfind cfind
store->alist
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Other ;; Other
@ -100,6 +104,17 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Helpers
(define (set-documentation! symbol docstring)
"Set the docstring for symbol in current module to docstring.
This is useful for symbols that do not support docstrings directly, such
as (define foo 123)."
;; https://git.wolfsden.cz/guile-wolfsden/tree/wolfsden/documentation.scm
(set-object-property! (module-ref (current-module) symbol)
'documentation docstring))
;; 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)
"Call FUNC for each key/value in the PLIST. "Call FUNC for each key/value in the PLIST.
@ -129,57 +144,79 @@ If not found, return #f."
(string->symbol (string-drop name 1)) (string->symbol (string-drop name 1))
sym))) sym)))
(define (emacs-time->epoch-secs lst)
"Convert emacs-style timestamp LST to a number of seconds since epoch.
If LST is #f, return #f."
(if lst
(+ (ash (car lst) 16) (cadr lst))
#f))
(define (plist->alist plist) (define (plist->alist plist)
"Convert a plist into an alist." "Convert a plist into an alist.
This is specific for message plists."
(let ((alist '())) (let ((alist '()))
(plist-for-each (plist-for-each
(lambda (k v) (lambda (k v)
(set! alist (let ((key (decolonize-symbol k)))
(append! alist (set! alist
(list (cons (decolonize-symbol k) (append! alist
v))))) (list (cons key
(cond
((member key '(from to cc bcc))
(map plist->alist v))
((member key '(date changed))
(emacs-time->epoch-secs v))
(else v))))))))
plist) plist)
alist)) alist))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MIME-parts ;; 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> () (define-class <mime-part> ()
(mimepart #:init-value #f #:init-keyword #:mimepart) (cc-mimepart #:init-value #f #:init-keyword #:mimepart #:getter cc-mimepart)
(alist #:init-value #f #:init-keyword #:alist #:getter mime-part->alist)) (alist #:init-value #f #:init-keyword #:alist #:getter mime-part->alist))
(set-documentation!
'<mime-part>
"A <mime-part> represents the information about a message's MIME-part.
It has a few slots:
- cc-mimepart: a 'foreign object' wrapping a GMimePart*
- alist: the association-list representation of the MIME-part.")
(define* (make-port mime-part #:key (content-only? #t) (decode? #t)) (define* (make-port mime-part #:key (content-only? #t) (decode? #t))
"Create a read port for MIME-PART. "Create a read port for MIME-PART.
If CONTENT-ONLY? is #t, only include the contents, not headers. 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, If DECODE? is #t, decode the content (from e.g., base64); in that case,
CONTENT-ONLY? is implied to be #t." CONTENT-ONLY? is implied to be #t."
(cc-mime-make-stream-port (slot-ref mime-part 'mimepart) content-only? decode?)) (cc-mime-make-stream-port (cc-mimepart mime-part) content-only? decode?))
(define* (make-output-file mime-part #:key (filename #f) (overwrite? #f)) (define-method (filename (mime-part <mime-part>))
"Determine the file-name for MIME-part.
Either 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."
(let ((alist (mime-part->alist mime-part)))
(or (assoc-ref alist 'filename)
(format #f "mime-part-~d" (assoc-ref alist 'index)))))
(define* (make-output-file mime-part #:key (path #f) (overwrite? #f))
"Create a port for the file to write MIME-PART to. "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' PATH is file-name or 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 field in the mime-part and if that does not exist, use 'mime-part-<index>' with
<index> being the number of the mime-part. <index> being the number of the mime-part.
OVERWRITE? specifies whether existing files by the same name or overwritten. OVERWRITE? specifies whether existing files by the same name or overwritten.
Otherwise, trying to overwrite an existing file raises an error." Otherwise, trying to overwrite an existing file raises an error."
(let* ((alist (mime-part->alist mime-part)) (let* ((alist (mime-part->alist mime-part))
(filename (or filename (path (or path (filename mime-part))))
(assoc-ref alist 'filename) ;; we need an fd-based port since we want to support overwrite?
(format #f "mime-part-~d" (assoc-ref alist 'index))))) (open path
;; we need an fd-based port since we want to support overwrite? (logior O_WRONLY O_CREAT O_TRUNC (if overwrite? O_EXCL 0)) #o644)))
(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)) (define* (write-to-file mime-part #:key (path #f) (overwrite? #f))
"Write MIME-PART to a file. "Write MIME-PART to a file.
FILENAME is the path to the file name. If not specified, use the 'filename' PATH is the path/filename for the file. If not specified, use the 'filename'
field in the mime-part and if that does not exist, use 'mime-part-<index>' with field in the mime-part and if that does not exist, use 'mime-part-<index>' with
<index> being the number of the mime-part. <index> being the number of the mime-part.
@ -187,7 +224,7 @@ OVERWRITE? specifies whether existing files by the same name or overwritten.
Otherwise, trying to overwrite an existing file raises an error." Otherwise, trying to overwrite an existing file raises an error."
(let* ((input (make-port mime-part)) (let* ((input (make-port mime-part))
(output (make-output-file mime-part (output (make-output-file mime-part
#:filename filename #:overwrite? overwrite?)) #:path path #:overwrite? overwrite?))
(buf (make-bytevector 4096)) ;; just a guess... (buf (make-bytevector 4096)) ;; just a guess...
(bytes 0)) (bytes 0))
(while (not (eof-object? bytes)) ;; XXX do this in a more elegant way. (while (not (eof-object? bytes)) ;; XXX do this in a more elegant way.
@ -197,42 +234,59 @@ Otherwise, trying to overwrite an existing file raises an error."
(close output))) (close output)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Message ;; Message
;;
;; 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"
;; parts --> MIME-parts
;; generally the plist is a bit cheaper, since the mu-message
;; captures a file-descriptor.
(define-class <message> () (define-class <message> ()
(object #:init-value #f #:init-keyword #:object) (cc-message #:init-value #f #:init-keyword #:cc-message)
(parts #:init-value #f #:init-keyword #:parts)
(plist #:init-value #f #:init-keyword #:plist) (plist #:init-value #f #:init-keyword #:plist)
(parts #:init-value #f #:init-keyword #:parts)) (alist #:init-value #f))
(set-documentation!
'<message>
"A <message> represents the information about a message.
Exactly what information depends on how the object came to be;
these are the slots:
- cc-message: this is a foreign-object representing the mu
message object, and needs to be passed to some 'cc-' methods.
- parts: a list of <mime-part> objects
- plist: this is an Emacs-style property list which is cached
for each message in the store; this was originally added
for use in mu4e, but we re-use it here.
- alist: an association list; this is just more 'scheme-like'
version of the plist, it is created on-demand (message->alist).
A message that came from a search such as 'mfind' initially only
has the plist, but when a message is loaded from file, either
through make-message or by calling a function that needs a
full message, such as header or body, the cc-message is initialized.
Only having a plist is cheaper.")
(define (make-message path) (define (make-message path)
"Create a <message> from file at PATH." "Create a <message> from file at PATH."
(make <message> #:object (cc-message-make path))) (make <message> #:cc-message (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)) (when (not (slot-ref message 'plist))
(slot-set! message 'plist (cc-message-plist (object message)))) (slot-set! message 'plist (cc-message-plist (cc-message message))))
(slot-ref message 'plist)) (slot-ref message 'plist))
(define-method (object (message <message>)) (define-method (message->alist (message <message>))
"Get an association-list (alist) representation for MESSAGE."
(when (not (slot-ref message 'alist))
(slot-set! message 'alist (plist->alist (plist message))))
(slot-ref message 'alist))
(define-method (cc-message (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, create 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 'cc-message))
(slot-set! message 'object (cc-message-make (path message)))) (slot-set! message 'cc-message (cc-message-make (path message))))
(slot-ref message 'object)) (slot-ref message 'cc-message))
(define-method (find-field (message <message>) field)
(plist-find (plist message) field))
(define-method (sexp (message <message>)) (define-method (sexp (message <message>))
"Get the s-expression (plist) for this MESSAGE. "Get the s-expression (plist) for this MESSAGE.
@ -241,51 +295,44 @@ This is an internal data-structure, originally for use with mu4e, but useful
here as well. However, the precise details are not part of mu-scm API." here as well. However, the precise details are not part of mu-scm API."
(plist message)) (plist message))
(define (emacs-time->epoch-secs lst)
"Convert emacs-style timestamp LST to a number of seconds since epoch.
If LST is #f, return #f."
(if lst
(+ (ash (car lst) 16) (cadr lst))
#f))
;; Accessors 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."
(find-field message ':subject)) (assoc-ref (message->alist message) 'subject))
(define-method (maildir (message <message>)) (define-method (maildir (message <message>))
"Get the maildir for MESSAGE or #f if not found." "Get the maildir for MESSAGE or #f if not found."
(find-field message ':maildir)) (assoc-ref (message->alist message) 'maildir))
(define-method (message-id (message <message>)) (define-method (message-id (message <message>))
"Get the message-id for MESSAGE or #f if not found." "Get the message-id for MESSAGE or #f if not found."
(find-field message ':message-id)) (assoc-ref (message->alist message) 'message-id))
(define-method (date (message <message>)) (define-method (date (message <message>))
"Get the date for MESSAGE was sent. "Get the timestamp for MESSAGE was sent.
This is the number of seconds since epoch; #f if not found." This is the number of seconds since epoch; #f if not found."
(emacs-time->epoch-secs (find-field message ':date))) (assoc-ref (message->alist message) 'date))
(define-method (last-change (message <message>)) (define-method (changed (message <message>))
"Get the date for the last change to MESSAGE. "Get the timestamp for the last change to MESSAGE.
This is the number of seconds since epoch; #f if not found." This is the number of seconds since epoch; #f if not found."
(emacs-time->epoch-secs (find-field message ':changed))) (assoc-ref (message->alist message) 'changed))
(define-method (path (message <message>)) (define-method (path (message <message>))
"Get the file-system path for MESSAGE. "Get the file-system path for MESSAGE.
A symbol, either 'high, 'low or 'normal, or #f if not found." A symbol, either 'high, 'low or 'normal, or #f if not found."
(find-field message ':path)) (assoc-ref (message->alist message) 'path))
(define-method (priority (message <message>)) (define-method (priority (message <message>))
"Get the priority for MESSAGE. "Get the priority for MESSAGE.
A symbol, either 'high, 'low or 'normal, or #f if not found." A symbol, either 'high, 'low or 'normal, or #f if not found."
(find-field message ':priority)) (assoc-ref (message->alist message) 'priority))
(define-method (language (message <message>)) (define-method (language (message <message>))
"Get the ISO-639-1 language code for the MESSAGE as a symbol, if detected. "Get the ISO-639-1 language code for the MESSAGE as a symbol, if detected.
Return #f otherwise." Return #f otherwise."
(let ((lang (find-field message ':language))) (let ((lang ( (assoc-ref (message->alist message) 'language))))
(if lang (if lang
(string->symbol lang) (string->symbol lang)
#f))) #f)))
@ -293,7 +340,7 @@ Return #f otherwise."
(define-method (size (message <message>)) (define-method (size (message <message>))
"Get the size of the MESSAGE in bytes or #f if not available." "Get the size of the MESSAGE in bytes or #f if not available."
(find-field message ':size)) (assoc-ref (message->alist message) 'size))
(define-method (references (message <message>)) (define-method (references (message <message>))
"Get the list of reference of MESSAGE or #f if not available. "Get the list of reference of MESSAGE or #f if not available.
@ -301,7 +348,7 @@ Return #f otherwise."
reference (message-id) will appear at most once, duplicates and reference (message-id) will appear at most once, duplicates and
fake-message-id (see impls) are filtered out. If there are no references, return fake-message-id (see impls) are filtered out. If there are no references, return
#f." #f."
(find-field message ':references)) (assoc-ref (message->alist message) 'references))
(define-method (thread-id (message <message>)) (define-method (thread-id (message <message>))
"Get the oldest (first) reference for MESSAGE, or message-id if there are none. "Get the oldest (first) reference for MESSAGE, or message-id if there are none.
@ -314,20 +361,19 @@ This is method is useful to determine the thread a message is in."
(define-method (mailing-list (message <message>)) (define-method (mailing-list (message <message>))
"Get the mailing-list id for MESSAGE or #f if not available." "Get the mailing-list id for MESSAGE or #f if not available."
(find-field message ':list)) (assoc-ref (message->alist message) 'list))
;; Flags. ;; Flags.
(define-method (flags (message <message>)) (define-method (flags (message <message>))
"Get the size of the MESSAGE in bytes or #f if not available." "Get the size of the MESSAGE in bytes or #f if not available."
(find-field message ':flags)) (assoc-ref (message->alist 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 ((flgs (flags message)))
(find-field message ':flags))) (if flgs
(if flags (if (member flag flgs) #t #f)
(if (member flag flags) #t #f)
#f))) #f)))
(define-method (draft? (message <message>)) (define-method (draft? (message <message>))
@ -386,76 +432,89 @@ This is method is useful to determine the thread a message is in."
"Does MESSAGE have a calender invitation?" "Does MESSAGE have a calender invitation?"
(flag? message 'calendar)) (flag? message 'calendar))
(define-method (find-contact-field (message <message>) field)
"Get contact FIELD from MESSAGE as an alist.
Helper method "
(let ((cs (find-field message field)))
(if cs
(map plist->alist cs)
#f)))
(define-method (from (message <message>)) (define-method (from (message <message>))
"Get the sender (the From: field) for MESSAGE or #f if not found." "Get the sender (the From: field) for MESSAGE or #f if not found."
(find-contact-field message ':from)) (assoc-ref (message->alist message) 'from))
(define-method (to (message <message>)) (define-method (to (message <message>))
"Get the (intended) recipient for MESSAGE (the To: field) or #f if not found." "Get the (intended) recipient for MESSAGE (the To: field) or #f if not found."
(find-contact-field message ':to)) (assoc-ref (message->alist message) 'to))
(define-method (cc (message <message>)) (define-method (cc (message <message>))
"Get the (intended) carbon-copy recipient for MESSAGE (the Cc: field) or #f if "Get the (intended) carbon-copy recipient for MESSAGE (the Cc: field) or #f if
not found." not found."
(find-contact-field message ':cc)) (assoc-ref (message->alist message) 'cc))
(define-method (bcc (message <message>)) (define-method (bcc (message <message>))
"Get the (intended) blind carbon-copy recipient for MESSAGE (the Bcc: field) or "Get the (intended) blind carbon-copy recipient for MESSAGE (the Bcc: field) or
#f if not found." #f if not found."
(find-contact-field message ':bcc)) (assoc-ref (message->alist 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."
(cc-message-body (object message) html?)) (cc-message-body (cc-message 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."
(cc-message-header (object message) field)) (cc-message-header (cc-message message) field))
(define-method (mime-parts (message <message>)) (define-method (mime-parts (message <message>))
"Get the MIME-parts for this message. "Get the MIME-parts for this message.
This is a list of <mime-part> objects." This is a list of <mime-part> objects."
(let ((msgobj (object message))) (map (lambda (mimepart-alist)
(map (lambda (mimepart-alist) (make <mime-part>
(make <mime-part> #:mimepart (car mimepart-alist)
#:mimepart (car mimepart-alist) #:alist (cdr mimepart-alist)))
#:alist (cdr mimepart-alist))) (cc-message-parts (cc-message message))))
(cc-message-parts msgobj))))
;; 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 startup;
;; startup; for now that's the only store supported, but we keep things ;; for now that's the only store supported, but we keep things open.
;; open.
;; ;;
;; Since it's the default store, we'd like to call the methods without ;; Since it's the default store, we'd like to call the methods without
;; explicitly using %default-store; with GOOPS, we cannot pass a default for ;; explicitly using %default-store; with GOOPS, we cannot pass a default for
;; that, nor can we use keyword arguments (I think?). So use define* for that. ;; that, nor can we use keyword arguments (I think?). So use define* for that.
;; the 'store-object' is a foreign object wrapping a const Store*.
(define-class <store> () (define-class <store> ()
(store-object #:init-keyword #:store-object #:getter store-object)) (cc-store #:init-keyword #:cc-store #:getter cc-store)
(alist #:init-value #f))
(set-documentation!
'<store>
"A <store> represents mu's message store (database).
It has a few slots:
- cc-store: this is a foreign-object for a Mu::Store*.
It needs to be passed to some 'cc-' methods.
- alist: an association list; this is the cached representation
of some store properties.")
;; not exported ;; not exported
(define-method (make-store store-object) (define-method (make-store store-obj)
"Make a store from some STORE-OBJECT." "Make a store from some STORE-OBJ.
(make <store> #:store-object store-object)) STORE-OBJ a 'foreign-object' for a mu Store pointer."
(make <store> #:cc-store store-obj))
(define %default-store (define %default-store
;; %default-store-object is defined in mu-scm-store.cc (make-store %cc-default-store))
(make-store %default-store-object))
(set-documentation! '%default-store "Default store.")
(set-documentation! '%cc-default-store
"Default store object.
This is defined in the C++ code, and represents a \"foreign\" Store* object.")
(define* (store->alist #:key (store %default-store))
"Get an alist-representation for some store.
Keyword arguments:
#:store %default-store. Leave at default."
(when (not (slot-ref store 'alist))
(slot-set! store 'alist (cc-store-alist (cc-store store))))
(slot-ref store 'alist))
(define* (mfind query (define* (mfind query
#:key #:key
@ -465,7 +524,7 @@ This is a list of <mime-part> objects."
(sort-field 'date) (sort-field 'date)
(reverse? #f) (reverse? #f)
(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. The query is mandatory, the other (keyword) arguments are optional.
(mfind QUERY (mfind QUERY
@ -475,16 +534,16 @@ The query is mandatory, the other (keyword) arguments are optional.
#:sort-field? field to sort by, a symbol. Default: date #:sort-field? field to sort by, a symbol. Default: date
#:reverse? sort in descending order (z-a) #:reverse? sort in descending order (z-a)
#: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))
(cc-store-mfind (store-object store) query (cc-store-mfind (cc-store 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."
(cc-store-mcount (store-object store))) (cc-store-mcount (cc-store store)))
(define* (cfind pattern (define* (cfind pattern
#:key #:key
@ -500,7 +559,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))."
(cc-store-cfind (store-object store) pattern personal? after max-results)) (cc-store-cfind (cc-store store) pattern personal? after max-results))
;;; Misc ;;; Misc
@ -511,16 +570,18 @@ The pattern is mandatory; the other (keyword) arguments are optional.
;; The alist maps symbols to values; a value of #f indicates that the value is at ;; The alist maps symbols to values; a value of #f indicates that the value is at
;; its default. ;; its default.
%options ;; defined in c++ %options ;; defined in c++
(set-documentation! '%options
"Alist with the command-line parameters.")
;; Alist of user-preferences.
;;
;; - short-date: a strftime-compatibie string for the display
;; format of short dates.
;; - utc? : whether to assume use UTC for dates/times
(define %preferences (define %preferences
'( (short-date . "%F %T") '( (short-date . "%F %T")
(utc? . #f))) (utc? . #f)))
;; XXX; not exposed yet. Perhaps we need a "fluid" here? ;; XXX; not exposed yet. Perhaps we need a "fluid" here?
(set-documentation! '%preferences
"Alist with user-preferences.
- short-date: a strftime-compatibie string for the display
format of short dates.
- utc? : whether to assume use UTC for dates/times")
(define (value-or-preference val key) (define (value-or-preference val key)
"If VAL is the symbol 'preference, return the value for KEY from %preferences. "If VAL is the symbol 'preference, return the value for KEY from %preferences.

View File

@ -260,7 +260,7 @@ few key concepts, represented in some GOOP objects and other data-structures:
@end itemize @end itemize
@menu @menu
* Store:: the database of all information * Store:: where message information lives
* Message:: inspecting individual messages * Message:: inspecting individual messages
* Miscellaneous:: other functions * Miscellaneous:: other functions
* Helpers:: some helper functions * Helpers:: some helper functions
@ -352,6 +352,18 @@ Example usage:
=> 140728 => 140728
@end lisp @end lisp
@deffn {Scheme Procedure} store->alist
@end deffn
Retrieve an association list (``alist'') with information about the store.
Example:
@lisp
(store->alist)
=> ((batch-size . 50000) (created . 1741180008) (max-message-size . 100000000)
(personal-addresses "djcb@@example.com" "msx@@example.com")
(root-maildir . "/home/user/Maildir") (schema-version . 500))
@end lisp
@node Message @node Message
@section Message @section Message
@ -515,12 +527,20 @@ case, @code{content-only?} is implied to be #t.
Write MIME-part to file. Write MIME-part to file.
Use @code{filename} is the file/path to use for writing; if this is @code{#f}, 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 the name using the @code{filename} procedure.
does not exist, a generic name is chosen.
If @code{overwrite?} is true, overwrite existing files of the same name; If @code{overwrite?} is true, overwrite existing files of the same name;
otherwise, raise an error if the file already exists. otherwise, raise an error if the file already exists.
@deffn {Scheme Procedure} filename mime-part
@end deffn
Determine a filename for the given MIME-part.
This is either taken from the @t{filename} property of the MIME-part alist, or,
If that does not exist, a generic name.
@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}.
@ -638,13 +658,13 @@ Is this a personal message? Returns @t{#t} or @t{#f}.
@deffn {Scheme Procedure} calendar? message @deffn {Scheme Procedure} calendar? message
@end deffn @end deffn
Does this message include a calendar invitation? Returns @t{#t} or @t{#f}. Does this message include a calendar invitation? Returns @t{#t} or @code{#f}.
@subsection Miscellaneous @subsection Miscellaneous
@deffn {Scheme Procedure} last-change message @deffn {Scheme Procedure} changed message
@end deffn @end deffn
Get the time of the message's last change (through @t{mu}), or @t{#f} if there Get the time of the message's last change (through @t{mu}), or @code{#f} if there
is none. The time is expressed the data as the number of seconds since epoch, is none. The time is expressed the data as the number of seconds since epoch,
@t{time_t}. @t{time_t}.
@ -657,7 +677,7 @@ For example:
@deffn {Scheme Procedure} priority message @deffn {Scheme Procedure} priority message
@end deffn @end deffn
Get the message's priority. This is a symbol, either @t{high}, @t{normal} or Get the message's priority. This is a symbol, either @t{high}, @t{normal} or
@t{low}, or @t{#f} if not present. @t{low}, or @code{#f} if not present.
For example: For example:
@lisp @lisp
@ -677,7 +697,7 @@ For example:
@deffn {Scheme Procedure} language message @deffn {Scheme Procedure} language message
@end deffn @end deffn
Get the ISO-639-1 language code for message's primary language or @t{#f} if not Get the ISO-639-1 language code for message's primary language or @code{#f} if not
found. This is available only if @t{mu} was built with CLD2 support, see found. This is available only if @t{mu} was built with CLD2 support, see
@command{mu info}. The language code is represented as a symbol, such as @t{en}, @command{mu info}. The language code is represented as a symbol, such as @t{en},
@t{nl} or @t{fi}. @t{nl} or @t{fi}.
@ -813,7 +833,7 @@ Convert a @t{time_t} value (``seconds-since-epoch'') to a string. The optional
output format, while the @code{#:utc?} determines whether to use UTC. output format, while the @code{#:utc?} determines whether to use UTC.
@c Defaults are determined by the @code{%preferences} variable. @c Defaults are determined by the @code{%preferences} variable.
If @var{time_t} is @t{#f}, return @code{#f}. If @var{time_t} is @code{#f}, return @code{#f}.
@node GNU Free Documentation License @node GNU Free Documentation License
@appendix GNU Free Documentation License @appendix GNU Free Documentation License