diff --git a/IDEAS.org b/IDEAS.org index f0435d6d2..73f1332b5 100644 --- a/IDEAS.org +++ b/IDEAS.org @@ -27,9 +27,6 @@ future. the newest though) https://github.com/djcb/mu/issues/2759. Or, from new-to-old, reversed in thread: https://github.com/djcb/mu/issues/2807 -- perhaps use =--personal-addres= instead of =--my-address= for consistency - https://github.com/djcb/mu/issues/2806 - ** mu4e - Allow for *muting* messages https://github.com/djcb/mu/issues/636 Useful; @@ -41,11 +38,13 @@ future. https://github.com/djcb/mu/issues/2308 Shouldn't be _too_ hard, for someone that uses the functionality. -- Support "aggregate actions" apply to a set of messages, e.g. apply patch-set - in a set of messages. That'll require some advanced scripting, maybe using - Guile. - https://github.com/djcb/mu/issues/301 - https://github.com/djcb/mu/issues/2704 +- Apply the same action on multiple message, i.e., support "aggregate actions" + apply to a set of messages, e.g. apply patch-set in a set of messages. That'll + require some advanced scripting, maybe using Guile. + https://github.com/djcb/mu/issues/301 https://github.com/djcb/mu/issues/2704 + +- As the flip-side of the previous, apply multiple marks/actions on the same + message. https://github.com/djcb/mu/issues/2671 - Make sorting stable if there are multiple messages with the same date. We _could_ do this by adding some random millisecs to each message's timestamp; _or_ @@ -56,6 +55,11 @@ future. invisible unicode to fool crm-separator? https://github.com/djcb/mu/issues/2768 +- More general, make the MIME-part handling in the viewer a bit better, we have + the ~e~-path and the ~A~-path which is a bit confusing. Perhaps we should generate + the MIME parts off-screen (buffer). + https://github.com/djcb/mu/issues/2647 + - Org-link type for any message matching some reference See: https://github.com/djcb/mu/issues/2787. Some first steps implemented (searching for references). @@ -81,11 +85,11 @@ future. * Done -- Support mu4e-mark-handle-when also for when leaving emacs +** DONE Support mu4e-mark-handle-when also for when leaving emacs (kill-emacs-query-functions). https://github.com/djcb/mu/issues/2649 -- Support automatic handling for List-Unsubscribe headers and more in general, +** DONE Support automatic handling for List-Unsubscribe headers and more in general, handle mailing-list commands https://github.com/djcb/mu/issues/2623 and https://github.com/djcb/mu/issues/2724 This seems useful, but probably requires a lot of testing to get right. Can we re-use the Gnus code for this? @@ -93,3 +97,6 @@ future. Yes: this is implemented now, in 1.12.9. Various Gnus' mailing list commands are now available in the mu4e message view as well, such as ~gnus-mailing-list-subscribe~, ~gnus-mailing-list-unsubscribe~. + +** DONE perhaps use =--personal-addres= instead of =--my-address= for consistency + https://github.com/djcb/mu/issues/2806 (done as per 1.12.12) diff --git a/Makefile b/Makefile index c30a48e59..a5883d459 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,6 @@ endif # for the better error messages (esp. for fmt). ifneq (${MU_HACKER},) MESON_FLAGS:=$(MESON_FLAGS) '-Dbuildtype=debug' \ - '-Db_sanitize=address' \ '-Dreadline=enabled' \ '-Dcpp_std=c++23' endif @@ -93,6 +92,7 @@ $(BUILDDIR_VALGRIND): vg_opts:=--enable-debuginfod=no --leak-check=full --error-exitcode=1 test-valgrind: export G_SLICE=always-malloc test-valgrind: export G_DEBUG=gc-friendly +test-valgrind: export MU_VALGRIND=memcheck test-valgrind: build-valgrind @$(MESON) test -C $(BUILDDIR_VALGRIND) \ --wrap="$(VALGRIND) $(vg_opts)" \ @@ -102,6 +102,7 @@ check-valgrind: test-valgrind # we do _not_ pass helgrind; but this seems to be a false-alarm # https://gitlab.gnome.org/GNOME/glib/-/issues/2662 +test-helgrind: export MU_VALGRIND=helgrind test-helgrind: $(BUILDDIR_VALGRIND) $(MESON) -C $(BUILDDIR_VALGRIND) test \ --wrap="$(VALGRIND) --tool=helgrind --error-exitcode=1" \ @@ -152,7 +153,31 @@ dist: $(BUILDDIR) distclean: clean -HTMLPATH=${BUILDDIR}/mu4e/mu4e + +# +# documentation +# +BUILDAUX:=./build-aux +DOCPATH=${BUILDDIR}/doc +MU4E_DOCHTML=${DOCPATH}/mu4e + mu4e-doc-html: - @mkdir -p ${HTMLPATH} && cp mu4e/texinfo-klare.css ${HTMLPATH} - @cd mu4e; makeinfo -v -I ${BUILDDIR} -I ${BUILDDIR}/mu4e --html --css-ref=texinfo-klare.css -o ${HTMLPATH} mu4e.texi + @mkdir -p ${MU4E_DOCHTML} && cp $(BUILDAUX)/texinfo-klare.css ${MU4E_DOCHTML} + @cd mu4e; makeinfo -v -I ${BUILDDIR} -I ${BUILDAUX} \ + -I ${BUILDDIR}/mu4e --html --css-ref=texinfo-klare.css -o ${MU4E_DOCHTML} mu4e.texi +mu4e-doc-pdf: + @mkdir -p ${DOCPATH} + @cd mu4e; makeinfo -v -I ${BUILDDIR} -I ${BUILDDIR}/mu4e -I ${BUILDAUX} \ + --pdf -o ${DOCPATH}/mu4e.pdf mu4e.texi + +MU_SCM_DOCHTML=${DOCPATH}/mu-scm +mu-scm-doc-html: + @mkdir -p ${MU_SCM_DOCHTML} && cp $(BUILDAUX)/texinfo-klare.css ${MU_SCM_DOCHTML} + @cd scm; makeinfo -v -I ${BUILDDIR} -I ${BUILDDIR}/scm -I ${BUILDAUX} \ + --html --css-ref=texinfo-klare.css -o ${MU_SCM_DOCHTML} mu-scm.texi +mu-scm-doc-pdf: + @mkdir -p ${DOCPATH} + @cd scm; makeinfo -v -I ${BUILDDIR} -I ${BUILDDIR}/scm -I ${BUILDAUX} \ + --pdf -o ${DOCPATH}/mu-scm.pdf mu-scm.texi + +doc: mu4e-doc-html mu4e-doc-pdf mu-scm-doc-html mu-scm-doc-pdf diff --git a/NEWS.org b/NEWS.org index 4df464a2f..23648a7d6 100644 --- a/NEWS.org +++ b/NEWS.org @@ -3,10 +3,10 @@ * 1.12 (released on February 24, 2024) - The 1.12 series has been stable for a fairly long time, and gained some - changes in mean-time. Most of the changes are for bug fixes and documentation - improvements, but new features are available as well. Those are listed below - the 1.12 changes, prefixed by the version in which they appeared. + The 1.12 series has been "stable" for a fairly long time, and gained some + changes in the mean-time. Most of the changes are for bug fixes and + documentation improvements, but some new features are available as well. Those + are listed below, with the version in which they first appeared. We decided to put off a new development series (1.13 -> 1.14) until incompatible changes are required; for now working in the 1.12 series seems a @@ -23,8 +23,7 @@ - Better and faster indexing of HTML messages - Experimental search by (human) language wit CLD2 - For details & more, see below. Note that a few minor new features were added - _after_ the initial 1.12.0. + For details & more, see below. *** mu @@ -123,9 +122,25 @@ - 1.12.9: the cleanup phase after indexing is significantly faster now - - 1.12.10: ~mu4e-maildir-shortcuts~ and ~mu4e-bookmarks~ now understand a - property ~:hide-if-no-unread~, which hides the maildir/bookmark from the - main-view if there are no unread messages which the corresponding query. + - 1.12.12: the ~--my-address~ parameter to ~--mu-init~ has been renamed into + ~--personal-address~, for consistency with the other places where we refer + to it as a personal address. ~--my-address~ is still supported as an alias, + for backward compatibility. + + - 1.12.13: if available, *mu* now uses the system versions of ~CLI11~ and ~fmt~ + instead of the "vendored" ones. This can be overridden by passing the + ~-Duse-embedded-cli11=true~ and ~-Duse-embedded-fmt=true~ to Meson. You can + influence where the build system (i.e., meson) looks through the + `PKG_CONFIG_PATH` environment variable (see the pkg-config/pkgconf + man-pages) + + - 1.12.13: test commands are now built "lazily", i.e., building is only + triggered when running the tests. This speed up the build when not + testing + + - 1.12.13: new (sub)command ~labels~, for associating searchable labels with + messages. Labels are similar to /tags/, but better integrated with *mu* (and + *mu4e*). See =man mu-labels= for details. *** mu4e @@ -180,6 +195,10 @@ - iCalendar support is a work-in-progress with the new editor. One change is that support is now _automatically_ available. + - 1.12.12: add a =defcustom= ~mu4e-compose-jump-to-a-reasonable-place~ for + customizing whether mu4e jumps to some "reasonable place" when opening the + message composition buffer. Default is ~t~, as the current behavior. + **** other - New command ~mu4e-search-query~ (bound to =c=) which lets you pick a query @@ -230,7 +249,7 @@ - links (in text-mode emails) are now clickable through , to be consistent with eww. - - support new-mail notifications on MacOS out-of-the-box + - support new-mail notifications on MacOS/OSX out-of-the-box - allow sorting by tag @@ -245,7 +264,7 @@ details. - 1.12.8: a new variable ~mu4e-trash-without-flag~, which, when set to non-nil - makes trashing _not_ add the the Maildir ~T~ flag. See its docstring for + makes trashing _not_ add the Maildir ~T~ flag. See its docstring for details. - 1.12.9: A (experimental) "transient" menu has been added for mu4e. You can @@ -270,14 +289,48 @@ message view as well, such as ~gnus-mailing-list-subscribe~, ~gnus-mailing-list-unsubscribe~. + - 1.12.10: ~mu4e-maildir-shortcuts~ and ~mu4e-bookmarks~ now understand a + property ~:hide-if-no-unread~, which hides the maildir/bookmark from the + main-view if there are no unread messages which the corresponding query. + + - 1.12.10: ~mu4e-maildir-shortcuts~ and ~mu4e-bookmarks~ now understand a + property ~:hide-if-no-unread~, which hides the maildir/bookmark from the + main-view if there are no unread messages which the corresponding query. + + - 1.12.12: it now possible to create Emacs bookmarks for both messages + (default) and queries. See the new variable ~mu4e-emacs-bookmark-policy~. + + - 1.12.12: ~mu4e-get-mail-command~ which specifies the shell command to use + for getting mail, can now also be a function return that shell-command. + This makes it easier to use different shell commands in different + situations. + + - 1.12.13: with an SCM-enabled ~mu~, you can set ~mu4e-mu-scm-server~ to non-nil + and connect to the SCM REPL with ~M-x mu4e-mu-scm-repl~ after restart (see + their docstrings for details) + +*** scm + + - 1.12.12: add new guile/scheme binding in ~scm/~. These are to replace the + long-deprecated ~guile/~ bindings. For now, this is all rather new and + experimental, but the basics are there. + + This requires a slightly newer gmime (3.2.8?) than the one ~mu~ requires. + + - 1.12.13: add the ~--listen~ flag for ~mu scm~ and ~mu server~, to start a REPL + on a Unix domain socket. See the reference manual for details. + + - 1.12.13: ~mu scm~ also gained support for labels and logging; furthermore, + ~mfind~ was made much faster. + *** Contributors Thanks to our contributors - code committers belows, but also to everyone who filed tickets, asked questions, answered them etc. - Babak Farrokhi, Christophe Troestler, Christoph Reichenbach, Daniel Fleischer, - David Edmondson, Davide Masserut, Dirk-Jan C. Binnema, Jeremy Sowden, - Lin Jian, Martin R. Albrecht, Nacho Barrientos, Nicholas Vollmer, + Babak Farrokhi, Christophe Troestler, Christoph Reichenbach, Daniel + Fleischer, David Edmondson, Davide Masserut, Dirk-Jan C. Binnema, Jeremy + Sowden, Lin Jian, Martin R. Albrecht, Nacho Barrientos, Nicholas Vollmer, Nicolas P. Rougier, ramon diaz-uriarte (at Phelsuma), reindert, Ruijie Yu, Sean Farley, stardiviner, Tassilo Horn and Thierry Volpiatto @@ -1513,7 +1566,7 @@ End of search results - allow for shell commands with arguments in `mu4e-get-mail-command'. - support marking messages as 'read' and 'unread' - - show the current query in the the mode-line (`global-mode-string'). + - show the current query in the mode-line (`global-mode-string'). - don't repeat 'Re:' / 'Fwd:' - colorize cited message parts - better handling of text-based, embedded message attachments diff --git a/README.org b/README.org index a78b205c3..d07961c55 100644 --- a/README.org +++ b/README.org @@ -11,11 +11,6 @@ [[https://www.djcbsoftware.nl/code/mu/mu4e/Installation.html#Building-from-a-release-tarball-1][https://img.shields.io/badge/Platform-FreeBSD-8b3a3a?logo=freebsd&logoColor=c32136&.svg]] [[https://formulae.brew.sh/formula/mu#default][https://img.shields.io/badge/Platform-macOS-101010?logo=apple&logoColor=ffffff&.svg]] - [ *Note*: you are looking at the *development* branch, which is where new code is - being developed and tested, and which may occasionally break. Distributions and - non-adventurous users are instead recommended to use the [[https://github.com/djcb/mu/tree/release/1.10][1.10 Release Branch]] or - to pick up one of the [[https://github.com/djcb/mu/releases][1.10 Releases]]. ] - Welcome to ~mu~! Latest development news: [[NEWS.org]]. @@ -25,29 +20,39 @@ e-mail message in our work-flows, it's essential to quickly deal with all that mail - in particular, to instantly find that one important e-mail you need right now, and quickly file away message for later use. -~mu~ is a tool for dealing with e-mail messages stored in the Maildir-format. ~mu~'s -purpose in life is to help you to quickly find the messages you need; in -addition, it allows you to view messages, extract attachments, create new -maildirs, and so on. +~mu~ is a set of command-line tools for dealing with e-mail messages stored in the +Maildir-format. ~mu~'s goal is to help you to quickly find the messages you need, +view them, extract attachments, create new maildirs, and so on. -After indexing your messages into a [[http://www.xapian.org][Xapian]]-database, you can search them using a -custom query language. You can use various message fields or words in the body -text to find the right messages. +After indexing your messages into a [[http://www.xapian.org][Xapian]]-database, you can search them through +a query language. You can use various message fields or words in the body text +to find the right messages. Built on top of ~mu~ are some extensions (included in this package): - ~mu4e~: a full-featured e-mail client that runs inside emacs -- ~mu-guile~: bindings for the Guile/Scheme programming language (version 3.0 and +- ~mu-scm~: bindings for the Guile/Scheme programming language (version 3.0 and later) -~mu~ is written in C++; ~mu4e~ is written in ~elisp~ and ~mu-guile~ in a mix of C++ and -Scheme. +~mu~ is written in C++; ~mu4e~ is written in ~elisp~ and ~mu-scm~ is written in a mix of +C++ and Scheme. + +~mu~ is available in many Linux distributions (e.g. Debian/Ubuntu and Fedora) +under the name ~maildir-utils~; apparently because they don't like short names. +All of the code is distributed under the terms of the [[https://www.gnu.org/licenses/gpl-3.0.en.html][GNU General Public License +version 3]] (or higher). + +* Versions + +*mu* attempts to balance development speed and stability. For this, every few +months there is a stable version (e.g. ~1.12.12~); development then continues in +in ~master~, with ~-dev~ (e.g. ~1.12.13-dev2~): ~-dev~-suffixed versions refer to the +current _development_ code, until it is released (and then looses its ~-dev~ suffix, +i.e., becomes the ~1.12.13~ release). -~mu~ is available in Linux distributions (e.g. Debian/Ubuntu and Fedora) under the -name ~maildir-utils~; apparently because they don't like short names. All of the -code is distributed under the terms of the [[https://www.gnu.org/licenses/gpl-3.0.en.html][GNU General Public License version 3]] -(or higher). +So if you want stability, grab the [[https://github.com/djcb/mu/releases/][release]]; if you are more +adventurous/impatient, git ~master~ is there as well. * Installation @@ -61,7 +66,7 @@ as a Linux distribution. Many have packages available. To be able to build ~mu~, ensure you have: -- a C++17 compiler (~gcc~ or ~clang~ are known to work) +- a C++17 compiler (~gcc~ and ~clang~ are known to work) - development packages for /Xapian/ and /GMime/ and /GLib/ (see ~meson.build~ for the versions) - basic tools such as ~make~, ~sed~, ~grep~ @@ -91,8 +96,8 @@ $ ./autogen.sh && make $ sudo make install #+end_example -Alternatively, you can run ~meson~ directly (see the ~meson~ documentation for -more details): +You can of course also run ~meson~ directly (see the ~meson~ documentation for more +details): #+begin_example $ meson setup build $ meson compile -C build diff --git a/mu4e/htmlxref.cnf b/build-aux/Texinfo_GNU.cnf similarity index 91% rename from mu4e/htmlxref.cnf rename to build-aux/Texinfo_GNU.cnf index 1af587bb3..9f4e21226 100644 --- a/mu4e/htmlxref.cnf +++ b/build-aux/Texinfo_GNU.cnf @@ -1,18 +1,17 @@ -# htmlxref.cnf - reference file for free Texinfo manuals on the web. +# Texinfo_GNU.cnf - reference file for GNU Texinfo manuals on the web. -htmlxrefversion=2023-04-02.12; # UTC +htmlxrefversion=2025-08-01.01; # UTC -# Copyright 2010-2023 Free Software Foundation, Inc. +# Copyright 2010-2025 Free Software Foundation, Inc. # # Copying and distribution of this file, with or without modification, # are permitted in any medium without royalty provided the copyright # notice and this notice are preserved. # # The latest version of this file is available at -# http://ftpmirror.gnu.org/texinfo/htmlxref.cnf. +# http://ftpmirror.gnu.org/texinfo/htmlxref.d/Texinfo_GNU.cnf. # Email corrections or additions to bug-texinfo@gnu.org. -# The primary goal is to list all relevant GNU manuals; -# other free manuals are also welcome. +# The primary goal is to list all relevant GNU manuals. # # To be included in this list, a manual must: # @@ -20,7 +19,7 @@ htmlxrefversion=2023-04-02.12; # UTC # - have a unique file name (e.g., manual identifier), i.e., be related to the # package name. Things like "refman" or "tutorial" don't work. # - follow the naming convention for nodes described at -# http://www.gnu.org/software/texinfo/manual/texinfo/html_node/HTML-Xref.html +# https://www.gnu.org/software/texinfo/manual/texinfo/html_node/HTML-Xref.html # This is what makeinfo and texi2html implement. # # Unless the above criteria are met, it's not possible to generate @@ -63,10 +62,13 @@ autoconf node ${GS}/autoconf/manual/html_node/ autogen mono ${GS}/autogen/manual/autogen.html autogen chapter ${GS}/autogen/manual/html_chapter/ -autogen node ${GS}/autoconf/manual/html_node/ +autogen node ${GS}/autogen/manual/html_node/ automake mono ${GS}/automake/manual/automake.html automake node ${GS}/automake/manual/html_node/ + # + automake-history mono ${GS}/automake/history/automake-history.html + automake-history node ${GS}/automake/history/html_node/ avl node http://adtinfo.org/libavl.html/ @@ -86,6 +88,8 @@ binutils node ${BINUTILS}/binutils/ gprof mono ${BINUTILS}/gprof.html gprof node ${BINUTILS}/gprof/ # + gprofng mono ${BINUTILS}/gprofng.html + # ld mono ${BINUTILS}/ld.html ld node ${BINUTILS}/ld/ @@ -130,10 +134,7 @@ ddrescue mono ${GS}/ddrescue/manual/ddrescue_manual.html dejagnu node ${GS}/dejagnu/manual/ DICO = https://www.gnu.org.ua/software/dico/manual -dico mono ${DICO}/dico.html -dico chapter ${DICO}/html_chapter/ -dico section ${DICO}/html_section/ -dico node ${DICO}/html_node/ +dico node ${DICO} diffutils mono ${GS}/diffutils/manual/diffutils.html diffutils node ${GS}/diffutils/manual/html_node/ @@ -174,9 +175,18 @@ emacs node ${EMACS}/html_node/emacs/ ediff mono ${EMACS}/html_mono/ediff.html ediff node ${EMACS}/html_node/ediff/ # + eglot mono ${EMACS}/html_mono/eglot.html + eglot node ${EMACS}/html_node/eglot/ + # eieio mono ${EMACS}/html_mono/eieio.html eieio node ${EMACS}/html_node/eieio/ # + eintr mono ${EMACS}/html_mono/eintr.html + eintr node ${EMACS}/html_node/eintr/ + # possibly an old name + lispintro mono ${EMACS}/html_mono/eintr.html + lispintro node ${EMACS}/html_node/eintr/ + # elisp mono ${EMACS}/html_mono/elisp.html elisp node ${EMACS}/html_node/elisp/ # @@ -282,8 +292,8 @@ emacs node ${EMACS}/html_node/emacs/ sieve mono ${EMACS}/html_mono/sieve.html sieve node ${EMACS}/html_node/sieve/ # - smtp mono ${EMACS}/html_mono/smtpmail.html - smtp node ${EMACS}/html_node/smtpmail/ + smtpmail mono ${EMACS}/html_mono/smtpmail.html + smtpmail node ${EMACS}/html_node/smtpmail/ # speedbar mono ${EMACS}/html_mono/speedbar.html speedbar node ${EMACS}/html_node/speedbar/ @@ -300,6 +310,9 @@ emacs node ${EMACS}/html_node/emacs/ url mono ${EMACS}/html_mono/url.html url node ${EMACS}/html_node/url/ # + use-package mono ${EMACS}/html_mono/use-package.html + use-package node ${EMACS}/html_node/use-package/ + # vhdl-mode mono ${EMACS}/html_mono/vhdl-mode.html vhdl-mode node ${EMACS}/html_node/vhdl-mode/ # @@ -327,7 +340,7 @@ emacs-muse node ${GS}/emacs-muse/manual/html_node/ emms node ${GS}/emms/manual/ -ada-mode mono https://elpa.gnu.org/packages/ada-mode.html +ada-mode mono https://elpa.gnu.org/packages/doc/ada-mode.html gpr-mode mono https://elpa.gnu.org/packages/doc/gpr-mode.html @@ -563,39 +576,17 @@ LILYPOND = http://lilypond.org/doc/stable/Documentation lilypond-snippets node ${LILYPOND}/snippets/ lilypond-usage node ${LILYPOND}/usage/ lilypond-web node ${LILYPOND}/web/ - music-glossary node ${LILYPOND}/music-glossary/ + music-glossary node ${LILYPOND}/glossary/ liquidwar6 mono ${GS}/liquidwar6/manual/liquidwar6.html liquidwar6 node ${GS}/liquidwar6/manual/html_node/ -lispintro mono ${GS}/emacs/emacs-lisp-intro/html_mono/emacs-lisp-intro.html -lispintro node ${GS}/emacs/emacs-lisp-intro/html_node/index.html - LSH = http://www.lysator.liu.se/~nisse/lsh lsh mono ${LSH}/lsh.html m4 mono ${GS}/m4/manual/m4.html m4 node ${GS}/m4/manual/html_node/ -MITGNUSCHEME = ${GS}/mit-scheme/documentation/stable -mit-scheme-user mono ${MITGNUSCHEME}/mit-scheme-user.html -mit-scheme-user node ${MITGNUSCHEME}/mit-scheme-user/ - # - mit-scheme-ref mono ${MITGNUSCHEME}/mit-scheme-ref.html - mit-scheme-ref node ${MITGNUSCHEME}/mit-scheme-ref/ - # - mit-scheme-ffi mono ${MITGNUSCHEME}/mit-scheme-ffi.html - mit-scheme-ffi node ${MITGNUSCHEME}/mit-scheme-ffi/ - # - mit-scheme-sos mono ${MITGNUSCHEME}/mit-scheme-sos.html - mit-scheme-sos node ${MITGNUSCHEME}/mit-scheme-sos/ - # - mit-scheme-imail mono ${MITGNUSCHEME}/mit-scheme-imail.html - # - mit-scheme-blowfish mono ${MITGNUSCHEME}/mit-scheme-blowfish.html - # - mit-scheme-gdbm mono ${MITGNUSCHEME}/mit-scheme-gdbm.html - mailutils mono ${GS}/mailutils/manual/mailutils.html mailutils chapter ${GS}/mailutils/manual/html_chapter/ mailutils section ${GS}/mailutils/manual/html_section/ @@ -607,15 +598,29 @@ make node ${GS}/make/manual/html_node/ mdk mono ${GS}/mdk/manual/mdk.html mdk node ${GS}/mdk/manual/html_node/ +# GNU Metadata Exchange Utilities. Documentation is the IWF Metadata +# Harvester documentation, which is the package the GNU Metadata Exchange +# Utilities is derived from. METAEXCHANGE = https://ftp.gwdg.de/pub/gnu2/iwfmdh/doc/texinfo iwf_mh node ${METAEXCHANGE}/iwf_mh.html scantest node ${METAEXCHANGE}/scantest.html MIT_SCHEME = ${GS}/mit-scheme/documentation/stable + mit-scheme-ref mono ${MIT_SCHEME}/mit-scheme-ref.html mit-scheme-ref node ${MIT_SCHEME}/mit-scheme-ref/ + # + mit-scheme-ffi mono ${MIT_SCHEME}/mit-scheme-ffi.html + mit-scheme-ffi node ${MIT_SCHEME}/mit-scheme-ffi/ + # + mit-scheme-user mono ${MIT_SCHEME}/mit-scheme-user.html mit-scheme-user node ${MIT_SCHEME}/mit-scheme-user/ - sos node ${MIT_SCHEME}/mit-scheme-sos/ + # + mit-scheme-sos mono ${MIT_SCHEME}/mit-scheme-sos.html + mit-scheme-sos node ${MIT_SCHEME}/mit-scheme-sos/ + # mit-scheme-imail mono ${MIT_SCHEME}/mit-scheme-imail.html + mit-scheme-gdbm mono ${MIT_SCHEME}/mit-scheme-gdbm.html + mit-scheme-blowfish mono ${MIT_SCHEME}/mit-scheme-blowfish.html moe mono ${GS}/moe/manual/moe_manual.html @@ -656,13 +661,14 @@ pspp node ${GS}/pspp/manual/html_node/ pyconfigure mono ${GS}/pyconfigure/manual/pyconfigure.html pyconfigure node ${GS}/pyconfigure/manual/html_node/ -R = https://cran.r-project.org/doc/manuals +R = https://CRAN.R-project.org/doc/manuals R-intro mono ${R}/R-intro.html R-lang mono ${R}/R-lang.html R-exts mono ${R}/R-exts.html R-data mono ${R}/R-data.html R-admin mono ${R}/R-admin.html R-ints mono ${R}/R-ints.html + R-FAQ mono ${R}/R-FAQ.html rcs mono ${GS}/rcs/manual/rcs.html rcs node ${GS}/rcs/manual/html_node/ @@ -720,7 +726,9 @@ sourceinstall node ${GS}/sourceinstall/manual/html_node/ sqltutor mono ${GS}/sqltutor/manual/sqltutor.html sqltutor node ${GS}/sqltutor/manual/html_node/ +# maybe an old name for the project src-highlite mono ${GS}/src-highlite/source-highlight.html +source-highlight mono ${GS}/src-highlite/source-highlight.html swbis mono ${GS}/swbis/manual.html @@ -732,6 +740,8 @@ tar node ${GS}/tar/manual/html_node/ teseq mono ${GS}/teseq/manual/teseq.html teseq node ${GS}/teseq/manual/html_node/ +termcap mono ${GS}/termutils/manual/termcap-1.3/html_mono/termcap.html + TEXINFO = ${GS}/texinfo/manual texinfo mono ${TEXINFO}/texinfo/texinfo.html texinfo node ${TEXINFO}/texinfo/html_node/ @@ -766,18 +776,6 @@ xboard mono ${GS}/xboard/manual/xboard.html xboard node ${GS}/xboard/manual/html_node/ # emacs-page -# Free TeX-related Texinfo manuals on tug.org. - -T = https://tug.org/texinfohtml - -dvipng mono ${T}/dvipng.html -dvips mono ${T}/dvips.html -eplain mono ${T}/eplain.html -kpathsea mono ${T}/kpathsea.html -latex2e mono ${T}/latex2e.html -tlbuild mono ${T}/tlbuild.html -web2c mono ${T}/web2c.html - # Local Variables: # eval: (add-hook 'write-file-hooks 'time-stamp) diff --git a/build-aux/Texinfo_nonGNU.cnf b/build-aux/Texinfo_nonGNU.cnf new file mode 100644 index 000000000..5141d9b43 --- /dev/null +++ b/build-aux/Texinfo_nonGNU.cnf @@ -0,0 +1,47 @@ +# Texinfo_nonGNU.cnf - reference file for free Texinfo manuals on the web. + +htmlxrefversion=2024-09-17.20; # UTC + +# Copyright 2010-2024 Free Software Foundation, Inc. +# +# Copying and distribution of this file, with or without modification, +# are permitted in any medium without royalty provided the copyright +# notice and this notice are preserved. +# +# The latest version of this file is available at +# http://ftpmirror.gnu.org/texinfo/htmlxref.cnf. +# Email corrections or additions to bug-texinfo@gnu.org. +# The primary goal is to list free manuals. +# +# To be included in this list, a manual must: +# +# - have a generic url, e.g., no version numbers; +# - have a unique file name (e.g., manual identifier), i.e., be related to the +# package name. Things like "refman" or "tutorial" don't work. +# - follow the naming convention for nodes described at +# https://www.gnu.org/software/texinfo/manual/texinfo/html_node/HTML-Xref.html +# This is what makeinfo and texi2html implement. +# +# Unless the above criteria are met, it's not possible to generate +# reliable cross-manual references. + +# Free TeX-related Texinfo manuals on tug.org. + +T = https://tug.org/texinfohtml + +dvipng mono ${T}/dvipng.html +dvips mono ${T}/dvips.html +eplain mono ${T}/eplain.html +kpathsea mono ${T}/kpathsea.html +latex2e mono ${T}/latex2e.html +tlbuild mono ${T}/tlbuild.html +web2c mono ${T}/web2c.html + + +# Local Variables: +# eval: (add-hook 'write-file-hooks 'time-stamp) +# time-stamp-start: "htmlxrefversion=" +# time-stamp-format: "%:y-%02m-%02d.%02H" +# time-stamp-time-zone: "UTC" +# time-stamp-end: "; # UTC" +# End: diff --git a/guile/fdl.texi b/build-aux/fdl.texi similarity index 100% rename from guile/fdl.texi rename to build-aux/fdl.texi diff --git a/mu4e/texinfo-klare.css b/build-aux/texinfo-klare.css similarity index 100% rename from mu4e/texinfo-klare.css rename to build-aux/texinfo-klare.css diff --git a/guile/mu/stats.scm b/guile/mu/stats.scm index 1e73605f1..80ca30590 100644 --- a/guile/mu/stats.scm +++ b/guile/mu/stats.scm @@ -100,7 +100,7 @@ EXPR (or #t for all). Returns #f if undefined." (average (map func (mu:message-list expr)))) (define* (mu:stddev func #:optional (expr #t)) - "Get the standard deviation the the values of FUNC applied to all + "Get the standard deviation the values of FUNC applied to all messages matching EXPR (or #t for all). This is the 'population' stddev, not the 'sample' stddev. Returns #f if undefined." (stddev (map func (mu:message-list expr)))) diff --git a/guile/tests/meson.build b/guile/tests/meson.build index 07b379059..05bb14e81 100644 --- a/guile/tests/meson.build +++ b/guile/tests/meson.build @@ -1,4 +1,4 @@ -## Copyright (C) 2024 Dirk-Jan C. Binnema +## Copyright (C) 2025 Dirk-Jan C. Binnema ## ## 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 @@ -27,6 +27,7 @@ if get_option('b_sanitize') == 'none' executable('test-mu-guile', 'test-mu-guile.cc', install: false, + build_by_default: false, cpp_args: [ '-DABS_SRCDIR="' + meson.current_source_dir() + '"', '-DGUILE_LOAD_PATH="' + guile_load_path + '"', diff --git a/lib/meson.build b/lib/meson.build index c3a798dbb..eb772efd3 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -1,4 +1,4 @@ -## Copyright (C) 2021-2023 Dirk-Jan C. Binnema +## Copyright (C) 2021-2025 Dirk-Jan C. Binnema ## ## 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 @@ -24,7 +24,9 @@ lib_mu=static_library( # db 'mu-config.cc', 'mu-contacts-cache.cc', + 'mu-labels-cache.cc', 'mu-store.cc', + 'mu-store-labels.cc', 'mu-xapian-db.cc', # querying 'mu-query-macros.cc', diff --git a/lib/message/meson.build b/lib/message/meson.build index 006bb189a..31052b835 100644 --- a/lib/message/meson.build +++ b/lib/message/meson.build @@ -1,4 +1,4 @@ -## Copyright (C) 2022-2024 Dirk-Jan C. Binnema +## Copyright (C) 2022-2025 Dirk-Jan C. Binnema ## ## 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 @@ -25,6 +25,7 @@ lib_mu_message=static_library( 'mu-document.cc', 'mu-fields.cc', 'mu-flags.cc', + 'mu-labels.cc', 'mu-priority.cc', 'mu-mime-object.cc', ], diff --git a/lib/message/mu-contact.cc b/lib/message/mu-contact.cc index c6439b066..22795db87 100644 --- a/lib/message/mu-contact.cc +++ b/lib/message/mu-contact.cc @@ -61,16 +61,6 @@ Mu::to_string(const Mu::Contacts& contacts) return res; } -size_t -Mu::lowercase_hash(const std::string& s) -{ - std::size_t djb = 5381; // djb hash - for (const auto c : s) - djb = ((djb << 5) + djb) + - static_cast(g_ascii_tolower(c)); - return djb; -} - #ifdef BUILD_TESTS /* * Tests. diff --git a/lib/message/mu-contact.hh b/lib/message/mu-contact.hh index d417d4ec0..90052938e 100644 --- a/lib/message/mu-contact.hh +++ b/lib/message/mu-contact.hh @@ -1,5 +1,5 @@ /* -** Copyright (C) 2022 Dirk-Jan C. Binnema +** Copyright (C) 2022-2025 Dirk-Jan C. Binnema ** ** 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 @@ -32,101 +32,118 @@ #include #include "mu-fields.hh" -struct _InternetAddressList; - namespace Mu { - -/** - * Get the hash value for a lowercase value of s; useful for email-addresses - * - * @param s a string - * - * @return a hash value. - */ -size_t lowercase_hash(const std::string& s); - struct Contact { - enum struct Type { - None, Sender, From, ReplyTo, To, Cc, Bcc + enum struct Type:char { + None='\0', Sender='s', From='f', + ReplyTo='r', To='t', Cc='c', Bcc='b' }; /** * Construct a new Contact * - * @param email_ email address - * @param name_ name or empty - * @param type_ contact field type - * @param message_date_ data for the message for this contact + * @param email email address + * @param name name or empty + * @param type contact field type + * @param message_date data for the message for this contact */ - Contact(const std::string& email_, const std::string& name_ = "", - Type type_ = Type::None, ::time_t message_date_ = 0) - : email{email_}, name{name_}, type{type_}, - message_date{message_date_}, personal{}, frequency{1}, tstamp{} + Contact(const std::string& email, const std::string& name = {}, + Type type = {}, int64_t message_date ={}) + : email{email}, name{name}, type{type}, + message_date{message_date}, personal{}, frequency{1}, tstamp{} { cleanup_name(); } /** * Construct a new Contact * - * @param email_ email address - * @param name_ name or empty - * @param message_date_ date of message this contact originate from - * @param personal_ is this a personal contact? - * @param freq_ how often was this contact seen? - * @param tstamp_ timestamp for last change + * @param email email address + * @param name name or empty + * @param message_date date of message this contact originate from + * @param personal is this a personal contact? + * @param freq how often was this contact seen? + * @param tstamp timestamp for last change */ - Contact(const std::string& email_, const std::string& name_, - time_t message_date_, bool personal_, size_t freq_, - int64_t tstamp_) - : email{email_}, name{name_}, type{Type::None}, - message_date{message_date_}, personal{personal_}, frequency{freq_}, - tstamp{tstamp_} - { cleanup_name();} + Contact(const std::string& email, const std::string& name, + int64_t message_date, bool personal, size_t freq, + int64_t tstamp) + : email{email}, name{name}, type{}, + message_date{message_date}, personal{personal}, frequency{freq}, + tstamp{tstamp} + { cleanup_name(); } /** * Get the "display name" for this contact: * - * If there's a non-empty name, it's Jane Doe - * otherwise it's just the e-mail address. Names with commas are quoted + * If there is a non-empty name, it is of the form + * Jane Doe + * Otherwise it is just the e-mail address. Names with commas are quoted * (with the quotes escaped). * * @return the display name */ std::string display_name() const; - /** * Does the contact contain a valid email address as per * https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address * ? - * * @return true or false */ bool has_valid_email() const; /** - * Operator==; based on the hash values (ie. lowercase e-mail address) + * Operator==; based on the e-mail address only * * @param rhs some other Contact * - * @return true orf false. + * @return true or false. */ bool operator== (const Contact& rhs) const noexcept { - return hash() == rhs.hash(); + return email == rhs.email; + } + /** + * Operator!= + * + * @param rhs some other Contact + * + * @return true or false. + */ + bool operator!= (const Contact& rhs) const noexcept { + return !(*this == rhs); } + static constexpr int64_t RecentOffset{15 * 24 * 3600}; + /**< Contacts seen after now - RecentOffset seconds are considered + * "recent" */ + /** - * Get a hash-value for this contact, which gets lazily calculated. This - * * is for use with container classes. This uses the _lowercase_ email - * address. + * operator< + * + * Less "relevant" contacts are smaller than more relevant. + * + * Used as a somewhat over-engineered way to sort by relevance + * + * This is currently used for the ordering in mu-cfind and + * auto-completion in mu4e, if the various completion methods don't + * override it... * - * @return the hash + * @param rhs some other contact + * + * @return true of false */ - size_t hash() const { - static size_t cached_hash; - if (cached_hash == 0) { - cached_hash = lowercase_hash(email); - } - return cached_hash; + bool operator<(const Contact& rhs) const noexcept { + // non-personal is less relevant. + if (personal != rhs.personal) + return personal < rhs.personal; + // older is less relevant for recent messages + if (message_date != rhs.message_date && + (message_date > recently() || rhs.message_date > recently())) + return message_date < rhs.message_date; + // less frequent is less relevant + if (frequency != rhs.frequency) + return frequency < rhs.frequency; + // if all else fails, alphabetically + return email < rhs.email; } /** @@ -153,18 +170,16 @@ struct Contact { #pragma GCC diagnostic pop } - /* * data members */ - - std::string email; /**< Email address for this contact.Not empty */ - std::string name; /**< Name for this contact; can be empty. */ - Type type; /**< Type of contact */ - int64_t message_date; /**< Date of the contact's message */ - bool personal; /**< A personal message? */ - size_t frequency; /**< Frequency of this contact */ - int64_t tstamp; /**< Timestamp for this contact (internal use) */ + std::string email{}; /**< Email address for this contact.Not empty */ + std::string name{}; /**< Name for this contact; can be empty. */ + Type type{Type::None};/**< Type of contact */ + int64_t message_date{}; /**< Date of the contact's message */ + bool personal{}; /**< A personal message? */ + size_t frequency{}; /**< Frequency of this contact */ + int64_t tstamp{}; /**< Timestamp for this contact (internal use) */ private: void cleanup_name() { // replace control characters by spaces. @@ -172,6 +187,18 @@ private: if (iscntrl(c)) c = ' '; } + + /** + * Oldest timestamp considered "recent" + * + * This is arbitrary of course + * + * @return timestamp + */ + static int64_t recently() { + static const auto recent{::time({}) - RecentOffset}; + return recent; + } }; constexpr Option @@ -212,7 +239,7 @@ std::string to_string(const Contacts& contacts); */ template<> struct std::hash { std::size_t operator()(const Mu::Contact& c) const noexcept { - return c.hash(); + return std::hash{}(c.email); } }; diff --git a/lib/message/mu-document.cc b/lib/message/mu-document.cc index 428b946a2..a8f0b248f 100644 --- a/lib/message/mu-document.cc +++ b/lib/message/mu-document.cc @@ -1,5 +1,5 @@ /* -** Copyright (C) 2022-2023 Dirk-Jan C. Binnema +** Copyright (C) 2022-2025 Dirk-Jan C. Binnema ** ** 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 @@ -50,18 +50,28 @@ Document::xapian_document() const return xdoc_; } -template void -Document::put_prop(const std::string& pname, SexpType&& val) +static std::string +propname(const Field& field) { - cached_sexp().put_props(pname, std::forward(val)); - dirty_sexp_ = true; + return std::string(":") + std::string{field.name}; } template void -Document::put_prop(const Field& field, SexpType&& val) +Document::sexp_put_prop(const Field& field, SexpType&& val) { - put_prop(std::string(":") + std::string{field.name}, - std::forward(val)); + if (field.include_in_sexp()) { + cached_sexp().put_props(propname(field), std::forward(val)); + dirty_sexp_ = true; + } +} + +void +Document::sexp_remove_prop(const Field& field) +{ + if (field.include_in_sexp()) { + cached_sexp().del_prop(propname(field)); + dirty_sexp_ = true; + } } static Xapian::TermGenerator @@ -106,16 +116,13 @@ Document::add(Field::Id id, const std::string& val) if (field.is_searchable()) add_search_term(xdoc_, field, val, options_); - if (field.include_in_sexp()) - put_prop(field, val); + sexp_put_prop(field, val); + } void Document::add(Field::Id id, const std::vector& vals) { - if (vals.empty()) - return; - const auto field{field_from_id(id)}; if (field.is_value()) xdoc_.add_value(field.value_no(), Mu::join(vals, SepaChar1)); @@ -129,7 +136,7 @@ Document::add(Field::Id id, const std::vector& vals) Sexp elms{}; for(auto&& val: vals) elms.add(val); - put_prop(field, std::move(elms)); + sexp_put_prop(field, std::move(elms)); } } @@ -192,8 +199,7 @@ Document::add(Field::Id id, const Contacts& contacts) if (!cvec.empty()) xdoc_.add_value(field.value_no(), join(cvec, SepaChar1)); - if (field.include_in_sexp()) - put_prop(field, make_contacts_sexp(contacts)); + sexp_put_prop(field, make_contacts_sexp(contacts)); } Contacts @@ -225,15 +231,14 @@ Document::contacts_value(Field::Id id) const noexcept } void -Document::add_extra_contacts(const std::string& propname, const Contacts& contacts) +Document::add_extra_contacts(const std::string& prop_name, const Contacts& contacts) { if (!contacts.empty()) { - put_prop(propname, make_contacts_sexp(contacts)); + cached_sexp().put_props(prop_name, make_contacts_sexp(contacts)); dirty_sexp_ = true; } } - static Sexp make_emacs_time_sexp(::time_t t) { @@ -256,12 +261,10 @@ Document::add(Field::Id id, int64_t val) if (field.is_value()) xdoc_.add_value(field.value_no(), to_lexnum(val)); - if (field.include_in_sexp()) { - if (field.is_time_t()) - put_prop(field, make_emacs_time_sexp(val)); - else - put_prop(field, val); - } + if (field.is_time_t()) + sexp_put_prop(field, make_emacs_time_sexp(val)); + else + sexp_put_prop(field, val); } int64_t @@ -282,7 +285,7 @@ Document::add(Priority prio) xdoc_.add_boolean_term(field.xapian_term(to_char(prio))); if (field.include_in_sexp()) - put_prop(field, Sexp::Symbol(priority_name(prio))); + sexp_put_prop(field, Sexp::Symbol(priority_name(prio))); } Priority @@ -308,10 +311,9 @@ Document::add(Flags flags) }); if (field.include_in_sexp()) - put_prop(field, std::move(flaglist)); + sexp_put_prop(field, std::move(flaglist)); } - Flags Document::flags_value() const noexcept { @@ -324,12 +326,15 @@ Document::remove(Field::Id field_id) const auto field{field_from_id(field_id)}; const auto pfx{field.xapian_prefix()}; + bool updated{}; + xapian_try([&]{ if (auto&& val{xdoc_.get_value(field.value_no())}; !val.empty()) { // g_debug("removing value<%u>: '%s'", field.value_no(), // val.c_str()); xdoc_.remove_value(field.value_no()); + updated = true; } std::vector kill_list; @@ -344,13 +349,16 @@ Document::remove(Field::Id field_id) // g_debug("removing term '%s'", term.c_str()); try { xdoc_.remove_term(term); + updated = true; } catch(const Xapian::InvalidArgumentError& xe) { mu_critical("failed to remove '{}'", term); } } }); -} + if (updated) + sexp_remove_prop(field); +} #ifdef BUILD_TESTS @@ -367,8 +375,6 @@ Document::remove(Field::Id field_id) assert_same_contact(CV1[i], CV2[i]); \ } while(0) - - static const Contacts test_contacts = {{ Contact{"john@example.com", "John", Contact::Type::Bcc}, Contact{"ringo@example.com", "Ringo", Contact::Type::Bcc}, diff --git a/lib/message/mu-document.hh b/lib/message/mu-document.hh index 5119044c7..d9855f0f8 100644 --- a/lib/message/mu-document.hh +++ b/lib/message/mu-document.hh @@ -1,4 +1,4 @@ -/** Copyright (C) 2022-2023 Dirk-Jan C. Binnema +/** Copyright (C) 2022-2025 Dirk-Jan C. Binnema ** ** 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 @@ -28,6 +28,7 @@ #include "mu-priority.hh" #include "mu-flags.hh" #include "mu-contact.hh" +#include "mu-labels.hh" #include #include @@ -102,7 +103,6 @@ public: */ void add(Field::Id field_id, const std::vector& vals); - /** * Add message-contacts to the document, if non-empty * @@ -139,12 +139,13 @@ public: /** - * Add message flags to the document + * Add message flags to the document * * @param flags mesage flags. */ void add(Flags flags); + /** * Remove values and terms for some field. * @@ -239,8 +240,8 @@ public: Flags flags_value() const noexcept; private: - template void put_prop(const Field& field, SexpType&& val); - template void put_prop(const std::string& pname, SexpType&& val); + template void sexp_put_prop(const Field& field, SexpType&& val); + void sexp_remove_prop(const Field& field); Sexp& cached_sexp() const { if (cached_sexp_.empty()) diff --git a/lib/message/mu-fields.cc b/lib/message/mu-fields.cc index 040e68c88..7f53b6a75 100644 --- a/lib/message/mu-fields.cc +++ b/lib/message/mu-fields.cc @@ -79,17 +79,15 @@ Mu::field_is_combi(const std::string& name) std::string Field::xapian_term(const std::string& s) const { - const auto start{std::string(1U, xapian_prefix())}; - if (const auto& size = s.size(); size == 0) - return start; + auto res{std::string(1U, xapian_prefix())}; + if (s.empty()) + return res; - std::string res{start}; res.reserve(s.size() + 10); - /* slightly optimized common pure-ascii. */ if (G_LIKELY(g_str_is_ascii(s.c_str()))) { res += s; - for (auto i = 1; res[i]; ++i) + for (auto i = 1U; i != res.length(); ++i) res[i] = g_ascii_tolower(res[i]); } else res += utf8_flatten(s); diff --git a/lib/message/mu-fields.hh b/lib/message/mu-fields.hh index 62c79e024..698f09204 100644 --- a/lib/message/mu-fields.hh +++ b/lib/message/mu-fields.hh @@ -1,5 +1,5 @@ /* -** Copyright (C) 2022-2024 Dirk-Jan C. Binnema +** Copyright (C) 2022-2025 Dirk-Jan C. Binnema ** ** 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 @@ -65,6 +65,10 @@ struct Field { Tags, /**< Message Tags */ ThreadId, /**< Thread Id */ To, /**< To: recipient */ + + // XXX: re-order when we update the db-schema. + Labels, /**< Labels */ + // _count_ /**< Number of Ids */ }; @@ -462,6 +466,19 @@ static constexpr std::array Field::Flag::NormalTerm | Field::Flag::PhrasableTerm, }, + { + Field::Id::Labels, + Field::Type::StringList, + "labels", "label", + "Message label(s)", + "label:projectx", + 'q', + Field::Flag::BooleanTerm | + Field::Flag::Value | + Field::Flag::IncludeInSexp +, + }, + }}; /* @@ -476,8 +493,7 @@ static constexpr std::array * @return ref of the message field. */ constexpr const Field& -field_from_id(Field::Id id) -{ +field_from_id(Field::Id id) { return Fields.at(static_cast(id)); } @@ -508,7 +524,7 @@ constexpr Option field_find_if(Pred&& pred) { } /** - * Get the the message-field for the given name or shortcut + * Get the message-field for the given name or shortcut * * @param name_or_shortcut * diff --git a/lib/message/mu-flags.hh b/lib/message/mu-flags.hh index 8e424dd0f..ee01702c4 100644 --- a/lib/message/mu-flags.hh +++ b/lib/message/mu-flags.hh @@ -1,5 +1,5 @@ /* -** Copyright (C) 2022-2023 Dirk-Jan C. Binnema +** Copyright (C) 2022-2025 Dirk-Jan C. Binnema ** ** 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 @@ -86,7 +86,7 @@ enum struct MessageFlagCategory { }; /** - * Info about invidual message flags + * Info about individual message flags * */ struct MessageFlagInfo { @@ -373,9 +373,6 @@ flags_maildir_file(Flags flags) return flags; } - - - /** * Return flags, where flags = new_flags but with unmutable_flag in the * result the same as in old_flags diff --git a/lib/message/mu-labels.cc b/lib/message/mu-labels.cc new file mode 100644 index 000000000..87b92e377 --- /dev/null +++ b/lib/message/mu-labels.cc @@ -0,0 +1,281 @@ +/* +** Copyright (C) 2025 Dirk-Jan C. Binnema +** +** 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-labels.hh" +#include + +using namespace Mu; +using namespace Mu::Labels; + +Result +Mu::Labels::validate_label(const std::string &label) +{ + if (label.empty()) + return Err(Error{Error::Code::InvalidArgument, + "labels cannot be empty"}); + else if (!g_utf8_validate(label.c_str(), label.size(), {})) // perhaps put hex in err str? + return Err(Error{Error::Code::InvalidArgument, + "labels must be valid UTF-8"}); + + const auto cstr{label.c_str()}; + + // labels must be at least two characters and not start with a + // dash. these limitations are there to avoid confusion with + // command-line parameters. + if (cstr[0] == '-' || cstr[0] == '+') + return Err(Error{Error::Code::InvalidArgument, + "labels cannot start with '+' or '-' ({})", label}); + + for (auto cur = cstr; cur && *cur; cur = g_utf8_next_char(cur)) { + + const gunichar uc = g_utf8_get_char(cur); + if (g_unichar_isalnum(uc)) + continue; // alphanum is okay + + if (::iscntrl(uc)) + return Err(Error{Error::Code::InvalidArgument, + "control character {} not allowed in label", + static_cast(uc)}); + if (::isblank(uc)) + return Err(Error{Error::Code::InvalidArgument, + "blank character {} not allowed in label", + static_cast(uc)}); + if (uc == '"' || uc == '\'' || uc == '`' || uc == ',' || + uc == '\\' || uc == '/' || uc == '$') + return Err(Error{Error::Code::InvalidArgument, + "character '{}' not allowed in label", uc}); + } + + return Ok(); +} + +Result +Mu::Labels::parse_delta_label(const std::string &expr) +{ + if (expr.size() < 1) + return Err(Error{Error::Code::InvalidArgument, + "empty labels are invalid"}); + const auto cstr{expr.c_str()}; + + // first char; either '+' or '-' + if (cstr[0] != '+' && cstr[0] != '-') + return Err(Error{Error::Code::InvalidArgument, + "invalid label expression '{}'; " + "must start with '+' or '-'", + expr}); + Delta delta{cstr[0] == '+' ? Delta::Add : Delta::Remove}; + std::string label{expr.substr(1)}; + + if (const auto res = validate_label(label); !res) + return Err(res.error()); + + return Ok(DeltaLabel{std::move(delta), std::move(label)}); +} + +Result +Mu::Labels::parse_delta_labels(const std::string& exprs, + const std::string sepa) +{ + + DeltaLabelVec deltas{}; + for (const auto& expr: split(exprs, sepa)) { + if (auto delta = parse_delta_label(expr); !delta) + return Err(std::move(delta.error())); + else + deltas.emplace_back(*delta); + } + + return Ok(std::move(deltas)); +} + +struct cmp_delta_label { // can not yet be a λ in C++17 + bool operator()(const DeltaLabel& dl1, const DeltaLabel& dl2) const { + return dl1.second < dl2.second; + } +}; +std::pair +Mu::Labels::updated_labels(const LabelVec& labels, const DeltaLabelVec& deltas) +{ + // quite complicated! + + // First, the delta; put in a set for uniqueness; and use a special + // comparison operator so "add" and "remove" deltas are considered "the same" + // for the set; then fill the set from the end of the deltas vec to the begining, + // so "the last one wins", as we want. + + // only one change per label, last one wins + std::set working_deltas{ + deltas.rbegin(), deltas.rend() + }; + + // working set of lables; we start with _all_ (uniquified) + std::set working_labels{labels.begin(), labels.end()}; + + // keep track of the deltas that actually changed something (ie. + // removing a non-existing label or adding an already existing one is + // not a change.) + DeltaLabelVec effective_deltas; + + // now check each of our "workin deltas", apply on the working_labels, and + // if they changed anything, add to 'effectivc_deltas + for (auto& delta: working_deltas) { + switch (delta.first) { + case Delta::Add: + // add to the _effective_ deltas if the element wasn't + // there before. + if (working_labels.emplace(delta.second).second) + effective_deltas.emplace_back(std::move(delta)); + break; + case Delta::Remove: + // add to the _effective_ deltas if the element was + // actually removed. + if (working_labels.erase(delta.second) > 0U) + effective_deltas.emplace_back(std::move(delta)); + break; + default: + // can't have Neutral here. + throw std::runtime_error("invalid delta"); + } + } + + + return {{ working_labels.begin(), working_labels.end()}, effective_deltas}; +} + + + +#ifdef BUILD_TESTS + +#include "utils/mu-test-utils.hh" + +static void +test_parse_delta_label() +{ + { + const auto expr = parse_delta_label("+foo"); + assert_valid_result(expr); + g_assert_true(expr->first == Delta::Add); + assert_equal(expr->second, "foo"); + } + + { + const auto expr = parse_delta_label("-bar@cuux"); + assert_valid_result(expr); + g_assert_true(expr->first == Delta::Remove); + assert_equal(expr->second, "bar@cuux"); + } + + g_assert_false(!!parse_delta_label("ravenking")); + g_assert_false(!!parse_delta_label("+norrell strange")); + g_assert_true(!!parse_delta_label("-😨")); +} + + +static void +test_parse_delta_labels() +{ + { + const auto deltas = parse_delta_labels("+foo,-bar@cuux",","); + assert_valid_result(deltas); + + g_assert_true(deltas->at(0).first == Delta::Add); + assert_equal(deltas->at(0).second, "foo"); + + g_assert_true(deltas->at(1).first == Delta::Remove); + assert_equal(deltas->at(1).second, "bar@cuux"); + } + + { + const auto expr = parse_delta_labels("+foo @boo🐂", " "); + g_assert_false(!!expr); + } +} + + + +static void +test_validate_label() +{ + g_assert_true(!!validate_label("ravenking")); + g_assert_true(!!validate_label("@raven+king")); + g_assert_true(!!validate_label("operation:mindcrime")); + g_assert_true(!!validate_label("😨")); + g_assert_true(!!validate_label("foo%bar+1")); + + g_assert_false(!!validate_label("norrell strange")); + g_assert_false(!!validate_label("")); + g_assert_false(!!validate_label("+")); + g_assert_false(!!validate_label("-")); + g_assert_false(!!validate_label("foo$bar")); + g_assert_false(!!validate_label("foo,bar")); + g_assert_false(!!validate_label("foo`bar")); + g_assert_false(!!validate_label("\"quoted\"")); +} + +static void +test_updated_labels() +{ + const auto assert_eq=[](const LabelVec& labels, const DeltaLabelVec& deltas, + const LabelVec& exp_labels, const DeltaLabelVec& exp_deltas) { + + const auto& [res_labels, res_deltas] = updated_labels(labels, deltas); + + assert_equal_seq_str(res_labels, exp_labels); + g_assert_cmpuint(res_deltas.size(), ==, exp_deltas.size()); + for (size_t i{}; i != res_deltas.size(); ++i) { + g_assert_true(res_deltas[i].first == exp_deltas[i].first); + assert_equal(res_deltas[i].second, exp_deltas[i].second); + } + }; + + const auto delta_labels = [](std::initializer_list strs)->DeltaLabelVec { + DeltaLabelVec deltas; + std::transform(strs.begin(), strs.end(), std::back_inserter(deltas), + [](auto str) { + const auto res = parse_delta_label(str); + assert_valid_result(res); + return *res; + }); + return deltas; + }; + + assert_eq({"foo", "bar", "cuux"}, delta_labels({"+fnorb", "+bar", "-bar", "+bar", "-cuux"}), + {"bar", "fnorb", "foo"}, delta_labels({"-cuux", "+fnorb"})); + + assert_eq({}, delta_labels({"-fnorb", "-fnorb", "+whiteward", "+altesia", "+fnorb"}), + {"altesia", "fnorb", "whiteward"}, delta_labels({"+altesia", "+fnorb", "+whiteward"})); + + assert_eq({"piranesi", "hyperion", "mordor", "piranesi"}, delta_labels({}), + {"hyperion", "mordor", "piranesi"}, delta_labels({})); +} + +int +main(int argc, char* argv[]) +{ + mu_test_init(&argc, &argv); + + g_test_add_func("/message/labels/parse-delta-label", test_parse_delta_label); + g_test_add_func("/message/labels/parse-delta-labels", test_parse_delta_labels); + g_test_add_func("/message/labels/validate-label", test_validate_label); + g_test_add_func("/message/labels/updated-labels", test_updated_labels); + + return g_test_run(); +} +#endif /*BUILD_TESTS*/ diff --git a/lib/message/mu-labels.hh b/lib/message/mu-labels.hh new file mode 100644 index 000000000..85337f722 --- /dev/null +++ b/lib/message/mu-labels.hh @@ -0,0 +1,100 @@ +/* +** Copyright (C) 2025 Dirk-Jan C. Binnema +** +** 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_LABELS_HH +#define MU_LABELS_HH + +#include + +#include +#include +#include + +namespace Mu { +namespace Labels { + +using LabelVec = std::vector; + +enum struct Delta { Add='+', Remove='-'}; +using DeltaLabel = std::pair; +using DeltaLabelVec = std::vector; + +/** + * Parse a label expression, i.e., a label prefixed with '+' or '-' + * + * This also validates the label, as per valid_label() + * + * @param expr expression + * + * @return a result with either a DeltaLabel or an error + */ +Result parse_delta_label(const std::string& expr); + +/** + * Parse a series of label expressions, with some separator + * + * This also validates the labels, as per valid_label() + * + * @param exprs label expressions + * + * @return a result with either a DeltaLabel or an error + */ +Result parse_delta_labels(const std::string& exprs, + const std::string sepa); + +/** + * Is the label (without +/- prefix) valid? + * + * @param label some label + * + * @return either Ok or some error + */ +Result validate_label(const std::string &label); + +/** + * Apply deltas to labels and return the result as well as the + * effective changes. + * + * The deltas are handled in order; 'last one wins', hence: + * { +foo, -foo } ==> no foo in the result + * and + * { -foo, +foo } ==> foo in results + * + * The result labels do not contain duplicates. Order is not necessarily + * maintained. + * + * The result is a pair, the first element is LabelVec with the results + * as explained. + * + * The second is a DeltaVec with the _effective_ changes; this the input + * DeltaVec but without any +